PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.7.3
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.7.3
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 replicate.php 1 year ago
openai.php
1509 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-05-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 ( !empty( $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 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
783 $isStreaming = !is_null( $streamCallback );
784
785 if ( $isStreaming ) {
786 $this->streamCallback = $streamCallback;
787 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
788 }
789
790 $this->reset_stream();
791 $body = $this->build_body( $query, $streamCallback );
792 $url = $this->build_url( $query );
793 $headers = $this->build_headers( $query );
794 $options = $this->build_options( $headers, $body );
795
796 try {
797 $res = $this->run_query( $url, $options, $streamCallback );
798 $reply = new Meow_MWAI_Reply( $query );
799
800 $returned_id = null;
801 $returned_model = $this->inModel;
802 $returned_in_tokens = null;
803 $returned_out_tokens = null;
804 $returned_price = null;
805 $returned_choices = [];
806
807 // Streaming Mode
808 if ( $isStreaming ) {
809 if ( empty( $this->streamContent ) ) {
810 $error = $this->try_decode_error( $this->streamBuffer );
811 if ( !is_null( $error ) ) {
812 throw new Exception( $error );
813 }
814 }
815 $returned_id = $this->inId;
816 $returned_model = $this->inModel ? $this->inModel : $query->model;
817 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
818 if ( !empty( $this->streamFunctionCall ) ) {
819 $message['function_call'] = $this->streamFunctionCall;
820 }
821 if ( !empty( $this->streamToolCalls ) ) {
822 $message['tool_calls'] = $this->streamToolCalls;
823 }
824 if ( !is_null( $this->streamInTokens ) ) {
825 $returned_in_tokens = $this->streamInTokens;
826 }
827 if ( !is_null( $this->streamOutTokens ) ) {
828 $returned_out_tokens = $this->streamOutTokens;
829 }
830 $returned_choices = [ [ 'message' => $message ] ];
831 }
832 // Standard Mode
833 else {
834 $data = $res['data'];
835 if ( empty( $data ) ) {
836 throw new Exception( 'No content received (res is null).' );
837 }
838 if ( !$data['model'] ) {
839 $service = $this->get_service_name();
840 Meow_MWAI_Logging::error( "$service: Invalid response (no model information)." );
841 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
842 throw new Exception( 'Invalid response (no model information).' );
843 }
844 $returned_id = $data['id'];
845 $returned_model = $data['model'];
846 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
847 $data['usage']['prompt_tokens'] : null;
848 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
849 $data['usage']['completion_tokens'] : null;
850 $returned_price = isset( $data['usage']['total_cost'] ) ?
851 $data['usage']['total_cost'] : null;
852 $returned_choices = $data['choices'];
853 }
854
855 // Set the results.
856 $reply->set_choices( $returned_choices );
857 if ( !empty( $returned_id ) ) {
858 $reply->set_id( $returned_id );
859 }
860 if ( !empty( $returned_id ) ) {
861 $reply->set_id( $returned_id );
862 }
863
864 // Handle tokens.
865 $this->handle_tokens_usage( $reply, $query, $returned_model,
866 $returned_in_tokens, $returned_out_tokens, $returned_price
867 );
868
869 return $reply;
870 }
871 catch ( Exception $e ) {
872 $service = $this->get_service_name();
873 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
874 $message = "$service: " . $e->getMessage();
875 throw new Exception( $message );
876 }
877 finally {
878 if ( !is_null( $streamCallback ) ) {
879 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
880 }
881 }
882 }
883
884 public function handle_tokens_usage( $reply, $query, $returned_model,
885 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
886 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens :
887 $reply->get_in_tokens( $query );
888 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens :
889 $reply->get_out_tokens();
890 $returned_price = !is_null( $returned_price ) ? $returned_price :
891 $reply->get_price();
892 $usage = $this->core->record_tokens_usage(
893 $returned_model,
894 $returned_in_tokens,
895 $returned_out_tokens,
896 $returned_price
897 );
898 $reply->set_usage( $usage );
899 }
900
901 // Request to DALL-E API
902 public function run_image_query( $query ) {
903 $body = $this->build_body( $query );
904 $url = $this->build_url( $query );
905 $headers = $this->build_headers( $query );
906 $options = $this->build_options( $headers, $body );
907
908 try {
909 $res = $this->run_query( $url, $options );
910 $data = $res['data'];
911 $choices = [];
912 if ( $this->envType === 'azure' ) {
913 foreach ( $data['data'] as $entry ) {
914 $choices[] = [ 'url' => $entry['url'] ];
915 }
916 }
917 else {
918 $choices = $data['data'];
919 }
920
921 $reply = new Meow_MWAI_Reply( $query );
922 $model = $query->model;
923 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
924 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
925 $reply->set_usage( $usage );
926 $reply->set_choices( $choices );
927 $reply->set_type( 'images' );
928
929 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
930 foreach ( $reply->results as &$result ) {
931 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
932 'query_envId' => $query->envId,
933 'query_session' => $query->session,
934 'query_model' => $query->model,
935 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
936 $fileUrl = $this->core->files->get_url( $fileId );
937 $result = $fileUrl;
938 }
939 }
940 $reply->result = $reply->results[0];
941 return $reply;
942 }
943 catch ( Exception $e ) {
944 $service = $this->get_service_name();
945 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
946 throw new Exception( "$service: " . $e->getMessage() );
947 }
948 }
949
950 /*
951 This is the rest of the OpenAI API support, not related to the models directly.
952 */
953
954 // Check if there are errors in the response from OpenAI, and throw an exception if so.
955 protected function handle_response_errors( $data ) {
956 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
957 $message = $data['error']['message'];
958 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
959 $message = str_replace( $matches[1], '', $message );
960 }
961 throw new Exception( $message );
962 }
963 }
964
965 public function list_files( $purposeFilter = null )
966 {
967 if ( empty( $purposeFilter ) ) {
968 return $this->execute( 'GET', '/files' );
969 }
970 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
971 }
972
973 static function get_suffix_for_model($model)
974 {
975 // Legacy fine-tuned models
976 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
977 if ( count( $matches ) > 0 ) {
978 return $matches[1];
979 }
980
981 // New fine-tuned models
982 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
983 if (count($matches) > 0) {
984 return $matches[1];
985 }
986
987 return 'N/A';
988 }
989
990 static function get_model_without_release_date( $model )
991 {
992 if ( empty( $model ) ) {
993 return null;
994 }
995 return preg_replace( '/-\d{4}-\d{2}-\d{2}$/', '', $model );
996 }
997
998 public function list_deleted_finetunes( $envId = null, $legacy = false )
999 {
1000 $finetunes = $this->list_finetunes( $legacy );
1001 $deleted = [];
1002
1003 foreach ( $finetunes as $finetune ) {
1004 $name = $finetune['model'];
1005 $isSucceeded = $finetune['status'] === 'succeeded';
1006 if ( $isSucceeded ) {
1007 try {
1008 $finetune = $this->get_model( $name );
1009 }
1010 catch ( Exception $e ) {
1011 $deleted[] = $name;
1012 }
1013 }
1014 }
1015 if ( $legacy ) {
1016 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
1017 }
1018 else {
1019 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
1020 }
1021 return $deleted;
1022 }
1023
1024 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
1025 // retrieve all the models since Summer 2023, let's see! WIP.
1026 public function list_finetunes( $legacy = false )
1027 {
1028 if ( $legacy ) {
1029 $res = $this->execute( 'GET', '/fine-tunes' );
1030 }
1031 else {
1032 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
1033 }
1034 $finetunes = $res['data'];
1035
1036 // Add suffix
1037 $finetunes = array_map( function ( $finetune ) {
1038 if ( isset( $finetune['user_provided_suffix'] ) ) {
1039 $finetune['suffix'] = $finetune['user_provided_suffix'];
1040 }
1041 else {
1042 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
1043 }
1044 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] ) . ' UTC';
1045 if ( isset( $finetune['estimated_finish'] ) ) {
1046 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] ) . ' UTC';
1047 }
1048 else {
1049 $finetune['estimatedOn'] = null;
1050 }
1051 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
1052 $finetune['base_model'] = $finetune['model'];
1053 $finetune['model'] = $finetune['fine_tuned_model'];
1054 unset( $finetune['object'] );
1055 unset( $finetune['hyperparams'] );
1056 unset( $finetune['result_files'] );
1057 unset( $finetune['training_files'] );
1058 unset( $finetune['validation_files'] );
1059 unset( $finetune['created_at'] );
1060 unset( $finetune['updated_at'] );
1061 unset( $finetune['fine_tuned_model'] );
1062 return $finetune;
1063 }, $finetunes);
1064
1065 usort( $finetunes, function ( $a, $b ) {
1066 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
1067 });
1068
1069 if ( $legacy ) {
1070 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
1071 }
1072 else {
1073 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
1074 }
1075
1076 return $finetunes;
1077 }
1078
1079 public function moderate( $input ) {
1080 $result = $this->execute('POST', '/moderations', [
1081 'input' => $input
1082 ]);
1083 return $result;
1084 }
1085
1086 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
1087 {
1088 $result = $this->execute('POST', '/files', null, [
1089 'purpose' => $purpose,
1090 'data' => $data,
1091 'file' => $filename
1092 ] );
1093 return $result;
1094 }
1095
1096 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
1097 $body = [
1098 'name' => !empty( $name ) ? $name : 'default',
1099 'metadata' => $metadata
1100 ];
1101 if ( $expiry !== 'never' ) {
1102 if ( is_string( $expiry ) ) {
1103 error_log( 'AI Engine: Expiry is a string, setting it to 7 days.' );
1104 $expiry = 7;
1105 }
1106 $expiryInDays = $expiry ? max( 1, ceil( (int)$expiry / 86400 ) ) : 7;
1107 if ( $expiry && is_numeric( $expiry ) ) {
1108 $body['expires_after'] = [
1109 'anchor' => 'last_active_at',
1110 'days' => $expiryInDays
1111 ];
1112 }
1113 }
1114 $result = $this->execute( 'POST', '/vector_stores', $body, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1115 return $result['id'];
1116 }
1117
1118 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1119 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1120 'file_id' => $fileId
1121 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1122 return $result['id'];
1123
1124 }
1125
1126 public function delete_file( $fileId )
1127 {
1128 return $this->execute( 'DELETE', '/files/' . $fileId );
1129 }
1130
1131 public function get_model( $modelId )
1132 {
1133 return $this->execute( 'GET', '/models/' . $modelId );
1134 }
1135
1136 public function cancel_finetune( $fineTuneId )
1137 {
1138 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1139 }
1140
1141 public function delete_finetune( $modelId )
1142 {
1143 return $this->execute( 'DELETE', '/models/' . $modelId );
1144 }
1145
1146 public function download_file( $fileId, $newFile = null ) {
1147 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1148 $fileInfo = json_decode( (string)$fileInfo, true );
1149 if ( empty( $fileInfo ) ) {
1150 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1151 }
1152 $filename = $fileInfo['filename'];
1153 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1154 if ( empty( $newFile ) ) {
1155 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1156 $tempFile = wp_tempnam( $filename );
1157 if ( !$tempFile ) {
1158 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1159 }
1160 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1161 $newFile = $tempFile . '.' . $extension;
1162 }
1163 else {
1164 $newFile = $tempFile;
1165 }
1166 }
1167 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1168 file_put_contents( $newFile, $data );
1169 return $newFile;
1170 }
1171
1172 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1173 {
1174 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1175 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1176 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1177 (float)$hyperparams['learningRateMultiplier'] : null;
1178 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1179 (float)$hyperparams['promptLossWeight'] : null;
1180 $arguments = [
1181 'training_file' => $fileId,
1182 'model' => $model,
1183 'suffix' => $suffix
1184 ];
1185 if ( $legacy ) {
1186 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1187 }
1188 else {
1189 if ( $n_epochs ) {
1190 $arguments['hyperparams'] = [];
1191 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1192 }
1193 if ( $batch_size ) {
1194 if ( empty( $arguments['hyperparams'] ) ) {
1195 $arguments['hyperparams'] = [];
1196 }
1197 $arguments['hyperparams']['batch_size'] = $batch_size;
1198 }
1199 if ( $learning_rate_multiplier ) {
1200 if ( empty( $arguments['hyperparams'] ) ) {
1201 $arguments['hyperparams'] = [];
1202 }
1203 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1204 }
1205 if ( $prompt_loss_weight ) {
1206 if ( empty( $arguments['hyperparams'] ) ) {
1207 $arguments['hyperparams'] = [];
1208 }
1209 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1210 }
1211 if ( $model === 'turbo' ) {
1212 $arguments['model'] = 'gpt-3.5-turbo';
1213 }
1214 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1215 }
1216 return $result;
1217 }
1218
1219 /**
1220 * Build the body of a form request.
1221 * If the field name is 'file', then the field value is the filename of the file to upload.
1222 * The file contents are taken from the 'data' field.
1223 *
1224 * @param array $fields
1225 * @param string $boundary
1226 * @return string
1227 */
1228 public function build_form_body( $fields, $boundary )
1229 {
1230 $body = '';
1231 foreach ( $fields as $name => $value ) {
1232 if ( $name == 'data' ) {
1233 continue;
1234 }
1235 $body .= "--$boundary\r\n";
1236 $body .= "Content-Disposition: form-data; name=\"$name\"";
1237 if ( $name == 'file' ) {
1238 $body .= "; filename=\"{$value}\"\r\n";
1239 $body .= "Content-Type: application/json\r\n\r\n";
1240 $body .= $fields['data'] . "\r\n";
1241 }
1242 else {
1243 $body .= "\r\n\r\n$value\r\n";
1244 }
1245 }
1246 $body .= "--$boundary--\r\n";
1247 return $body;
1248 }
1249
1250 /**
1251 * Run a request to the OpenAI API.
1252 * Fore more information about the $formFields, refer to the build_form_body method.
1253 *
1254 * @param string $method POST, PUT, GET, DELETE...
1255 * @param string $url The API endpoint
1256 * @param array $query The query parameters (json)
1257 * @param array $formFields The form fields (multipart/form-data)
1258 * @param bool $json Whether to return the response as json or not
1259 * @return array
1260 */
1261 public function execute( $method, $url, $query = null, $formFields = null,
1262 $json = true, $extraHeaders = null, $streamCallback = null )
1263 {
1264 $isAzure = $this->envType === 'azure';
1265 $isOpenAI = !$isAzure;
1266
1267 // Prepare the headers
1268 $headers = "Content-Type: application/json\r\n";
1269 if ( $isOpenAI ) {
1270 $headers .= "Authorization: Bearer " . $this->apiKey . "\r\n";
1271 if ( $this->organizationId ) {
1272 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1273 }
1274 }
1275 else if ( $isAzure ) {
1276 $headers .= "api-key: " . $this->apiKey . "\r\n";
1277 }
1278
1279 // Prepare the body with json_encode, if it's not a string or null, otherwise we keep it as is.
1280 if ( !empty( $query ) && !is_string( $query ) ) {
1281 $body = json_encode( $query );
1282 }
1283 else {
1284 $body = $query;
1285 }
1286
1287 // If we have form fields, we need to change the headers and the body.
1288 if ( !empty( $formFields ) ) {
1289 $boundary = wp_generate_password( 24, false );
1290 $headers = [
1291 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
1292 ];
1293 if ( $isOpenAI ) {
1294 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
1295 if ( $this->organizationId ) {
1296 $headers['OpenAI-Organization'] = $this->organizationId;
1297 }
1298 }
1299 else if ( $isAzure ) {
1300 $headers['api-key'] = $this->apiKey;
1301 }
1302 $body = $this->build_form_body( $formFields, $boundary );
1303 }
1304
1305 // Maybe we should have headers always as an array... not sure why we have it as a string.
1306 if ( !empty( $extraHeaders ) ) {
1307 foreach ( $extraHeaders as $key => $value ) {
1308 if ( is_array( $headers ) ) {
1309 $headers[$key] = $value;
1310 }
1311 else {
1312 $headers .= "$key: $value\r\n";
1313 }
1314 }
1315 }
1316
1317 // Create the URL
1318 if ( $isOpenAI ) {
1319 $url = 'https://api.openai.com/v1' . $url;
1320 }
1321 else if ( $isAzure ) {
1322 $url = trailingslashit( $this->env['endpoint'] ) . 'openai' . $url;
1323 $hasQuery = strpos( $url, '?' ) !== false;
1324 $url = $url . ( $hasQuery ? '&' : '?' ) . $this->azureApiVersion;
1325 }
1326
1327 // If it's a GET, body should be null, and we should append the query to the URL.
1328 if ( $method === 'GET' ) {
1329 if ( !empty( $query ) ) {
1330 $hasQuery = strpos( $url, '?' ) !== false;
1331 $url = $url . ( $hasQuery ? '&' : '?' ) . http_build_query( $query );
1332 }
1333 $body = null;
1334 }
1335
1336 $options = [
1337 "headers" => $headers,
1338 "method" => $method,
1339 "timeout" => MWAI_TIMEOUT,
1340 "body" => $body,
1341 "sslverify" => false
1342 ];
1343
1344 try {
1345 if ( !is_null( $streamCallback ) ) {
1346 $options['stream'] = true;
1347 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1348 // The stream handler calls the streamCallback every time there is content
1349 // TODO: For assistants, we should probably have a different stream handler to
1350 // handle the assistant's specific reply and perform the necessary actions.
1351 $this->streamCallback = $streamCallback;
1352 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1353 }
1354 $res = wp_remote_request( $url, $options );
1355 if ( is_wp_error( $res ) ) {
1356 throw new Exception( $res->get_error_message() );
1357 }
1358 $res = wp_remote_retrieve_body( $res );
1359 $data = $json ? json_decode( $res, true ) : $res;
1360 $this->handle_response_errors( $data );
1361 return $data;
1362 }
1363 catch ( Exception $e ) {
1364 $service = $this->get_service_name();
1365 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1366 throw new Exception( "$service: " . $e->getMessage() );
1367 }
1368 finally {
1369 if ( !is_null( $streamCallback ) ) {
1370 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1371 }
1372 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1373 unlink( $options['filename'] );
1374 }
1375 }
1376 }
1377
1378 public function get_models() {
1379 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1380 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1381 foreach ( $finetunes as $finetune ) {
1382 if ( $finetune['status'] !== 'succeeded' ) {
1383 continue;
1384 }
1385 $baseModel = SELF::get_model_without_release_date( $finetune['base_model'] );
1386 if ( !empty( $baseModel ) ) {
1387 $model = null;
1388 foreach ( $models as $currentModel ) {
1389 if ( $currentModel['model'] === $baseModel ) {
1390 $model = $currentModel;
1391 break;
1392 }
1393 }
1394 if ( !empty( $model ) ) {
1395 $model['model'] = $finetune['model'];
1396 $model['name'] = $finetune['suffix'];
1397 $models[] = $model;
1398 }
1399 }
1400 }
1401 return $models;
1402 }
1403
1404 static public function get_models_static() {
1405 return MWAI_OPENAI_MODELS;
1406 }
1407
1408 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false )
1409 {
1410 $modelFamily = SELF::get_model_without_release_date( $modelFamily );
1411 $models = $this->get_models();
1412 foreach ( $models as $currentModel ) {
1413 if ( $currentModel['model'] === $modelFamily ) {
1414 if ( $currentModel['type'] === 'image' ) {
1415 if ( !$resolution ) {
1416 Meow_MWAI_Logging::warn( "(OpenAI) Image models require a resolution." );
1417 return null;
1418 }
1419 else {
1420 foreach ( $currentModel['resolutions'] as $r ) {
1421 if ( $r['name'] == $resolution ) {
1422 return $r['price'] * $outUnits;
1423 }
1424 }
1425 }
1426 }
1427 else {
1428 if ( $finetune ) {
1429 if ( isset( $currentModel['finetune']['price'] ) ) {
1430 $currentModel['price'] = $currentModel['finetune']['price'];
1431 }
1432 else if ( isset( $currentModel['finetune']['in'] ) ) {
1433 $currentModel['price'] = [
1434 'in' => $currentModel['finetune']['in'],
1435 'out' => $currentModel['finetune']['out']
1436 ];
1437 }
1438 }
1439 $inPrice = $currentModel['price'];
1440 $outPrice = $currentModel['price'];
1441 if ( is_array( $currentModel['price'] ) ) {
1442 $inPrice = $currentModel['price']['in'];
1443 $outPrice = $currentModel['price']['out'];
1444 }
1445 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1446 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1447 return $inTotalPrice + $outTotalPrice;
1448 }
1449 }
1450 }
1451 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1452 return null;
1453 }
1454
1455 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1456 {
1457 $model = $query->model;
1458 $units = 0;
1459 $finetune = false;
1460 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1461 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1462 $finetune = true;
1463 }
1464 $inUnits = $reply->get_in_tokens( $query );
1465 $outUnits = $reply->get_out_tokens();
1466 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1467 }
1468 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1469 $units = $query->maxResults;
1470 $resolution = $query->resolution;
1471 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1472 }
1473 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1474 $model = 'whisper';
1475 $units = $reply->get_units();
1476 return $this->calculate_price( $model, 0, $units, null, $finetune );
1477 }
1478 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1479 $units = $reply->get_total_tokens();
1480 return $this->calculate_price( $model, 0, $units, null, $finetune );
1481 }
1482 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1483 return null;
1484 }
1485
1486 public function get_incidents() {
1487 $url = 'https://status.openai.com/history.rss';
1488 $response = wp_remote_get( $url );
1489 if ( is_wp_error( $response ) ) {
1490 throw new Exception( $response->get_error_message() );
1491 }
1492 $response = wp_remote_retrieve_body( $response );
1493 $xml = simplexml_load_string( $response );
1494 $incidents = array();
1495 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1496 foreach ( $xml->channel->item as $item ) {
1497 $date = strtotime( $item->pubDate );
1498 if ( $date > $oneWeekAgo ) {
1499 $incidents[] = array(
1500 'title' => (string) $item->title,
1501 'description' => (string) $item->description,
1502 'date' => $date
1503 );
1504 }
1505 }
1506 return $incidents;
1507 }
1508 }
1509