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