PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.7.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.7.7
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 / openai.php
ai-engine / classes / engines Last commit date
anthropic.php 1 year ago core.php 1 year ago factory.php 1 year ago google.php 1 year ago huggingface.php 1 year ago openai.php 1 year ago openrouter.php 1 year ago perplexity.php 1 year ago replicate.php 1 year ago
openai.php
1528 lines
1 <?php
2
3 class Meow_MWAI_Engines_OpenAI extends Meow_MWAI_Engines_Core
4 {
5 // Base (OpenAI)
6 protected $apiKey = null;
7 protected $organizationId = null;
8
9 // Azure
10 private $azureDeployments = null;
11 private $azureApiVersion = 'api-version=2024-12-01-preview';
12
13 // Response
14 protected $inModel = null;
15 protected $inId = null;
16 protected $inThreadId = null;
17
18 // Streaming
19 protected $streamFunctionCall = null;
20 protected $streamToolCalls = [];
21 protected $streamLastMessage = null;
22 protected $streamAnnotations = [];
23 protected $streamImageIds = [];
24
25 protected $streamInTokens = null;
26 protected $streamOutTokens = null;
27 protected $streamCost = null;
28
29 // Static
30 private static $creating = false;
31
32 public static function create( $core, $env ) {
33 self::$creating = true;
34 if ( class_exists( 'MeowPro_MWAI_OpenAI' ) ) {
35 $instance = new MeowPro_MWAI_OpenAI( $core, $env );
36 }
37 else {
38 $instance = new self( $core, $env );
39 }
40 self::$creating = false;
41 return $instance;
42 }
43
44 public function __construct( $core, $env )
45 {
46 $isOwnClass = get_class( $this ) === 'Meow_MWAI_Engines_OpenAI';
47 if ( $isOwnClass && !self::$creating ) {
48 throw new \Exception( "Please use the create() method to instantiate the Meow_MWAI_Engines_OpenAI class." );
49 }
50 parent::__construct( $core, $env );
51 $this->set_environment();
52 }
53
54 public function reset_stream() {
55 $this->streamContent = null;
56 $this->streamBuffer = null;
57 $this->streamFunctionCall = null;
58 $this->streamToolCalls = [];
59 $this->streamLastMessage = null;
60 $this->streamInTokens = null;
61 $this->streamOutTokens = null;
62 $this->inModel = null;
63 $this->inId = null;
64 }
65
66 protected function set_environment() {
67 $env = $this->env;
68 $this->apiKey = $env['apikey'];
69
70 if ( isset( $env['organizationId'] ) ) {
71 $this->organizationId = $env['organizationId'];
72 }
73 if ( $this->envType === 'azure' ) {
74 $this->azureDeployments = isset( $env['deployments'] ) ? $env['deployments'] : [];
75 $this->azureDeployments[] = [ 'model' => 'dall-e', 'name' => 'dall-e' ];
76 }
77 }
78
79 private function get_azure_deployment_name( $model ) {
80 foreach ( $this->azureDeployments as $deployment ) {
81 if ( $deployment['model'] === $model && !empty( $deployment['name'] ) ) {
82 return $deployment['name'];
83 }
84 }
85 throw new Exception( 'Unknown deployment for model: ' . $model );
86 }
87
88 protected function get_service_name() {
89 return $this->envType === 'azure' ? 'Azure' : 'OpenAI';
90 }
91
92 private function is_o1_model( $model ) {
93 $modelDef = $this->retrieve_model_info( $model );
94 return !empty( $modelDef['tags'] ) && in_array( 'o1-model', $modelDef['tags'] );
95 }
96
97 private function requires_developer_roles( $model ) {
98 if ( $model === 'o1' ) {
99 return true;
100 }
101 return false;
102 }
103
104 protected function build_messages( $query ) {
105 $messages = [];
106
107 // First, we need to add the first message (the instructions).
108 if ( !empty( $query->instructions ) ) {
109 //if ( !$this->is_o1_model( $query->model ) ) {
110 $messages[] = [ 'role' => 'system', 'content' => $query->instructions ];
111 //}
112 }
113
114 // Then, if any, we need to add the 'messages', they are already formatted.
115 foreach ( $query->messages as $message ) {
116 $messages[] = $message;
117 }
118
119 // If there is a context, we need to add it.
120 if ( !empty( $query->context ) ) {
121 $messages[] = [ 'role' => 'system', 'content' => $query->context ];
122 }
123
124 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
125 if ( $query->attachedFile ) {
126 $finalUrl = null;
127 if ( $query->image_remote_upload === 'url' ) {
128 $finalUrl = $query->attachedFile->get_url();
129 }
130 else {
131 $finalUrl = $query->attachedFile->get_inline_base64_url();
132 }
133 $messages[] = [
134 'role' => 'user',
135 'content' => [
136 [
137 "type" => "text",
138 "text" => $query->get_message()
139 ],
140 [
141 "type" => "image_url",
142 "image_url" => [
143 "url" => $finalUrl
144 ]
145 ]
146 ]
147 ];
148 }
149 else {
150 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
151 }
152
153 // We need to convert all the 'system' role into 'developer' role.
154 if ( $this->requires_developer_roles( $query->model ) ) {
155 foreach ( $messages as &$message ) {
156 if ( $message['role'] === 'system' ) {
157 $message['role'] = 'developer';
158 }
159 }
160 }
161 // But otherwise, if it's o1, we need to remove the message which are 'system'
162 else if ( $this->is_o1_model( $query->model ) ) {
163 $hasChanges = false;
164 foreach ( $messages as $index => $message ) {
165 if ( $message['role'] === 'system' ) {
166 unset( $messages[$index] );
167 $hasChanges = true;
168 }
169 }
170 if ( $hasChanges ) {
171 $messages = array_values( $messages );
172 Meow_MWAI_Logging::warn( 'The model ' . $query->model . ' doesn\'t support System nor Developer messages. They were removed.' );
173 }
174 }
175
176 return $messages;
177 }
178
179 protected function build_body( $query, $streamCallback = null, $extra = null ) {
180 if ( $query instanceof Meow_MWAI_Query_Text ) {
181 $body = array(
182 "model" => $query->model,
183 "stream" => !is_null( $streamCallback ),
184 );
185
186 if ( !empty( $query->maxTokens ) ) {
187 // max_tokens has been deprecated in favor of max_completion_tokens in 2025.
188 $body['max_completion_tokens'] = $query->maxTokens;
189 }
190
191 if ( !empty( $query->temperature ) ) {
192 if ( !$this->is_o1_model( $query->model ) ) {
193 $body['temperature'] = $query->temperature;
194 }
195 else {
196 $body['temperature'] = 1;
197 }
198 }
199
200 if ( !empty( $query->maxResults ) ) {
201 $body['n'] = $query->maxResults;
202 }
203
204 if ( !empty( $query->stop ) ) {
205 $body['stop'] = $query->stop;
206 }
207
208 if ( !empty( $query->responseFormat ) ) {
209 if ( $query->responseFormat === 'json' ) {
210 $body['response_format'] = [ 'type' => 'json_object' ];
211 }
212 }
213
214 // Usage Data (only for OpenAI)
215 // https://cookbook.openai.com/examples/how_to_stream_completions#4-how-to-get-token-usage-data-for-streamed-chat-completion-response
216 if ( !empty( $streamCallback ) && $this->envType === 'openai' ) {
217 $body['stream_options'] = [
218 'include_usage' => true,
219 ];
220 }
221
222 if ( !empty( $query->functions ) ) {
223 $model = $this->retrieve_model_info( $query->model );
224 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
225 Meow_MWAI_Logging::warn( 'The model ' . $query->model . ' doesn\'t support Function Calling.' );
226 }
227 else if ( strpos( $query->model, 'ft:' ) === 0 ) {
228 Meow_MWAI_Logging::warn( 'OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
229 }
230 else {
231 $body['tools'] = [];
232 // Dynamic function: they will interactively enhance the completion (tools).
233 foreach ( $query->functions as $function ) {
234 $body['tools'][] = [
235 'type' => 'function',
236 'function' => $function->serializeForOpenAI()
237 ];
238 }
239 // Static functions: they will be executed at the end of the completion.
240 //$body['function_call'] = $query->functionCall;
241 }
242 }
243 $body['messages'] = $this->build_messages( $query );
244
245 // Add the feedback if it's a feedback query.
246 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
247 if ( !empty( $query->blocks ) ) {
248 foreach ( $query->blocks as $feedback_block ) {
249 $body['messages'][] = $feedback_block['rawMessage'];
250 foreach ( $feedback_block['feedbacks'] as $feedback ) {
251 $body['messages'][] = [
252 'tool_call_id' => $feedback['request']['toolId'],
253 "role" => "tool",
254 'name' => $feedback['request']['name'],
255 'content' => $feedback['reply']['value']
256 ];
257 }
258 }
259 }
260 return $body;
261 }
262
263 return $body;
264 }
265 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
266 $body = array(
267 'prompt' => $query->message,
268 'model' => $query->model,
269 'response_format' => 'text',
270 'file' => basename( $query->url ),
271 'data' => $extra
272 );
273 return $body;
274 }
275 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
276 $body = array( 'input' => $query->message, 'model' => $query->model );
277 if ( $this->envType === 'azure' ) {
278 $body = array( "input" => $query->message );
279 }
280 // Dimensions are only supported by v3 models
281 if ( !empty( $query->dimensions ) && strpos( $query->model, 'ada-002' ) === false ) {
282 $body['dimensions'] = $query->dimensions;
283 }
284 return $body;
285 }
286 else if ( $query instanceof Meow_MWAI_Query_Image ) {
287 $model = $query->model;
288 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
289 $body = array(
290 "prompt" => $query->message,
291 "n" => $query->maxResults,
292 "size" => $resolution,
293 );
294
295 // TODO: Let's clean this up; with a better Query Image class.
296 // https://platform.openai.com/docs/api-reference/images/create#images-create-quality
297
298 if ( $model === 'gpt-image-1' ) {
299 // If it's GPT Image 1, we need to set the quality and moderation.
300 $body['model'] = 'gpt-image-1';
301 $body['quality'] = 'high';
302 $body['moderation'] = 'low';
303 }
304 else {
305 // If it's DALL-E 3, we need to set the response format.
306 $body['response_format'] = 'b64_json';
307 if ( $model === 'dall-e-3' ) {
308 $body['model'] = 'dall-e-3';
309 }
310 if ( $model === 'dall-e-3-hd' ) {
311 $body['model'] = 'dall-e-3';
312 $body['quality'] = 'hd';
313 }
314 if ( !empty( $query->style ) && strpos( $model, 'dall-e-3' ) === 0 ) {
315 $body['style'] = $query->style;
316 }
317 }
318 return $body;
319 }
320 }
321
322 protected function build_url( $query, $endpoint = null ) {
323 $url = "";
324 $env = $this->env;
325 // This endpoint is basically OpenAI or Azure, but in the case this class
326 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
327 if ( empty( $endpoint ) ) {
328 if ( $this->envType === 'openai' ) {
329 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
330 $this->organizationId = isset( $env['organizationId'] ) ? $env['organizationId'] : null;
331 }
332 else if ( $this->envType === 'azure' ) {
333 $endpoint = isset( $env['endpoint'] ) ? $env['endpoint'] : null;
334 }
335 else {
336 if ( empty( $this->envType ) ) {
337 throw new Exception( 'Endpoint is not defined, and this envType is not known.' );
338 }
339 throw new Exception( 'Endpoint is not defined, and this envType is not known: ' . $this->envType );
340 }
341 }
342 // Add the base API to the URL
343 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
344 if ( $this->envType === 'azure' ) {
345 $deployment_name = $this->get_azure_deployment_name( $query->model );
346 $url = trailingslashit( $endpoint ) . 'openai/deployments/' . $deployment_name;
347 $url .= '/chat/completions?' . $this->azureApiVersion;
348 }
349 else {
350 $url .= trailingslashit( $endpoint ) . 'chat/completions';
351 }
352 return $url;
353 }
354 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
355 $modeEndpoint = $query->feature === 'translation' ? 'translations' : 'transcriptions';
356 $url .= trailingslashit( $endpoint ) . 'audio/' . $modeEndpoint;
357 return $url;
358 }
359 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
360 $url .= trailingslashit( $endpoint ) . 'embeddings';
361 if ( $this->envType === 'azure' ) {
362 $deployment_name = $this->get_azure_deployment_name( $query->model );
363 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
364 $deployment_name . '/embeddings?' . $this->azureApiVersion;
365 }
366 return $url;
367 }
368 else if ( $query instanceof Meow_MWAI_Query_Image ) {
369 $url .= trailingslashit( $endpoint ) . 'images/generations';
370 if ( $this->envType === 'azure' ) {
371 $deployment_name = $this->get_azure_deployment_name( $query->model );
372 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
373 $deployment_name . '/images/generations?' . $this->azureApiVersion;
374 }
375 return $url;
376 }
377 throw new Exception( 'The query is not supported by build_url().' );
378 }
379
380 protected function build_headers( $query ) {
381 if ( $query->apiKey ) {
382 $this->apiKey = $query->apiKey;
383 }
384 if ( empty( $this->apiKey ) ) {
385 throw new Exception( 'No API Key provided. Please visit the Settings.' );
386 }
387 $headers = array(
388 'Content-Type' => 'application/json',
389 'Authorization' => 'Bearer ' . $this->apiKey,
390 );
391 if ( $this->organizationId ) {
392 $headers['OpenAI-Organization'] = $this->organizationId;
393 }
394 if ( $this->envType === 'azure' ) {
395 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $this->apiKey );
396 }
397 return $headers;
398 }
399
400 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
401 $body = null;
402 if ( !empty( $forms ) ) {
403 $boundary = wp_generate_password ( 24, false );
404 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
405 $body = $this->build_form_body( $forms, $boundary );
406 }
407 else if ( !empty( $json ) ) {
408 $body = json_encode( $json );
409 }
410 $options = array(
411 'headers' => $headers,
412 'method' => $method,
413 'timeout' => MWAI_TIMEOUT,
414 'body' => $body,
415 'sslverify' => false
416 );
417 return $options;
418 }
419 // object: "thread.message.delta"
420 protected function stream_data_handler( $json ) {
421 $content = null;
422
423 // Get additional data from the JSON
424 if ( isset( $json['model'] ) ) {
425 $this->inModel = $json['model'];
426 }
427 if ( isset( $json['id'] ) ) {
428 $this->inId = $json['id'];
429 }
430
431 $object = $json['object'] ?? null;
432
433 if ( $object === 'thread.run' ) {
434 $this->inThreadId = $json['thread_id'];
435 if ( $json['status'] === 'failed' ) {
436 $error = $json['last_error']['message'] ?? 'The run failed.';
437 throw new Exception( $error );
438 }
439 }
440 else if ( $object === 'thread.run.step.delta' ) {
441 if ( $json['delta']['step_details']['type'] === 'tool_calls' ) {
442 foreach ( $json['delta']['step_details']['tool_calls'] as $tool_call ) {
443 $index = $tool_call['index'] ?? null;
444 $currentStreamToolCall = null;
445 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
446 $currentStreamToolCall = &$this->streamToolCalls[$index];
447 }
448 else {
449 $this->streamToolCalls[] = [
450 'id' => null,
451 'type' => null,
452 'function' => [ 'name' => "", 'arguments' => "" ],
453 'code_interpreter' => [ 'input' => "", 'outputs' => [] ],
454 ];
455 end( $this->streamToolCalls );
456 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
457 }
458 if ( !empty( $tool_call['id'] ) ) {
459 $currentStreamToolCall['id'] = $tool_call['id'];
460 }
461 if ( !empty( $tool_call['type'] ) ) {
462 $currentStreamToolCall['type'] = $tool_call['type'];
463 }
464 if ( isset( $tool_call['function'] ) ) {
465 $function = $tool_call['function'];
466 if ( isset( $function['name'] ) ) {
467 $currentStreamToolCall['function']['name'] .= $function['name'];
468 }
469 if ( isset( $function['arguments'] ) ) {
470 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
471 }
472 }
473 if ( isset( $tool_call['code_interpreter'] ) ) {
474 $code_interpreter = $tool_call['code_interpreter'];
475 if ( isset( $code_interpreter['input'] ) ) {
476 $currentStreamToolCall['code_interpreter']['input'] .= $code_interpreter['input'];
477 }
478 if ( isset( $code_interpreter['outputs'] ) ) {
479 $currentStreamToolCall['code_interpreter']['outputs'] = $code_interpreter['outputs'];
480 }
481 }
482 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
483 }
484 }
485 }
486 else if ( $object === 'thread.message.delta' ) {
487 $delta = $json['delta']['content'][0] ?? null;
488 if ( $delta ) {
489 switch ( $delta['type'] ?? null ) {
490 case 'text':
491 if ( !empty( $delta['value'] ) && is_string( $delta['value'] ) ) {
492 $content = $delta['value'];
493 }
494 else if ( !empty( $delta['text'] ) && is_string( $delta['text'] ) ) {
495 $content = $delta['text'];
496 }
497 else if ( !empty( $delta['text'] ) && is_array( $delta['text'] ) ) {
498 $text = $delta['text'];
499 if ( !empty( $text['annotations'] ) ) {
500 $this->streamAnnotations = array_merge( $this->streamAnnotations, $text['annotations'] );
501 }
502 if ( isset( $text['value'] ) ) {
503 $content = $text['value'];
504 }
505 }
506 else {
507 error_log( 'AI Engine: Unknown text format: ' . json_encode( $delta ) );
508 }
509 break;
510 case 'image':
511 $content = $delta['url'];
512 break;
513 case 'image_file':
514 $fileId = $delta['image_file']['file_id'];
515 $content = "<!-- IMG #" . $fileId . " -->";
516 $this->streamImageIds[] = $fileId;
517 break;
518 case 'function_call':
519 if ( empty( $this->streamFunctionCall ) ) {
520 $this->streamFunctionCall = [ 'name' => "", 'arguments' => [] ];
521 }
522 $this->streamFunctionCall['name'] = $delta['function_call']['name'] ?? $this->streamFunctionCall['name'];
523 if ( isset( $delta['function_call']['arguments'] ) ) {
524 $args = json_decode( $delta['function_call']['arguments'], true );
525 $this->streamFunctionCall['arguments'] = $args ?? [];
526 }
527 break;
528 case 'tool_call':
529 $tool_call = $delta['tool_call'];
530 $index = $tool_call['index'] ?? null;
531 $currentStreamToolCall = null;
532 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
533 $currentStreamToolCall = &$this->streamToolCalls[$index];
534 }
535 else {
536 $this->streamToolCalls[] = [
537 'id' => null,
538 'type' => null,
539 'function' => [ 'name' => "", 'arguments' => "" ]
540 ];
541 end( $this->streamToolCalls );
542 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
543 }
544 break;
545 }
546 }
547 }
548 else if ( $object === 'thread.run.step' ) {
549 //$type = $json['step'];
550 // Could be tool_calls, means an OpenAI Assistant is doing something.
551 }
552 else {
553 if ( isset( $json['choices'][0]['text'] ) ) {
554 $content = $json['choices'][0]['text'];
555 }
556 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
557 $content = $json['choices'][0]['delta']['content'];
558 }
559 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
560 if ( empty( $this->streamFunctionCall ) ) {
561 $this->streamFunctionCall = [ 'name' => "", 'arguments' => [] ];
562 }
563 $this->streamFunctionCall['name'] = $json['choices'][0]['delta']['function_call']['name'] ?? $this->streamFunctionCall['name'];
564 if ( isset( $json['choices'][0]['delta']['function_call']['arguments'] ) ) {
565 $args = json_decode( $json['choices'][0]['delta']['function_call']['arguments'], true );
566 $this->streamFunctionCall['arguments'] = $args ?? [];
567 }
568 }
569 else if ( isset( $json['choices'][0]['delta']['tool_calls'] ) ) {
570 foreach ( $json['choices'][0]['delta']['tool_calls'] as $tool_call ) {
571 $index = $tool_call['index'] ?? null;
572 $currentStreamToolCall = null;
573 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
574 $currentStreamToolCall = &$this->streamToolCalls[$index];
575 }
576 else {
577 $this->streamToolCalls[] = [
578 'id' => null,
579 'type' => null,
580 'function' => [ 'name' => "", 'arguments' => "" ]
581 ];
582 end( $this->streamToolCalls );
583 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
584 }
585 if ( !empty( $tool_call['id'] ) ) {
586 $currentStreamToolCall['id'] = $tool_call['id'];
587 }
588 if ( !empty( $tool_call['type'] ) ) {
589 $currentStreamToolCall['type'] = $tool_call['type'];
590 }
591 if ( isset( $tool_call['function'] ) ) {
592 $function = $tool_call['function'];
593 if ( isset( $function['name'] ) ) {
594 $currentStreamToolCall['function']['name'] .= $function['name'];
595 }
596 if ( isset( $function['arguments'] ) ) {
597 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
598 }
599 }
600 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
601 }
602 }
603 else if ( isset( $json['choices'][0]['delta']['role'] ) ) {
604 $this->streamLastMessage = [
605 'role' => $json['choices'][0]['delta']['role'],
606 'content' => null
607 ];
608 }
609 }
610
611 $usage = $json['usage'] ?? [];
612 if ( isset( $usage['prompt_tokens'], $usage['completion_tokens'] ) ) {
613 $this->streamInTokens = (int)$usage['prompt_tokens'];
614 $this->streamOutTokens = (int)$usage['completion_tokens'];
615 if ( isset($usage['cost'] ) ) {
616 $this->streamCost = (float)$usage['cost'];
617 }
618 }
619
620 // If content is an array, let's try to convert it into a string. Normally, there would be a 'value' key.
621 if ( is_array( $content ) ) {
622 if ( isset( $content['value'] ) ) {
623 $content = $content['value'];
624 }
625 else {
626 throw new Exception( 'AI Engine: Could not read this: ' . json_encode( $content ) );
627 }
628 }
629
630 // Avoid some endings
631 $endings = [ "", "</s>" ];
632 if ( in_array( $content, $endings ) ) {
633 $content = null;
634 }
635
636 return ( $content === '0' || !empty( $content ) ) ? $content : null;
637 }
638
639 public function run( $query, $streamCallback = null, $maxDepth = 5 ) {
640 if ( $streamCallback ) {
641 // Disable streaming only for "o1" (as December 2024, it works for preview and mini)
642 if ( $query->model === 'o1' ) {
643 $streamCallback = null;
644 }
645 }
646 return parent::run( $query, $streamCallback, $maxDepth );
647 }
648
649 public function run_query( $url, $options, $isStream = false ) {
650 try {
651 $options['stream'] = $isStream;
652 if ( $isStream ) {
653 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
654 }
655 $res = wp_remote_get( $url, $options );
656
657 if ( is_wp_error( $res ) ) {
658 throw new Exception( $res->get_error_message() );
659 }
660
661 $responseCode = wp_remote_retrieve_response_code( $res );
662 if ( $responseCode === 404 ) {
663 throw new Exception( 'The model\'s API URL was not found: ' . $url );
664 }
665 if ( $responseCode === 400 ) {
666 $message = wp_remote_retrieve_body( $res );
667 if ( empty( $message ) ) {
668 $message = wp_remote_retrieve_response_message( $res );
669 }
670 if ( empty( $message ) ) {
671 $message = 'Bad Request';
672 }
673 throw new Exception( $message );
674 }
675
676 if ( $isStream ) {
677 return [ 'stream' => true ];
678 }
679
680 $response = wp_remote_retrieve_body( $res );
681 $headersRes = wp_remote_retrieve_headers( $res );
682 $headers = $headersRes->getAll();
683
684 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
685 // If so, we don't need to decode the response
686 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
687 $resContentType = $normalizedHeaders['content-type'] ?? '';
688 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
689 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
690 }
691
692 $data = json_decode( $response, true );
693 $this->handle_response_errors( $data );
694 return [ 'headers' => $headers, 'data' => $data ];
695 }
696 catch ( Exception $e ) {
697 $service = $this->get_service_name();
698 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
699 throw $e;
700 }
701 finally {
702 if ( $isStream && file_exists( $options['filename'] ) ) {
703 unlink( $options['filename'] );
704 }
705 }
706 }
707
708 private function get_audio( $url ) {
709 require_once( ABSPATH . 'wp-admin/includes/media.php' );
710 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
711 file_put_contents( $tmpFile, file_get_contents( $url ) );
712 $length = null;
713 $metadata = wp_read_audio_metadata( $tmpFile );
714 if ( isset( $metadata['length'] ) ) {
715 $length = $metadata['length'];
716 }
717 $data = file_get_contents( $tmpFile );
718 unlink( $tmpFile );
719 return [ 'data' => $data, 'length' => $length ];
720 }
721
722 public function run_transcribe_query( $query ) {
723 // TODO: This function currently only supports the URL method.
724 // But as with Vision, we should support the file upload method too.
725 $attachedFile = $query->attachedFile ?? null;
726 $audioUrl = $query->url ?? null;
727 if ( $attachedFile ) {
728 $audioUrl = $attachedFile->get_url();
729 $query->set_url( $audioUrl );
730 }
731 if ( !filter_var( $audioUrl, FILTER_VALIDATE_URL ) ) {
732 throw new Exception( 'Invalid URL for transcription.' );
733 }
734
735 $audioData = $this->get_audio( $audioUrl );
736 $body = $this->build_body( $query, null, $audioData['data'] );
737 $url = $this->build_url( $query );
738 $headers = $this->build_headers( $query );
739 $options = $this->build_options( $headers, null, $body );
740
741 // Perform the request
742 try {
743 $res = $this->run_query( $url, $options );
744 $data = $res['data'];
745 if ( empty( $data ) ) {
746 throw new Exception( 'Invalid data for transcription.' );
747 }
748 $usage = $this->core->record_audio_usage( $query->model, $audioData['length'] );
749 $reply = new Meow_MWAI_Reply( $query );
750 $reply->set_usage( $usage );
751 $reply->set_choices( $data );
752 return $reply;
753 }
754 catch ( Exception $e ) {
755 $service = $this->get_service_name();
756 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
757 throw new Exception( "$service: " . $e->getMessage() );
758 }
759 }
760
761 public function run_embedding_query( $query ) {
762 $body = $this->build_body( $query );
763 $url = $this->build_url( $query );
764 $headers = $this->build_headers( $query );
765 $options = $this->build_options( $headers, $body );
766
767 try {
768 $res = $this->run_query( $url, $options );
769 $data = $res['data'];
770 if ( empty( $data ) || !isset( $data['data'] ) ) {
771 throw new Exception( 'Invalid data for embedding.' );
772 }
773 $usage = $data['usage'];
774 $this->core->record_tokens_usage( $query->model, $usage['prompt_tokens'] );
775 $reply = new Meow_MWAI_Reply( $query );
776 $reply->set_usage( $usage );
777 $reply->set_choices( $data['data'] );
778 return $reply;
779 }
780 catch ( Exception $e ) {
781 $message = $e->getMessage();
782 $error = $this->try_decode_error( $message );
783 if ( !is_null( $error ) ) {
784 $message = $error;
785 }
786 $service = $this->get_service_name();
787 Meow_MWAI_Logging::error( "$service: " . $message );
788 throw new Exception( "$service: " . $message );
789 }
790 }
791
792 public function try_decode_error( $data ) {
793 $json = json_decode( $data, true );
794 if ( isset( $json['error']['message'] ) ) {
795 return $json['error']['message'];
796 }
797 return null;
798 }
799
800 protected function finalize_choices( $choices, $responseData, $query ) {
801 return $choices;
802 }
803
804 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
805 $isStreaming = !is_null( $streamCallback );
806
807 if ( $isStreaming ) {
808 $this->streamCallback = $streamCallback;
809 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
810 }
811
812 $this->reset_stream();
813 $body = $this->build_body( $query, $streamCallback );
814 $url = $this->build_url( $query );
815 $headers = $this->build_headers( $query );
816 $options = $this->build_options( $headers, $body );
817
818 try {
819 $res = $this->run_query( $url, $options, $streamCallback );
820 $reply = new Meow_MWAI_Reply( $query );
821
822 $returned_id = null;
823 $returned_model = $this->inModel;
824 $returned_in_tokens = null;
825 $returned_out_tokens = null;
826 $returned_price = null;
827 $returned_choices = [];
828
829 // Streaming Mode
830 if ( $isStreaming ) {
831 if ( empty( $this->streamContent ) ) {
832 $error = $this->try_decode_error( $this->streamBuffer );
833 if ( !is_null( $error ) ) {
834 throw new Exception( $error );
835 }
836 }
837 $returned_id = $this->inId;
838 $returned_model = $this->inModel ? $this->inModel : $query->model;
839 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
840 if ( !empty( $this->streamFunctionCall ) ) {
841 $message['function_call'] = $this->streamFunctionCall;
842 }
843 if ( !empty( $this->streamToolCalls ) ) {
844 $message['tool_calls'] = $this->streamToolCalls;
845 }
846 if ( !is_null( $this->streamInTokens ) ) {
847 $returned_in_tokens = $this->streamInTokens;
848 }
849 if ( !is_null( $this->streamOutTokens ) ) {
850 $returned_out_tokens = $this->streamOutTokens;
851 }
852 if ( !is_null( $this->streamCost ) ) {
853 $returned_price = $this->streamCost;
854 }
855 $returned_choices = [ [ 'message' => $message ] ];
856 $returned_choices = $this->finalize_choices( $returned_choices, null, $query );
857 }
858 // Standard Mode
859 else {
860 $data = $res['data'];
861 if ( empty( $data ) ) {
862 throw new Exception( 'No content received (res is null).' );
863 }
864 if ( !$data['model'] ) {
865 $service = $this->get_service_name();
866 Meow_MWAI_Logging::error( "$service: Invalid response (no model information)." );
867 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
868 throw new Exception( 'Invalid response (no model information).' );
869 }
870 $returned_id = $data['id'];
871 $returned_model = $data['model'];
872 $usage = $data['usage'] ?? [];
873 $returned_in_tokens = $usage['prompt_tokens'] ?? null;
874 $returned_out_tokens = $usage['completion_tokens'] ?? null;
875 $returned_price = $usage['total_cost'] ?? $usage['cost'] ?? null;
876 $returned_choices = $data['choices'];
877 $returned_choices = $this->finalize_choices( $returned_choices, $data, $query );
878 }
879
880 // Set the results.
881 $reply->set_choices( $returned_choices );
882 if ( !empty( $returned_id ) ) {
883 $reply->set_id( $returned_id );
884 }
885 if ( !empty( $returned_id ) ) {
886 $reply->set_id( $returned_id );
887 }
888
889 // Handle tokens.
890 $this->handle_tokens_usage( $reply, $query, $returned_model,
891 $returned_in_tokens, $returned_out_tokens, $returned_price
892 );
893
894 return $reply;
895 }
896 catch ( Exception $e ) {
897 $service = $this->get_service_name();
898 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
899 $message = "$service: " . $e->getMessage();
900 throw new Exception( $message );
901 }
902 finally {
903 if ( !is_null( $streamCallback ) ) {
904 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
905 }
906 }
907 }
908
909 public function handle_tokens_usage( $reply, $query, $returned_model,
910 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
911 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens :
912 $reply->get_in_tokens( $query );
913 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens :
914 $reply->get_out_tokens();
915 $returned_price = !is_null( $returned_price ) ? $returned_price :
916 $reply->get_price();
917 $usage = $this->core->record_tokens_usage(
918 $returned_model,
919 $returned_in_tokens,
920 $returned_out_tokens,
921 $returned_price
922 );
923 $reply->set_usage( $usage );
924 }
925
926 // Request to DALL-E API
927 public function run_image_query( $query ) {
928 $body = $this->build_body( $query );
929 $url = $this->build_url( $query );
930 $headers = $this->build_headers( $query );
931 $options = $this->build_options( $headers, $body );
932
933 try {
934 $res = $this->run_query( $url, $options );
935 $data = $res['data'];
936 $choices = [];
937 if ( $this->envType === 'azure' ) {
938 foreach ( $data['data'] as $entry ) {
939 $choices[] = [ 'url' => $entry['url'] ];
940 }
941 }
942 else {
943
944 // With DALL-E, I receive this:
945 // created =
946 // 1745468700
947 // data =
948 // array(1)
949 // 0 =
950 // array(2)
951 // revised_prompt =
952 // "Create an image like an oil painting, capturing the resonance of a quaint, old house in the Japanese countryside. This house, bearing the components of a traditional izakaya, is nestled among plentiful trees and appraisal of vibrant rice fields. The environment is awash with the comforting, mellow sparkle of a descending sun, resulting in delicate shadows and constructing a peaceful, wistful air. The composition should be abundant in texture, embodying the quiet splendor of rural Japan in a whimsical, romantic style reminiscent of late 19th century impressionism."
953 // url =
954 // "https://oaidalleapiprodscus.blob.core.windows.net/private/org-q35wZ0EjtCYovYqCQa19Gm3d/user-ueEri5XabAqm2Gy4rsJsEGwt/img-04OkUKnMX0msUFGzREiqzG5F.png?st=2025-04-24T03%3A25%3A00Z&se=2025-04-24T05%3A25%3A00Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=cc612491-d948-4d2e-9821-2683df3719f5&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-04-24T02%3A48%3A27Z&ske=2025-04-25T02%3A48%3A27Z&sks=b&skv=2024-08-04&sig=ACJD7%2BSDaQrJPVZmYO2szr/8p57krdGI0CdeQu1FX0Q%3D"
955
956 $choices = $data['data'];
957 }
958
959 $reply = new Meow_MWAI_Reply( $query );
960 $model = $query->model;
961 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
962 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
963 $reply->set_usage( $usage );
964 $reply->set_choices( $choices );
965 $reply->set_type( 'images' );
966
967 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
968 foreach ( $reply->results as &$result ) {
969 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
970 'query_envId' => $query->envId,
971 'query_session' => $query->session,
972 'query_model' => $query->model,
973 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
974 $fileUrl = $this->core->files->get_url( $fileId );
975 $result = $fileUrl;
976 }
977 }
978 $reply->result = $reply->results[0];
979 return $reply;
980 }
981 catch ( Exception $e ) {
982 $service = $this->get_service_name();
983 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
984 throw new Exception( "$service: " . $e->getMessage() );
985 }
986 }
987
988 /*
989 This is the rest of the OpenAI API support, not related to the models directly.
990 */
991
992 // Check if there are errors in the response from OpenAI, and throw an exception if so.
993 protected function handle_response_errors( $data ) {
994 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
995 $message = $data['error']['message'];
996 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
997 $message = str_replace( $matches[1], '', $message );
998 }
999 throw new Exception( $message );
1000 }
1001 }
1002
1003 public function list_files( $purposeFilter = null )
1004 {
1005 if ( empty( $purposeFilter ) ) {
1006 return $this->execute( 'GET', '/files' );
1007 }
1008 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
1009 }
1010
1011 static function get_suffix_for_model($model)
1012 {
1013 // Legacy fine-tuned models
1014 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
1015 if ( count( $matches ) > 0 ) {
1016 return $matches[1];
1017 }
1018
1019 // New fine-tuned models
1020 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
1021 if (count($matches) > 0) {
1022 return $matches[1];
1023 }
1024
1025 return 'N/A';
1026 }
1027
1028 static function get_model_without_release_date( $model )
1029 {
1030 if ( empty( $model ) ) {
1031 return null;
1032 }
1033 return preg_replace( '/-\d{4}-\d{2}-\d{2}$/', '', $model );
1034 }
1035
1036 public function list_deleted_finetunes( $envId = null, $legacy = false )
1037 {
1038 $finetunes = $this->list_finetunes( $legacy );
1039 $deleted = [];
1040
1041 foreach ( $finetunes as $finetune ) {
1042 $name = $finetune['model'];
1043 $isSucceeded = $finetune['status'] === 'succeeded';
1044 if ( $isSucceeded ) {
1045 try {
1046 $finetune = $this->get_model( $name );
1047 }
1048 catch ( Exception $e ) {
1049 $deleted[] = $name;
1050 }
1051 }
1052 }
1053 if ( $legacy ) {
1054 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
1055 }
1056 else {
1057 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
1058 }
1059 return $deleted;
1060 }
1061
1062 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
1063 // retrieve all the models since Summer 2023, let's see! WIP.
1064 public function list_finetunes( $legacy = false )
1065 {
1066 if ( $legacy ) {
1067 $res = $this->execute( 'GET', '/fine-tunes' );
1068 }
1069 else {
1070 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
1071 }
1072 $finetunes = $res['data'];
1073
1074 // Add suffix
1075 $finetunes = array_map( function ( $finetune ) {
1076 if ( isset( $finetune['user_provided_suffix'] ) ) {
1077 $finetune['suffix'] = $finetune['user_provided_suffix'];
1078 }
1079 else {
1080 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
1081 }
1082 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] ) . ' UTC';
1083 if ( isset( $finetune['estimated_finish'] ) ) {
1084 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] ) . ' UTC';
1085 }
1086 else {
1087 $finetune['estimatedOn'] = null;
1088 }
1089 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
1090 $finetune['base_model'] = $finetune['model'];
1091 $finetune['model'] = $finetune['fine_tuned_model'];
1092 unset( $finetune['object'] );
1093 unset( $finetune['hyperparams'] );
1094 unset( $finetune['result_files'] );
1095 unset( $finetune['training_files'] );
1096 unset( $finetune['validation_files'] );
1097 unset( $finetune['created_at'] );
1098 unset( $finetune['updated_at'] );
1099 unset( $finetune['fine_tuned_model'] );
1100 return $finetune;
1101 }, $finetunes);
1102
1103 usort( $finetunes, function ( $a, $b ) {
1104 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
1105 });
1106
1107 if ( $legacy ) {
1108 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
1109 }
1110 else {
1111 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
1112 }
1113
1114 return $finetunes;
1115 }
1116
1117 public function moderate( $input ) {
1118 $result = $this->execute('POST', '/moderations', [
1119 'input' => $input
1120 ]);
1121 return $result;
1122 }
1123
1124 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
1125 {
1126 $result = $this->execute('POST', '/files', null, [
1127 'purpose' => $purpose,
1128 'data' => $data,
1129 'file' => $filename
1130 ] );
1131 return $result;
1132 }
1133
1134 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
1135 $body = [
1136 'name' => !empty( $name ) ? $name : 'default',
1137 'metadata' => $metadata
1138 ];
1139 if ( $expiry !== 'never' ) {
1140 if ( is_string( $expiry ) ) {
1141 error_log( 'AI Engine: Expiry is a string, setting it to 7 days.' );
1142 $expiry = 7;
1143 }
1144 $expiryInDays = $expiry ? max( 1, ceil( (int)$expiry / 86400 ) ) : 7;
1145 if ( $expiry && is_numeric( $expiry ) ) {
1146 $body['expires_after'] = [
1147 'anchor' => 'last_active_at',
1148 'days' => $expiryInDays
1149 ];
1150 }
1151 }
1152 $result = $this->execute( 'POST', '/vector_stores', $body, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1153 return $result['id'];
1154 }
1155
1156 public function get_vector_store( $vectorStoreId ) {
1157 return $this->execute( 'GET', '/vector_stores/' . $vectorStoreId, null, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1158 }
1159
1160 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1161 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1162 'file_id' => $fileId
1163 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1164 return $result['id'];
1165
1166 }
1167
1168 public function delete_file( $fileId )
1169 {
1170 return $this->execute( 'DELETE', '/files/' . $fileId );
1171 }
1172
1173 public function get_model( $modelId )
1174 {
1175 return $this->execute( 'GET', '/models/' . $modelId );
1176 }
1177
1178 public function cancel_finetune( $fineTuneId )
1179 {
1180 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1181 }
1182
1183 public function delete_finetune( $modelId )
1184 {
1185 return $this->execute( 'DELETE', '/models/' . $modelId );
1186 }
1187
1188 public function download_file( $fileId, $newFile = null ) {
1189 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1190 $fileInfo = json_decode( (string)$fileInfo, true );
1191 if ( empty( $fileInfo ) ) {
1192 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1193 }
1194 $filename = $fileInfo['filename'];
1195 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1196 if ( empty( $newFile ) ) {
1197 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1198 $tempFile = wp_tempnam( $filename );
1199 if ( !$tempFile ) {
1200 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1201 }
1202 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1203 $newFile = $tempFile . '.' . $extension;
1204 }
1205 else {
1206 $newFile = $tempFile;
1207 }
1208 }
1209 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1210 file_put_contents( $newFile, $data );
1211 return $newFile;
1212 }
1213
1214 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1215 {
1216 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1217 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1218 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1219 (float)$hyperparams['learningRateMultiplier'] : null;
1220 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1221 (float)$hyperparams['promptLossWeight'] : null;
1222 $arguments = [
1223 'training_file' => $fileId,
1224 'model' => $model,
1225 'suffix' => $suffix
1226 ];
1227 if ( $legacy ) {
1228 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1229 }
1230 else {
1231 if ( $n_epochs ) {
1232 $arguments['hyperparams'] = [];
1233 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1234 }
1235 if ( $batch_size ) {
1236 if ( empty( $arguments['hyperparams'] ) ) {
1237 $arguments['hyperparams'] = [];
1238 }
1239 $arguments['hyperparams']['batch_size'] = $batch_size;
1240 }
1241 if ( $learning_rate_multiplier ) {
1242 if ( empty( $arguments['hyperparams'] ) ) {
1243 $arguments['hyperparams'] = [];
1244 }
1245 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1246 }
1247 if ( $prompt_loss_weight ) {
1248 if ( empty( $arguments['hyperparams'] ) ) {
1249 $arguments['hyperparams'] = [];
1250 }
1251 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1252 }
1253 if ( $model === 'turbo' ) {
1254 $arguments['model'] = 'gpt-3.5-turbo';
1255 }
1256 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1257 }
1258 return $result;
1259 }
1260
1261 /**
1262 * Build the body of a form request.
1263 * If the field name is 'file', then the field value is the filename of the file to upload.
1264 * The file contents are taken from the 'data' field.
1265 *
1266 * @param array $fields
1267 * @param string $boundary
1268 * @return string
1269 */
1270 public function build_form_body( $fields, $boundary )
1271 {
1272 $body = '';
1273 foreach ( $fields as $name => $value ) {
1274 if ( $name == 'data' ) {
1275 continue;
1276 }
1277 $body .= "--$boundary\r\n";
1278 $body .= "Content-Disposition: form-data; name=\"$name\"";
1279 if ( $name == 'file' ) {
1280 $body .= "; filename=\"{$value}\"\r\n";
1281 $body .= "Content-Type: application/json\r\n\r\n";
1282 $body .= $fields['data'] . "\r\n";
1283 }
1284 else {
1285 $body .= "\r\n\r\n$value\r\n";
1286 }
1287 }
1288 $body .= "--$boundary--\r\n";
1289 return $body;
1290 }
1291
1292 /**
1293 * Run a request to the OpenAI API.
1294 * Fore more information about the $formFields, refer to the build_form_body method.
1295 *
1296 * @param string $method POST, PUT, GET, DELETE...
1297 * @param string $url The API endpoint
1298 * @param array $query The query parameters (json)
1299 * @param array $formFields The form fields (multipart/form-data)
1300 * @param bool $json Whether to return the response as json or not
1301 * @return array
1302 */
1303 public function execute( $method, $url, $query = null, $formFields = null,
1304 $json = true, $extraHeaders = null, $streamCallback = null )
1305 {
1306 $isAzure = $this->envType === 'azure';
1307 $isOpenAI = !$isAzure;
1308
1309 // Prepare the headers
1310 $headers = "Content-Type: application/json\r\n";
1311 if ( $isOpenAI ) {
1312 $headers .= "Authorization: Bearer " . $this->apiKey . "\r\n";
1313 if ( $this->organizationId ) {
1314 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1315 }
1316 }
1317 else if ( $isAzure ) {
1318 $headers .= "api-key: " . $this->apiKey . "\r\n";
1319 }
1320
1321 // Prepare the body with json_encode, if it's not a string or null, otherwise we keep it as is.
1322 if ( !empty( $query ) && !is_string( $query ) ) {
1323 $body = json_encode( $query );
1324 }
1325 else {
1326 $body = $query;
1327 }
1328
1329 // If we have form fields, we need to change the headers and the body.
1330 if ( !empty( $formFields ) ) {
1331 $boundary = wp_generate_password( 24, false );
1332 $headers = [
1333 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
1334 ];
1335 if ( $isOpenAI ) {
1336 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
1337 if ( $this->organizationId ) {
1338 $headers['OpenAI-Organization'] = $this->organizationId;
1339 }
1340 }
1341 else if ( $isAzure ) {
1342 $headers['api-key'] = $this->apiKey;
1343 }
1344 $body = $this->build_form_body( $formFields, $boundary );
1345 }
1346
1347 // Maybe we should have headers always as an array... not sure why we have it as a string.
1348 if ( !empty( $extraHeaders ) ) {
1349 foreach ( $extraHeaders as $key => $value ) {
1350 if ( is_array( $headers ) ) {
1351 $headers[$key] = $value;
1352 }
1353 else {
1354 $headers .= "$key: $value\r\n";
1355 }
1356 }
1357 }
1358
1359 // Create the URL
1360 if ( $isOpenAI ) {
1361 $url = 'https://api.openai.com/v1' . $url;
1362 }
1363 else if ( $isAzure ) {
1364 $url = trailingslashit( $this->env['endpoint'] ) . 'openai' . $url;
1365 $hasQuery = strpos( $url, '?' ) !== false;
1366 $url = $url . ( $hasQuery ? '&' : '?' ) . $this->azureApiVersion;
1367 }
1368
1369 // If it's a GET, body should be null, and we should append the query to the URL.
1370 if ( $method === 'GET' ) {
1371 if ( !empty( $query ) ) {
1372 $hasQuery = strpos( $url, '?' ) !== false;
1373 $url = $url . ( $hasQuery ? '&' : '?' ) . http_build_query( $query );
1374 }
1375 $body = null;
1376 }
1377
1378 $options = [
1379 "headers" => $headers,
1380 "method" => $method,
1381 "timeout" => MWAI_TIMEOUT,
1382 "body" => $body,
1383 "sslverify" => false
1384 ];
1385
1386 try {
1387 if ( !is_null( $streamCallback ) ) {
1388 $options['stream'] = true;
1389 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1390 // The stream handler calls the streamCallback every time there is content
1391 // TODO: For assistants, we should probably have a different stream handler to
1392 // handle the assistant's specific reply and perform the necessary actions.
1393 $this->streamCallback = $streamCallback;
1394 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1395 }
1396 $res = wp_remote_request( $url, $options );
1397 if ( is_wp_error( $res ) ) {
1398 throw new Exception( $res->get_error_message() );
1399 }
1400 $res = wp_remote_retrieve_body( $res );
1401 $data = $json ? json_decode( $res, true ) : $res;
1402 $this->handle_response_errors( $data );
1403 return $data;
1404 }
1405 catch ( Exception $e ) {
1406 $service = $this->get_service_name();
1407 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1408 throw new Exception( "$service: " . $e->getMessage() );
1409 }
1410 finally {
1411 if ( !is_null( $streamCallback ) ) {
1412 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1413 }
1414 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1415 unlink( $options['filename'] );
1416 }
1417 }
1418 }
1419
1420 public function get_models() {
1421 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1422 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1423 foreach ( $finetunes as $finetune ) {
1424 if ( $finetune['status'] !== 'succeeded' ) {
1425 continue;
1426 }
1427 $baseModel = SELF::get_model_without_release_date( $finetune['base_model'] );
1428 if ( !empty( $baseModel ) ) {
1429 $model = null;
1430 foreach ( $models as $currentModel ) {
1431 if ( $currentModel['model'] === $baseModel ) {
1432 $model = $currentModel;
1433 break;
1434 }
1435 }
1436 if ( !empty( $model ) ) {
1437 $model['model'] = $finetune['model'];
1438 $model['name'] = $finetune['suffix'];
1439 $models[] = $model;
1440 }
1441 }
1442 }
1443 return $models;
1444 }
1445
1446 static public function get_models_static() {
1447 return MWAI_OPENAI_MODELS;
1448 }
1449
1450 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false )
1451 {
1452 $modelFamily = SELF::get_model_without_release_date( $modelFamily );
1453 $models = $this->get_models();
1454 foreach ( $models as $currentModel ) {
1455 if ( $currentModel['model'] === $modelFamily ) {
1456 if ( $currentModel['type'] === 'image' ) {
1457 if ( !$resolution ) {
1458 Meow_MWAI_Logging::warn( "(OpenAI) Image models require a resolution." );
1459 return null;
1460 }
1461 else {
1462 foreach ( $currentModel['resolutions'] as $r ) {
1463 if ( $r['name'] == $resolution ) {
1464 return $r['price'] * $outUnits;
1465 }
1466 }
1467 }
1468 }
1469 else {
1470 if ( $finetune ) {
1471 if ( isset( $currentModel['finetune']['price'] ) ) {
1472 $currentModel['price'] = $currentModel['finetune']['price'];
1473 }
1474 else if ( isset( $currentModel['finetune']['in'] ) ) {
1475 $currentModel['price'] = [
1476 'in' => $currentModel['finetune']['in'],
1477 'out' => $currentModel['finetune']['out']
1478 ];
1479 }
1480 }
1481 $inPrice = $currentModel['price'];
1482 $outPrice = $currentModel['price'];
1483 if ( is_array( $currentModel['price'] ) ) {
1484 $inPrice = $currentModel['price']['in'];
1485 $outPrice = $currentModel['price']['out'];
1486 }
1487 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1488 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1489 return $inTotalPrice + $outTotalPrice;
1490 }
1491 }
1492 }
1493 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1494 return null;
1495 }
1496
1497 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1498 {
1499 $model = $query->model;
1500 $units = 0;
1501 $finetune = false;
1502 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1503 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1504 $finetune = true;
1505 }
1506 $inUnits = $reply->get_in_tokens( $query );
1507 $outUnits = $reply->get_out_tokens();
1508 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1509 }
1510 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1511 $units = $query->maxResults;
1512 $resolution = $query->resolution;
1513 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1514 }
1515 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1516 $model = 'whisper';
1517 $units = $reply->get_units();
1518 return $this->calculate_price( $model, 0, $units, null, $finetune );
1519 }
1520 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1521 $units = $reply->get_total_tokens();
1522 return $this->calculate_price( $model, 0, $units, null, $finetune );
1523 }
1524 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1525 return null;
1526 }
1527 }
1528