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