PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.2
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 6 months ago openai.php 6 months ago perplexity.php 6 months ago replicate.php 6 months ago
open-router.php
365 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 // Only add transforms and usage for chat completions, not embeddings
55 if ( !( $query instanceof Meow_MWAI_Query_Embed ) ) {
56 $body['transforms'] = ['middle-out'];
57 $body['usage'] = [ 'include' => true ];
58 }
59 else {
60 // Only OpenAI embedding models support the dimensions parameter
61 // Remove it for other providers to avoid errors
62 $model = $query->model ?? '';
63 if ( isset( $body['dimensions'] ) && strpos( $model, 'openai/' ) !== 0 ) {
64 unset( $body['dimensions'] );
65 }
66 }
67 return $body;
68 }
69
70 protected function get_service_name() {
71 return 'OpenRouter';
72 }
73
74 public function get_models() {
75 return $this->core->get_engine_models( 'openrouter' );
76 }
77
78 /**
79 * Requests usage data if streaming was used and the usage is incomplete.
80 */
81 public function handle_tokens_usage(
82 $reply,
83 $query,
84 $returned_model,
85 $returned_in_tokens,
86 $returned_out_tokens,
87 $returned_price = null
88 ) {
89 // If streaming is not enabled, we might already have all usage data
90 $everything_is_set = !is_null( $returned_model )
91 && !is_null( $returned_in_tokens )
92 && !is_null( $returned_out_tokens );
93
94 // Clean up the data
95 $returned_in_tokens = $returned_in_tokens ?? $reply->get_in_tokens( $query );
96 $returned_out_tokens = $returned_out_tokens ?? $reply->get_out_tokens();
97 $returned_price = $returned_price ?? $reply->get_price();
98
99 // Record the usage in the database
100 $usage = $this->core->record_tokens_usage(
101 $returned_model,
102 $returned_in_tokens,
103 $returned_out_tokens,
104 $returned_price
105 );
106
107 // Set the usage back on the reply
108 $reply->set_usage( $usage );
109
110 // Set accuracy based on data availability
111 if ( !is_null( $returned_price ) && !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
112 // OpenRouter returns price from API = full accuracy
113 $reply->set_usage_accuracy( 'full' );
114 }
115 elseif ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
116 // Tokens from API but price calculated = tokens accuracy
117 $reply->set_usage_accuracy( 'tokens' );
118 }
119 else {
120 // Everything estimated
121 $reply->set_usage_accuracy( 'estimated' );
122 }
123 }
124
125 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
126 $price = $reply->get_price();
127 return is_null( $price ) ? parent::get_price( $query, $reply ) : $price;
128 }
129
130 /**
131 * Retrieve the models from OpenRouter, adding tags/features accordingly.
132 */
133 public function retrieve_models() {
134
135 // 1. Get the list of models supporting "tools"
136 $toolsModels = $this->get_supported_models( 'tools' );
137
138 // 2. Retrieve the full list of chat models
139 $url = 'https://openrouter.ai/api/v1/models';
140 $response = wp_remote_get( $url );
141 if ( is_wp_error( $response ) ) {
142 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
143 }
144 $body = json_decode( $response['body'], true );
145 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
146 throw new Exception( 'AI Engine: Invalid response for the list of models.' );
147 }
148
149 $models = [];
150 foreach ( $body['data'] as $model ) {
151 $models[] = $this->build_model_entry( $model, $toolsModels );
152 }
153
154 // 3. Retrieve embedding models
155 $embeddingsUrl = 'https://openrouter.ai/api/v1/embeddings/models';
156 $embeddingsResponse = wp_remote_get( $embeddingsUrl );
157 if ( !is_wp_error( $embeddingsResponse ) ) {
158 $embeddingsBody = json_decode( $embeddingsResponse['body'], true );
159 if ( isset( $embeddingsBody['data'] ) && is_array( $embeddingsBody['data'] ) ) {
160 foreach ( $embeddingsBody['data'] as $model ) {
161 $models[] = $this->build_model_entry( $model, [], true );
162 }
163 }
164 }
165
166 return $models;
167 }
168
169 /**
170 * Build a model entry from OpenRouter API data.
171 */
172 private function build_model_entry( $model, $toolsModels = [], $isEmbedding = false ) {
173 // Basic defaults
174 $family = 'n/a';
175 $maxCompletionTokens = 4096;
176 $maxContextualTokens = 8096;
177 $priceIn = 0;
178 $priceOut = 0;
179
180 // Family from model ID (e.g. "openai/gpt-4/32k" -> "openai")
181 if ( isset( $model['id'] ) ) {
182 $parts = explode( '/', $model['id'] );
183 $family = $parts[0] ?? 'n/a';
184 }
185
186 // maxCompletionTokens
187 if ( isset( $model['top_provider']['max_completion_tokens'] ) ) {
188 $maxCompletionTokens = (int) $model['top_provider']['max_completion_tokens'];
189 }
190
191 // maxContextualTokens
192 if ( isset( $model['context_length'] ) ) {
193 $maxContextualTokens = (int) $model['context_length'];
194 }
195
196 // Pricing
197 if ( isset( $model['pricing']['prompt'] ) && $model['pricing']['prompt'] > 0 ) {
198 $priceIn = $this->truncate_float( floatval( $model['pricing']['prompt'] ) * 1000 );
199 }
200 if ( isset( $model['pricing']['completion'] ) && $model['pricing']['completion'] > 0 ) {
201 $priceOut = $this->truncate_float( floatval( $model['pricing']['completion'] ) * 1000 );
202 }
203
204 // Handle embedding models
205 if ( $isEmbedding ) {
206 $features = [ 'embeddings' ];
207 $tags = [ 'core', 'embedding' ];
208
209 // Try to extract dimensions from description
210 $dimensions = null;
211 if ( isset( $model['description'] ) && preg_match( '/(\d+)-dimensional/', $model['description'], $matches ) ) {
212 $dimensions = (int) $matches[1];
213 }
214
215 $entry = [
216 'model' => $model['id'] ?? '',
217 'name' => trim( $model['name'] ?? '' ),
218 'family' => $family,
219 'features' => $features,
220 'price' => [
221 'in' => $priceIn,
222 'out' => $priceOut,
223 ],
224 'type' => 'token',
225 'unit' => 1 / 1000,
226 'maxContextualTokens' => $maxContextualTokens,
227 'tags' => $tags,
228 ];
229
230 if ( $dimensions ) {
231 $entry['dimensions'] = $dimensions;
232 }
233
234 return $entry;
235 }
236
237 // Basic features and tags for chat models
238 $features = [ 'completion' ];
239 $tags = [ 'core', 'chat' ];
240
241 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
242 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'] ) ) {
243 $tags[] = 'preview';
244 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
245 }
246
247 // If model supports tools
248 if ( in_array( $model['id'], $toolsModels, true ) ) {
249 $tags[] = 'functions';
250 $features[] = 'functions';
251 }
252
253 // Check if the model supports "vision" (if "image" is in the left side of the arrow)
254 // e.g. "text+image->text" or "image->text"
255 $modality = $model['architecture']['modality'] ?? '';
256 $modality_lc = strtolower( $modality );
257 if (
258 strpos( $modality_lc, 'image->' ) !== false ||
259 strpos( $modality_lc, 'image+' ) !== false ||
260 strpos( $modality_lc, '+image->' ) !== false
261 ) {
262 // Means it can handle images as input, so we consider that "vision"
263 $tags[] = 'vision';
264 }
265
266 return [
267 'model' => $model['id'] ?? '',
268 'name' => trim( $model['name'] ?? '' ),
269 'family' => $family,
270 'features' => $features,
271 'price' => [
272 'in' => $priceIn,
273 'out' => $priceOut,
274 ],
275 'type' => 'token',
276 'unit' => 1 / 1000,
277 'maxCompletionTokens' => $maxCompletionTokens,
278 'maxContextualTokens' => $maxContextualTokens,
279 'tags' => $tags,
280 ];
281 }
282
283 /**
284 * Return an array of model IDs that support a certain feature (e.g. "tools").
285 */
286 private function get_supported_models( $feature ) {
287 // Make a request to get models supporting that feature
288 $url = 'https://openrouter.ai/api/v1/models?supported_parameters=' . urlencode( $feature );
289 $response = wp_remote_get( $url );
290 if ( is_wp_error( $response ) ) {
291 Meow_MWAI_Logging::error( "OpenRouter: Failed to retrieve models for '$feature': " . $response->get_error_message() );
292 return [];
293 }
294 $body = json_decode( $response['body'], true );
295 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
296 Meow_MWAI_Logging::error( "OpenRouter: Invalid response for '$feature' models." );
297 return [];
298 }
299
300 $modelIDs = [];
301 foreach ( $body['data'] as $m ) {
302 if ( isset( $m['id'] ) ) {
303 $modelIDs[] = $m['id'];
304 }
305 }
306
307 return $modelIDs;
308 }
309
310 /**
311 * Utility function to truncate a float to a specific precision.
312 */
313 private function truncate_float( $number, $precision = 4 ) {
314 $factor = pow( 10, $precision );
315 return floor( $number * $factor ) / $factor;
316 }
317
318 /**
319 * Check the connection to OpenRouter by listing models.
320 * Uses the existing retrieve_models method for consistency.
321 */
322 public function connection_check() {
323 try {
324 // Use the existing retrieve_models method
325 $models = $this->retrieve_models();
326
327 if ( !is_array( $models ) ) {
328 throw new Exception( 'Invalid response format from OpenRouter' );
329 }
330
331 $modelCount = count( $models );
332 $availableModels = [];
333
334 // Get first 5 models for display
335 $displayModels = array_slice( $models, 0, 5 );
336 foreach ( $displayModels as $model ) {
337 if ( isset( $model['model'] ) ) {
338 $availableModels[] = $model['model'];
339 }
340 }
341
342 return [
343 'success' => true,
344 'service' => 'OpenRouter',
345 'message' => "Connection successful. Found {$modelCount} models.",
346 'details' => [
347 'endpoint' => 'https://openrouter.ai/api/v1/models',
348 'model_count' => $modelCount,
349 'sample_models' => $availableModels
350 ]
351 ];
352 }
353 catch ( Exception $e ) {
354 return [
355 'success' => false,
356 'service' => 'OpenRouter',
357 'error' => $e->getMessage(),
358 'details' => [
359 'endpoint' => 'https://openrouter.ai/api/v1/models'
360 ]
361 ];
362 }
363 }
364 }
365