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