PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.9.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.9.6
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 / chatml.php
ai-engine / classes / engines Last commit date
traits 1 year ago anthropic.php 1 year ago chatml.php 11 months ago core.php 11 months ago factory.php 1 year ago google.php 11 months ago hugging-face.php 1 year ago open-router.php 1 year ago openai.php 11 months ago perplexity.php 1 year ago replicate.php 1 year ago
chatml.php
1823 lines
1 <?php
2
3 /**
4 * Base implementation of the ChatML API.
5 * This was first introduced by OpenAI and many providers keep compatibility with it.
6 * Engines relying on this original API can extend this class.
7 *
8 */
9
10 class Meow_MWAI_Engines_ChatML extends Meow_MWAI_Engines_Core {
11 // Base (OpenAI)
12 protected $apiKey = null;
13 protected $organizationId = null;
14
15 // Azure
16 private $azureDeployments = null;
17 private $azureApiVersion = 'api-version=2024-12-01-preview';
18
19 // Response
20 protected $inModel = null;
21 protected $inId = null;
22 protected $inThreadId = null;
23
24 // Streaming
25 protected $streamFunctionCall = null;
26 protected $streamToolCalls = [];
27 protected $streamLastMessage = null;
28 protected $streamAnnotations = [];
29 protected $streamImageIds = [];
30
31 protected $streamInTokens = null;
32 protected $streamOutTokens = null;
33 protected $streamCost = null;
34 protected $streamStartEmitted = false;
35
36 public function __construct( $core, $env ) {
37 parent::__construct( $core, $env );
38 $this->set_environment();
39 }
40
41 public function reset_stream() {
42 $this->streamContent = null;
43 $this->streamBuffer = null;
44 $this->streamFunctionCall = null;
45 $this->streamToolCalls = [];
46 $this->streamLastMessage = null;
47 $this->streamInTokens = null;
48 $this->streamOutTokens = null;
49 $this->inModel = null;
50 $this->inId = null;
51 $this->emittedFunctionResults = [];
52 $this->streamStartEmitted = false;
53 }
54
55 protected function set_environment() {
56 $env = $this->env;
57 $this->apiKey = $env['apikey'];
58
59 if ( isset( $env['organizationId'] ) ) {
60 $this->organizationId = $env['organizationId'];
61 }
62 if ( $this->envType === 'azure' ) {
63 $this->azureDeployments = isset( $env['deployments'] ) ? $env['deployments'] : [];
64 $this->azureDeployments[] = [ 'model' => 'dall-e', 'name' => 'dall-e' ];
65 }
66 }
67
68 private function get_azure_deployment_name( $model ) {
69 foreach ( $this->azureDeployments as $deployment ) {
70 if ( $deployment['model'] === $model && !empty( $deployment['name'] ) ) {
71 return $deployment['name'];
72 }
73 }
74 throw new Exception( 'Unknown deployment for model: ' . $model );
75 }
76
77 protected function get_service_name() {
78 return $this->envType === 'azure' ? 'Azure' : 'OpenAI';
79 }
80
81 private function is_o1_model( $model ) {
82 $modelDef = $this->retrieve_model_info( $model );
83 return !empty( $modelDef['tags'] ) && in_array( 'o1-model', $modelDef['tags'] );
84 }
85
86 private function requires_developer_roles( $model ) {
87 if ( $model === 'o1' ) {
88 return true;
89 }
90 return false;
91 }
92
93 protected function build_messages( $query ) {
94 $messages = [];
95
96 // First, we need to add the first message (the instructions).
97 if ( !empty( $query->instructions ) ) {
98 //if ( !$this->is_o1_model( $query->model ) ) {
99 $messages[] = [ 'role' => 'system', 'content' => $query->instructions ];
100 //}
101 }
102
103 // Then, if any, we need to add the 'messages', they are already formatted.
104 foreach ( $query->messages as $message ) {
105 $messages[] = $message;
106 }
107
108 // If there is a context, we need to add it.
109 if ( !empty( $query->context ) ) {
110 $messages[] = [ 'role' => 'system', 'content' => $query->context ];
111 }
112
113 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
114 if ( $query->attachedFile ) {
115 $finalUrl = null;
116 if ( $query->image_remote_upload === 'url' ) {
117 $finalUrl = $query->attachedFile->get_url();
118 }
119 else {
120 $finalUrl = $query->attachedFile->get_inline_base64_url();
121 }
122 $messages[] = [
123 'role' => 'user',
124 'content' => [
125 [
126 'type' => 'text',
127 'text' => $query->get_message()
128 ],
129 [
130 'type' => 'image_url',
131 'image_url' => [
132 'url' => $finalUrl
133 ]
134 ]
135 ]
136 ];
137 }
138 else {
139 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
140 }
141
142 // We need to convert all the 'system' role into 'developer' role.
143 if ( $this->requires_developer_roles( $query->model ) ) {
144 foreach ( $messages as &$message ) {
145 if ( $message['role'] === 'system' ) {
146 $message['role'] = 'developer';
147 }
148 }
149 }
150 // But otherwise, if it's o1, we need to remove the message which are 'system'
151 else if ( $this->is_o1_model( $query->model ) ) {
152 $hasChanges = false;
153 foreach ( $messages as $index => $message ) {
154 if ( $message['role'] === 'system' ) {
155 unset( $messages[$index] );
156 $hasChanges = true;
157 }
158 }
159 if ( $hasChanges ) {
160 $messages = array_values( $messages );
161 Meow_MWAI_Logging::warn( 'The model ' . $query->model . ' doesn\'t support System nor Developer messages. They were removed.' );
162 }
163 }
164
165 return $messages;
166 }
167
168 protected function build_body( $query, $streamCallback = null, $extra = null ) {
169 if ( $query instanceof Meow_MWAI_Query_Text ) {
170 $body = [
171 'model' => $query->model,
172 'stream' => !is_null( $streamCallback ),
173 ];
174
175 if ( !empty( $query->maxTokens ) ) {
176 // max_tokens has been deprecated in favor of max_completion_tokens in 2025.
177 $body['max_completion_tokens'] = $query->maxTokens;
178 }
179
180 if ( !empty( $query->temperature ) ) {
181 if ( !$this->is_o1_model( $query->model ) ) {
182 $body['temperature'] = $query->temperature;
183 }
184 else {
185 $body['temperature'] = 1;
186 }
187 }
188
189 if ( !empty( $query->maxResults ) ) {
190 $body['n'] = $query->maxResults;
191 }
192
193 if ( !empty( $query->stop ) ) {
194 $body['stop'] = $query->stop;
195 }
196
197 if ( !empty( $query->responseFormat ) ) {
198 if ( $query->responseFormat === 'json' ) {
199 $body['response_format'] = [ 'type' => 'json_object' ];
200 }
201 }
202
203 // Usage Data (only for OpenAI)
204 // https://cookbook.openai.com/examples/how_to_stream_completions#4-how-to-get-token-usage-data-for-streamed-chat-completion-response
205 if ( !empty( $streamCallback ) && $this->envType === 'openai' ) {
206 $body['stream_options'] = [
207 'include_usage' => true,
208 ];
209 }
210
211 if ( !empty( $query->functions ) ) {
212 $model = $this->retrieve_model_info( $query->model );
213 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
214 Meow_MWAI_Logging::warn( 'The model ' . $query->model . ' doesn\'t support Function Calling.' );
215 }
216 else if ( strpos( $query->model, 'ft:' ) === 0 ) {
217 Meow_MWAI_Logging::warn( 'OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
218 }
219 else {
220 $body['tools'] = [];
221 // Dynamic function: they will interactively enhance the completion (tools).
222 foreach ( $query->functions as $function ) {
223 $body['tools'][] = [
224 'type' => 'function',
225 'function' => $function->serializeForOpenAI()
226 ];
227 }
228 // Static functions: they will be executed at the end of the completion.
229 //$body['function_call'] = $query->functionCall;
230 }
231 }
232 $body['messages'] = $this->build_messages( $query );
233
234 // Add the feedback if it's a feedback query.
235 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
236 if ( !empty( $query->blocks ) ) {
237 foreach ( $query->blocks as $feedback_block ) {
238 $body['messages'][] = $feedback_block['rawMessage'];
239 foreach ( $feedback_block['feedbacks'] as $feedback ) {
240 // Ensure content is a string for the API
241 $content = $feedback['reply']['value'];
242 if ( !is_string( $content ) ) {
243 $content = json_encode( $content );
244 }
245
246 $body['messages'][] = [
247 'tool_call_id' => $feedback['request']['toolId'],
248 'role' => 'tool',
249 'name' => $feedback['request']['name'],
250 'content' => $content
251 ];
252
253 // Note: Function result events are now emitted centrally in core.php
254 // when the function is actually executed
255 }
256 }
257 }
258 return $body;
259 }
260
261 return $body;
262 }
263 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
264 // Determine filename
265 $filename = 'audio.mp3'; // default
266 if ( !empty( $query->url ) ) {
267 $filename = basename( $query->url );
268 }
269 else if ( $query->attachedFile && method_exists( $query->attachedFile, 'get_filename' ) ) {
270 $filename = $query->attachedFile->get_filename();
271 }
272
273 $body = [
274 'prompt' => $query->message,
275 'model' => $query->model,
276 'response_format' => 'text',
277 'file' => $filename,
278 'data' => $extra
279 ];
280 return $body;
281 }
282 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
283 $body = [ 'input' => $query->message, 'model' => $query->model ];
284 if ( $this->envType === 'azure' ) {
285 $body = [ 'input' => $query->message ];
286 }
287 // Dimensions are only supported by v3 models
288 if ( !empty( $query->dimensions ) && strpos( $query->model, 'ada-002' ) === false ) {
289 $body['dimensions'] = $query->dimensions;
290 }
291 return $body;
292 }
293 else if ( $query instanceof Meow_MWAI_Query_EditImage ) {
294 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
295 $filename = $query->attachedFile ? $query->attachedFile->get_filename() : '';
296 $mimeType = $query->attachedFile ? $query->attachedFile->get_mimeType() : null;
297 $body = [
298 'prompt' => $query->message,
299 'n' => $query->maxResults,
300 'size' => $resolution,
301 'image' => $filename,
302 'data' => $extra
303 ];
304 if ( !empty( $mimeType ) ) {
305 $body['mime'] = $mimeType;
306 }
307
308 // Add mask if provided
309 if ( !empty( $query->mask ) ) {
310 $maskData = $query->mask->get_data();
311 $maskFilename = 'mask.png';
312 $maskMimeType = $query->mask->get_mimeType();
313
314 $body['mask'] = $maskFilename;
315 $body['mask_data'] = $maskData;
316 if ( !empty( $maskMimeType ) ) {
317 $body['mask_mime'] = $maskMimeType;
318 }
319 }
320 // 'response_format' => 'b64_json',
321 if ( !empty( $query->model ) ) {
322 $body['model'] = $query->model;
323 }
324 return $body;
325 }
326 else if ( $query instanceof Meow_MWAI_Query_Image ) {
327 $model = $query->model;
328 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
329 $body = [
330 'prompt' => $query->message,
331 'n' => $query->maxResults,
332 'size' => $resolution,
333 ];
334
335 // TODO: Let's clean this up; with a better Query Image class.
336 // https://platform.openai.com/docs/api-reference/images/create#images-create-quality
337
338 if ( $model === 'gpt-image-1' ) {
339 // If it's GPT Image 1, we need to set the quality and moderation.
340 $body['model'] = 'gpt-image-1';
341 $body['quality'] = 'high';
342 $body['moderation'] = 'low';
343 }
344 else {
345 // If it's DALL-E 3, we need to set the response format.
346 $body['response_format'] = 'b64_json';
347 if ( $model === 'dall-e-3' ) {
348 $body['model'] = 'dall-e-3';
349 }
350 if ( $model === 'dall-e-3-hd' ) {
351 $body['model'] = 'dall-e-3';
352 $body['quality'] = 'hd';
353 }
354 if ( !empty( $query->style ) && strpos( $model, 'dall-e-3' ) === 0 ) {
355 $body['style'] = $query->style;
356 }
357 }
358 return $body;
359 }
360 }
361
362 protected function build_url( $query, $endpoint = null ) {
363 $url = '';
364 $env = $this->env;
365 // This endpoint is basically OpenAI or Azure, but in the case this class
366 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
367 if ( empty( $endpoint ) ) {
368 if ( $this->envType === 'openai' ) {
369 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
370 $this->organizationId = isset( $env['organizationId'] ) ? $env['organizationId'] : null;
371 }
372 else if ( $this->envType === 'azure' ) {
373 $endpoint = isset( $env['endpoint'] ) ? $env['endpoint'] : null;
374 }
375 else {
376 if ( empty( $this->envType ) ) {
377 throw new Exception( 'Endpoint is not defined, and this envType is not known.' );
378 }
379 throw new Exception( 'Endpoint is not defined, and this envType is not known: ' . $this->envType );
380 }
381 }
382 // Add the base API to the URL
383 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
384 if ( $this->envType === 'azure' ) {
385 $deployment_name = $this->get_azure_deployment_name( $query->model );
386 $url = trailingslashit( $endpoint ) . 'openai/deployments/' . $deployment_name;
387 $url .= '/chat/completions?' . $this->azureApiVersion;
388 }
389 else {
390 $url .= trailingslashit( $endpoint ) . 'chat/completions';
391 }
392 return $url;
393 }
394 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
395 $modeEndpoint = $query->feature === 'translation' ? 'translations' : 'transcriptions';
396 $url .= trailingslashit( $endpoint ) . 'audio/' . $modeEndpoint;
397 return $url;
398 }
399 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
400 $url .= trailingslashit( $endpoint ) . 'embeddings';
401 if ( $this->envType === 'azure' ) {
402 $deployment_name = $this->get_azure_deployment_name( $query->model );
403 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
404 $deployment_name . '/embeddings?' . $this->azureApiVersion;
405 }
406 return $url;
407 }
408 else if ( $query instanceof Meow_MWAI_Query_EditImage ) {
409 $url .= trailingslashit( $endpoint ) . 'images/edits';
410 if ( $this->envType === 'azure' ) {
411 $deployment_name = $this->get_azure_deployment_name( $query->model );
412 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
413 $deployment_name . '/images/edits?' . $this->azureApiVersion;
414 }
415 return $url;
416 }
417 else if ( $query instanceof Meow_MWAI_Query_Image ) {
418 $url .= trailingslashit( $endpoint ) . 'images/generations';
419 if ( $this->envType === 'azure' ) {
420 $deployment_name = $this->get_azure_deployment_name( $query->model );
421 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
422 $deployment_name . '/images/generations?' . $this->azureApiVersion;
423 }
424 return $url;
425 }
426 throw new Exception( 'The query is not supported by build_url().' );
427 }
428
429 protected function build_headers( $query ) {
430 if ( $query->apiKey ) {
431 $this->apiKey = $query->apiKey;
432 }
433 if ( empty( $this->apiKey ) ) {
434 throw new Exception( 'No API Key provided. Please visit the Settings.' );
435 }
436 $headers = [
437 'Content-Type' => 'application/json',
438 'Authorization' => 'Bearer ' . $this->apiKey,
439 ];
440 if ( $this->organizationId ) {
441 $headers['OpenAI-Organization'] = $this->organizationId;
442 }
443 if ( $this->envType === 'azure' ) {
444 $headers = [ 'Content-Type' => 'application/json', 'api-key' => $this->apiKey ];
445 }
446 return $headers;
447 }
448
449 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
450 $body = null;
451 if ( !empty( $forms ) ) {
452 $boundary = wp_generate_password( 24, false );
453 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
454 $body = $this->build_form_body( $forms, $boundary );
455 }
456 else if ( !empty( $json ) ) {
457 $body = json_encode( $json );
458 }
459 $options = [
460 'headers' => $headers,
461 'method' => $method,
462 'timeout' => MWAI_TIMEOUT,
463 'body' => $body,
464 'sslverify' => false
465 ];
466 return $options;
467 }
468 // object: "thread.message.delta"
469 protected function stream_data_handler( $json ) {
470 $content = null;
471
472 // Get additional data from the JSON
473 if ( isset( $json['model'] ) ) {
474 $this->inModel = $json['model'];
475 }
476 if ( isset( $json['id'] ) ) {
477 $this->inId = $json['id'];
478
479 // Send start event if debug mode is enabled and not already sent
480 if ( $this->currentDebugMode && $this->streamCallback && !$this->streamStartEmitted ) {
481 $this->streamStartEmitted = true;
482 $event = Meow_MWAI_Event::status( 'Starting stream...' )
483 ->set_metadata( 'model', $this->inModel )
484 ->set_metadata( 'id', $this->inId );
485 call_user_func( $this->streamCallback, $event );
486 }
487 }
488
489 $object = $json['object'] ?? null;
490
491 if ( $object === 'thread.run' ) {
492 $this->inThreadId = $json['thread_id'];
493 if ( $json['status'] === 'failed' ) {
494 $error = $json['last_error']['message'] ?? 'The run failed.';
495 throw new Exception( $error );
496 }
497 }
498 else if ( $object === 'thread.run.step.delta' ) {
499 if ( $json['delta']['step_details']['type'] === 'tool_calls' ) {
500 foreach ( $json['delta']['step_details']['tool_calls'] as $tool_call ) {
501 $index = $tool_call['index'] ?? null;
502 $currentStreamToolCall = null;
503 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
504 $currentStreamToolCall = &$this->streamToolCalls[$index];
505 }
506 else {
507 $this->streamToolCalls[] = [
508 'id' => null,
509 'type' => null,
510 'function' => [ 'name' => '', 'arguments' => '' ],
511 'code_interpreter' => [ 'input' => '', 'outputs' => [] ],
512 ];
513 end( $this->streamToolCalls );
514 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
515
516 // Send tool call initiated event
517 if ( $this->currentDebugMode && $this->streamCallback ) {
518 $event = Meow_MWAI_Event::status( 'Initiating tool call...' );
519 call_user_func( $this->streamCallback, $event );
520 }
521 }
522 if ( !empty( $tool_call['id'] ) ) {
523 $currentStreamToolCall['id'] = $tool_call['id'];
524 }
525 if ( !empty( $tool_call['type'] ) ) {
526 $currentStreamToolCall['type'] = $tool_call['type'];
527 }
528 if ( isset( $tool_call['function'] ) ) {
529 $function = $tool_call['function'];
530 if ( isset( $function['name'] ) ) {
531 $currentStreamToolCall['function']['name'] .= $function['name'];
532 }
533 if ( isset( $function['arguments'] ) ) {
534 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
535 }
536 }
537 if ( isset( $tool_call['code_interpreter'] ) ) {
538 $code_interpreter = $tool_call['code_interpreter'];
539 if ( isset( $code_interpreter['input'] ) ) {
540 $currentStreamToolCall['code_interpreter']['input'] .= $code_interpreter['input'];
541 }
542 if ( isset( $code_interpreter['outputs'] ) ) {
543 $currentStreamToolCall['code_interpreter']['outputs'] = $code_interpreter['outputs'];
544 }
545 }
546 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
547 }
548 }
549 }
550 else if ( $object === 'thread.message.delta' ) {
551 $delta = $json['delta']['content'][0] ?? null;
552 if ( $delta ) {
553 switch ( $delta['type'] ?? null ) {
554 case 'text':
555 if ( !empty( $delta['value'] ) && is_string( $delta['value'] ) ) {
556 $content = $delta['value'];
557 }
558 else if ( !empty( $delta['text'] ) && is_string( $delta['text'] ) ) {
559 $content = $delta['text'];
560 }
561 else if ( !empty( $delta['text'] ) && is_array( $delta['text'] ) ) {
562 $text = $delta['text'];
563 if ( !empty( $text['annotations'] ) ) {
564 $this->streamAnnotations = array_merge( $this->streamAnnotations, $text['annotations'] );
565 }
566 if ( isset( $text['value'] ) ) {
567 $content = $text['value'];
568 }
569 }
570 else {
571 error_log( 'AI Engine: Unknown text format: ' . json_encode( $delta ) );
572 }
573 break;
574 case 'image':
575 $content = $delta['url'];
576 break;
577 case 'image_file':
578 $fileId = $delta['image_file']['file_id'];
579 $content = '<!-- IMG #' . $fileId . ' -->';
580 $this->streamImageIds[] = $fileId;
581 break;
582 case 'function_call':
583 if ( empty( $this->streamFunctionCall ) ) {
584 $this->streamFunctionCall = [ 'name' => '', 'arguments' => [] ];
585 }
586 $this->streamFunctionCall['name'] = $delta['function_call']['name'] ?? $this->streamFunctionCall['name'];
587 if ( isset( $delta['function_call']['arguments'] ) ) {
588 $args = json_decode( $delta['function_call']['arguments'], true );
589 $this->streamFunctionCall['arguments'] = $args ?? [];
590 }
591 break;
592 case 'tool_call':
593 $tool_call = $delta['tool_call'];
594 $index = $tool_call['index'] ?? null;
595 $currentStreamToolCall = null;
596 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
597 $currentStreamToolCall = &$this->streamToolCalls[$index];
598 }
599 else {
600 $this->streamToolCalls[] = [
601 'id' => null,
602 'type' => null,
603 'function' => [ 'name' => '', 'arguments' => '' ]
604 ];
605 end( $this->streamToolCalls );
606 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
607 }
608 break;
609 }
610 }
611 }
612 else if ( $object === 'thread.run.step' ) {
613 //$type = $json['step'];
614 // Could be tool_calls, means an OpenAI Assistant is doing something.
615 }
616 else {
617 if ( isset( $json['choices'][0]['text'] ) ) {
618 $content = $json['choices'][0]['text'];
619 }
620 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
621 $content = $json['choices'][0]['delta']['content'];
622 }
623 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
624 if ( empty( $this->streamFunctionCall ) ) {
625 $this->streamFunctionCall = [ 'name' => '', 'arguments' => [] ];
626 }
627 $this->streamFunctionCall['name'] = $json['choices'][0]['delta']['function_call']['name'] ?? $this->streamFunctionCall['name'];
628 if ( isset( $json['choices'][0]['delta']['function_call']['arguments'] ) ) {
629 $args = json_decode( $json['choices'][0]['delta']['function_call']['arguments'], true );
630 $this->streamFunctionCall['arguments'] = $args ?? [];
631 }
632 }
633 else if ( isset( $json['choices'][0]['delta']['tool_calls'] ) ) {
634 // New schema detected – drop any half-built legacy call to prevent duplicates
635 $this->streamFunctionCall = null;
636
637 foreach ( $json['choices'][0]['delta']['tool_calls'] as $tool_call ) {
638 $index = $tool_call['index'] ?? null;
639 $currentStreamToolCall = null;
640 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
641 $currentStreamToolCall = &$this->streamToolCalls[$index];
642 }
643 else {
644 $this->streamToolCalls[] = [
645 'id' => null,
646 'type' => null,
647 'function' => [ 'name' => '', 'arguments' => '' ]
648 ];
649 end( $this->streamToolCalls );
650 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
651 }
652 if ( !empty( $tool_call['id'] ) ) {
653 $currentStreamToolCall['id'] = $tool_call['id'];
654 }
655 if ( !empty( $tool_call['type'] ) ) {
656 $currentStreamToolCall['type'] = $tool_call['type'];
657 }
658 if ( isset( $tool_call['function'] ) ) {
659 $function = $tool_call['function'];
660 if ( isset( $function['name'] ) ) {
661 $currentStreamToolCall['function']['name'] .= $function['name'];
662 }
663 if ( isset( $function['arguments'] ) ) {
664 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
665 }
666 }
667 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
668 }
669 }
670 else if ( isset( $json['choices'][0]['delta']['role'] ) ) {
671 $this->streamLastMessage = [
672 'role' => $json['choices'][0]['delta']['role'],
673 'content' => null
674 ];
675 }
676 }
677
678 $usage = $json['usage'] ?? [];
679 if ( isset( $usage['prompt_tokens'], $usage['completion_tokens'] ) ) {
680 $this->streamInTokens = (int) $usage['prompt_tokens'];
681 $this->streamOutTokens = (int) $usage['completion_tokens'];
682
683 if ( isset( $usage['cost'] ) ) {
684 $this->streamCost = (float) $usage['cost'];
685 }
686 }
687
688 // If content is an array, let's try to convert it into a string. Normally, there would be a 'value' key.
689 if ( is_array( $content ) ) {
690 if ( isset( $content['value'] ) ) {
691 $content = $content['value'];
692 }
693 else {
694 throw new Exception( 'AI Engine: Could not read this: ' . json_encode( $content ) );
695 }
696 }
697
698 // Avoid some endings
699 $endings = [ '', '</s>' ];
700 if ( in_array( $content, $endings ) ) {
701 $content = null;
702 }
703
704 return ( $content === '0' || !empty( $content ) ) ? $content : null;
705 }
706
707 public function run( $query, $streamCallback = null, $maxDepth = 5 ) {
708 if ( $streamCallback ) {
709 // Disable streaming only for "o1" (as December 2024, it works for preview and mini)
710 if ( $query->model === 'o1' ) {
711 $streamCallback = null;
712 }
713 }
714 return parent::run( $query, $streamCallback, $maxDepth );
715 }
716
717 public function run_query( $url, $options, $isStream = false ) {
718 try {
719 $options['stream'] = $isStream;
720 if ( $isStream ) {
721 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
722 }
723
724 // Check if queries debug is enabled
725 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
726
727 // Log the request if queries debug is enabled
728 if ( $queries_debug ) {
729 error_log( '[AI Engine Queries] --> Request to: ' . $url );
730
731 if ( isset( $options['body'] ) ) {
732 // This is the actual body being sent to the AI service
733 $body_log = is_string( $options['body'] ) ? $options['body'] : json_encode( $options['body'] );
734
735 // Pretty print JSON if possible
736 $decoded = json_decode( $body_log, true );
737 if ( json_last_error() === JSON_ERROR_NONE ) {
738 error_log( json_encode( $decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
739 }
740 else {
741 error_log( $body_log );
742 }
743 }
744 }
745
746 $res = wp_remote_get( $url, $options );
747
748 if ( is_wp_error( $res ) ) {
749 throw new Exception( $res->get_error_message() );
750 }
751
752 $responseCode = wp_remote_retrieve_response_code( $res );
753 if ( $responseCode === 404 ) {
754 throw new Exception( 'The model\'s API URL was not found: ' . $url );
755 }
756 else if ( $responseCode === 400 ) {
757 $message = wp_remote_retrieve_body( $res );
758 if ( empty( $message ) ) {
759 $message = wp_remote_retrieve_response_message( $res );
760 }
761 if ( empty( $message ) ) {
762 $message = 'Bad Request';
763 }
764 throw new Exception( $message );
765 }
766 else if ( $responseCode === 500 ) {
767 $message = wp_remote_retrieve_body( $res );
768 if ( empty( $message ) ) {
769 $message = wp_remote_retrieve_response_message( $res );
770 }
771 if ( empty( $message ) ) {
772 $message = 'Internal Server Error';
773 }
774 throw new Exception( $message );
775 }
776
777 if ( $isStream ) {
778 return [ 'stream' => true ];
779 }
780
781 $response = wp_remote_retrieve_body( $res );
782 $headersRes = wp_remote_retrieve_headers( $res );
783 $headers = $headersRes->getAll();
784
785 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
786 // If so, we don't need to decode the response
787 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
788 $resContentType = $normalizedHeaders['content-type'] ?? '';
789 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
790 // Log the response if queries debug is enabled
791 if ( $queries_debug && !$isStream ) {
792 error_log( '[AI Engine Queries] Response Headers: ' . json_encode( $headers ) );
793 error_log( '[AI Engine Queries] Response Body (raw): ' . substr( $response, 0, 1000 ) . '...' );
794 }
795 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
796 }
797
798 $data = json_decode( $response, true );
799 $this->handle_response_errors( $data );
800
801 // Log the response if queries debug is enabled
802 if ( $queries_debug && !$isStream ) {
803 // Log the raw response as received from the AI service
804 error_log( '[AI Engine Queries] <-- Response:' );
805
806 // Pretty print JSON if possible
807 if ( json_last_error() === JSON_ERROR_NONE && is_array( $data ) ) {
808 error_log( json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
809 }
810 else {
811 error_log( $response );
812 }
813 }
814
815 return [ 'headers' => $headers, 'data' => $data ];
816 }
817 catch ( Exception $e ) {
818 $service = $this->get_service_name();
819 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
820
821 // Log error response if queries debug is enabled
822 if ( $queries_debug ) {
823 error_log( '[AI Engine Queries] Error occurred: ' . $e->getMessage() );
824 }
825
826 throw $e;
827 }
828 finally {
829 if ( $isStream && file_exists( $options['filename'] ) ) {
830 unlink( $options['filename'] );
831 }
832 }
833 }
834
835 private function get_audio( $url ) {
836 require_once( ABSPATH . 'wp-admin/includes/media.php' );
837
838 // Validate URL scheme to prevent SSRF attacks
839 $parts = wp_parse_url( $url );
840 if ( ! isset( $parts['scheme'] ) || ! in_array( $parts['scheme'], [ 'http', 'https' ], true ) ) {
841 throw new Exception( 'Invalid URL scheme; only HTTP/HTTPS allowed.' );
842 }
843
844 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
845 file_put_contents( $tmpFile, file_get_contents( $url ) );
846 $length = null;
847 $metadata = wp_read_audio_metadata( $tmpFile );
848 if ( isset( $metadata['length'] ) ) {
849 $length = $metadata['length'];
850 }
851 $data = file_get_contents( $tmpFile );
852 unlink( $tmpFile );
853 return [ 'data' => $data, 'length' => $length ];
854 }
855
856 public function run_transcribe_query( $query ) {
857 $audioData = null;
858
859 // Priority 1: Direct audio data
860 if ( !empty( $query->audioData ) ) {
861 $audioData = [
862 'data' => $query->audioData,
863 'length' => strlen( $query->audioData ) / 1024 // KB
864 ];
865 }
866 // Priority 2: File path
867 else if ( !empty( $query->path ) ) {
868 if ( !file_exists( $query->path ) ) {
869 throw new Exception( 'Audio file not found: ' . $query->path );
870 }
871 if ( !is_readable( $query->path ) ) {
872 throw new Exception( 'Audio file is not readable: ' . $query->path );
873 }
874 $audioData = [
875 'data' => file_get_contents( $query->path ),
876 'length' => filesize( $query->path ) / 1024 // KB
877 ];
878 }
879 // Priority 3: Attached file object
880 else if ( $query->attachedFile ) {
881 $audioData = [
882 'data' => $query->attachedFile->get_data(),
883 'length' => strlen( $query->attachedFile->get_data() ) / 1024 // KB
884 ];
885 }
886 // Priority 4: URL (backward compatibility)
887 else if ( !empty( $query->url ) ) {
888 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
889 throw new Exception( 'Invalid URL for transcription.' );
890 }
891 $audioData = $this->get_audio( $query->url );
892 }
893 else {
894 throw new Exception( 'No audio source provided for transcription. Please provide either audioData, path, attachedFile, or url.' );
895 }
896
897 $body = $this->build_body( $query, null, $audioData['data'] );
898 $url = $this->build_url( $query );
899 $headers = $this->build_headers( $query );
900 $options = $this->build_options( $headers, null, $body );
901
902 // Perform the request
903 try {
904 $res = $this->run_query( $url, $options );
905 $data = $res['data'];
906 if ( empty( $data ) ) {
907 throw new Exception( 'Invalid data for transcription.' );
908 }
909 $usage = $this->core->record_audio_usage( $query->model, $audioData['length'] );
910 $reply = new Meow_MWAI_Reply( $query );
911 $reply->set_usage( $usage );
912 $reply->set_choices( $data );
913 return $reply;
914 }
915 catch ( Exception $e ) {
916 $service = $this->get_service_name();
917 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
918 throw new Exception( "$service: " . $e->getMessage() );
919 }
920 }
921
922 public function run_embedding_query( $query ) {
923 $body = $this->build_body( $query );
924 $url = $this->build_url( $query );
925 $headers = $this->build_headers( $query );
926 $options = $this->build_options( $headers, $body );
927
928 try {
929 $res = $this->run_query( $url, $options );
930 $data = $res['data'];
931 if ( empty( $data ) || !isset( $data['data'] ) ) {
932 throw new Exception( 'Invalid data for embedding.' );
933 }
934 $usage = $data['usage'];
935 $this->core->record_tokens_usage( $query->model, $usage['prompt_tokens'] );
936 $reply = new Meow_MWAI_Reply( $query );
937 $reply->set_usage( $usage );
938 $reply->set_choices( $data['data'] );
939 return $reply;
940 }
941 catch ( Exception $e ) {
942 $message = $e->getMessage();
943 $error = $this->try_decode_error( $message );
944 if ( !is_null( $error ) ) {
945 $message = $error;
946 }
947 $service = $this->get_service_name();
948 Meow_MWAI_Logging::error( "$service: " . $message );
949 throw new Exception( "$service: " . $message );
950 }
951 }
952
953 public function try_decode_error( $data ) {
954 $json = json_decode( $data, true );
955 if ( isset( $json['error']['message'] ) ) {
956 return $json['error']['message'];
957 }
958 return null;
959 }
960
961 protected function finalize_choices( $choices, $responseData, $query ) {
962 // Clean up duplicate function calls: prefer tool_calls over legacy function_call
963 foreach ( $choices as &$choice ) {
964 if ( isset( $choice['message'] ) ) {
965 // If we have both tool_calls and function_call, remove function_call
966 if ( isset( $choice['message']['tool_calls'] ) && !empty( $choice['message']['tool_calls'] ) &&
967 isset( $choice['message']['function_call'] ) ) {
968 unset( $choice['message']['function_call'] );
969 }
970 }
971 }
972 return $choices;
973 }
974
975 public function run_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
976 $isStreaming = !is_null( $streamCallback );
977
978 // Initialize debug mode
979 $this->init_debug_mode( $query );
980
981 // Store the callback for event emission (both streaming and non-streaming debug mode)
982 if ( !is_null( $streamCallback ) ) {
983 $this->streamCallback = $streamCallback;
984 }
985
986 if ( $isStreaming ) {
987 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
988 }
989
990 $this->reset_stream();
991 $body = $this->build_body( $query, $streamCallback );
992 $url = $this->build_url( $query );
993 $headers = $this->build_headers( $query );
994 $options = $this->build_options( $headers, $body );
995
996 // Emit "Request sent" event for feedback queries
997 if ( $this->currentDebugMode && !empty( $streamCallback ) &&
998 ( $query instanceof Meow_MWAI_Query_Feedback || $query instanceof Meow_MWAI_Query_AssistFeedback ) ) {
999 $event = Meow_MWAI_Event::request_sent()
1000 ->set_metadata( 'is_feedback', true )
1001 ->set_metadata( 'feedback_count', count( $query->blocks ) );
1002 call_user_func( $streamCallback, $event );
1003 }
1004
1005 try {
1006 $res = $this->run_query( $url, $options, $streamCallback );
1007 $reply = new Meow_MWAI_Reply( $query );
1008
1009 $returned_id = null;
1010 $returned_model = $this->inModel;
1011 $returned_in_tokens = null;
1012 $returned_out_tokens = null;
1013 $returned_price = null;
1014 $returned_choices = [];
1015
1016 // Streaming Mode
1017 if ( $isStreaming ) {
1018 if ( empty( $this->streamContent ) ) {
1019 $error = $this->try_decode_error( $this->streamBuffer );
1020 if ( !is_null( $error ) ) {
1021 throw new Exception( $error );
1022 }
1023 }
1024 $returned_id = $this->inId;
1025 $returned_model = $this->inModel ? $this->inModel : $query->model;
1026 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
1027 // Prefer tool_calls; fall back to legacy only if necessary
1028 if ( !empty( $this->streamToolCalls ) ) {
1029 $message['tool_calls'] = $this->streamToolCalls;
1030 }
1031 elseif ( !empty( $this->streamFunctionCall ) ) {
1032 $message['function_call'] = $this->streamFunctionCall;
1033 }
1034 if ( !is_null( $this->streamInTokens ) ) {
1035 $returned_in_tokens = $this->streamInTokens;
1036 }
1037 if ( !is_null( $this->streamOutTokens ) ) {
1038 $returned_out_tokens = $this->streamOutTokens;
1039 }
1040 if ( !is_null( $this->streamCost ) ) {
1041 $returned_price = $this->streamCost;
1042 }
1043 $returned_choices = [ [ 'message' => $message ] ];
1044 $returned_choices = $this->finalize_choices( $returned_choices, null, $query );
1045
1046 // Log streaming response data if queries debug is enabled
1047 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
1048 if ( $queries_debug ) {
1049 error_log( '[AI Engine Queries] Streaming Response Collected (ChatML):' );
1050 $streaming_data = [
1051 'id' => $returned_id,
1052 'model' => $returned_model,
1053 'content_length' => strlen( $this->streamContent ),
1054 'content_preview' => substr( $this->streamContent, 0, 200 ) . ( strlen( $this->streamContent ) > 200 ? '...' : '' ),
1055 'function_calls' => !empty( $this->streamFunctionCall ) ? '1 function call' : 'none',
1056 'tool_calls' => !empty( $this->streamToolCalls ) ? count( $this->streamToolCalls ) . ' tool calls' : 'none',
1057 'usage' => [
1058 'input_tokens' => $returned_in_tokens,
1059 'output_tokens' => $returned_out_tokens,
1060 'cost' => $returned_price
1061 ]
1062 ];
1063
1064 // Log tool calls details if present
1065 if ( !empty( $this->streamToolCalls ) ) {
1066 $streaming_data['tool_calls_details'] = [];
1067 foreach ( $this->streamToolCalls as $tool_call ) {
1068 $streaming_data['tool_calls_details'][] = [
1069 'id' => $tool_call['id'] ?? 'unknown',
1070 'name' => $tool_call['function']['name'] ?? 'unknown',
1071 'arguments' => substr( $tool_call['function']['arguments'] ?? '{}', 0, 100 ) . '...'
1072 ];
1073 }
1074 }
1075
1076 // Log function call if present
1077 if ( !empty( $this->streamFunctionCall ) ) {
1078 $streaming_data['function_call'] = [
1079 'name' => $this->streamFunctionCall['name'] ?? 'unknown',
1080 'arguments' => substr( $this->streamFunctionCall['arguments'] ?? '{}', 0, 100 ) . '...'
1081 ];
1082 }
1083
1084 error_log( json_encode( $streaming_data, JSON_PRETTY_PRINT ) );
1085 }
1086 }
1087 // Standard Mode
1088 else {
1089 $data = $res['data'];
1090 if ( empty( $data ) ) {
1091 throw new Exception( 'No content received (res is null).' );
1092 }
1093 if ( !$data['model'] ) {
1094 $service = $this->get_service_name();
1095 Meow_MWAI_Logging::error( "$service: Invalid response (no model information)." );
1096 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
1097 throw new Exception( 'Invalid response (no model information).' );
1098 }
1099 $returned_id = $data['id'];
1100 $returned_model = $data['model'];
1101 $usage = $data['usage'] ?? [];
1102 $returned_in_tokens = $usage['prompt_tokens'] ?? null;
1103 $returned_out_tokens = $usage['completion_tokens'] ?? null;
1104 $returned_price = $usage['total_cost'] ?? $usage['cost'] ?? null;
1105 $returned_choices = $data['choices'];
1106 $returned_choices = $this->finalize_choices( $returned_choices, $data, $query );
1107 }
1108
1109 // Set the results.
1110 $reply->set_choices( $returned_choices );
1111 if ( !empty( $returned_id ) ) {
1112 $reply->set_id( $returned_id );
1113 }
1114 if ( !empty( $returned_id ) ) {
1115 $reply->set_id( $returned_id );
1116 }
1117
1118 // Handle tokens.
1119 $this->handle_tokens_usage(
1120 $reply,
1121 $query,
1122 $returned_model,
1123 $returned_in_tokens,
1124 $returned_out_tokens,
1125 $returned_price
1126 );
1127
1128 return $reply;
1129 }
1130 catch ( Exception $e ) {
1131 $service = $this->get_service_name();
1132 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1133 $message = "$service: " . $e->getMessage();
1134 throw new Exception( $message );
1135 }
1136 finally {
1137 if ( !is_null( $streamCallback ) ) {
1138 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1139 }
1140 }
1141 }
1142
1143 public function handle_tokens_usage(
1144 $reply,
1145 $query,
1146 $returned_model,
1147 $returned_in_tokens,
1148 $returned_out_tokens,
1149 $returned_price = null
1150 ) {
1151 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens :
1152 $reply->get_in_tokens( $query );
1153 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens :
1154 $reply->get_out_tokens();
1155 $returned_price = !is_null( $returned_price ) ? $returned_price :
1156 $reply->get_price();
1157 $usage = $this->core->record_tokens_usage(
1158 $returned_model,
1159 $returned_in_tokens,
1160 $returned_out_tokens,
1161 $returned_price
1162 );
1163 $reply->set_usage( $usage );
1164
1165 // Set default accuracy to 'estimated' for engines that don't override
1166 // Most engines (Google, Anthropic, etc.) estimate tokens and calculate price
1167 $reply->set_usage_accuracy( 'estimated' );
1168 }
1169
1170 // Request to DALL-E API
1171 public function run_image_query( $query ) {
1172 $body = $this->build_body( $query );
1173 $url = $this->build_url( $query );
1174 $headers = $this->build_headers( $query );
1175 $options = $this->build_options( $headers, $body );
1176
1177 try {
1178 $res = $this->run_query( $url, $options );
1179 $data = $res['data'];
1180 $choices = [];
1181 $choices = $data['data'];
1182 $reply = new Meow_MWAI_Reply( $query );
1183 $model = $query->model;
1184 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
1185 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
1186 $reply->set_usage( $usage );
1187 $reply->set_usage_accuracy( 'estimated' ); // Image generation always uses estimated pricing
1188 $reply->set_choices( $choices );
1189 $reply->set_type( 'images' );
1190
1191 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
1192 foreach ( $reply->results as &$result ) {
1193 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
1194 'query_envId' => $query->envId,
1195 'query_session' => $query->session,
1196 'query_model' => $query->model,
1197 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
1198 $fileUrl = $this->core->files->get_url( $fileId );
1199 $result = $fileUrl;
1200 }
1201 }
1202 $reply->result = $reply->results[0];
1203 return $reply;
1204 }
1205 catch ( Exception $e ) {
1206 $service = $this->get_service_name();
1207 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1208 throw new Exception( "$service: " . $e->getMessage() );
1209 }
1210 }
1211
1212 public function run_editimage_query( $query ) {
1213 if ( empty( $query->attachedFile ) ) {
1214 throw new Exception( 'No image provided for editing.' );
1215 }
1216 // Ensure the model supports image editing
1217 $modelInfo = $this->retrieve_model_info( $query->model );
1218 if ( empty( $modelInfo['tags'] ) || !in_array( 'image-edit', $modelInfo['tags'] ) ) {
1219 throw new Exception( 'The model ' . $query->model . ' does not support image editing.' );
1220 }
1221 $imageData = $query->attachedFile->get_data();
1222 $body = $this->build_body( $query, null, $imageData );
1223 $url = $this->build_url( $query );
1224 $headers = $this->build_headers( $query );
1225 $options = $this->build_options( $headers, null, $body );
1226
1227 try {
1228 $res = $this->run_query( $url, $options );
1229 $data = $res['data'];
1230 $choices = $data['data'];
1231 $reply = new Meow_MWAI_Reply( $query );
1232 $model = $query->model;
1233 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
1234 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
1235 $reply->set_usage( $usage );
1236 $reply->set_usage_accuracy( 'estimated' ); // Image generation always uses estimated pricing
1237 $reply->set_choices( $choices );
1238 $reply->set_type( 'images' );
1239
1240 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
1241 foreach ( $reply->results as &$result ) {
1242 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
1243 'query_envId' => $query->envId,
1244 'query_session' => $query->session,
1245 'query_model' => $query->model,
1246 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
1247 $fileUrl = $this->core->files->get_url( $fileId );
1248 $result = $fileUrl;
1249 }
1250 }
1251 $reply->result = $reply->results[0];
1252 return $reply;
1253 }
1254 catch ( Exception $e ) {
1255 $service = $this->get_service_name();
1256 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1257 throw new Exception( "$service: " . $e->getMessage() );
1258 }
1259 }
1260
1261 /*
1262 This is the rest of the OpenAI API support, not related to the models directly.
1263 */
1264
1265 // Check if there are errors in the response from OpenAI, and throw an exception if so.
1266 protected function handle_response_errors( $data ) {
1267 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
1268 $message = $data['error']['message'];
1269 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
1270 $message = str_replace( $matches[1], '', $message );
1271 }
1272 throw new Exception( $message );
1273 }
1274 }
1275
1276 public function list_files( $purposeFilter = null ) {
1277 if ( empty( $purposeFilter ) ) {
1278 return $this->execute( 'GET', '/files' );
1279 }
1280 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
1281 }
1282
1283 public static function get_suffix_for_model( $model ) {
1284 // Legacy fine-tuned models
1285 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches );
1286 if ( count( $matches ) > 0 ) {
1287 return $matches[1];
1288 }
1289
1290 // New fine-tuned models
1291 preg_match( '/:([^:]+)(?=:[^:]+$)/', $model, $matches );
1292 if ( count( $matches ) > 0 ) {
1293 return $matches[1];
1294 }
1295
1296 return 'N/A';
1297 }
1298
1299 public static function get_model_without_release_date( $model ) {
1300 if ( empty( $model ) ) {
1301 return null;
1302 }
1303 return preg_replace( '/-\d{4}-\d{2}-\d{2}$/', '', $model );
1304 }
1305
1306 public function list_deleted_finetunes( $envId = null, $legacy = false ) {
1307 $finetunes = $this->list_finetunes( $legacy );
1308 $deleted = [];
1309
1310 foreach ( $finetunes as $finetune ) {
1311 $name = $finetune['model'];
1312 $isSucceeded = $finetune['status'] === 'succeeded';
1313 if ( $isSucceeded ) {
1314 try {
1315 $finetune = $this->get_model( $name );
1316 }
1317 catch ( Exception $e ) {
1318 $deleted[] = $name;
1319 }
1320 }
1321 }
1322 if ( $legacy ) {
1323 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
1324 }
1325 else {
1326 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
1327 }
1328 return $deleted;
1329 }
1330
1331 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
1332 // retrieve all the models since Summer 2023, let's see! WIP.
1333 public function list_finetunes( $legacy = false ) {
1334 if ( $legacy ) {
1335 $res = $this->execute( 'GET', '/fine-tunes' );
1336 }
1337 else {
1338 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
1339 }
1340 $finetunes = $res['data'];
1341
1342 // Add suffix
1343 $finetunes = array_map( function ( $finetune ) {
1344 if ( isset( $finetune['user_provided_suffix'] ) ) {
1345 $finetune['suffix'] = $finetune['user_provided_suffix'];
1346 }
1347 else {
1348 $finetune['suffix'] = self::get_suffix_for_model( $finetune['fine_tuned_model'] );
1349 }
1350 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] ) . ' UTC';
1351 if ( isset( $finetune['estimated_finish'] ) ) {
1352 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] ) . ' UTC';
1353 }
1354 else {
1355 $finetune['estimatedOn'] = null;
1356 }
1357 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
1358 $finetune['base_model'] = $finetune['model'];
1359 $finetune['model'] = $finetune['fine_tuned_model'];
1360 unset( $finetune['object'] );
1361 unset( $finetune['hyperparams'] );
1362 unset( $finetune['result_files'] );
1363 unset( $finetune['training_files'] );
1364 unset( $finetune['validation_files'] );
1365 unset( $finetune['created_at'] );
1366 unset( $finetune['updated_at'] );
1367 unset( $finetune['fine_tuned_model'] );
1368 return $finetune;
1369 }, $finetunes );
1370
1371 usort( $finetunes, function ( $a, $b ) {
1372 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
1373 } );
1374
1375 if ( $legacy ) {
1376 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
1377 }
1378 else {
1379 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
1380 }
1381
1382 return $finetunes;
1383 }
1384
1385 public function moderate( $input ) {
1386 $result = $this->execute( 'POST', '/moderations', [
1387 'input' => $input
1388 ] );
1389 return $result;
1390 }
1391
1392 public function upload_file( $filename, $data, $purpose = 'fine-tune' ) {
1393 $result = $this->execute( 'POST', '/files', null, [
1394 'purpose' => $purpose,
1395 'data' => $data,
1396 'file' => $filename
1397 ] );
1398 return $result;
1399 }
1400
1401 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
1402 $body = [
1403 'name' => !empty( $name ) ? $name : 'default',
1404 'metadata' => $metadata
1405 ];
1406 if ( $expiry !== 'never' ) {
1407 if ( is_string( $expiry ) ) {
1408 error_log( 'AI Engine: Expiry is a string, setting it to 7 days.' );
1409 $expiry = 7;
1410 }
1411 $expiryInDays = $expiry ? max( 1, ceil( (int) $expiry / 86400 ) ) : 7;
1412 if ( $expiry && is_numeric( $expiry ) ) {
1413 $body['expires_after'] = [
1414 'anchor' => 'last_active_at',
1415 'days' => $expiryInDays
1416 ];
1417 }
1418 }
1419 $result = $this->execute( 'POST', '/vector_stores', $body, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1420 return $result['id'];
1421 }
1422
1423 public function get_vector_store( $vectorStoreId ) {
1424 return $this->execute( 'GET', '/vector_stores/' . $vectorStoreId, null, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1425 }
1426
1427 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1428 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1429 'file_id' => $fileId
1430 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1431 return $result['id'];
1432
1433 }
1434
1435 public function delete_file( $fileId ) {
1436 return $this->execute( 'DELETE', '/files/' . $fileId );
1437 }
1438
1439 public function get_model( $modelId ) {
1440 return $this->execute( 'GET', '/models/' . $modelId );
1441 }
1442
1443 public function cancel_finetune( $fineTuneId ) {
1444 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1445 }
1446
1447 public function delete_finetune( $modelId ) {
1448 return $this->execute( 'DELETE', '/models/' . $modelId );
1449 }
1450
1451 public function download_file( $fileId, $newFile = null ) {
1452 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1453 $fileInfo = json_decode( (string) $fileInfo, true );
1454 if ( empty( $fileInfo ) ) {
1455 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1456 }
1457 $filename = $fileInfo['filename'];
1458 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1459 if ( empty( $newFile ) ) {
1460 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1461 $tempFile = wp_tempnam( $filename );
1462 if ( !$tempFile ) {
1463 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1464 }
1465 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1466 $newFile = $tempFile . '.' . $extension;
1467 }
1468 else {
1469 $newFile = $tempFile;
1470 }
1471 }
1472 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1473 file_put_contents( $newFile, $data );
1474 return $newFile;
1475 }
1476
1477 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false ) {
1478 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int) $hyperparams['nEpochs'] : null;
1479 $batch_size = isset( $hyperparams['batchSize'] ) ? (int) $hyperparams['batchSize'] : null;
1480 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1481 (float) $hyperparams['learningRateMultiplier'] : null;
1482 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1483 (float) $hyperparams['promptLossWeight'] : null;
1484 $arguments = [
1485 'training_file' => $fileId,
1486 'model' => $model,
1487 'suffix' => $suffix
1488 ];
1489 if ( $legacy ) {
1490 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1491 }
1492 else {
1493 if ( $n_epochs ) {
1494 $arguments['hyperparams'] = [];
1495 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1496 }
1497 if ( $batch_size ) {
1498 if ( empty( $arguments['hyperparams'] ) ) {
1499 $arguments['hyperparams'] = [];
1500 }
1501 $arguments['hyperparams']['batch_size'] = $batch_size;
1502 }
1503 if ( $learning_rate_multiplier ) {
1504 if ( empty( $arguments['hyperparams'] ) ) {
1505 $arguments['hyperparams'] = [];
1506 }
1507 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1508 }
1509 if ( $prompt_loss_weight ) {
1510 if ( empty( $arguments['hyperparams'] ) ) {
1511 $arguments['hyperparams'] = [];
1512 }
1513 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1514 }
1515 if ( $model === 'turbo' ) {
1516 $arguments['model'] = 'gpt-3.5-turbo';
1517 }
1518 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1519 }
1520 return $result;
1521 }
1522
1523 /**
1524 * Build the body of a form request.
1525 * If the field name is 'file', then the field value is the filename of the file to upload.
1526 * The file contents are taken from the 'data' field.
1527 *
1528 * @param array $fields
1529 * @param string $boundary
1530 * @return string
1531 */
1532 public function build_form_body( $fields, $boundary ) {
1533 $body = '';
1534 foreach ( $fields as $name => $value ) {
1535 if ( in_array( $name, [ 'data', 'mime', 'mask_data', 'mask_mime' ] ) ) {
1536 continue;
1537 }
1538 $body .= "--$boundary\r\n";
1539 $body .= "Content-Disposition: form-data; name=\"$name\"";
1540 if ( $name === 'image' || $name === 'file' ) {
1541 $body .= "; filename=\"{$value}\"\r\n";
1542 $mime = !empty( $fields['mime'] ) ? $fields['mime'] : 'application/octet-stream';
1543 $body .= "Content-Type: {$mime}\r\n\r\n";
1544 $body .= $fields['data'] . "\r\n";
1545 }
1546 else if ( $name === 'mask' ) {
1547 $body .= "; filename=\"{$value}\"\r\n";
1548 $mime = !empty( $fields['mask_mime'] ) ? $fields['mask_mime'] : 'application/octet-stream';
1549 $body .= "Content-Type: {$mime}\r\n\r\n";
1550 $body .= $fields['mask_data'] . "\r\n";
1551 }
1552 else {
1553 $body .= "\r\n\r\n$value\r\n";
1554 }
1555 }
1556 $body .= "--$boundary--\r\n";
1557 return $body;
1558 }
1559
1560 /**
1561 * Run a request to the OpenAI API.
1562 * Fore more information about the $formFields, refer to the build_form_body method.
1563 *
1564 * @param string $method POST, PUT, GET, DELETE...
1565 * @param string $url The API endpoint
1566 * @param array $query The query parameters (json)
1567 * @param array $formFields The form fields (multipart/form-data)
1568 * @param bool $json Whether to return the response as json or not
1569 * @return array
1570 */
1571 public function execute(
1572 $method,
1573 $url,
1574 $query = null,
1575 $formFields = null,
1576 $json = true,
1577 $extraHeaders = null,
1578 $streamCallback = null
1579 ) {
1580 $isAzure = $this->envType === 'azure';
1581 $isOpenAI = !$isAzure;
1582
1583 // Prepare the headers
1584 $headers = "Content-Type: application/json\r\n";
1585 if ( $isOpenAI ) {
1586 $headers .= 'Authorization: Bearer ' . $this->apiKey . "\r\n";
1587 if ( $this->organizationId ) {
1588 $headers .= 'OpenAI-Organization: ' . $this->organizationId . "\r\n";
1589 }
1590 }
1591 else if ( $isAzure ) {
1592 $headers .= 'api-key: ' . $this->apiKey . "\r\n";
1593 }
1594
1595 // Prepare the body with json_encode, if it's not a string or null, otherwise we keep it as is.
1596 if ( !empty( $query ) && !is_string( $query ) ) {
1597 $body = json_encode( $query );
1598 }
1599 else {
1600 $body = $query;
1601 }
1602
1603 // If we have form fields, we need to change the headers and the body.
1604 if ( !empty( $formFields ) ) {
1605 $boundary = wp_generate_password( 24, false );
1606 $headers = [
1607 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
1608 ];
1609 if ( $isOpenAI ) {
1610 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
1611 if ( $this->organizationId ) {
1612 $headers['OpenAI-Organization'] = $this->organizationId;
1613 }
1614 }
1615 else if ( $isAzure ) {
1616 $headers['api-key'] = $this->apiKey;
1617 }
1618 $body = $this->build_form_body( $formFields, $boundary );
1619 }
1620
1621 // Maybe we should have headers always as an array... not sure why we have it as a string.
1622 if ( !empty( $extraHeaders ) ) {
1623 foreach ( $extraHeaders as $key => $value ) {
1624 if ( is_array( $headers ) ) {
1625 $headers[$key] = $value;
1626 }
1627 else {
1628 $headers .= "$key: $value\r\n";
1629 }
1630 }
1631 }
1632
1633 // Create the URL
1634 if ( $isOpenAI ) {
1635 $url = 'https://api.openai.com/v1' . $url;
1636 }
1637 else if ( $isAzure ) {
1638 $url = trailingslashit( $this->env['endpoint'] ) . 'openai' . $url;
1639 $hasQuery = strpos( $url, '?' ) !== false;
1640 $url = $url . ( $hasQuery ? '&' : '?' ) . $this->azureApiVersion;
1641 }
1642
1643 // If it's a GET, body should be null, and we should append the query to the URL.
1644 if ( $method === 'GET' ) {
1645 if ( !empty( $query ) ) {
1646 $hasQuery = strpos( $url, '?' ) !== false;
1647 $url = $url . ( $hasQuery ? '&' : '?' ) . http_build_query( $query );
1648 }
1649 $body = null;
1650 }
1651
1652 $options = [
1653 'headers' => $headers,
1654 'method' => $method,
1655 'timeout' => MWAI_TIMEOUT,
1656 'body' => $body,
1657 'sslverify' => false
1658 ];
1659
1660 // Check if queries debug is enabled
1661 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
1662
1663 // Log the request if queries debug is enabled
1664 if ( $queries_debug ) {
1665 error_log( '[AI Engine Queries] HTTP Request to: ' . $url );
1666
1667 if ( !empty( $body ) ) {
1668 error_log( '[AI Engine Queries] Request Body:' );
1669 error_log( $body );
1670 }
1671
1672 if ( !is_null( $streamCallback ) ) {
1673 error_log( '[AI Engine Queries] (Streaming mode - response will be streamed)' );
1674 }
1675 }
1676
1677 try {
1678 if ( !is_null( $streamCallback ) ) {
1679 $options['stream'] = true;
1680 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1681 // The stream handler calls the streamCallback every time there is content
1682 // TODO: For assistants, we should probably have a different stream handler to
1683 // handle the assistant's specific reply and perform the necessary actions.
1684 $this->streamCallback = $streamCallback;
1685 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1686 }
1687 $res = wp_remote_request( $url, $options );
1688 if ( is_wp_error( $res ) ) {
1689 throw new Exception( $res->get_error_message() );
1690 }
1691 $res = wp_remote_retrieve_body( $res );
1692 $data = $json ? json_decode( $res, true ) : $res;
1693 $this->handle_response_errors( $data );
1694
1695 // Log the response if queries debug is enabled
1696 if ( $queries_debug && is_null( $streamCallback ) ) {
1697 error_log( '[AI Engine Queries] Response Body:' );
1698 error_log( $res );
1699 }
1700 return $data;
1701 }
1702 catch ( Exception $e ) {
1703 $service = $this->get_service_name();
1704 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1705 throw new Exception( "$service: " . $e->getMessage() );
1706 }
1707 finally {
1708 if ( !is_null( $streamCallback ) ) {
1709 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1710 }
1711 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1712 unlink( $options['filename'] );
1713 }
1714 }
1715 }
1716
1717 public function get_models() {
1718 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1719 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1720 foreach ( $finetunes as $finetune ) {
1721 if ( $finetune['status'] !== 'succeeded' ) {
1722 continue;
1723 }
1724 $baseModel = self::get_model_without_release_date( $finetune['base_model'] );
1725 if ( !empty( $baseModel ) ) {
1726 $model = null;
1727 foreach ( $models as $currentModel ) {
1728 if ( $currentModel['model'] === $baseModel ) {
1729 $model = $currentModel;
1730 break;
1731 }
1732 }
1733 if ( !empty( $model ) ) {
1734 $model['model'] = $finetune['model'];
1735 $model['name'] = $finetune['suffix'];
1736 $models[] = $model;
1737 }
1738 }
1739 }
1740 return $models;
1741 }
1742
1743 public static function get_models_static() {
1744 return MWAI_OPENAI_MODELS;
1745 }
1746
1747 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false ) {
1748 $modelFamily = self::get_model_without_release_date( $modelFamily );
1749 $models = $this->get_models();
1750 foreach ( $models as $currentModel ) {
1751 if ( $currentModel['model'] === $modelFamily ) {
1752 if ( $currentModel['type'] === 'image' ) {
1753 if ( !$resolution ) {
1754 Meow_MWAI_Logging::warn( '(OpenAI) Image models require a resolution.' );
1755 return null;
1756 }
1757 else {
1758 foreach ( $currentModel['resolutions'] as $r ) {
1759 if ( $r['name'] == $resolution ) {
1760 return $r['price'] * $outUnits;
1761 }
1762 }
1763 }
1764 }
1765 else {
1766 if ( $finetune ) {
1767 if ( isset( $currentModel['finetune']['price'] ) ) {
1768 $currentModel['price'] = $currentModel['finetune']['price'];
1769 }
1770 else if ( isset( $currentModel['finetune']['in'] ) ) {
1771 $currentModel['price'] = [
1772 'in' => $currentModel['finetune']['in'],
1773 'out' => $currentModel['finetune']['out']
1774 ];
1775 }
1776 }
1777 $inPrice = $currentModel['price'];
1778 $outPrice = $currentModel['price'];
1779 if ( is_array( $currentModel['price'] ) ) {
1780 $inPrice = $currentModel['price']['in'];
1781 $outPrice = $currentModel['price']['out'];
1782 }
1783 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1784 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1785 return $inTotalPrice + $outTotalPrice;
1786 }
1787 }
1788 }
1789 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1790 return null;
1791 }
1792
1793 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
1794 $model = $query->model;
1795 $units = 0;
1796 $finetune = false;
1797 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1798 if ( preg_match( '/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1799 $finetune = true;
1800 }
1801 $inUnits = $reply->get_in_tokens( $query );
1802 $outUnits = $reply->get_out_tokens();
1803 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1804 }
1805 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) || is_a( $query, 'Meow_MWAI_Query_EditImage' ) ) {
1806 $units = $query->maxResults;
1807 $resolution = $query->resolution;
1808 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1809 }
1810 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1811 $model = 'whisper';
1812 $units = $reply->get_units();
1813 return $this->calculate_price( $model, 0, $units, null, $finetune );
1814 }
1815 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1816 $units = $reply->get_total_tokens();
1817 return $this->calculate_price( $model, 0, $units, null, $finetune );
1818 }
1819 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1820 return null;
1821 }
1822 }
1823