PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.3.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.3.4
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 5 months ago chatml.php 5 months ago core.php 7 months ago factory.php 8 months ago google.php 5 months ago mistral.php 5 months ago open-router.php 5 months ago openai.php 5 months ago perplexity.php 6 months ago replicate.php 5 months ago
open-router.php
514 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 * OpenRouter uses /chat/completions with modalities parameter for image generation,
132 * not the standard /images/generations endpoint.
133 */
134 public function run_image_query( $query, $streamCallback = null ) {
135 $body = [
136 'model' => $query->model,
137 'messages' => [
138 [
139 'role' => 'user',
140 'content' => $query->get_message()
141 ]
142 ],
143 'modalities' => [ 'text', 'image' ],
144 ];
145
146 // Add number of images if specified
147 if ( !empty( $query->maxResults ) && $query->maxResults > 1 ) {
148 $body['n'] = $query->maxResults;
149 }
150
151 // Add image config for Gemini models (aspect ratio support)
152 if ( !empty( $query->resolution ) && strpos( $query->model, 'google/' ) === 0 ) {
153 $body['image_config'] = [
154 'aspect_ratio' => $query->resolution
155 ];
156 }
157
158 $endpoint = apply_filters( 'mwai_openrouter_endpoint', 'https://openrouter.ai/api/v1', $this->env );
159 $url = trailingslashit( $endpoint ) . 'chat/completions';
160 $headers = $this->build_headers( $query );
161 $options = $this->build_options( $headers, $body );
162
163 try {
164 $res = $this->run_query( $url, $options );
165 $data = $res['data'];
166
167 if ( empty( $data ) || !isset( $data['choices'] ) ) {
168 throw new Exception( 'No image generated in response.' );
169 }
170
171 $reply = new Meow_MWAI_Reply( $query );
172 $reply->set_type( 'images' );
173 $images = [];
174
175 // Extract images from the response
176 foreach ( $data['choices'] as $choice ) {
177 $message = $choice['message'] ?? [];
178
179 // Check for images in the message (OpenRouter format)
180 // Each image is: { "type": "image_url", "image_url": { "url": "data:image/png;base64,..." } }
181 if ( isset( $message['images'] ) && is_array( $message['images'] ) ) {
182 foreach ( $message['images'] as $image ) {
183 if ( is_array( $image ) && isset( $image['image_url']['url'] ) ) {
184 $images[] = [ 'url' => $image['image_url']['url'] ];
185 }
186 elseif ( is_array( $image ) && isset( $image['image_url'] ) && is_string( $image['image_url'] ) ) {
187 $images[] = [ 'url' => $image['image_url'] ];
188 }
189 elseif ( is_string( $image ) ) {
190 // Direct base64 string
191 $images[] = [ 'url' => $image ];
192 }
193 }
194 }
195
196 // Also check content array for image parts
197 if ( isset( $message['content'] ) && is_array( $message['content'] ) ) {
198 foreach ( $message['content'] as $part ) {
199 if ( isset( $part['type'] ) && $part['type'] === 'image_url' ) {
200 if ( isset( $part['image_url']['url'] ) ) {
201 $images[] = [ 'url' => $part['image_url']['url'] ];
202 }
203 elseif ( is_string( $part['image_url'] ) ) {
204 $images[] = [ 'url' => $part['image_url'] ];
205 }
206 }
207 }
208 }
209 }
210
211 if ( empty( $images ) ) {
212 throw new Exception( 'No images found in the response.' );
213 }
214
215 // Record usage
216 $model = $query->model;
217 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
218
219 if ( isset( $data['usage'] ) ) {
220 $usage = $data['usage'];
221 $promptTokens = $usage['prompt_tokens'] ?? 0;
222 $completionTokens = $usage['completion_tokens'] ?? 0;
223 $this->core->record_tokens_usage( $model, $promptTokens, $completionTokens );
224 $usage['queries'] = 1;
225 $usage['accuracy'] = 'tokens';
226 $reply->set_usage( $usage );
227 $reply->set_usage_accuracy( 'tokens' );
228 }
229 else {
230 $usage = $this->core->record_images_usage( $model, $resolution, count( $images ) );
231 $reply->set_usage( $usage );
232 $reply->set_usage_accuracy( 'estimated' );
233 }
234
235 $reply->set_choices( $images );
236
237 // Handle local download if enabled
238 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
239 foreach ( $reply->results as &$result ) {
240 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
241 'query_envId' => $query->envId,
242 'query_session' => $query->session,
243 'query_model' => $query->model,
244 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
245 $fileUrl = $this->core->files->get_url( $fileId );
246 $result = $fileUrl;
247 }
248 }
249
250 $reply->result = $reply->results[0];
251 return $reply;
252 }
253 catch ( Exception $e ) {
254 Meow_MWAI_Logging::error( 'OpenRouter: ' . $e->getMessage() );
255 throw new Exception( 'OpenRouter: ' . $e->getMessage() );
256 }
257 }
258
259 /**
260 * Retrieve the models from OpenRouter, adding tags/features accordingly.
261 */
262 public function retrieve_models() {
263
264 // 1. Get the list of models supporting "tools"
265 $toolsModels = $this->get_supported_models( 'tools' );
266
267 // 2. Retrieve the full list of chat models
268 $url = 'https://openrouter.ai/api/v1/models';
269 $response = wp_remote_get( $url );
270 if ( is_wp_error( $response ) ) {
271 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
272 }
273 $body = json_decode( $response['body'], true );
274 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
275 throw new Exception( 'AI Engine: Invalid response for the list of models.' );
276 }
277
278 $models = [];
279 foreach ( $body['data'] as $model ) {
280 $models[] = $this->build_model_entry( $model, $toolsModels );
281 }
282
283 // 3. Retrieve embedding models
284 $embeddingsUrl = 'https://openrouter.ai/api/v1/embeddings/models';
285 $embeddingsResponse = wp_remote_get( $embeddingsUrl );
286 if ( !is_wp_error( $embeddingsResponse ) ) {
287 $embeddingsBody = json_decode( $embeddingsResponse['body'], true );
288 if ( isset( $embeddingsBody['data'] ) && is_array( $embeddingsBody['data'] ) ) {
289 foreach ( $embeddingsBody['data'] as $model ) {
290 $models[] = $this->build_model_entry( $model, [], true );
291 }
292 }
293 }
294
295 return $models;
296 }
297
298 /**
299 * Build a model entry from OpenRouter API data.
300 */
301 private function build_model_entry( $model, $toolsModels = [], $isEmbedding = false ) {
302 // Basic defaults
303 $family = 'n/a';
304 $maxCompletionTokens = 4096;
305 $maxContextualTokens = 8096;
306 $priceIn = 0;
307 $priceOut = 0;
308
309 // Family from model ID (e.g. "openai/gpt-4/32k" -> "openai")
310 if ( isset( $model['id'] ) ) {
311 $parts = explode( '/', $model['id'] );
312 $family = $parts[0] ?? 'n/a';
313 }
314
315 // maxCompletionTokens
316 if ( isset( $model['top_provider']['max_completion_tokens'] ) ) {
317 $maxCompletionTokens = (int) $model['top_provider']['max_completion_tokens'];
318 }
319
320 // maxContextualTokens
321 if ( isset( $model['context_length'] ) ) {
322 $maxContextualTokens = (int) $model['context_length'];
323 }
324
325 // Pricing
326 if ( isset( $model['pricing']['prompt'] ) && $model['pricing']['prompt'] > 0 ) {
327 $priceIn = $this->truncate_float( floatval( $model['pricing']['prompt'] ) * 1000 );
328 }
329 if ( isset( $model['pricing']['completion'] ) && $model['pricing']['completion'] > 0 ) {
330 $priceOut = $this->truncate_float( floatval( $model['pricing']['completion'] ) * 1000 );
331 }
332
333 // Handle embedding models
334 if ( $isEmbedding ) {
335 $features = [ 'embeddings' ];
336 $tags = [ 'core', 'embedding' ];
337
338 // Try to extract dimensions from description
339 $dimensions = null;
340 if ( isset( $model['description'] ) && preg_match( '/(\d+)-dimensional/', $model['description'], $matches ) ) {
341 $dimensions = (int) $matches[1];
342 }
343
344 $entry = [
345 'model' => $model['id'] ?? '',
346 'name' => trim( $model['name'] ?? '' ),
347 'family' => $family,
348 'features' => $features,
349 'price' => [
350 'in' => $priceIn,
351 'out' => $priceOut,
352 ],
353 'type' => 'token',
354 'unit' => 1 / 1000,
355 'maxContextualTokens' => $maxContextualTokens,
356 'tags' => $tags,
357 ];
358
359 if ( $dimensions ) {
360 $entry['dimensions'] = $dimensions;
361 }
362
363 return $entry;
364 }
365
366 // Basic features and tags for chat models
367 $features = [ 'completion' ];
368 $tags = [ 'core', 'chat' ];
369
370 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
371 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'] ) ) {
372 $tags[] = 'preview';
373 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
374 }
375
376 // If model supports tools
377 if ( in_array( $model['id'], $toolsModels, true ) ) {
378 $tags[] = 'functions';
379 $features[] = 'functions';
380 }
381
382 // Check if the model supports "vision" (if "image" is in the left side of the arrow)
383 // e.g. "text+image->text" or "image->text"
384 $modality = $model['architecture']['modality'] ?? '';
385 $modality_lc = strtolower( $modality );
386 if (
387 strpos( $modality_lc, 'image->' ) !== false ||
388 strpos( $modality_lc, 'image+' ) !== false ||
389 strpos( $modality_lc, '+image->' ) !== false
390 ) {
391 // Means it can handle images as input, so we consider that "vision"
392 $tags[] = 'vision';
393 }
394
395 // Check if the model supports image generation (if "image" is in the output part after "->")
396 // e.g. "text->image" or "text+image->text+image" means it can generate images
397 $isImageGeneration = false;
398 if ( strpos( $modality_lc, '->' ) !== false ) {
399 $parts = explode( '->', $modality_lc );
400 $outputPart = $parts[1] ?? '';
401 $isImageGeneration = strpos( $outputPart, 'image' ) !== false;
402 }
403 if ( $isImageGeneration ) {
404 $features = [ 'text-to-image' ];
405 $tags = [ 'core', 'image' ];
406 }
407
408 $entry = [
409 'model' => $model['id'] ?? '',
410 'name' => trim( $model['name'] ?? '' ),
411 'family' => $family,
412 'features' => $features,
413 'price' => [
414 'in' => $priceIn,
415 'out' => $priceOut,
416 ],
417 'type' => 'token',
418 'unit' => 1 / 1000,
419 'maxCompletionTokens' => $maxCompletionTokens,
420 'maxContextualTokens' => $maxContextualTokens,
421 'tags' => $tags,
422 ];
423
424 // Add mode for image generation models
425 if ( $isImageGeneration ) {
426 $entry['mode'] = 'image';
427 }
428
429 return $entry;
430 }
431
432 /**
433 * Return an array of model IDs that support a certain feature (e.g. "tools").
434 */
435 private function get_supported_models( $feature ) {
436 // Make a request to get models supporting that feature
437 $url = 'https://openrouter.ai/api/v1/models?supported_parameters=' . urlencode( $feature );
438 $response = wp_remote_get( $url );
439 if ( is_wp_error( $response ) ) {
440 Meow_MWAI_Logging::error( "OpenRouter: Failed to retrieve models for '$feature': " . $response->get_error_message() );
441 return [];
442 }
443 $body = json_decode( $response['body'], true );
444 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
445 Meow_MWAI_Logging::error( "OpenRouter: Invalid response for '$feature' models." );
446 return [];
447 }
448
449 $modelIDs = [];
450 foreach ( $body['data'] as $m ) {
451 if ( isset( $m['id'] ) ) {
452 $modelIDs[] = $m['id'];
453 }
454 }
455
456 return $modelIDs;
457 }
458
459 /**
460 * Utility function to truncate a float to a specific precision.
461 */
462 private function truncate_float( $number, $precision = 4 ) {
463 $factor = pow( 10, $precision );
464 return floor( $number * $factor ) / $factor;
465 }
466
467 /**
468 * Check the connection to OpenRouter by listing models.
469 * Uses the existing retrieve_models method for consistency.
470 */
471 public function connection_check() {
472 try {
473 // Use the existing retrieve_models method
474 $models = $this->retrieve_models();
475
476 if ( !is_array( $models ) ) {
477 throw new Exception( 'Invalid response format from OpenRouter' );
478 }
479
480 $modelCount = count( $models );
481 $availableModels = [];
482
483 // Get first 5 models for display
484 $displayModels = array_slice( $models, 0, 5 );
485 foreach ( $displayModels as $model ) {
486 if ( isset( $model['model'] ) ) {
487 $availableModels[] = $model['model'];
488 }
489 }
490
491 return [
492 'success' => true,
493 'service' => 'OpenRouter',
494 'message' => "Connection successful. Found {$modelCount} models.",
495 'details' => [
496 'endpoint' => 'https://openrouter.ai/api/v1/models',
497 'model_count' => $modelCount,
498 'sample_models' => $availableModels
499 ]
500 ];
501 }
502 catch ( Exception $e ) {
503 return [
504 'success' => false,
505 'service' => 'OpenRouter',
506 'error' => $e->getMessage(),
507 'details' => [
508 'endpoint' => 'https://openrouter.ai/api/v1/models'
509 ]
510 ];
511 }
512 }
513 }
514