PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.0
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 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
1452 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 $expiryInDays = $expiry ? max( 1, ceil( $expiry / 86400 ) ) : 7;
1078 $result = $this->execute( 'POST', '/vector_stores', [
1079 'name' => !empty( $name ) ? $name : 'default',
1080 'metadata' => $metadata,
1081 'expires_after' => [
1082 'anchor' => 'last_active_at',
1083 'days' => $expiryInDays
1084 ]
1085 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1086 return $result['id'];
1087 }
1088
1089 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1090 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1091 'file_id' => $fileId
1092 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1093 return $result['id'];
1094
1095 }
1096
1097 public function delete_file( $fileId )
1098 {
1099 return $this->execute( 'DELETE', '/files/' . $fileId );
1100 }
1101
1102 public function get_model( $modelId )
1103 {
1104 return $this->execute( 'GET', '/models/' . $modelId );
1105 }
1106
1107 public function cancel_finetune( $fineTuneId )
1108 {
1109 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1110 }
1111
1112 public function delete_finetune( $modelId )
1113 {
1114 return $this->execute( 'DELETE', '/models/' . $modelId );
1115 }
1116
1117 public function download_file( $fileId, $newFile = null ) {
1118 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1119 $fileInfo = json_decode( (string)$fileInfo, true );
1120 if ( empty( $fileInfo ) ) {
1121 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1122 }
1123 $filename = $fileInfo['filename'];
1124 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1125 if ( empty( $newFile ) ) {
1126 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1127 $tempFile = wp_tempnam( $filename );
1128 if ( !$tempFile ) {
1129 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1130 }
1131 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1132 $newFile = $tempFile . '.' . $extension;
1133 }
1134 else {
1135 $newFile = $tempFile;
1136 }
1137 }
1138 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1139 file_put_contents( $newFile, $data );
1140 return $newFile;
1141 }
1142
1143 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1144 {
1145 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1146 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1147 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1148 (float)$hyperparams['learningRateMultiplier'] : null;
1149 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1150 (float)$hyperparams['promptLossWeight'] : null;
1151 $arguments = [
1152 'training_file' => $fileId,
1153 'model' => $model,
1154 'suffix' => $suffix
1155 ];
1156 if ( $legacy ) {
1157 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1158 }
1159 else {
1160 if ( $n_epochs ) {
1161 $arguments['hyperparams'] = [];
1162 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1163 }
1164 if ( $batch_size ) {
1165 if ( empty( $arguments['hyperparams'] ) ) {
1166 $arguments['hyperparams'] = [];
1167 }
1168 $arguments['hyperparams']['batch_size'] = $batch_size;
1169 }
1170 if ( $learning_rate_multiplier ) {
1171 if ( empty( $arguments['hyperparams'] ) ) {
1172 $arguments['hyperparams'] = [];
1173 }
1174 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1175 }
1176 if ( $prompt_loss_weight ) {
1177 if ( empty( $arguments['hyperparams'] ) ) {
1178 $arguments['hyperparams'] = [];
1179 }
1180 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1181 }
1182 if ( $model === 'turbo' ) {
1183 $arguments['model'] = 'gpt-3.5-turbo';
1184 }
1185 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1186 }
1187 return $result;
1188 }
1189
1190 /**
1191 * Build the body of a form request.
1192 * If the field name is 'file', then the field value is the filename of the file to upload.
1193 * The file contents are taken from the 'data' field.
1194 *
1195 * @param array $fields
1196 * @param string $boundary
1197 * @return string
1198 */
1199 public function build_form_body( $fields, $boundary )
1200 {
1201 $body = '';
1202 foreach ( $fields as $name => $value ) {
1203 if ( $name == 'data' ) {
1204 continue;
1205 }
1206 $body .= "--$boundary\r\n";
1207 $body .= "Content-Disposition: form-data; name=\"$name\"";
1208 if ( $name == 'file' ) {
1209 $body .= "; filename=\"{$value}\"\r\n";
1210 $body .= "Content-Type: application/json\r\n\r\n";
1211 $body .= $fields['data'] . "\r\n";
1212 }
1213 else {
1214 $body .= "\r\n\r\n$value\r\n";
1215 }
1216 }
1217 $body .= "--$boundary--\r\n";
1218 return $body;
1219 }
1220
1221 /**
1222 * Run a request to the OpenAI API.
1223 * Fore more information about the $formFields, refer to the build_form_body method.
1224 *
1225 * @param string $method POST, PUT, GET, DELETE...
1226 * @param string $url The API endpoint
1227 * @param array $query The query parameters (json)
1228 * @param array $formFields The form fields (multipart/form-data)
1229 * @param bool $json Whether to return the response as json or not
1230 * @return array
1231 */
1232 public function execute( $method, $url, $query = null, $formFields = null,
1233 $json = true, $extraHeaders = null, $streamCallback = null )
1234 {
1235 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
1236 if ( $this->organizationId ) {
1237 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1238 }
1239 $body = $query ? json_encode( $query ) : null;
1240 if ( !empty( $formFields ) ) {
1241 $boundary = wp_generate_password( 24, false );
1242 $headers = [
1243 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
1244 'Authorization' => 'Bearer ' . $this->apiKey
1245 ];
1246 if ( $this->organizationId ) {
1247 $headers['OpenAI-Organization'] = $this->organizationId;
1248 }
1249 $body = $this->build_form_body( $formFields, $boundary );
1250 }
1251
1252 // Maybe we should have headers always as an array... not sure why we have it as a string.
1253 if ( !empty( $extraHeaders ) ) {
1254 foreach ( $extraHeaders as $key => $value ) {
1255 if ( is_array( $headers ) ) {
1256 $headers[$key] = $value;
1257 }
1258 else {
1259 $headers .= "$key: $value\r\n";
1260 }
1261 }
1262 }
1263
1264 // If it's a GET, body should be null, and we should append the query to the URL.
1265 if ( $method === 'GET' ) {
1266 if ( !empty( $query ) ) {
1267 $url .= '?' . http_build_query( $query );
1268 }
1269 $body = null;
1270 }
1271
1272 $url = 'https://api.openai.com/v1' . $url;
1273 $options = [
1274 "headers" => $headers,
1275 "method" => $method,
1276 "timeout" => MWAI_TIMEOUT,
1277 "body" => $body,
1278 "sslverify" => false
1279 ];
1280
1281 try {
1282 if ( !is_null( $streamCallback ) ) {
1283 $options['stream'] = true;
1284 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1285 // The stream handler calls the streamCallback every time there is content
1286 // TODO: For assistants, we should probably have a different stream handler to
1287 // handle the assistant's specific reply and perform the necessary actions.
1288 $this->streamCallback = $streamCallback;
1289 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1290 }
1291 $res = wp_remote_request( $url, $options );
1292 if ( is_wp_error( $res ) ) {
1293 throw new Exception( $res->get_error_message() );
1294 }
1295 $res = wp_remote_retrieve_body( $res );
1296 $data = $json ? json_decode( $res, true ) : $res;
1297 $this->handle_response_errors( $data );
1298 return $data;
1299 }
1300 catch ( Exception $e ) {
1301 $this->core->log( '❌ (OpenAI) ' . $e->getMessage() );
1302 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
1303 }
1304 finally {
1305 if ( !is_null( $streamCallback ) ) {
1306 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1307 }
1308 }
1309 }
1310
1311 public function get_models() {
1312 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1313 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1314 foreach ( $finetunes as $finetune ) {
1315 if ( $finetune['status'] !== 'succeeded' ) {
1316 continue;
1317 }
1318 $baseModel = SELF::get_finetune_base_model( $finetune['model'] );
1319 if ( !empty( $baseModel ) ) {
1320 $model = null;
1321 foreach ( $models as $currentModel ) {
1322 if ( $currentModel['model'] === $baseModel ) {
1323 $model = $currentModel;
1324 break;
1325 }
1326 }
1327 if ( !empty( $model ) ) {
1328 $model['model'] = $finetune['model'];
1329 $model['name'] = SELF::get_suffix_for_model( $finetune['model'] );
1330 $models[] = $model;
1331 }
1332 }
1333 }
1334 return $models;
1335 }
1336
1337 static public function get_models_static() {
1338 return MWAI_OPENAI_MODELS;
1339 }
1340
1341 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
1342 {
1343 // For fine-tuned models:
1344 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
1345 if ( !empty( $potentialBaseModel ) ) {
1346 $modelFamily = $potentialBaseModel;
1347 $finetune = true;
1348 }
1349
1350 $models = $this->get_models();
1351 foreach ( $models as $currentModel ) {
1352 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
1353 if ( $currentModel['type'] === 'image' ) {
1354 if ( !$option ) {
1355 $this->core->log( "⚠️ (OpenAI) Image models require an option." );
1356 return null;
1357 }
1358 else {
1359 foreach ( $currentModel['options'] as $imageType ) {
1360 if ( $imageType['option'] == $option ) {
1361 return $imageType['price'] * $outUnits;
1362 }
1363 }
1364 }
1365 }
1366 else {
1367 if ( $finetune ) {
1368
1369 if ( isset( $currentModel['finetune']['price'] ) ) {
1370 $currentModel['price'] = $currentModel['finetune']['price'];
1371 }
1372 else if ( isset( $currentModel['finetune']['in'] ) ) {
1373 $currentModel['price'] = [
1374 'in' => $currentModel['finetune']['in'],
1375 'out' => $currentModel['finetune']['out']
1376 ];
1377 }
1378 }
1379 $inPrice = $currentModel['price'];
1380 $outPrice = $currentModel['price'];
1381 if ( is_array( $currentModel['price'] ) ) {
1382 $inPrice = $currentModel['price']['in'];
1383 $outPrice = $currentModel['price']['out'];
1384 }
1385 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1386 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1387 return $inTotalPrice + $outTotalPrice;
1388 }
1389 }
1390 }
1391 $this->core->log( "⚠️ (OpenAI) Invalid model ($modelFamily)." );
1392 return null;
1393 }
1394
1395 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1396 {
1397 $model = $query->model;
1398 $units = 0;
1399 $option = null;
1400
1401 $finetune = false;
1402 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1403 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1404 $finetune = true;
1405 }
1406 $inUnits = $reply->get_in_tokens( $query );
1407 $outUnits = $reply->get_out_tokens();
1408 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
1409 }
1410 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1411 /** @var Meow_MWAI_Query_Image $query */
1412 $units = $query->maxResults;
1413 $option = $query->resolution;
1414 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1415 }
1416 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1417 $model = 'whisper';
1418 $units = $reply->get_units();
1419 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1420 }
1421 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1422 $units = $reply->get_total_tokens();
1423 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1424 }
1425 $this->core->log( "⚠️ (OpenAI) Cannot calculate price for $model.");
1426 return null;
1427 }
1428
1429 public function get_incidents() {
1430 $url = 'https://status.openai.com/history.rss';
1431 $response = wp_remote_get( $url );
1432 if ( is_wp_error( $response ) ) {
1433 throw new Exception( $response->get_error_message() );
1434 }
1435 $response = wp_remote_retrieve_body( $response );
1436 $xml = simplexml_load_string( $response );
1437 $incidents = array();
1438 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1439 foreach ( $xml->channel->item as $item ) {
1440 $date = strtotime( $item->pubDate );
1441 if ( $date > $oneWeekAgo ) {
1442 $incidents[] = array(
1443 'title' => (string) $item->title,
1444 'description' => (string) $item->description,
1445 'date' => $date
1446 );
1447 }
1448 }
1449 return $incidents;
1450 }
1451 }
1452