PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.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
traits 1 year ago anthropic.php 10 months ago chatml.php 10 months ago core.php 11 months ago factory.php 1 year ago google.php 10 months ago hugging-face.php 1 year ago open-router.php 1 year ago openai.php 10 months ago perplexity.php 10 months ago replicate.php 1 year ago
open-router.php
302 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.' );
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 } elseif ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
105 // Tokens from API but price calculated = tokens accuracy
106 $reply->set_usage_accuracy( 'tokens' );
107 } else {
108 // Everything estimated
109 $reply->set_usage_accuracy( 'estimated' );
110 }
111 }
112
113 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
114 $price = $reply->get_price();
115 return is_null( $price ) ? parent::get_price( $query, $reply ) : $price;
116 }
117
118 /**
119 * Retrieve the models from OpenRouter, adding tags/features accordingly.
120 */
121 public function retrieve_models() {
122
123 // 1. Get the list of models supporting "tools"
124 $toolsModels = $this->get_supported_models( 'tools' );
125
126 // 2. Retrieve the full list of models
127 $url = 'https://openrouter.ai/api/v1/models';
128 $response = wp_remote_get( $url );
129 if ( is_wp_error( $response ) ) {
130 throw new Exception( 'AI Engine: ' . $response->get_error_message() );
131 }
132 $body = json_decode( $response['body'], true );
133 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
134 throw new Exception( 'AI Engine: Invalid response for the list of models.' );
135 }
136
137 $models = [];
138 foreach ( $body['data'] as $model ) {
139
140 // Basic defaults
141 $family = 'n/a';
142 $maxCompletionTokens = 4096;
143 $maxContextualTokens = 8096;
144 $priceIn = 0;
145 $priceOut = 0;
146
147 // Family from model ID (e.g. "openai/gpt-4/32k" -> "openai")
148 if ( isset( $model['id'] ) ) {
149 $parts = explode( '/', $model['id'] );
150 $family = $parts[0] ?? 'n/a';
151 }
152
153 // maxCompletionTokens
154 if ( isset( $model['top_provider']['max_completion_tokens'] ) ) {
155 $maxCompletionTokens = (int) $model['top_provider']['max_completion_tokens'];
156 }
157
158 // maxContextualTokens
159 if ( isset( $model['context_length'] ) ) {
160 $maxContextualTokens = (int) $model['context_length'];
161 }
162
163 // Pricing
164 if ( isset( $model['pricing']['prompt'] ) && $model['pricing']['prompt'] > 0 ) {
165 $priceIn = $this->truncate_float( floatval( $model['pricing']['prompt'] ) * 1000 );
166 }
167 if ( isset( $model['pricing']['completion'] ) && $model['pricing']['completion'] > 0 ) {
168 $priceOut = $this->truncate_float( floatval( $model['pricing']['completion'] ) * 1000 );
169 }
170
171 // Basic features and tags
172 $features = [ 'completion' ];
173 $tags = [ 'core', 'chat' ];
174
175 // If the name contains (beta), (alpha) or (preview), add 'preview' tag and remove from name
176 if ( preg_match( '/\((beta|alpha|preview)\)/i', $model['name'] ) ) {
177 $tags[] = 'preview';
178 $model['name'] = preg_replace( '/\((beta|alpha|preview)\)/i', '', $model['name'] );
179 }
180
181 // If model supports tools
182 if ( in_array( $model['id'], $toolsModels, true ) ) {
183 $tags[] = 'functions';
184 $features[] = 'functions';
185 }
186
187 // Check if the model supports "vision" (if "image" is in the left side of the arrow)
188 // e.g. "text+image->text" or "image->text"
189 $modality = $model['architecture']['modality'] ?? '';
190 $modality_lc = strtolower( $modality );
191 if (
192 strpos( $modality_lc, 'image->' ) !== false ||
193 strpos( $modality_lc, 'image+' ) !== false ||
194 strpos( $modality_lc, '+image->' ) !== false
195 ) {
196 // Means it can handle images as input, so we consider that "vision"
197 $tags[] = 'vision';
198 }
199
200 $models[] = [
201 'model' => $model['id'] ?? '',
202 'name' => trim( $model['name'] ?? '' ),
203 'family' => $family,
204 'features' => $features,
205 'price' => [
206 'in' => $priceIn,
207 'out' => $priceOut,
208 ],
209 'type' => 'token',
210 'unit' => 1 / 1000,
211 'maxCompletionTokens' => $maxCompletionTokens,
212 'maxContextualTokens' => $maxContextualTokens,
213 'tags' => $tags,
214 ];
215 }
216
217 return $models;
218 }
219
220 /**
221 * Return an array of model IDs that support a certain feature (e.g. "tools").
222 */
223 private function get_supported_models( $feature ) {
224 // Make a request to get models supporting that feature
225 $url = 'https://openrouter.ai/api/v1/models?supported_parameters=' . urlencode( $feature );
226 $response = wp_remote_get( $url );
227 if ( is_wp_error( $response ) ) {
228 Meow_MWAI_Logging::error( "OpenRouter: Failed to retrieve models for '$feature': " . $response->get_error_message() );
229 return [];
230 }
231 $body = json_decode( $response['body'], true );
232 if ( !isset( $body['data'] ) || !is_array( $body['data'] ) ) {
233 Meow_MWAI_Logging::error( "OpenRouter: Invalid response for '$feature' models." );
234 return [];
235 }
236
237 $modelIDs = [];
238 foreach ( $body['data'] as $m ) {
239 if ( isset( $m['id'] ) ) {
240 $modelIDs[] = $m['id'];
241 }
242 }
243
244 return $modelIDs;
245 }
246
247 /**
248 * Utility function to truncate a float to a specific precision.
249 */
250 private function truncate_float( $number, $precision = 4 ) {
251 $factor = pow( 10, $precision );
252 return floor( $number * $factor ) / $factor;
253 }
254
255 /**
256 * Check the connection to OpenRouter by listing models.
257 * Uses the existing retrieve_models method for consistency.
258 */
259 public function connection_check() {
260 try {
261 // Use the existing retrieve_models method
262 $models = $this->retrieve_models();
263
264 if ( !is_array( $models ) ) {
265 throw new Exception( 'Invalid response format from OpenRouter' );
266 }
267
268 $modelCount = count( $models );
269 $availableModels = [];
270
271 // Get first 5 models for display
272 $displayModels = array_slice( $models, 0, 5 );
273 foreach ( $displayModels as $model ) {
274 if ( isset( $model['model'] ) ) {
275 $availableModels[] = $model['model'];
276 }
277 }
278
279 return [
280 'success' => true,
281 'service' => 'OpenRouter',
282 'message' => "Connection successful. Found {$modelCount} models.",
283 'details' => [
284 'endpoint' => 'https://openrouter.ai/api/v1/models',
285 'model_count' => $modelCount,
286 'sample_models' => $availableModels
287 ]
288 ];
289 }
290 catch ( Exception $e ) {
291 return [
292 'success' => false,
293 'service' => 'OpenRouter',
294 'error' => $e->getMessage(),
295 'details' => [
296 'endpoint' => 'https://openrouter.ai/api/v1/models'
297 ]
298 ];
299 }
300 }
301 }
302