PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.7.5
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.7.5
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
1496 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 get_vector_store( $vectorStoreId ) {
1125 return $this->execute( 'GET', '/vector_stores/' . $vectorStoreId, null, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1126 }
1127
1128 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1129 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1130 'file_id' => $fileId
1131 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1132 return $result['id'];
1133
1134 }
1135
1136 public function delete_file( $fileId )
1137 {
1138 return $this->execute( 'DELETE', '/files/' . $fileId );
1139 }
1140
1141 public function get_model( $modelId )
1142 {
1143 return $this->execute( 'GET', '/models/' . $modelId );
1144 }
1145
1146 public function cancel_finetune( $fineTuneId )
1147 {
1148 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1149 }
1150
1151 public function delete_finetune( $modelId )
1152 {
1153 return $this->execute( 'DELETE', '/models/' . $modelId );
1154 }
1155
1156 public function download_file( $fileId, $newFile = null ) {
1157 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1158 $fileInfo = json_decode( (string)$fileInfo, true );
1159 if ( empty( $fileInfo ) ) {
1160 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1161 }
1162 $filename = $fileInfo['filename'];
1163 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1164 if ( empty( $newFile ) ) {
1165 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1166 $tempFile = wp_tempnam( $filename );
1167 if ( !$tempFile ) {
1168 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1169 }
1170 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1171 $newFile = $tempFile . '.' . $extension;
1172 }
1173 else {
1174 $newFile = $tempFile;
1175 }
1176 }
1177 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1178 file_put_contents( $newFile, $data );
1179 return $newFile;
1180 }
1181
1182 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1183 {
1184 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1185 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1186 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1187 (float)$hyperparams['learningRateMultiplier'] : null;
1188 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1189 (float)$hyperparams['promptLossWeight'] : null;
1190 $arguments = [
1191 'training_file' => $fileId,
1192 'model' => $model,
1193 'suffix' => $suffix
1194 ];
1195 if ( $legacy ) {
1196 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1197 }
1198 else {
1199 if ( $n_epochs ) {
1200 $arguments['hyperparams'] = [];
1201 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1202 }
1203 if ( $batch_size ) {
1204 if ( empty( $arguments['hyperparams'] ) ) {
1205 $arguments['hyperparams'] = [];
1206 }
1207 $arguments['hyperparams']['batch_size'] = $batch_size;
1208 }
1209 if ( $learning_rate_multiplier ) {
1210 if ( empty( $arguments['hyperparams'] ) ) {
1211 $arguments['hyperparams'] = [];
1212 }
1213 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1214 }
1215 if ( $prompt_loss_weight ) {
1216 if ( empty( $arguments['hyperparams'] ) ) {
1217 $arguments['hyperparams'] = [];
1218 }
1219 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1220 }
1221 if ( $model === 'turbo' ) {
1222 $arguments['model'] = 'gpt-3.5-turbo';
1223 }
1224 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1225 }
1226 return $result;
1227 }
1228
1229 /**
1230 * Build the body of a form request.
1231 * If the field name is 'file', then the field value is the filename of the file to upload.
1232 * The file contents are taken from the 'data' field.
1233 *
1234 * @param array $fields
1235 * @param string $boundary
1236 * @return string
1237 */
1238 public function build_form_body( $fields, $boundary )
1239 {
1240 $body = '';
1241 foreach ( $fields as $name => $value ) {
1242 if ( $name == 'data' ) {
1243 continue;
1244 }
1245 $body .= "--$boundary\r\n";
1246 $body .= "Content-Disposition: form-data; name=\"$name\"";
1247 if ( $name == 'file' ) {
1248 $body .= "; filename=\"{$value}\"\r\n";
1249 $body .= "Content-Type: application/json\r\n\r\n";
1250 $body .= $fields['data'] . "\r\n";
1251 }
1252 else {
1253 $body .= "\r\n\r\n$value\r\n";
1254 }
1255 }
1256 $body .= "--$boundary--\r\n";
1257 return $body;
1258 }
1259
1260 /**
1261 * Run a request to the OpenAI API.
1262 * Fore more information about the $formFields, refer to the build_form_body method.
1263 *
1264 * @param string $method POST, PUT, GET, DELETE...
1265 * @param string $url The API endpoint
1266 * @param array $query The query parameters (json)
1267 * @param array $formFields The form fields (multipart/form-data)
1268 * @param bool $json Whether to return the response as json or not
1269 * @return array
1270 */
1271 public function execute( $method, $url, $query = null, $formFields = null,
1272 $json = true, $extraHeaders = null, $streamCallback = null )
1273 {
1274 $isAzure = $this->envType === 'azure';
1275 $isOpenAI = !$isAzure;
1276
1277 // Prepare the headers
1278 $headers = "Content-Type: application/json\r\n";
1279 if ( $isOpenAI ) {
1280 $headers .= "Authorization: Bearer " . $this->apiKey . "\r\n";
1281 if ( $this->organizationId ) {
1282 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1283 }
1284 }
1285 else if ( $isAzure ) {
1286 $headers .= "api-key: " . $this->apiKey . "\r\n";
1287 }
1288
1289 // Prepare the body with json_encode, if it's not a string or null, otherwise we keep it as is.
1290 if ( !empty( $query ) && !is_string( $query ) ) {
1291 $body = json_encode( $query );
1292 }
1293 else {
1294 $body = $query;
1295 }
1296
1297 // If we have form fields, we need to change the headers and the body.
1298 if ( !empty( $formFields ) ) {
1299 $boundary = wp_generate_password( 24, false );
1300 $headers = [
1301 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
1302 ];
1303 if ( $isOpenAI ) {
1304 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
1305 if ( $this->organizationId ) {
1306 $headers['OpenAI-Organization'] = $this->organizationId;
1307 }
1308 }
1309 else if ( $isAzure ) {
1310 $headers['api-key'] = $this->apiKey;
1311 }
1312 $body = $this->build_form_body( $formFields, $boundary );
1313 }
1314
1315 // Maybe we should have headers always as an array... not sure why we have it as a string.
1316 if ( !empty( $extraHeaders ) ) {
1317 foreach ( $extraHeaders as $key => $value ) {
1318 if ( is_array( $headers ) ) {
1319 $headers[$key] = $value;
1320 }
1321 else {
1322 $headers .= "$key: $value\r\n";
1323 }
1324 }
1325 }
1326
1327 // Create the URL
1328 if ( $isOpenAI ) {
1329 $url = 'https://api.openai.com/v1' . $url;
1330 }
1331 else if ( $isAzure ) {
1332 $url = trailingslashit( $this->env['endpoint'] ) . 'openai' . $url;
1333 $hasQuery = strpos( $url, '?' ) !== false;
1334 $url = $url . ( $hasQuery ? '&' : '?' ) . $this->azureApiVersion;
1335 }
1336
1337 // If it's a GET, body should be null, and we should append the query to the URL.
1338 if ( $method === 'GET' ) {
1339 if ( !empty( $query ) ) {
1340 $hasQuery = strpos( $url, '?' ) !== false;
1341 $url = $url . ( $hasQuery ? '&' : '?' ) . http_build_query( $query );
1342 }
1343 $body = null;
1344 }
1345
1346 $options = [
1347 "headers" => $headers,
1348 "method" => $method,
1349 "timeout" => MWAI_TIMEOUT,
1350 "body" => $body,
1351 "sslverify" => false
1352 ];
1353
1354 try {
1355 if ( !is_null( $streamCallback ) ) {
1356 $options['stream'] = true;
1357 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1358 // The stream handler calls the streamCallback every time there is content
1359 // TODO: For assistants, we should probably have a different stream handler to
1360 // handle the assistant's specific reply and perform the necessary actions.
1361 $this->streamCallback = $streamCallback;
1362 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1363 }
1364 $res = wp_remote_request( $url, $options );
1365 if ( is_wp_error( $res ) ) {
1366 throw new Exception( $res->get_error_message() );
1367 }
1368 $res = wp_remote_retrieve_body( $res );
1369 $data = $json ? json_decode( $res, true ) : $res;
1370 $this->handle_response_errors( $data );
1371 return $data;
1372 }
1373 catch ( Exception $e ) {
1374 $service = $this->get_service_name();
1375 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1376 throw new Exception( "$service: " . $e->getMessage() );
1377 }
1378 finally {
1379 if ( !is_null( $streamCallback ) ) {
1380 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1381 }
1382 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1383 unlink( $options['filename'] );
1384 }
1385 }
1386 }
1387
1388 public function get_models() {
1389 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1390 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1391 foreach ( $finetunes as $finetune ) {
1392 if ( $finetune['status'] !== 'succeeded' ) {
1393 continue;
1394 }
1395 $baseModel = SELF::get_model_without_release_date( $finetune['base_model'] );
1396 if ( !empty( $baseModel ) ) {
1397 $model = null;
1398 foreach ( $models as $currentModel ) {
1399 if ( $currentModel['model'] === $baseModel ) {
1400 $model = $currentModel;
1401 break;
1402 }
1403 }
1404 if ( !empty( $model ) ) {
1405 $model['model'] = $finetune['model'];
1406 $model['name'] = $finetune['suffix'];
1407 $models[] = $model;
1408 }
1409 }
1410 }
1411 return $models;
1412 }
1413
1414 static public function get_models_static() {
1415 return MWAI_OPENAI_MODELS;
1416 }
1417
1418 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false )
1419 {
1420 $modelFamily = SELF::get_model_without_release_date( $modelFamily );
1421 $models = $this->get_models();
1422 foreach ( $models as $currentModel ) {
1423 if ( $currentModel['model'] === $modelFamily ) {
1424 if ( $currentModel['type'] === 'image' ) {
1425 if ( !$resolution ) {
1426 Meow_MWAI_Logging::warn( "(OpenAI) Image models require a resolution." );
1427 return null;
1428 }
1429 else {
1430 foreach ( $currentModel['resolutions'] as $r ) {
1431 if ( $r['name'] == $resolution ) {
1432 return $r['price'] * $outUnits;
1433 }
1434 }
1435 }
1436 }
1437 else {
1438 if ( $finetune ) {
1439 if ( isset( $currentModel['finetune']['price'] ) ) {
1440 $currentModel['price'] = $currentModel['finetune']['price'];
1441 }
1442 else if ( isset( $currentModel['finetune']['in'] ) ) {
1443 $currentModel['price'] = [
1444 'in' => $currentModel['finetune']['in'],
1445 'out' => $currentModel['finetune']['out']
1446 ];
1447 }
1448 }
1449 $inPrice = $currentModel['price'];
1450 $outPrice = $currentModel['price'];
1451 if ( is_array( $currentModel['price'] ) ) {
1452 $inPrice = $currentModel['price']['in'];
1453 $outPrice = $currentModel['price']['out'];
1454 }
1455 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1456 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1457 return $inTotalPrice + $outTotalPrice;
1458 }
1459 }
1460 }
1461 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1462 return null;
1463 }
1464
1465 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1466 {
1467 $model = $query->model;
1468 $units = 0;
1469 $finetune = false;
1470 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1471 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1472 $finetune = true;
1473 }
1474 $inUnits = $reply->get_in_tokens( $query );
1475 $outUnits = $reply->get_out_tokens();
1476 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1477 }
1478 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1479 $units = $query->maxResults;
1480 $resolution = $query->resolution;
1481 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1482 }
1483 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1484 $model = 'whisper';
1485 $units = $reply->get_units();
1486 return $this->calculate_price( $model, 0, $units, null, $finetune );
1487 }
1488 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1489 $units = $reply->get_total_tokens();
1490 return $this->calculate_price( $model, 0, $units, null, $finetune );
1491 }
1492 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1493 return null;
1494 }
1495 }
1496