PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.6.4
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.6.4
3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 3.5.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 / ovh.php
ai-engine / classes / engines Last commit date
anthropic.php 2 days ago chatml.php 2 days ago core.php 23 hours ago custom.php 2 days ago factory.php 1 month ago google-interactions.php 2 days ago google.php 2 days ago mistral.php 1 month ago open-router.php 1 month ago openai.php 2 days ago ovh.php 2 days ago perplexity.php 7 months ago replicate.php 6 months ago xai.php 2 months ago
ovh.php
362 lines
1 <?php
2
3 class Meow_MWAI_Engines_OVH extends Meow_MWAI_Engines_ChatML {
4 private $endpoint = 'https://oai.endpoints.kepler.ai.cloud.ovh.net';
5 public $envType = 'ovh';
6
7 public function __construct( $core, $env ) {
8 parent::__construct( $core, $env );
9 }
10
11 /**
12 * Sets up the environment for our custom engine.
13 */
14 protected function set_environment() {
15 $env = $this->env;
16
17 if ( !isset( $env['apikey'] ) || empty( $env['apikey'] ) ) {
18 throw new Exception( 'AI Engine: OVH API key is not set. Please configure the OVH AI Endpoints access token in your settings.' );
19 }
20
21 $this->apiKey = $env['apikey'];
22
23 if ( isset( $env['endpoint'] ) && !empty( $env['endpoint'] ) ) {
24 $this->endpoint = $env['endpoint'];
25 }
26 }
27
28 protected function build_url( $query, $endpoint = null ) {
29 $endpoint = apply_filters( 'mwai_ovh_endpoint', trailingslashit( $this->endpoint ) . 'v1', $this->env );
30 return $endpoint . '/chat/completions';
31 }
32
33 protected function get_service_name() {
34 return 'OVH';
35 }
36
37 protected function build_messages( $query ) {
38 // Handle vision models like llava-next-mistral-7b
39 if ( strpos( $query->model, 'llava' ) !== false ) {
40 $query->image_remote_upload = 'data';
41 }
42
43 $messages = parent::build_messages( $query );
44 return $messages;
45 }
46
47 protected function stream_data_handler( $json ) {
48 // Handle usage-only chunks (final chunk with empty choices array).
49 // OVH sends usage data in a final chunk with choices: []
50 if ( isset( $json['usage'] ) && empty( $json['choices'] ) ) {
51 $usage = $json['usage'];
52 if ( isset( $usage['prompt_tokens'], $usage['completion_tokens'] ) ) {
53 $this->streamInTokens = (int) $usage['prompt_tokens'];
54 $this->streamOutTokens = (int) $usage['completion_tokens'];
55 }
56 return null; // No content in this chunk
57 }
58
59 // Let parent handle all other chunks
60 return parent::stream_data_handler( $json );
61 }
62
63 public function get_models() {
64 return $this->retrieve_models();
65 }
66
67 protected function build_headers( $query ) {
68 $headers = [
69 'Content-Type' => 'application/json',
70 'Authorization' => 'Bearer ' . $this->apiKey,
71 ];
72 return $headers;
73 }
74
75 protected function build_body( $query, $streamCallback = null, $extra = null ) {
76 $body = parent::build_body( $query, $streamCallback, $extra );
77
78 // Handle max_tokens parameter (OVH uses max_tokens, not max_completion_tokens)
79 if ( isset( $body['max_completion_tokens'] ) ) {
80 $body['max_tokens'] = $body['max_completion_tokens'];
81 unset( $body['max_completion_tokens'] );
82 }
83
84 // OVH does not error when max_tokens cannot fit: it returns 200 with an
85 // instantly-terminated stream (a lone [DONE]), which looked like the AI
86 // answering nothing. Its models also share ONE window between prompt and
87 // completion (max output == context), so any max_tokens at or above the
88 // model's cap can never fit once the prompt is counted. In that case drop
89 // the parameter and let the endpoint default to the remaining context.
90 if ( !empty( $body['max_tokens'] ) ) {
91 $modelInfo = $this->retrieve_model_info( $query->model );
92 if ( !empty( $modelInfo['maxCompletionTokens'] ) &&
93 (int) $body['max_tokens'] >= (int) $modelInfo['maxCompletionTokens'] ) {
94 unset( $body['max_tokens'] );
95 }
96 }
97
98 // OVH supports stream_options for accurate token usage in streaming
99 if ( !empty( $streamCallback ) && !isset( $body['stream_options'] ) ) {
100 $body['stream_options'] = [
101 'include_usage' => true,
102 ];
103 }
104
105 // Remove parallel_tool_calls - OVH might not support it
106 if ( isset( $body['parallel_tool_calls'] ) ) {
107 unset( $body['parallel_tool_calls'] );
108 }
109
110 // Remove other potentially unsupported parameters
111 $unsupported_params = [ 'response_format', 'seed', 'logit_bias', 'logprobs', 'top_logprobs' ];
112 foreach ( $unsupported_params as $param ) {
113 if ( isset( $body[$param] ) ) {
114 unset( $body[$param] );
115 }
116 }
117
118 return $body;
119 }
120
121 public function handle_tokens_usage(
122 $reply,
123 $query,
124 $returned_model,
125 $returned_in_tokens,
126 $returned_out_tokens,
127 $returned_price = null
128 ) {
129
130 // Clean up the data
131 $returned_in_tokens = !is_null( $returned_in_tokens ) ?
132 $returned_in_tokens : $reply->get_in_tokens( $query );
133 $returned_out_tokens = !is_null( $returned_out_tokens ) ?
134 $returned_out_tokens : $reply->get_out_tokens();
135
136 // Calculate price based on our model definitions
137 $models = $this->get_ovh_models();
138 $model_price = null;
139
140 foreach ( $models as $model ) {
141 if ( $model['model'] === $returned_model ) {
142 $model_price = $model['price'];
143 break;
144 }
145 }
146
147 if ( $model_price ) {
148 $returned_price = ( $returned_in_tokens * $model_price['in'] +
149 $returned_out_tokens * $model_price['out'] ) / 1000000;
150 }
151 else {
152 $returned_price = 0;
153 }
154
155 // Record the usage
156 $usage = $this->core->record_tokens_usage(
157 $returned_model,
158 $returned_in_tokens,
159 $returned_out_tokens,
160 $returned_price
161 );
162
163 // Set the usage in the reply
164 $reply->set_usage( $usage );
165
166 // Set accuracy based on data availability
167 if ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
168 // Tokens from API (via stream_options), price calculated locally
169 $reply->set_usage_accuracy( 'tokens' );
170 }
171 else {
172 // Everything estimated
173 $reply->set_usage_accuracy( 'estimated' );
174 }
175 }
176
177 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
178 $models = $this->get_ovh_models();
179
180 foreach ( $models as $model ) {
181 if ( $model['model'] === $query->model ) {
182 $in_tokens = $reply->get_in_tokens( $query );
183 $out_tokens = $reply->get_out_tokens();
184 return ( $in_tokens * $model['price']['in'] +
185 $out_tokens * $model['price']['out'] ) / 1000000;
186 }
187 }
188
189 return 0;
190 }
191
192 /**
193 * Retrieve the models from the OVH OpenRouter-compatible catalog.
194 */
195 public function retrieve_models() {
196 // Per-request cache: build_body and the pricing helpers may all ask for
197 // the model list during a single query.
198 static $cached = null;
199 if ( $cached !== null ) {
200 return $cached;
201 }
202
203 $url = 'https://catalog.endpoints.ai.ovh.net/rest/v2/openrouter';
204
205 // This can run in the middle of a streaming completion, where the
206 // engine's http_api_curl stream handler is registered: it would hijack
207 // this fetch too (empty body, catalog "streamed" to nowhere) and we would
208 // silently fall back to the wrong model list. Detach the hooks around it.
209 global $wp_filter;
210 $saved_hooks = null;
211 if ( isset( $wp_filter['http_api_curl'] ) ) {
212 $saved_hooks = $wp_filter['http_api_curl'];
213 unset( $wp_filter['http_api_curl'] );
214 }
215 $response = wp_remote_get( $url, [ 'timeout' => 10 ] );
216 if ( $saved_hooks !== null ) {
217 $wp_filter['http_api_curl'] = $saved_hooks;
218 }
219
220 if ( is_wp_error( $response ) ) {
221 return $this->get_fallback_models();
222 }
223
224 $body = wp_remote_retrieve_body( $response );
225 $data = json_decode( $body, true );
226
227 if ( empty( $data ) || !is_array( $data ) ) {
228 return $this->get_fallback_models();
229 }
230
231 // Extract models from 'data' key
232 $models_data = isset( $data['data'] ) ? $data['data'] : $data;
233
234 if ( empty( $models_data ) || !is_array( $models_data ) ) {
235 return $this->get_fallback_models();
236 }
237
238 $models = [];
239 foreach ( $models_data as $model_data ) {
240 $model = $this->map_openrouter_model( $model_data );
241 if ( $model ) {
242 $models[] = $model;
243 }
244 }
245
246 $cached = !empty( $models ) ? $models : $this->get_fallback_models();
247 return $cached;
248 }
249
250 /**
251 * Map OpenRouter model data to AI Engine format.
252 */
253 private function map_openrouter_model( $model_data ) {
254 if ( empty( $model_data['id'] ) ) {
255 return null;
256 }
257
258 $model_id = $model_data['id'];
259 $name = !empty( $model_data['name'] ) ? $model_data['name'] : $model_id;
260
261 // Extract family from model ID
262 $family_parts = explode( '/', $model_id );
263 $family = $family_parts[0];
264
265 // Base features and tags
266 $features = [ 'completion', 'chat' ];
267 $tags = [ 'core', 'chat' ];
268
269 // Check for vision support (input modality includes images)
270 if ( !empty( $model_data['input_modalities'] ) && is_array( $model_data['input_modalities'] ) ) {
271 if ( in_array( 'image', $model_data['input_modalities'] ) ) {
272 $features[] = 'vision';
273 $tags[] = 'vision';
274 }
275 }
276
277 // Map features from OpenRouter to AI Engine format
278 $openrouter_features = !empty( $model_data['supported_features'] ) ? $model_data['supported_features'] : [];
279 foreach ( $openrouter_features as $feature ) {
280 switch ( $feature ) {
281 case 'json_mode':
282 case 'structured_outputs':
283 if ( !in_array( 'json', $tags ) ) {
284 $tags[] = 'json';
285 }
286 break;
287 case 'reasoning':
288 if ( !in_array( 'reasoning', $tags ) ) {
289 $tags[] = 'reasoning';
290 }
291 break;
292 case 'tools':
293 // OVH now supports function calling via tools format
294 if ( !in_array( 'functions', $tags ) ) {
295 $tags[] = 'functions';
296 }
297 break;
298 }
299 }
300
301 // Convert pricing from per-token to per-million tokens
302 $price_in = 0;
303 $price_out = 0;
304 if ( !empty( $model_data['pricing']['prompt'] ) ) {
305 $price_in = floatval( $model_data['pricing']['prompt'] ) * 1000000;
306 }
307 if ( !empty( $model_data['pricing']['completion'] ) ) {
308 $price_out = floatval( $model_data['pricing']['completion'] ) * 1000000;
309 }
310
311 // Get context and max tokens
312 $max_context = !empty( $model_data['context_length'] ) ? intval( $model_data['context_length'] ) : 128000;
313 $max_completion = !empty( $model_data['max_output_length'] ) ? intval( $model_data['max_output_length'] ) : 4096;
314
315 return [
316 'model' => $model_id,
317 'name' => $name,
318 'family' => $family,
319 'features' => $features,
320 'price' => [
321 'in' => $price_in,
322 'out' => $price_out,
323 ],
324 'type' => 'token',
325 'unit' => 1 / 1000000,
326 'maxCompletionTokens' => $max_completion,
327 'maxContextualTokens' => $max_context,
328 'tags' => $tags,
329 ];
330 }
331
332 /**
333 * Get fallback models in case the catalog API is unavailable.
334 */
335 private function get_fallback_models() {
336 return [
337 [
338 'model' => 'meta-llama/Llama-3.3-70B-Instruct',
339 'name' => 'Llama 3.3 70B Instruct',
340 'family' => 'meta-llama',
341 'features' => [ 'completion', 'chat' ],
342 'price' => [
343 'in' => 0.06,
344 'out' => 0.09,
345 ],
346 'type' => 'token',
347 'unit' => 1 / 1000000,
348 'maxCompletionTokens' => 8192,
349 'maxContextualTokens' => 131072,
350 'tags' => [ 'core', 'chat', 'json', 'reasoning' ],
351 ],
352 ];
353 }
354
355 /**
356 * Get model info by model ID (for backward compatibility).
357 */
358 private function get_ovh_models() {
359 return $this->retrieve_models();
360 }
361 }
362