PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.9
3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / open-router.php
ai-engine / classes / engines Last commit date
anthropic.php 6 months ago chatml.php 6 months ago core.php 7 months ago factory.php 8 months ago google.php 6 months ago mistral.php 6 months ago open-router.php 7 months ago openai.php 6 months ago perplexity.php 6 months ago replicate.php 6 months ago
open-router.php
355 lines
1 <?php
2
3 // If this isn't defined elsewhere, set it here by default. You can override
4 // it in your theme's functions.php or your main wp-config.php. If set to true,
5 // additional time will be spent fetching exact pricing info from OpenRouter
6 // after each query, resulting in more accurate but potentially slower responses.
7 if ( !defined( 'MWAI_OPENROUTER_ACCURATE_PRICING' ) ) {
8 define( 'MWAI_OPENROUTER_ACCURATE_PRICING', false );
9 }
10
11 class Meow_MWAI_Engines_OpenRouter extends Meow_MWAI_Engines_ChatML {
12 /**
13 * Keep a static dictionary (query -> price) so that if we see the same query
14 * again in another instance, we can immediately return the stored price
15 * instead of recomputing.
16 * @var array
17 */
18 private static $accuratePrices = [];
19
20 public function __construct( $core, $env ) {
21 parent::__construct( $core, $env );
22 }
23
24 protected function set_environment() {
25 $env = $this->env;
26 $this->apiKey = $env['apikey'];
27 }
28
29 protected function build_url( $query, $endpoint = null ) {
30 $endpoint = apply_filters( 'mwai_openrouter_endpoint', 'https://openrouter.ai/api/v1', $this->env );
31 return parent::build_url( $query, $endpoint );
32 }
33
34 protected function build_headers( $query ) {
35 $site_url = apply_filters( 'mwai_openrouter_site_url', get_site_url(), $query );
36 $site_name = apply_filters( 'mwai_openrouter_site_name', get_bloginfo( 'name' ), $query );
37 if ( $query->apiKey ) {
38 $this->apiKey = $query->apiKey;
39 }
40 if ( empty( $this->apiKey ) ) {
41 throw new Exception( 'No API Key provided. Please visit the Settings. (OpenRouter Engine)' );
42 }
43 return [
44 'Content-Type' => 'application/json',
45 'Authorization' => 'Bearer ' . $this->apiKey,
46 'HTTP-Referer' => $site_url,
47 'X-Title' => $site_name,
48 'User-Agent' => 'AI Engine',
49 ];
50 }
51
52 protected function build_body( $query, $streamCallback = null, $extra = null ) {
53 $body = parent::build_body( $query, $streamCallback, $extra );
54 // Use transforms from OpenRouter docs
55 $body['transforms'] = ['middle-out'];
56 $body['usage'] = [ 'include' => true ];
57 return $body;
58 }
59
60 protected function get_service_name() {
61 return 'OpenRouter';
62 }
63
64 public function get_models() {
65 return $this->core->get_engine_models( 'openrouter' );
66 }
67
68 /**
69 * Requests usage data if streaming was used and the usage is incomplete.
70 */
71 public function handle_tokens_usage(
72 $reply,
73 $query,
74 $returned_model,
75 $returned_in_tokens,
76 $returned_out_tokens,
77 $returned_price = null
78 ) {
79 // If streaming is not enabled, we might already have all usage data
80 $everything_is_set = !is_null( $returned_model )
81 && !is_null( $returned_in_tokens )
82 && !is_null( $returned_out_tokens );
83
84 // Clean up the data
85 $returned_in_tokens = $returned_in_tokens ?? $reply->get_in_tokens( $query );
86 $returned_out_tokens = $returned_out_tokens ?? $reply->get_out_tokens();
87 $returned_price = $returned_price ?? $reply->get_price();
88
89 // Record the usage in the database
90 $usage = $this->core->record_tokens_usage(
91 $returned_model,
92 $returned_in_tokens,
93 $returned_out_tokens,
94 $returned_price
95 );
96
97 // Set the usage back on the reply
98 $reply->set_usage( $usage );
99
100 // Set accuracy based on data availability
101 if ( !is_null( $returned_price ) && !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
102 // OpenRouter returns price from API = full accuracy
103 $reply->set_usage_accuracy( 'full' );
104 }
105 elseif ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
106 // Tokens from API but price calculated = tokens accuracy
107 $reply->set_usage_accuracy( 'tokens' );
108 }
109 else {
110 // Everything estimated
111 $reply->set_usage_accuracy( 'estimated' );
112 }
113 }
114
115 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
116 $price = $reply->get_price();
117 return is_null( $price ) ? parent::get_price( $query, $reply ) : $price;
118 }
119
120 /**
121 * Retrieve the models from OpenRouter, adding tags/features accordingly.
122 */
123 public function retrieve_models() {
124
125 // 1. Get the list of models supporting "tools"
126 $toolsModels = $this->get_supported_models( 'tools' );
127
128 // 2. Retrieve the full list of chat models
129 $url = 'https://openrouter.ai/api/v1/models';
130 $response = wp_remote_get( $url );
131 if ( is_wp_error( $response ) ) {
132 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
133 }
134 $body = json_decode( $response['body'], true );
135 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
136 throw new Exception( 'AI Engine: Invalid response for the list of models.' );
137 }
138
139 $models = [];
140 foreach ( $body['data'] as $model ) {
141 $models[] = $this->build_model_entry( $model, $toolsModels );
142 }
143
144 // 3. Retrieve embedding models
145 $embeddingsUrl = 'https://openrouter.ai/api/v1/embeddings/models';
146 $embeddingsResponse = wp_remote_get( $embeddingsUrl );
147 if ( !is_wp_error( $embeddingsResponse ) ) {
148 $embeddingsBody = json_decode( $embeddingsResponse['body'], true );
149 if ( isset( $embeddingsBody['data'] ) && is_array( $embeddingsBody['data'] ) ) {
150 foreach ( $embeddingsBody['data'] as $model ) {
151 $models[] = $this->build_model_entry( $model, [], true );
152 }
153 }
154 }
155
156 return $models;
157 }
158
159 /**
160 * Build a model entry from OpenRouter API data.
161 */
162 private function build_model_entry( $model, $toolsModels = [], $isEmbedding = false ) {
163 // Basic defaults
164 $family = 'n/a';
165 $maxCompletionTokens = 4096;
166 $maxContextualTokens = 8096;
167 $priceIn = 0;
168 $priceOut = 0;
169
170 // Family from model ID (e.g. "openai/gpt-4/32k" -> "openai")
171 if ( isset( $model['id'] ) ) {
172 $parts = explode( '/', $model['id'] );
173 $family = $parts[0] ?? 'n/a';
174 }
175
176 // maxCompletionTokens
177 if ( isset( $model['top_provider']['max_completion_tokens'] ) ) {
178 $maxCompletionTokens = (int) $model['top_provider']['max_completion_tokens'];
179 }
180
181 // maxContextualTokens
182 if ( isset( $model['context_length'] ) ) {
183 $maxContextualTokens = (int) $model['context_length'];
184 }
185
186 // Pricing
187 if ( isset( $model['pricing']['prompt'] ) && $model['pricing']['prompt'] > 0 ) {
188 $priceIn = $this->truncate_float( floatval( $model['pricing']['prompt'] ) * 1000 );
189 }
190 if ( isset( $model['pricing']['completion'] ) && $model['pricing']['completion'] > 0 ) {
191 $priceOut = $this->truncate_float( floatval( $model['pricing']['completion'] ) * 1000 );
192 }
193
194 // Handle embedding models
195 if ( $isEmbedding ) {
196 $features = [ 'embeddings' ];
197 $tags = [ 'core', 'embedding' ];
198
199 // Try to extract dimensions from description
200 $dimensions = null;
201 if ( isset( $model['description'] ) && preg_match( '/(\d+)-dimensional/', $model['description'], $matches ) ) {
202 $dimensions = (int) $matches[1];
203 }
204
205 $entry = [
206 'model' => $model['id'] ?? '',
207 'name' => trim( $model['name'] ?? '' ),
208 'family' => $family,
209 'features' => $features,
210 'price' => [
211 'in' => $priceIn,
212 'out' => $priceOut,
213 ],
214 'type' => 'token',
215 'unit' => 1 / 1000,
216 'maxContextualTokens' => $maxContextualTokens,
217 'tags' => $tags,
218 ];
219
220 if ( $dimensions ) {
221 $entry['dimensions'] = $dimensions;
222 }
223
224 return $entry;
225 }
226
227 // Basic features and tags for chat models
228 $features = [ 'completion' ];
229 $tags = [ 'core', 'chat' ];
230
231 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
232 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'] ) ) {
233 $tags[] = 'preview';
234 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
235 }
236
237 // If model supports tools
238 if ( in_array( $model['id'], $toolsModels, true ) ) {
239 $tags[] = 'functions';
240 $features[] = 'functions';
241 }
242
243 // Check if the model supports "vision" (if "image" is in the left side of the arrow)
244 // e.g. "text+image->text" or "image->text"
245 $modality = $model['architecture']['modality'] ?? '';
246 $modality_lc = strtolower( $modality );
247 if (
248 strpos( $modality_lc, 'image->' ) !== false ||
249 strpos( $modality_lc, 'image+' ) !== false ||
250 strpos( $modality_lc, '+image->' ) !== false
251 ) {
252 // Means it can handle images as input, so we consider that "vision"
253 $tags[] = 'vision';
254 }
255
256 return [
257 'model' => $model['id'] ?? '',
258 'name' => trim( $model['name'] ?? '' ),
259 'family' => $family,
260 'features' => $features,
261 'price' => [
262 'in' => $priceIn,
263 'out' => $priceOut,
264 ],
265 'type' => 'token',
266 'unit' => 1 / 1000,
267 'maxCompletionTokens' => $maxCompletionTokens,
268 'maxContextualTokens' => $maxContextualTokens,
269 'tags' => $tags,
270 ];
271 }
272
273 /**
274 * Return an array of model IDs that support a certain feature (e.g. "tools").
275 */
276 private function get_supported_models( $feature ) {
277 // Make a request to get models supporting that feature
278 $url = 'https://openrouter.ai/api/v1/models?supported_parameters=' . urlencode( $feature );
279 $response = wp_remote_get( $url );
280 if ( is_wp_error( $response ) ) {
281 Meow_MWAI_Logging::error( "OpenRouter: Failed to retrieve models for '$feature': " . $response->get_error_message() );
282 return [];
283 }
284 $body = json_decode( $response['body'], true );
285 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
286 Meow_MWAI_Logging::error( "OpenRouter: Invalid response for '$feature' models." );
287 return [];
288 }
289
290 $modelIDs = [];
291 foreach ( $body['data'] as $m ) {
292 if ( isset( $m['id'] ) ) {
293 $modelIDs[] = $m['id'];
294 }
295 }
296
297 return $modelIDs;
298 }
299
300 /**
301 * Utility function to truncate a float to a specific precision.
302 */
303 private function truncate_float( $number, $precision = 4 ) {
304 $factor = pow( 10, $precision );
305 return floor( $number * $factor ) / $factor;
306 }
307
308 /**
309 * Check the connection to OpenRouter by listing models.
310 * Uses the existing retrieve_models method for consistency.
311 */
312 public function connection_check() {
313 try {
314 // Use the existing retrieve_models method
315 $models = $this->retrieve_models();
316
317 if ( !is_array( $models ) ) {
318 throw new Exception( 'Invalid response format from OpenRouter' );
319 }
320
321 $modelCount = count( $models );
322 $availableModels = [];
323
324 // Get first 5 models for display
325 $displayModels = array_slice( $models, 0, 5 );
326 foreach ( $displayModels as $model ) {
327 if ( isset( $model['model'] ) ) {
328 $availableModels[] = $model['model'];
329 }
330 }
331
332 return [
333 'success' => true,
334 'service' => 'OpenRouter',
335 'message' => "Connection successful. Found {$modelCount} models.",
336 'details' => [
337 'endpoint' => 'https://openrouter.ai/api/v1/models',
338 'model_count' => $modelCount,
339 'sample_models' => $availableModels
340 ]
341 ];
342 }
343 catch ( Exception $e ) {
344 return [
345 'success' => false,
346 'service' => 'OpenRouter',
347 'error' => $e->getMessage(),
348 'details' => [
349 'endpoint' => 'https://openrouter.ai/api/v1/models'
350 ]
351 ];
352 }
353 }
354 }
355