PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.7.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.7.2
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
anthropic.php 1 year ago core.php 1 year ago factory.php 1 year ago google.php 1 year ago huggingface.php 1 year ago openai.php 1 year ago openrouter.php 1 year ago replicate.php 1 year ago
openai.php
1513 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 with json_encode, if it's not a string or null, otherwise we keep it as is.
1284 if ( !empty( $query ) && !is_string( $query ) ) {
1285 $body = json_encode( $query );
1286 }
1287 else {
1288 $body = $query;
1289 }
1290
1291 // If we have form fields, we need to change the headers and the body.
1292 if ( !empty( $formFields ) ) {
1293 $boundary = wp_generate_password( 24, false );
1294 $headers = [
1295 'Content-Type' => 'multipart/form-data; boundary=' . $boundary
1296 ];
1297 if ( $isOpenAI ) {
1298 $headers['Authorization'] = 'Bearer ' . $this->apiKey;
1299 if ( $this->organizationId ) {
1300 $headers['OpenAI-Organization'] = $this->organizationId;
1301 }
1302 }
1303 else if ( $isAzure ) {
1304 $headers['api-key'] = $this->apiKey;
1305 }
1306 $body = $this->build_form_body( $formFields, $boundary );
1307 }
1308
1309 // Maybe we should have headers always as an array... not sure why we have it as a string.
1310 if ( !empty( $extraHeaders ) ) {
1311 foreach ( $extraHeaders as $key => $value ) {
1312 if ( is_array( $headers ) ) {
1313 $headers[$key] = $value;
1314 }
1315 else {
1316 $headers .= "$key: $value\r\n";
1317 }
1318 }
1319 }
1320
1321 // Create the URL
1322 if ( $isOpenAI ) {
1323 $url = 'https://api.openai.com/v1' . $url;
1324 }
1325 else if ( $isAzure ) {
1326 $url = trailingslashit( $this->env['endpoint'] ) . 'openai' . $url;
1327 $hasQuery = strpos( $url, '?' ) !== false;
1328 $url = $url . ( $hasQuery ? '&' : '?' ) . $this->azureApiVersion;
1329 }
1330
1331 // If it's a GET, body should be null, and we should append the query to the URL.
1332 if ( $method === 'GET' ) {
1333 if ( !empty( $query ) ) {
1334 $hasQuery = strpos( $url, '?' ) !== false;
1335 $url = $url . ( $hasQuery ? '&' : '?' ) . http_build_query( $query );
1336 }
1337 $body = null;
1338 }
1339
1340 $options = [
1341 "headers" => $headers,
1342 "method" => $method,
1343 "timeout" => MWAI_TIMEOUT,
1344 "body" => $body,
1345 "sslverify" => false
1346 ];
1347
1348 try {
1349 if ( !is_null( $streamCallback ) ) {
1350 $options['stream'] = true;
1351 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1352 // The stream handler calls the streamCallback every time there is content
1353 // TODO: For assistants, we should probably have a different stream handler to
1354 // handle the assistant's specific reply and perform the necessary actions.
1355 $this->streamCallback = $streamCallback;
1356 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1357 }
1358 $res = wp_remote_request( $url, $options );
1359 if ( is_wp_error( $res ) ) {
1360 throw new Exception( $res->get_error_message() );
1361 }
1362 $res = wp_remote_retrieve_body( $res );
1363 $data = $json ? json_decode( $res, true ) : $res;
1364 $this->handle_response_errors( $data );
1365 return $data;
1366 }
1367 catch ( Exception $e ) {
1368 $service = $this->get_service_name();
1369 Meow_MWAI_Logging::error( "$service: " . $e->getMessage() );
1370 throw new Exception( "$service: " . $e->getMessage() );
1371 }
1372 finally {
1373 if ( !is_null( $streamCallback ) ) {
1374 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1375 }
1376 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1377 unlink( $options['filename'] );
1378 }
1379 }
1380 }
1381
1382 public function get_models() {
1383 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1384 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1385 foreach ( $finetunes as $finetune ) {
1386 if ( $finetune['status'] !== 'succeeded' ) {
1387 continue;
1388 }
1389 $baseModel = SELF::get_model_without_release_date( $finetune['base_model'] );
1390 if ( !empty( $baseModel ) ) {
1391 $model = null;
1392 foreach ( $models as $currentModel ) {
1393 if ( $currentModel['model'] === $baseModel ) {
1394 $model = $currentModel;
1395 break;
1396 }
1397 }
1398 if ( !empty( $model ) ) {
1399 $model['model'] = $finetune['model'];
1400 $model['name'] = $finetune['suffix'];
1401 $models[] = $model;
1402 }
1403 }
1404 }
1405 return $models;
1406 }
1407
1408 static public function get_models_static() {
1409 return MWAI_OPENAI_MODELS;
1410 }
1411
1412 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false )
1413 {
1414 $modelFamily = SELF::get_model_without_release_date( $modelFamily );
1415 $models = $this->get_models();
1416 foreach ( $models as $currentModel ) {
1417 if ( $currentModel['model'] === $modelFamily ) {
1418 if ( $currentModel['type'] === 'image' ) {
1419 if ( !$resolution ) {
1420 Meow_MWAI_Logging::warn( "(OpenAI) Image models require a resolution." );
1421 return null;
1422 }
1423 else {
1424 foreach ( $currentModel['resolutions'] as $r ) {
1425 if ( $r['name'] == $resolution ) {
1426 return $r['price'] * $outUnits;
1427 }
1428 }
1429 }
1430 }
1431 else {
1432 if ( $finetune ) {
1433 if ( isset( $currentModel['finetune']['price'] ) ) {
1434 $currentModel['price'] = $currentModel['finetune']['price'];
1435 }
1436 else if ( isset( $currentModel['finetune']['in'] ) ) {
1437 $currentModel['price'] = [
1438 'in' => $currentModel['finetune']['in'],
1439 'out' => $currentModel['finetune']['out']
1440 ];
1441 }
1442 }
1443 $inPrice = $currentModel['price'];
1444 $outPrice = $currentModel['price'];
1445 if ( is_array( $currentModel['price'] ) ) {
1446 $inPrice = $currentModel['price']['in'];
1447 $outPrice = $currentModel['price']['out'];
1448 }
1449 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1450 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1451 return $inTotalPrice + $outTotalPrice;
1452 }
1453 }
1454 }
1455 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1456 return null;
1457 }
1458
1459 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1460 {
1461 $model = $query->model;
1462 $units = 0;
1463 $finetune = false;
1464 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1465 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1466 $finetune = true;
1467 }
1468 $inUnits = $reply->get_in_tokens( $query );
1469 $outUnits = $reply->get_out_tokens();
1470 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1471 }
1472 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1473 $units = $query->maxResults;
1474 $resolution = $query->resolution;
1475 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1476 }
1477 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1478 $model = 'whisper';
1479 $units = $reply->get_units();
1480 return $this->calculate_price( $model, 0, $units, null, $finetune );
1481 }
1482 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1483 $units = $reply->get_total_tokens();
1484 return $this->calculate_price( $model, 0, $units, null, $finetune );
1485 }
1486 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1487 return null;
1488 }
1489
1490 public function get_incidents() {
1491 $url = 'https://status.openai.com/history.rss';
1492 $response = wp_remote_get( $url );
1493 if ( is_wp_error( $response ) ) {
1494 throw new Exception( $response->get_error_message() );
1495 }
1496 $response = wp_remote_retrieve_body( $response );
1497 $xml = simplexml_load_string( $response );
1498 $incidents = array();
1499 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1500 foreach ( $xml->channel->item as $item ) {
1501 $date = strtotime( $item->pubDate );
1502 if ( $date > $oneWeekAgo ) {
1503 $incidents[] = array(
1504 'title' => (string) $item->title,
1505 'description' => (string) $item->description,
1506 'date' => $date
1507 );
1508 }
1509 }
1510 return $incidents;
1511 }
1512 }
1513