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