PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.8.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.8.8
3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / 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
1773 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 // Set default accuracy to 'estimated' for engines that don't override
1116 // Most engines (Google, Anthropic, etc.) estimate tokens and calculate price
1117 $reply->set_usage_accuracy( 'estimated' );
1118 }
1119
1120 // Request to DALL-E API
1121 public function run_image_query( $query ) {
1122 $body = $this->build_body( $query );
1123 $url = $this->build_url( $query );
1124 $headers = $this->build_headers( $query );
1125 $options = $this->build_options( $headers, $body );
1126
1127 try {
1128 $res = $this->run_query( $url, $options );
1129 $data = $res['data'];
1130 $choices = [];
1131 $choices = $data['data'];
1132 $reply = new Meow_MWAI_Reply( $query );
1133 $model = $query->model;
1134 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
1135 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
1136 $reply->set_usage( $usage );
1137 $reply->set_usage_accuracy( 'estimated' ); // Image generation always uses estimated pricing
1138 $reply->set_choices( $choices );
1139 $reply->set_type( 'images' );
1140
1141 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
1142 foreach ( $reply->results as &$result ) {
1143 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
1144 'query_envId' => $query->envId,
1145 'query_session' => $query->session,
1146 'query_model' => $query->model,
1147 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
1148 $fileUrl = $this->core->files->get_url( $fileId );
1149 $result = $fileUrl;
1150 }
1151 }
1152 $reply->result = $reply->results[0];
1153 return $reply;
1154 }
1155 catch ( Exception $e ) {
1156 $service = $this->get_service_name();
1157 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1158 throw new Exception( "$service: " . $e->getMessage() );
1159 }
1160 }
1161
1162 public function run_editimage_query( $query ) {
1163 if ( empty( $query->attachedFile ) ) {
1164 throw new Exception( 'No image provided for editing.' );
1165 }
1166 // Ensure the model supports image editing
1167 $modelInfo = $this->retrieve_model_info( $query->model );
1168 if ( empty( $modelInfo['tags'] ) || !in_array( 'image-edit', $modelInfo['tags'] ) ) {
1169 throw new Exception( 'The model ' . $query->model . ' does not support image editing.' );
1170 }
1171 $imageData = $query->attachedFile->get_data();
1172 $body = $this->build_body( $query, null, $imageData );
1173 $url = $this->build_url( $query );
1174 $headers = $this->build_headers( $query );
1175 $options = $this->build_options( $headers, null, $body );
1176
1177 try {
1178 $res = $this->run_query( $url, $options );
1179 $data = $res['data'];
1180 $choices = $data['data'];
1181 $reply = new Meow_MWAI_Reply( $query );
1182 $model = $query->model;
1183 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
1184 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
1185 $reply->set_usage( $usage );
1186 $reply->set_usage_accuracy( 'estimated' ); // Image generation always uses estimated pricing
1187 $reply->set_choices( $choices );
1188 $reply->set_type( 'images' );
1189
1190 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
1191 foreach ( $reply->results as &$result ) {
1192 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
1193 'query_envId' => $query->envId,
1194 'query_session' => $query->session,
1195 'query_model' => $query->model,
1196 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
1197 $fileUrl = $this->core->files->get_url( $fileId );
1198 $result = $fileUrl;
1199 }
1200 }
1201 $reply->result = $reply->results[0];
1202 return $reply;
1203 }
1204 catch ( Exception $e ) {
1205 $service = $this->get_service_name();
1206 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1207 throw new Exception( "$service: " . $e->getMessage() );
1208 }
1209 }
1210
1211 /*
1212 This is the rest of the OpenAI API support, not related to the models directly.
1213 */
1214
1215 // Check if there are errors in the response from OpenAI, and throw an exception if so.
1216 protected function handle_response_errors( $data ) {
1217 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
1218 $message = $data['error']['message'];
1219 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
1220 $message = str_replace( $matches[1], '', $message );
1221 }
1222 throw new Exception( $message );
1223 }
1224 }
1225
1226 public function list_files( $purposeFilter = null ) {
1227 if ( empty( $purposeFilter ) ) {
1228 return $this->execute( 'GET', '/files' );
1229 }
1230 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
1231 }
1232
1233 public static function get_suffix_for_model( $model ) {
1234 // Legacy fine-tuned models
1235 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches );
1236 if ( count( $matches ) > 0 ) {
1237 return $matches[1];
1238 }
1239
1240 // New fine-tuned models
1241 preg_match( '/:([^:]+)(?=:[^:]+$)/', $model, $matches );
1242 if ( count( $matches ) > 0 ) {
1243 return $matches[1];
1244 }
1245
1246 return 'N/A';
1247 }
1248
1249 public static function get_model_without_release_date( $model ) {
1250 if ( empty( $model ) ) {
1251 return null;
1252 }
1253 return preg_replace( '/-\d{4}-\d{2}-\d{2}$/', '', $model );
1254 }
1255
1256 public function list_deleted_finetunes( $envId = null, $legacy = false ) {
1257 $finetunes = $this->list_finetunes( $legacy );
1258 $deleted = [];
1259
1260 foreach ( $finetunes as $finetune ) {
1261 $name = $finetune['model'];
1262 $isSucceeded = $finetune['status'] === 'succeeded';
1263 if ( $isSucceeded ) {
1264 try {
1265 $finetune = $this->get_model( $name );
1266 }
1267 catch ( Exception $e ) {
1268 $deleted[] = $name;
1269 }
1270 }
1271 }
1272 if ( $legacy ) {
1273 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
1274 }
1275 else {
1276 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
1277 }
1278 return $deleted;
1279 }
1280
1281 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
1282 // retrieve all the models since Summer 2023, let's see! WIP.
1283 public function list_finetunes( $legacy = false ) {
1284 if ( $legacy ) {
1285 $res = $this->execute( 'GET', '/fine-tunes' );
1286 }
1287 else {
1288 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
1289 }
1290 $finetunes = $res['data'];
1291
1292 // Add suffix
1293 $finetunes = array_map( function ( $finetune ) {
1294 if ( isset( $finetune['user_provided_suffix'] ) ) {
1295 $finetune['suffix'] = $finetune['user_provided_suffix'];
1296 }
1297 else {
1298 $finetune['suffix'] = self::get_suffix_for_model( $finetune['fine_tuned_model'] );
1299 }
1300 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] ) . ' UTC';
1301 if ( isset( $finetune['estimated_finish'] ) ) {
1302 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] ) . ' UTC';
1303 }
1304 else {
1305 $finetune['estimatedOn'] = null;
1306 }
1307 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
1308 $finetune['base_model'] = $finetune['model'];
1309 $finetune['model'] = $finetune['fine_tuned_model'];
1310 unset( $finetune['object'] );
1311 unset( $finetune['hyperparams'] );
1312 unset( $finetune['result_files'] );
1313 unset( $finetune['training_files'] );
1314 unset( $finetune['validation_files'] );
1315 unset( $finetune['created_at'] );
1316 unset( $finetune['updated_at'] );
1317 unset( $finetune['fine_tuned_model'] );
1318 return $finetune;
1319 }, $finetunes );
1320
1321 usort( $finetunes, function ( $a, $b ) {
1322 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
1323 } );
1324
1325 if ( $legacy ) {
1326 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
1327 }
1328 else {
1329 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
1330 }
1331
1332 return $finetunes;
1333 }
1334
1335 public function moderate( $input ) {
1336 $result = $this->execute( 'POST', '/moderations', [
1337 'input' => $input
1338 ] );
1339 return $result;
1340 }
1341
1342 public function upload_file( $filename, $data, $purpose = 'fine-tune' ) {
1343 $result = $this->execute( 'POST', '/files', null, [
1344 'purpose' => $purpose,
1345 'data' => $data,
1346 'file' => $filename
1347 ] );
1348 return $result;
1349 }
1350
1351 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
1352 $body = [
1353 'name' => !empty( $name ) ? $name : 'default',
1354 'metadata' => $metadata
1355 ];
1356 if ( $expiry !== 'never' ) {
1357 if ( is_string( $expiry ) ) {
1358 error_log( 'AI Engine: Expiry is a string, setting it to 7 days.' );
1359 $expiry = 7;
1360 }
1361 $expiryInDays = $expiry ? max( 1, ceil( (int) $expiry / 86400 ) ) : 7;
1362 if ( $expiry && is_numeric( $expiry ) ) {
1363 $body['expires_after'] = [
1364 'anchor' => 'last_active_at',
1365 'days' => $expiryInDays
1366 ];
1367 }
1368 }
1369 $result = $this->execute( 'POST', '/vector_stores', $body, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1370 return $result['id'];
1371 }
1372
1373 public function get_vector_store( $vectorStoreId ) {
1374 return $this->execute( 'GET', '/vector_stores/' . $vectorStoreId, null, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1375 }
1376
1377 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1378 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1379 'file_id' => $fileId
1380 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1381 return $result['id'];
1382
1383 }
1384
1385 public function delete_file( $fileId ) {
1386 return $this->execute( 'DELETE', '/files/' . $fileId );
1387 }
1388
1389 public function get_model( $modelId ) {
1390 return $this->execute( 'GET', '/models/' . $modelId );
1391 }
1392
1393 public function cancel_finetune( $fineTuneId ) {
1394 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1395 }
1396
1397 public function delete_finetune( $modelId ) {
1398 return $this->execute( 'DELETE', '/models/' . $modelId );
1399 }
1400
1401 public function download_file( $fileId, $newFile = null ) {
1402 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1403 $fileInfo = json_decode( (string) $fileInfo, true );
1404 if ( empty( $fileInfo ) ) {
1405 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1406 }
1407 $filename = $fileInfo['filename'];
1408 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1409 if ( empty( $newFile ) ) {
1410 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1411 $tempFile = wp_tempnam( $filename );
1412 if ( !$tempFile ) {
1413 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1414 }
1415 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1416 $newFile = $tempFile . '.' . $extension;
1417 }
1418 else {
1419 $newFile = $tempFile;
1420 }
1421 }
1422 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1423 file_put_contents( $newFile, $data );
1424 return $newFile;
1425 }
1426
1427 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false ) {
1428 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int) $hyperparams['nEpochs'] : null;
1429 $batch_size = isset( $hyperparams['batchSize'] ) ? (int) $hyperparams['batchSize'] : null;
1430 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1431 (float) $hyperparams['learningRateMultiplier'] : null;
1432 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1433 (float) $hyperparams['promptLossWeight'] : null;
1434 $arguments = [
1435 'training_file' => $fileId,
1436 'model' => $model,
1437 'suffix' => $suffix
1438 ];
1439 if ( $legacy ) {
1440 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1441 }
1442 else {
1443 if ( $n_epochs ) {
1444 $arguments['hyperparams'] = [];
1445 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1446 }
1447 if ( $batch_size ) {
1448 if ( empty( $arguments['hyperparams'] ) ) {
1449 $arguments['hyperparams'] = [];
1450 }
1451 $arguments['hyperparams']['batch_size'] = $batch_size;
1452 }
1453 if ( $learning_rate_multiplier ) {
1454 if ( empty( $arguments['hyperparams'] ) ) {
1455 $arguments['hyperparams'] = [];
1456 }
1457 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1458 }
1459 if ( $prompt_loss_weight ) {
1460 if ( empty( $arguments['hyperparams'] ) ) {
1461 $arguments['hyperparams'] = [];
1462 }
1463 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1464 }
1465 if ( $model === 'turbo' ) {
1466 $arguments['model'] = 'gpt-3.5-turbo';
1467 }
1468 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1469 }
1470 return $result;
1471 }
1472
1473 /**
1474 * Build the body of a form request.
1475 * If the field name is 'file', then the field value is the filename of the file to upload.
1476 * The file contents are taken from the 'data' field.
1477 *
1478 * @param array $fields
1479 * @param string $boundary
1480 * @return string
1481 */
1482 public function build_form_body( $fields, $boundary ) {
1483 $body = '';
1484 foreach ( $fields as $name => $value ) {
1485 if ( in_array( $name, [ 'data', 'mime', 'mask_data', 'mask_mime' ] ) ) {
1486 continue;
1487 }
1488 $body .= "--$boundary\r\n";
1489 $body .= "Content-Disposition: form-data; name=\"$name\"";
1490 if ( $name === 'image' || $name === 'file' ) {
1491 $body .= "; filename=\"{$value}\"\r\n";
1492 $mime = !empty( $fields['mime'] ) ? $fields['mime'] : 'application/octet-stream';
1493 $body .= "Content-Type: {$mime}\r\n\r\n";
1494 $body .= $fields['data'] . "\r\n";
1495 }
1496 else if ( $name === 'mask' ) {
1497 $body .= "; filename=\"{$value}\"\r\n";
1498 $mime = !empty( $fields['mask_mime'] ) ? $fields['mask_mime'] : 'application/octet-stream';
1499 $body .= "Content-Type: {$mime}\r\n\r\n";
1500 $body .= $fields['mask_data'] . "\r\n";
1501 }
1502 else {
1503 $body .= "\r\n\r\n$value\r\n";
1504 }
1505 }
1506 $body .= "--$boundary--\r\n";
1507 return $body;
1508 }
1509
1510 /**
1511 * Run a request to the OpenAI API.
1512 * Fore more information about the $formFields, refer to the build_form_body method.
1513 *
1514 * @param string $method POST, PUT, GET, DELETE...
1515 * @param string $url The API endpoint
1516 * @param array $query The query parameters (json)
1517 * @param array $formFields The form fields (multipart/form-data)
1518 * @param bool $json Whether to return the response as json or not
1519 * @return array
1520 */
1521 public function execute(
1522 $method,
1523 $url,
1524 $query = null,
1525 $formFields = null,
1526 $json = true,
1527 $extraHeaders = null,
1528 $streamCallback = null
1529 ) {
1530 $isAzure = $this->envType === 'azure';
1531 $isOpenAI = !$isAzure;
1532
1533 // Prepare the headers
1534 $headers = "Content-Type: application/json\r\n";
1535 if ( $isOpenAI ) {
1536 $headers .= 'Authorization: Bearer ' . $this->apiKey . "\r\n";
1537 if ( $this->organizationId ) {
1538 $headers .= 'OpenAI-Organization: ' . $this->organizationId . "\r\n";
1539 }
1540 }
1541 else if ( $isAzure ) {
1542 $headers .= 'api-key: ' . $this->apiKey . "\r\n";
1543 }
1544
1545 // Prepare the body with json_encode, if it's not a string or null, otherwise we keep it as is.
1546 if ( !empty( $query ) && !is_string( $query ) ) {
1547 $body = json_encode( $query );
1548 }
1549 else {
1550 $body = $query;
1551 }
1552
1553 // If we have form fields, we need to change the headers and the body.
1554 if ( !empty( $formFields ) ) {
1555 $boundary = wp_generate_password( 24, false );
1556 $headers = [
1557 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
1558 ];
1559 if ( $isOpenAI ) {
1560 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
1561 if ( $this->organizationId ) {
1562 $headers['OpenAI-Organization'] = $this->organizationId;
1563 }
1564 }
1565 else if ( $isAzure ) {
1566 $headers['api-key'] = $this->apiKey;
1567 }
1568 $body = $this->build_form_body( $formFields, $boundary );
1569 }
1570
1571 // Maybe we should have headers always as an array... not sure why we have it as a string.
1572 if ( !empty( $extraHeaders ) ) {
1573 foreach ( $extraHeaders as $key => $value ) {
1574 if ( is_array( $headers ) ) {
1575 $headers[$key] = $value;
1576 }
1577 else {
1578 $headers .= "$key: $value\r\n";
1579 }
1580 }
1581 }
1582
1583 // Create the URL
1584 if ( $isOpenAI ) {
1585 $url = 'https://api.openai.com/v1' . $url;
1586 }
1587 else if ( $isAzure ) {
1588 $url = trailingslashit( $this->env['endpoint'] ) . 'openai' . $url;
1589 $hasQuery = strpos( $url, '?' ) !== false;
1590 $url = $url . ( $hasQuery ? '&' : '?' ) . $this->azureApiVersion;
1591 }
1592
1593 // If it's a GET, body should be null, and we should append the query to the URL.
1594 if ( $method === 'GET' ) {
1595 if ( !empty( $query ) ) {
1596 $hasQuery = strpos( $url, '?' ) !== false;
1597 $url = $url . ( $hasQuery ? '&' : '?' ) . http_build_query( $query );
1598 }
1599 $body = null;
1600 }
1601
1602 $options = [
1603 'headers' => $headers,
1604 'method' => $method,
1605 'timeout' => MWAI_TIMEOUT,
1606 'body' => $body,
1607 'sslverify' => false
1608 ];
1609
1610 // Check if queries debug is enabled
1611 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
1612
1613 // Log the request if queries debug is enabled
1614 if ( $queries_debug ) {
1615 error_log( '[AI Engine Queries Debug] HTTP Request to: ' . $url );
1616
1617 if ( !empty( $body ) ) {
1618 error_log( '[AI Engine Queries Debug] Request Body:' );
1619 error_log( $body );
1620 }
1621
1622 if ( !is_null( $streamCallback ) ) {
1623 error_log( '[AI Engine Queries Debug] (Streaming mode - response will be streamed)' );
1624 }
1625 }
1626
1627 try {
1628 if ( !is_null( $streamCallback ) ) {
1629 $options['stream'] = true;
1630 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1631 // The stream handler calls the streamCallback every time there is content
1632 // TODO: For assistants, we should probably have a different stream handler to
1633 // handle the assistant's specific reply and perform the necessary actions.
1634 $this->streamCallback = $streamCallback;
1635 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1636 }
1637 $res = wp_remote_request( $url, $options );
1638 if ( is_wp_error( $res ) ) {
1639 throw new Exception( $res->get_error_message() );
1640 }
1641 $res = wp_remote_retrieve_body( $res );
1642 $data = $json ? json_decode( $res, true ) : $res;
1643 $this->handle_response_errors( $data );
1644
1645 // Log the response if queries debug is enabled
1646 if ( $queries_debug && is_null( $streamCallback ) ) {
1647 error_log( '[AI Engine Queries Debug] Response Body:' );
1648 error_log( $res );
1649 }
1650 return $data;
1651 }
1652 catch ( Exception $e ) {
1653 $service = $this->get_service_name();
1654 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1655 throw new Exception( "$service: " . $e->getMessage() );
1656 }
1657 finally {
1658 if ( !is_null( $streamCallback ) ) {
1659 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1660 }
1661 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1662 unlink( $options['filename'] );
1663 }
1664 }
1665 }
1666
1667 public function get_models() {
1668 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1669 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1670 foreach ( $finetunes as $finetune ) {
1671 if ( $finetune['status'] !== 'succeeded' ) {
1672 continue;
1673 }
1674 $baseModel = self::get_model_without_release_date( $finetune['base_model'] );
1675 if ( !empty( $baseModel ) ) {
1676 $model = null;
1677 foreach ( $models as $currentModel ) {
1678 if ( $currentModel['model'] === $baseModel ) {
1679 $model = $currentModel;
1680 break;
1681 }
1682 }
1683 if ( !empty( $model ) ) {
1684 $model['model'] = $finetune['model'];
1685 $model['name'] = $finetune['suffix'];
1686 $models[] = $model;
1687 }
1688 }
1689 }
1690 return $models;
1691 }
1692
1693 public static function get_models_static() {
1694 return MWAI_OPENAI_MODELS;
1695 }
1696
1697 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false ) {
1698 $modelFamily = self::get_model_without_release_date( $modelFamily );
1699 $models = $this->get_models();
1700 foreach ( $models as $currentModel ) {
1701 if ( $currentModel['model'] === $modelFamily ) {
1702 if ( $currentModel['type'] === 'image' ) {
1703 if ( !$resolution ) {
1704 Meow_MWAI_Logging::warn( '(OpenAI) Image models require a resolution.' );
1705 return null;
1706 }
1707 else {
1708 foreach ( $currentModel['resolutions'] as $r ) {
1709 if ( $r['name'] == $resolution ) {
1710 return $r['price'] * $outUnits;
1711 }
1712 }
1713 }
1714 }
1715 else {
1716 if ( $finetune ) {
1717 if ( isset( $currentModel['finetune']['price'] ) ) {
1718 $currentModel['price'] = $currentModel['finetune']['price'];
1719 }
1720 else if ( isset( $currentModel['finetune']['in'] ) ) {
1721 $currentModel['price'] = [
1722 'in' => $currentModel['finetune']['in'],
1723 'out' => $currentModel['finetune']['out']
1724 ];
1725 }
1726 }
1727 $inPrice = $currentModel['price'];
1728 $outPrice = $currentModel['price'];
1729 if ( is_array( $currentModel['price'] ) ) {
1730 $inPrice = $currentModel['price']['in'];
1731 $outPrice = $currentModel['price']['out'];
1732 }
1733 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1734 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1735 return $inTotalPrice + $outTotalPrice;
1736 }
1737 }
1738 }
1739 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1740 return null;
1741 }
1742
1743 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply ) {
1744 $model = $query->model;
1745 $units = 0;
1746 $finetune = false;
1747 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1748 if ( preg_match( '/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1749 $finetune = true;
1750 }
1751 $inUnits = $reply->get_in_tokens( $query );
1752 $outUnits = $reply->get_out_tokens();
1753 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1754 }
1755 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) || is_a( $query, 'Meow_MWAI_Query_EditImage' ) ) {
1756 $units = $query->maxResults;
1757 $resolution = $query->resolution;
1758 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1759 }
1760 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1761 $model = 'whisper';
1762 $units = $reply->get_units();
1763 return $this->calculate_price( $model, 0, $units, null, $finetune );
1764 }
1765 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1766 $units = $reply->get_total_tokens();
1767 return $this->calculate_price( $model, 0, $units, null, $finetune );
1768 }
1769 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1770 return null;
1771 }
1772 }
1773