PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.3.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.3.6
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
anthropic.php 2 years ago core.php 2 years ago factory.php 2 years ago google.php 2 years ago huggingface.php 2 years ago openai.php 2 years ago openrouter.php 2 years ago
openai.php
1386 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=2023-12-01-preview';
12
13 // Response
14 protected $inModel = null;
15 protected $inId = null;
16 protected $inThreadId = null;
17
18 // Streaming
19 protected $streamFunctionCall = null;
20 protected $streamToolCalls = [];
21 protected $streamLastMessage = null;
22
23 // Static
24 private static $creating = false;
25
26 public static function create( $core, $env ) {
27 self::$creating = true;
28 if ( class_exists( 'MeowPro_MWAI_OpenAI' ) ) {
29 $instance = new MeowPro_MWAI_OpenAI( $core, $env );
30 }
31 else {
32 $instance = new self( $core, $env );
33 }
34 self::$creating = false;
35 return $instance;
36 }
37
38 public function __construct( $core, $env )
39 {
40 $isOwnClass = get_class( $this ) === 'Meow_MWAI_Engines_OpenAI';
41 if ( $isOwnClass && !self::$creating ) {
42 throw new \Exception( "Please use the create() method to instantiate the Meow_MWAI_Engines_OpenAI class." );
43 }
44 parent::__construct( $core, $env );
45 $this->set_environment();
46 }
47
48 public function reset_stream() {
49 $this->streamContent = null;
50 $this->streamBuffer = null;
51 $this->streamFunctionCall = null;
52 $this->streamToolCalls = [];
53 $this->streamLastMessage = null;
54 $this->inModel = null;
55 $this->inId = null;
56 }
57
58 protected function set_environment() {
59 $env = $this->env;
60 $this->apiKey = $env['apikey'];
61
62 if ( isset( $env['organizationId'] ) ) {
63 $this->organizationId = $env['organizationId'];
64 }
65 if ( $this->envType === 'azure' ) {
66 $this->azureDeployments = isset( $env['deployments'] ) ? $env['deployments'] : [];
67 $this->azureDeployments[] = [ 'model' => 'dall-e', 'name' => 'dall-e' ];
68 }
69 }
70
71 private function get_azure_deployment_name( $model ) {
72 foreach ( $this->azureDeployments as $deployment ) {
73 if ( $deployment['model'] === $model && !empty( $deployment['name'] ) ) {
74 return $deployment['name'];
75 }
76 }
77 throw new Exception( 'Unknown deployment for model: ' . $model );
78 }
79
80 protected function get_service_name() {
81 return $this->envType === 'azure' ? 'Azure' : 'OpenAI';
82 }
83
84 private function build_prompt( $query ) {
85 $prompt = "";
86 if ( $query->mode === 'chat' ) {
87 $prompt = $query->instructions . "\n\n";
88 foreach ( $query->messages as $message ) {
89 $role = $message['role'];
90 $content = $message['content'];
91 if ( $role === 'system' ) {
92 $prompt .= "$content\n\n";
93 }
94 if ( $role === 'user' ) {
95 $prompt .= "User: $content\n";
96 }
97 if ( $role === 'assistant' ) {
98 $prompt .= "AI: $content\n";
99 }
100 }
101 $prompt .= "AI: ";
102 }
103 else if ( $query->mode === 'completion' ) {
104 $prompt = $query->get_message();
105 }
106 return $prompt;
107 }
108
109 protected function build_messages( $query ) {
110 $messages = [];
111
112 // First, we need to add the first message (the instructions).
113 if ( !empty( $query->instructions ) ) {
114 $messages[] = [ 'role' => 'system', 'content' => $query->instructions ];
115 }
116
117 // Then, if any, we need to add the 'messages', they are already formatted.
118 foreach ( $query->messages as $message ) {
119 $messages[] = $message;
120 }
121
122 // If there is a context, we need to add it.
123 if ( !empty( $query->context ) ) {
124 $messages[] = [ 'role' => 'system', 'content' => $query->context ];
125 }
126
127 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
128 if ( $query->attachedFile ) {
129 $finalUrl = null;
130 $remote_upload = $this->core->get_option( 'image_remote_upload' );
131 if ( $remote_upload === 'url' ) {
132 $finalUrl = $query->attachedFile->get_url();
133 }
134 else {
135 $finalUrl = $query->attachedFile->get_inline_base64_url();
136 }
137 $messages[] = [
138 'role' => 'user',
139 'content' => [
140 [
141 "type" => "text",
142 "text" => $query->get_message()
143 ],
144 [
145 "type" => "image_url",
146 "image_url" => [
147 "url" => $finalUrl
148 ]
149 ]
150 ]
151 ];
152 }
153 else {
154 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
155 }
156
157 return $messages;
158 }
159
160 protected function build_body( $query, $streamCallback = null, $extra = null ) {
161 if ( $query instanceof Meow_MWAI_Query_Text ) {
162 $body = array(
163 "model" => $query->model,
164 "n" => $query->maxResults,
165 "max_tokens" => $query->maxTokens,
166 "temperature" => $query->temperature,
167 "stream" => !is_null( $streamCallback ),
168 );
169
170 if ( !empty( $query->stop ) ) {
171 $body['stop'] = $query->stop;
172 }
173
174 if ( !empty( $query->responseFormat ) ) {
175 if ( $query->responseFormat === 'json' ) {
176 $body['response_format'] = [ 'type' => 'json_object' ];
177 }
178 }
179
180 if ( !empty( $query->functions ) ) {
181 $model = $this->retrieve_model_info( $query->model );
182 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
183 $this->core->log( '⚠️ (OpenAI) The model ' . $query->model . ' doesn\'t support Function Calling.' );
184 }
185 else if ( strpos( $query->model, 'ft:' ) === 0 ) {
186 $this->core->log( '⚠️ (OpenAI) OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
187 }
188 else {
189 $body['tools'] = [];
190 // Dynamic function: they will interactively enhance the completion (tools).
191 foreach ( $query->functions as $function ) {
192 $body['tools'][] = [
193 'type' => 'function',
194 'function' => $function->serializeForOpenAI()
195 ];
196 }
197 // Static functions: they will be executed at the end of the completion.
198 //$body['function_call'] = $query->functionCall;
199 }
200 }
201
202 if ( $query->mode === 'chat' ) {
203 $body['messages'] = $this->build_messages( $query );
204 }
205 else if ( $query->mode === 'completion' ) {
206 $body['prompt'] = $this->build_prompt( $query );
207 }
208
209 // Add the feedback if it's a feedback query.
210 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
211 if ( !empty( $query->blocks ) ) {
212 foreach ( $query->blocks as $feedback_block ) {
213 $body['messages'][] = $feedback_block['rawMessage'];
214 foreach ( $feedback_block['feedbacks'] as $feedback ) {
215 $body['messages'][] = [
216 'tool_call_id' => $feedback['request']['toolId'],
217 "role" => "tool",
218 'name' => $feedback['request']['name'],
219 'content' => $feedback['reply']['value']
220 ];
221 }
222 }
223 }
224 return $body;
225 }
226
227 return $body;
228 }
229 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
230 $body = array(
231 'prompt' => $query->message,
232 'model' => $query->model,
233 'response_format' => 'text',
234 'file' => basename( $query->url ),
235 'data' => $extra
236 );
237 return $body;
238 }
239 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
240 $body = array( 'input' => $query->message, 'model' => $query->model );
241 if ( $this->envType === 'azure' ) {
242 $body = array( "input" => $query->message );
243 }
244 // Dimensions are only supported by v3 models
245 if ( !empty( $query->dimensions ) && strpos( $query->model, 'ada-002' ) === false ) {
246 $body['dimensions'] = $query->dimensions;
247 }
248 return $body;
249 }
250 else if ( $query instanceof Meow_MWAI_Query_Image ) {
251 $model = $query->model;
252 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
253 $body = array(
254 "prompt" => $query->message,
255 "n" => $query->maxResults,
256 "size" => $resolution,
257 );
258 if ( $model === 'dall-e-3' ) {
259 $body['model'] = 'dall-e-3';
260 }
261 if ( $model === 'dall-e-3-hd' ) {
262 $body['model'] = 'dall-e-3';
263 $body['quality'] = 'hd';
264 }
265 if ( !empty( $query->style ) && strpos( $model, 'dall-e-3' ) === 0 ) {
266 $body['style'] = $query->style;
267 }
268 return $body;
269 }
270 }
271
272 protected function build_url( $query, $endpoint = null ) {
273 $url = "";
274 $env = $this->env;
275 // This endpoint is basically OpenAI or Azure, but in the case this class
276 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
277 if ( empty( $endpoint ) ) {
278 if ( $this->envType === 'openai' ) {
279 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
280 $this->organizationId = isset( $env['organizationId'] ) ? $env['organizationId'] : null;
281 }
282 else if ( $this->envType === 'azure' ) {
283 $endpoint = isset( $env['endpoint'] ) ? $env['endpoint'] : null;
284 }
285 else {
286 if ( empty( $this->envType ) ) {
287 throw new Exception( 'Endpoint is not defined, and this envType is not known.' );
288 }
289 throw new Exception( 'Endpoint is not defined, and this envType is not known: ' . $this->envType );
290 }
291 }
292 // Add the base API to the URL
293 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
294 if ( $this->envType === 'azure' ) {
295 $deployment_name = $this->get_azure_deployment_name( $query->model );
296 $url = trailingslashit( $endpoint ) . 'openai/deployments/' . $deployment_name;
297 if ( $query->mode === 'chat' ) {
298 $url .= '/chat/completions?' . $this->azureApiVersion;
299 }
300 else if ($query->mode === 'completion') {
301 $url .= '/completions?' . $this->azureApiVersion;
302 }
303 }
304 else {
305 if ( $query->mode === 'chat' ) {
306 $url .= trailingslashit( $endpoint ) . 'chat/completions';
307 }
308 else if ( $query->mode === 'completion' ) {
309 $url .= trailingslashit( $endpoint ) . 'completions';
310 }
311 }
312 return $url;
313 }
314 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
315 $modeEndpoint = $query->mode === 'translation' ? 'translations' : 'transcriptions';
316 $url .= trailingslashit( $endpoint ) . 'audio/' . $modeEndpoint;
317 return $url;
318 }
319 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
320 $url .= trailingslashit( $endpoint ) . 'embeddings';
321 if ( $this->envType === 'azure' ) {
322 $deployment_name = $this->get_azure_deployment_name( $query->model );
323 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
324 $deployment_name . '/embeddings?' . $this->azureApiVersion;
325 }
326 return $url;
327 }
328 else if ( $query instanceof Meow_MWAI_Query_Image ) {
329 $url .= trailingslashit( $endpoint ) . 'images/generations';
330 if ( $this->envType === 'azure' ) {
331 $deployment_name = $this->get_azure_deployment_name( $query->model );
332 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
333 $deployment_name . '/images/generations?' . $this->azureApiVersion;
334 }
335 return $url;
336 }
337 throw new Exception( 'The query is not supported by build_url().' );
338 }
339
340 protected function build_headers( $query ) {
341 if ( $query->apiKey ) {
342 $this->apiKey = $query->apiKey;
343 }
344 if ( empty( $this->apiKey ) ) {
345 throw new Exception( 'No API Key provided. Please visit the Settings.' );
346 }
347 $headers = array(
348 'Content-Type' => 'application/json',
349 'Authorization' => 'Bearer ' . $this->apiKey,
350 );
351 if ( $this->organizationId ) {
352 $headers['OpenAI-Organization'] = $this->organizationId;
353 }
354 if ( $this->envType === 'azure' ) {
355 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $this->apiKey );
356 }
357 return $headers;
358 }
359
360 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
361 $body = null;
362 if ( !empty( $forms ) ) {
363 $boundary = wp_generate_password ( 24, false );
364 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
365 $body = $this->build_form_body( $forms, $boundary );
366 }
367 else if ( !empty( $json ) ) {
368 $body = json_encode( $json );
369 }
370 $options = array(
371 'headers' => $headers,
372 'method' => $method,
373 'timeout' => MWAI_TIMEOUT,
374 'body' => $body,
375 'sslverify' => false
376 );
377 return $options;
378 }
379 // object: "thread.message.delta"
380 protected function stream_data_handler( $json ) {
381 $content = null;
382
383 // Get additional data from the JSON
384 if ( isset( $json['model'] ) ) {
385 $this->inModel = $json['model'];
386 }
387 if ( isset( $json['id'] ) ) {
388 $this->inId = $json['id'];
389 }
390
391 $object = $json['object'] ?? null;
392
393 if ( $object === 'thread.run' ) {
394 $this->inThreadId = $json['thread_id'];
395 if ( $json['status'] === 'failed' ) {
396 $error = $json['last_error']['message'] ?? 'The run failed.';
397 throw new Exception( $error );
398 }
399 }
400 else if ( $object === 'thread.run.step.delta' ) {
401 if ( $json['delta']['step_details']['type'] === 'tool_calls' ) {
402 foreach ( $json['delta']['step_details']['tool_calls'] as $tool_call ) {
403 $index = $tool_call['index'] ?? null;
404 $currentStreamToolCall = null;
405 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
406 $currentStreamToolCall = &$this->streamToolCalls[$index];
407 }
408 else {
409 $this->streamToolCalls[] = [
410 'id' => null,
411 'type' => null,
412 'function' => [ 'name' => "", 'arguments' => "" ]
413 ];
414 end( $this->streamToolCalls );
415 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
416 }
417 if ( !empty( $tool_call['id'] ) ) {
418 $currentStreamToolCall['id'] = $tool_call['id'];
419 }
420 if ( !empty( $tool_call['type'] ) ) {
421 $currentStreamToolCall['type'] = $tool_call['type'];
422 }
423 if ( isset( $tool_call['function'] ) ) {
424 $function = $tool_call['function'];
425 if ( isset( $function['name'] ) ) {
426 $currentStreamToolCall['function']['name'] .= $function['name'];
427 }
428 if ( isset( $function['arguments'] ) ) {
429 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
430 }
431 }
432 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
433 }
434 }
435 }
436 else if ( $object === 'thread.message.delta' ) {
437 $delta = $json['delta']['content'][0] ?? null;
438 if ( $delta ) {
439 switch ( $delta['type'] ?? null ) {
440 case 'text':
441 $content = $delta['value'] ?? $delta['text'];
442 break;
443 case 'image':
444 $content = $delta['url'];
445 break;
446 case 'function_call':
447 if ( empty( $this->streamFunctionCall ) ) {
448 $this->streamFunctionCall = [ 'name' => "", 'arguments' => [] ];
449 }
450 $this->streamFunctionCall['name'] = $delta['function_call']['name'] ?? $this->streamFunctionCall['name'];
451 if ( isset( $delta['function_call']['arguments'] ) ) {
452 $args = json_decode( $delta['function_call']['arguments'], true );
453 $this->streamFunctionCall['arguments'] = $args ?? [];
454 }
455 break;
456 case 'tool_call':
457 $tool_call = $delta['tool_call'];
458 $index = $tool_call['index'] ?? null;
459 $currentStreamToolCall = null;
460 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
461 $currentStreamToolCall = &$this->streamToolCalls[$index];
462 }
463 else {
464 $this->streamToolCalls[] = [
465 'id' => null,
466 'type' => null,
467 'function' => [ 'name' => "", 'arguments' => "" ]
468 ];
469 end( $this->streamToolCalls );
470 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
471 }
472 break;
473 }
474 }
475 }
476 else {
477 if ( isset( $json['choices'][0]['text'] ) ) {
478 $content = $json['choices'][0]['text'];
479 }
480 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
481 $content = $json['choices'][0]['delta']['content'];
482 }
483 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
484 if ( empty( $this->streamFunctionCall ) ) {
485 $this->streamFunctionCall = [ 'name' => "", 'arguments' => [] ];
486 }
487 $this->streamFunctionCall['name'] = $json['choices'][0]['delta']['function_call']['name'] ?? $this->streamFunctionCall['name'];
488 if ( isset( $json['choices'][0]['delta']['function_call']['arguments'] ) ) {
489 $args = json_decode( $json['choices'][0]['delta']['function_call']['arguments'], true );
490 $this->streamFunctionCall['arguments'] = $args ?? [];
491 }
492 }
493 else if ( isset( $json['choices'][0]['delta']['tool_calls'] ) ) {
494 foreach ( $json['choices'][0]['delta']['tool_calls'] as $tool_call ) {
495 $index = $tool_call['index'] ?? null;
496 $currentStreamToolCall = null;
497 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
498 $currentStreamToolCall = &$this->streamToolCalls[$index];
499 }
500 else {
501 $this->streamToolCalls[] = [
502 'id' => null,
503 'type' => null,
504 'function' => [ 'name' => "", 'arguments' => "" ]
505 ];
506 end( $this->streamToolCalls );
507 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
508 }
509 if ( !empty( $tool_call['id'] ) ) {
510 $currentStreamToolCall['id'] = $tool_call['id'];
511 }
512 if ( !empty( $tool_call['type'] ) ) {
513 $currentStreamToolCall['type'] = $tool_call['type'];
514 }
515 if ( isset( $tool_call['function'] ) ) {
516 $function = $tool_call['function'];
517 if ( isset( $function['name'] ) ) {
518 $currentStreamToolCall['function']['name'] .= $function['name'];
519 }
520 if ( isset( $function['arguments'] ) ) {
521 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
522 }
523 }
524 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
525 }
526 }
527 else if ( isset( $json['choices'][0]['delta']['role'] ) ) {
528 $this->streamLastMessage = [
529 'role' => $json['choices'][0]['delta']['role'],
530 'content' => null
531 ];
532 }
533 }
534
535 // If content is an array, let's try to convert it into a string. Normally, there would be a 'value' key.
536 if ( is_array( $content ) ) {
537 if ( isset( $content['value'] ) ) {
538 $content = $content['value'];
539 }
540 else {
541 throw new Exception( 'AI Engine: Could not read this: ' . json_encode( $content ) );
542 }
543 }
544
545 // Avoid some endings
546 $endings = [ "", "</s>" ];
547 if ( in_array( $content, $endings ) ) {
548 $content = null;
549 }
550
551 return ( $content === '0' || !empty( $content ) ) ? $content : null;
552 }
553
554
555 public function run_query( $url, $options, $isStream = false ) {
556 try {
557 $options['stream'] = $isStream;
558 if ( $isStream ) {
559 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
560 }
561 $res = wp_remote_get( $url, $options );
562
563 if ( is_wp_error( $res ) ) {
564 throw new Exception( $res->get_error_message() );
565 }
566
567 $responseCode = wp_remote_retrieve_response_code( $res );
568 if ( $responseCode === 404 ) {
569 throw new Exception( 'The model\'s API URL was not found: ' . $url );
570 }
571 if ( $responseCode === 400 ) {
572 $message = wp_remote_retrieve_body( $res );
573 if ( empty( $message ) ) {
574 $message = wp_remote_retrieve_response_message( $res );
575 }
576 if ( empty( $message ) ) {
577 $message = 'Bad Request';
578 }
579 throw new Exception( $message );
580 }
581
582 if ( $isStream ) {
583 return [ 'stream' => true ];
584 }
585
586 $response = wp_remote_retrieve_body( $res );
587 $headersRes = wp_remote_retrieve_headers( $res );
588 $headers = $headersRes->getAll();
589
590 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
591 // If so, we don't need to decode the response
592 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
593 $resContentType = $normalizedHeaders['content-type'] ?? '';
594 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
595 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
596 }
597
598 $data = json_decode( $response, true );
599 $this->handle_response_errors( $data );
600 return [ 'headers' => $headers, 'data' => $data ];
601 }
602 catch ( Exception $e ) {
603 $this->core->log( '❌ (OpenAI) ' . $e->getMessage() );
604 throw $e;
605 }
606 }
607
608 private function get_audio( $url ) {
609 require_once( ABSPATH . 'wp-admin/includes/media.php' );
610 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
611 file_put_contents( $tmpFile, file_get_contents( $url ) );
612 $length = null;
613 $metadata = wp_read_audio_metadata( $tmpFile );
614 if ( isset( $metadata['length'] ) ) {
615 $length = $metadata['length'];
616 }
617 $data = file_get_contents( $tmpFile );
618 unlink( $tmpFile );
619 return [ 'data' => $data, 'length' => $length ];
620 }
621
622 public function run_transcribe_query( $query ) {
623 // Check if the URL is valid.
624 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
625 throw new Exception( 'Invalid URL for transcription.' );
626 }
627
628 $audioData = $this->get_audio( $query->url );
629 $body = $this->build_body( $query, null, $audioData['data'] );
630 $url = $this->build_url( $query );
631 $headers = $this->build_headers( $query );
632 $options = $this->build_options( $headers, null, $body );
633
634 // Perform the request
635 try {
636 $res = $this->run_query( $url, $options );
637 $data = $res['data'];
638 if ( empty( $data ) ) {
639 throw new Exception( 'Invalid data for transcription.' );
640 }
641 $usage = $this->core->record_audio_usage( $query->model, $audioData['length'] );
642 $reply = new Meow_MWAI_Reply( $query );
643 $reply->set_usage( $usage );
644 $reply->set_choices( $data );
645 return $reply;
646 }
647 catch ( Exception $e ) {
648 $this->core->log( '❌ (OpenAI) ' . $e->getMessage() );
649 $service = $this->get_service_name();
650 throw new Exception( "From $service: " . $e->getMessage() );
651 }
652 }
653
654 public function run_embedding_query( $query ) {
655 $body = $this->build_body( $query );
656 $url = $this->build_url( $query );
657 $headers = $this->build_headers( $query );
658 $options = $this->build_options( $headers, $body );
659
660 try {
661 $res = $this->run_query( $url, $options );
662 $data = $res['data'];
663 if ( empty( $data ) || !isset( $data['data'] ) ) {
664 throw new Exception( 'Invalid data for embedding.' );
665 }
666 $usage = $data['usage'];
667 $this->core->record_tokens_usage( $query->model, $usage['prompt_tokens'] );
668 $reply = new Meow_MWAI_Reply( $query );
669 $reply->set_usage( $usage );
670 $reply->set_choices( $data['data'] );
671 return $reply;
672 }
673 catch ( Exception $e ) {
674 $message = $e->getMessage();
675 $error = $this->try_decode_error( $message );
676 if ( !is_null( $error ) ) {
677 $message = $error;
678 }
679 $this->core->log( '❌ (OpenAI) ' . $message );
680 $service = $this->get_service_name();
681 throw new Exception( "From $service: " . $message );
682 }
683 }
684
685 public function try_decode_error( $data ) {
686 $json = json_decode( $data, true );
687 if ( isset( $json['error']['message'] ) ) {
688 return $json['error']['message'];
689 }
690 return null;
691 }
692
693 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
694 $isStreaming = !is_null( $streamCallback );
695
696 if ( $isStreaming ) {
697 $this->streamCallback = $streamCallback;
698 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
699 }
700 if ( $query->mode !== 'chat' && $query->mode !== 'completion' ) {
701 throw new Exception( 'Unknown mode for query: ' . $query->mode );
702 }
703
704 $this->reset_stream();
705 $body = $this->build_body( $query, $streamCallback );
706 $url = $this->build_url( $query );
707 $headers = $this->build_headers( $query );
708 $options = $this->build_options( $headers, $body );
709
710 try {
711 $res = $this->run_query( $url, $options, $streamCallback );
712 $reply = new Meow_MWAI_Reply( $query );
713
714 $returned_id = null;
715 $returned_model = $this->inModel;
716 $returned_in_tokens = null;
717 $returned_out_tokens = null;
718 $returned_price = null;
719 $returned_choices = [];
720
721 // Streaming Mode
722 if ( $isStreaming ) {
723 if ( empty( $this->streamContent ) ) {
724 $error = $this->try_decode_error( $this->streamBuffer );
725 if ( !is_null( $error ) ) {
726 throw new Exception( $error );
727 }
728 }
729 $returned_id = $this->inId;
730 $returned_model = $this->inModel ? $this->inModel : $query->model;
731 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
732 if ( !empty( $this->streamFunctionCall ) ) {
733 $message['function_call'] = $this->streamFunctionCall;
734 }
735 if ( !empty( $this->streamToolCalls ) ) {
736 $message['tool_calls'] = $this->streamToolCalls;
737 }
738 $returned_choices = [ [ 'message' => $message ] ];
739 }
740 // Standard Mode
741 else {
742 $data = $res['data'];
743 if ( empty( $data ) ) {
744 throw new Exception( 'No content received (res is null).' );
745 }
746 if ( !$data['model'] ) {
747 $this->core->log( '❌ (OpenAI) Invalid response (no model information):' );
748 $this->core->log( '❌ (OpenAI) ' . print_r( $data, 1 ) );
749 throw new Exception( 'Invalid response (no model information).' );
750 }
751 $returned_id = $data['id'];
752 $returned_model = $data['model'];
753 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
754 $data['usage']['prompt_tokens'] : null;
755 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
756 $data['usage']['completion_tokens'] : null;
757 $returned_price = isset( $data['usage']['total_cost'] ) ?
758 $data['usage']['total_cost'] : null;
759 $returned_choices = $data['choices'];
760 }
761
762 // Set the results.
763 $reply->set_choices( $returned_choices );
764 if ( !empty( $returned_id ) ) {
765 $reply->set_id( $returned_id );
766 }
767 if ( !empty( $returned_id ) ) {
768 $reply->set_id( $returned_id );
769 }
770
771 // Handle tokens.
772 $this->handle_tokens_usage( $reply, $query, $returned_model,
773 $returned_in_tokens, $returned_out_tokens, $returned_price
774 );
775
776 return $reply;
777 }
778 catch ( Exception $e ) {
779 $this->core->log( '❌ (OpenAI) ' . $e->getMessage() );
780 $service = $this->get_service_name();
781 $message = "From $service: " . $e->getMessage();
782 throw new Exception( $message );
783 }
784 finally {
785 if ( !is_null( $streamCallback ) ) {
786 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
787 }
788 }
789 }
790
791 public function handle_tokens_usage( $reply, $query, $returned_model,
792 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
793 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens :
794 $reply->get_in_tokens( $query );
795 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens :
796 $reply->get_out_tokens();
797 $returned_price = !is_null( $returned_price ) ? $returned_price :
798 $reply->get_price();
799 $usage = $this->core->record_tokens_usage(
800 $returned_model,
801 $returned_in_tokens,
802 $returned_out_tokens,
803 $returned_price
804 );
805 $reply->set_usage( $usage );
806 }
807
808 // Request to DALL-E API
809 public function run_images_query( $query ) {
810 $body = $this->build_body( $query );
811 $url = $this->build_url( $query );
812 $headers = $this->build_headers( $query );
813 $options = $this->build_options( $headers, $body );
814
815 try {
816 $res = $this->run_query( $url, $options );
817 $data = $res['data'];
818 $choices = [];
819 if ( $this->envType === 'azure' ) {
820 foreach ( $data['data'] as $entry ) {
821 $choices[] = [ 'url' => $entry['url'] ];
822 }
823 }
824 else {
825 $choices = $data['data'];
826 }
827
828 $reply = new Meow_MWAI_Reply( $query );
829 $model = $query->model;
830 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
831 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
832 $reply->set_usage( $usage );
833 $reply->set_choices( $choices );
834 $reply->set_type( 'images' );
835
836 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
837 foreach ( $reply->results as &$result ) {
838 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
839 'query_envId' => $query->envId,
840 'query_session' => $query->session,
841 'query_model' => $query->model,
842 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
843 $fileUrl = $this->core->files->get_url( $fileId );
844 $result = $fileUrl;
845 }
846 }
847 $reply->result = $reply->results[0];
848 return $reply;
849 }
850 catch ( Exception $e ) {
851 $this->core->log( '❌ (OpenAI) ' . $e->getMessage() );
852 $service = $this->get_service_name();
853 throw new Exception( "From $service: " . $e->getMessage() );
854 }
855 }
856
857 /*
858 This is the rest of the OpenAI API support, not related to the models directly.
859 */
860
861 // Check if there are errors in the response from OpenAI, and throw an exception if so.
862 protected function handle_response_errors( $data ) {
863 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
864 $message = $data['error']['message'];
865 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
866 $message = str_replace( $matches[1], '', $message );
867 }
868 throw new Exception( $message );
869 }
870 }
871
872 public function list_files( $purposeFilter = null )
873 {
874 if ( empty( $purposeFilter ) ) {
875 return $this->execute( 'GET', '/files' );
876 }
877 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
878 }
879
880 static function get_suffix_for_model($model)
881 {
882 // Legacy fine-tuned models
883 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
884 if ( count( $matches ) > 0 ) {
885 return $matches[1];
886 }
887
888 // New fine-tuned models
889 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
890 if (count($matches) > 0) {
891 return $matches[1];
892 }
893
894 return 'N/A';
895 }
896
897 static function get_finetune_base_model( $model )
898 {
899 // New fine-tuned models
900 preg_match("/^ft:([^:]+):/", $model, $matches);
901 if (count($matches) > 0) {
902 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
903 return "gpt-3.5-turbo";
904 }
905 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
906 return "gpt-4";
907 }
908 return $matches[1];
909 }
910
911 // Legacy fine-tuned models
912 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
913 if ( count( $matches ) > 0 ) {
914 return $matches[1];
915 }
916
917 return null;
918 }
919
920 public function list_deleted_finetunes( $envId = null, $legacy = false )
921 {
922 $finetunes = $this->list_finetunes( $legacy );
923 $deleted = [];
924
925 foreach ( $finetunes as $finetune ) {
926 $name = $finetune['model'];
927 $isSucceeded = $finetune['status'] === 'succeeded';
928 if ( $isSucceeded ) {
929 try {
930 $finetune = $this->get_model( $name );
931 }
932 catch ( Exception $e ) {
933 $deleted[] = $name;
934 }
935 }
936 }
937 if ( $legacy ) {
938 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
939 }
940 else {
941 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
942 }
943 return $deleted;
944 }
945
946 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
947 // retrieve all the models since Summer 2023, let's see! WIP.
948 public function list_finetunes( $legacy = false )
949 {
950 if ( $legacy ) {
951 $res = $this->execute( 'GET', '/fine-tunes' );
952 }
953 else {
954 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
955 }
956 $finetunes = $res['data'];
957
958 // Add suffix
959 $finetunes = array_map( function ( $finetune ) {
960 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
961 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
962 if ( isset( $finetune['estimated_finish'] ) ) {
963 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] );
964 }
965 else {
966 $finetune['estimatedOn'] = null;
967 }
968 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
969 $finetune['base_model'] = $finetune['model'];
970 $finetune['model'] = $finetune['fine_tuned_model'];
971 unset( $finetune['object'] );
972 unset( $finetune['hyperparams'] );
973 unset( $finetune['result_files'] );
974 unset( $finetune['training_files'] );
975 unset( $finetune['validation_files'] );
976 unset( $finetune['created_at'] );
977 unset( $finetune['updated_at'] );
978 unset( $finetune['fine_tuned_model'] );
979 return $finetune;
980 }, $finetunes);
981
982 usort( $finetunes, function ( $a, $b ) {
983 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
984 });
985
986 if ( $legacy ) {
987 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
988 }
989 else {
990 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
991 }
992
993 return $finetunes;
994 }
995
996 public function moderate( $input ) {
997 $result = $this->execute('POST', '/moderations', [
998 'input' => $input
999 ]);
1000 return $result;
1001 }
1002
1003 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
1004 {
1005 $result = $this->execute('POST', '/files', null, [
1006 'purpose' => $purpose,
1007 'data' => $data,
1008 'file' => $filename
1009 ] );
1010 return $result;
1011 }
1012
1013 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
1014 $expiryInDays = $expiry ? max( 1, ceil( $expiry / 86400 ) ) : 7;
1015 $result = $this->execute( 'POST', '/vector_stores', [
1016 'name' => !empty( $name ) ? $name : 'default',
1017 'metadata' => $metadata,
1018 'expires_after' => [
1019 'anchor' => 'last_active_at',
1020 'days' => $expiryInDays
1021 ]
1022 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1023 return $result['id'];
1024 }
1025
1026 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1027 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1028 'file_id' => $fileId
1029 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1030 return $result['id'];
1031
1032 }
1033
1034 public function delete_file( $fileId )
1035 {
1036 return $this->execute( 'DELETE', '/files/' . $fileId );
1037 }
1038
1039 public function get_model( $modelId )
1040 {
1041 return $this->execute( 'GET', '/models/' . $modelId );
1042 }
1043
1044 public function cancel_finetune( $fineTuneId )
1045 {
1046 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1047 }
1048
1049 public function delete_finetune( $modelId )
1050 {
1051 return $this->execute( 'DELETE', '/models/' . $modelId );
1052 }
1053
1054 public function download_file( $fileId, $newFile = null ) {
1055 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1056 $fileInfo = json_decode( (string)$fileInfo, true );
1057 $filename = $fileInfo['filename'];
1058 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1059 if ( empty( $newFile ) ) {
1060 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1061 $tempFile = wp_tempnam( $filename );
1062 if ( !$tempFile ) {
1063 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1064 }
1065 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1066 $newFile = $tempFile . '.' . $extension;
1067 }
1068 else {
1069 $newFile = $tempFile;
1070 }
1071 }
1072 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1073 file_put_contents( $newFile, $data );
1074 return $newFile;
1075 }
1076
1077 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1078 {
1079 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1080 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1081 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1082 (float)$hyperparams['learningRateMultiplier'] : null;
1083 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1084 (float)$hyperparams['promptLossWeight'] : null;
1085 $arguments = [
1086 'training_file' => $fileId,
1087 'model' => $model,
1088 'suffix' => $suffix
1089 ];
1090 if ( $legacy ) {
1091 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1092 }
1093 else {
1094 if ( $n_epochs ) {
1095 $arguments['hyperparams'] = [];
1096 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1097 }
1098 if ( $batch_size ) {
1099 if ( empty( $arguments['hyperparams'] ) ) {
1100 $arguments['hyperparams'] = [];
1101 }
1102 $arguments['hyperparams']['batch_size'] = $batch_size;
1103 }
1104 if ( $learning_rate_multiplier ) {
1105 if ( empty( $arguments['hyperparams'] ) ) {
1106 $arguments['hyperparams'] = [];
1107 }
1108 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1109 }
1110 if ( $prompt_loss_weight ) {
1111 if ( empty( $arguments['hyperparams'] ) ) {
1112 $arguments['hyperparams'] = [];
1113 }
1114 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1115 }
1116 if ( $model === 'turbo' ) {
1117 $arguments['model'] = 'gpt-3.5-turbo';
1118 }
1119 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1120 }
1121 return $result;
1122 }
1123
1124 /**
1125 * Build the body of a form request.
1126 * If the field name is 'file', then the field value is the filename of the file to upload.
1127 * The file contents are taken from the 'data' field.
1128 *
1129 * @param array $fields
1130 * @param string $boundary
1131 * @return string
1132 */
1133 public function build_form_body( $fields, $boundary )
1134 {
1135 $body = '';
1136 foreach ( $fields as $name => $value ) {
1137 if ( $name == 'data' ) {
1138 continue;
1139 }
1140 $body .= "--$boundary\r\n";
1141 $body .= "Content-Disposition: form-data; name=\"$name\"";
1142 if ( $name == 'file' ) {
1143 $body .= "; filename=\"{$value}\"\r\n";
1144 $body .= "Content-Type: application/json\r\n\r\n";
1145 $body .= $fields['data'] . "\r\n";
1146 }
1147 else {
1148 $body .= "\r\n\r\n$value\r\n";
1149 }
1150 }
1151 $body .= "--$boundary--\r\n";
1152 return $body;
1153 }
1154
1155 /**
1156 * Run a request to the OpenAI API.
1157 * Fore more information about the $formFields, refer to the build_form_body method.
1158 *
1159 * @param string $method POST, PUT, GET, DELETE...
1160 * @param string $url The API endpoint
1161 * @param array $query The query parameters (json)
1162 * @param array $formFields The form fields (multipart/form-data)
1163 * @param bool $json Whether to return the response as json or not
1164 * @return array
1165 */
1166 public function execute( $method, $url, $query = null, $formFields = null,
1167 $json = true, $extraHeaders = null, $streamCallback = null )
1168 {
1169 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
1170 if ( $this->organizationId ) {
1171 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1172 }
1173 $body = $query ? json_encode( $query ) : null;
1174 if ( !empty( $formFields ) ) {
1175 $boundary = wp_generate_password( 24, false );
1176 $headers = [
1177 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
1178 'Authorization' => 'Bearer ' . $this->apiKey
1179 ];
1180 if ( $this->organizationId ) {
1181 $headers['OpenAI-Organization'] = $this->organizationId;
1182 }
1183 $body = $this->build_form_body( $formFields, $boundary );
1184 }
1185
1186 // Maybe we should have headers always as an array... not sure why we have it as a string.
1187 if ( !empty( $extraHeaders ) ) {
1188 foreach ( $extraHeaders as $key => $value ) {
1189 if ( is_array( $headers ) ) {
1190 $headers[$key] = $value;
1191 }
1192 else {
1193 $headers .= "$key: $value\r\n";
1194 }
1195 }
1196 }
1197
1198 // If it's a GET, body should be null, and we should append the query to the URL.
1199 if ( $method === 'GET' ) {
1200 if ( !empty( $query ) ) {
1201 $url .= '?' . http_build_query( $query );
1202 }
1203 $body = null;
1204 }
1205
1206 $url = 'https://api.openai.com/v1' . $url;
1207 $options = [
1208 "headers" => $headers,
1209 "method" => $method,
1210 "timeout" => MWAI_TIMEOUT,
1211 "body" => $body,
1212 "sslverify" => false
1213 ];
1214
1215 try {
1216 if ( !is_null( $streamCallback ) ) {
1217 $options['stream'] = true;
1218 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1219 // The stream handler calls the streamCallback every time there is content
1220 // TODO: For assistants, we should probably have a different stream handler to
1221 // handle the assistant's specific reply and perform the necessary actions.
1222 $this->streamCallback = $streamCallback;
1223 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1224 }
1225 $res = wp_remote_request( $url, $options );
1226 if ( is_wp_error( $res ) ) {
1227 throw new Exception( $res->get_error_message() );
1228 }
1229 $res = wp_remote_retrieve_body( $res );
1230 $data = $json ? json_decode( $res, true ) : $res;
1231 $this->handle_response_errors( $data );
1232 return $data;
1233 }
1234 catch ( Exception $e ) {
1235 $this->core->log( '❌ (OpenAI) ' . $e->getMessage() );
1236 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
1237 }
1238 finally {
1239 if ( !is_null( $streamCallback ) ) {
1240 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1241 }
1242 }
1243 }
1244
1245 public function get_models() {
1246 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1247 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1248 foreach ( $finetunes as $finetune ) {
1249 if ( $finetune['status'] !== 'succeeded' ) {
1250 continue;
1251 }
1252 $baseModel = SELF::get_finetune_base_model( $finetune['model'] );
1253 if ( !empty( $baseModel ) ) {
1254 $model = null;
1255 foreach ( $models as $currentModel ) {
1256 if ( $currentModel['model'] === $baseModel ) {
1257 $model = $currentModel;
1258 break;
1259 }
1260 }
1261 if ( !empty( $model ) ) {
1262 $model['model'] = $finetune['model'];
1263 $model['name'] = SELF::get_suffix_for_model( $finetune['model'] );
1264 $models[] = $model;
1265 }
1266 }
1267 }
1268 return $models;
1269 }
1270
1271 static public function get_models_static() {
1272 return MWAI_OPENAI_MODELS;
1273 }
1274
1275 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
1276 {
1277 // For fine-tuned models:
1278 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
1279 if ( !empty( $potentialBaseModel ) ) {
1280 $modelFamily = $potentialBaseModel;
1281 $finetune = true;
1282 }
1283
1284 $models = $this->get_models();
1285 foreach ( $models as $currentModel ) {
1286 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
1287 if ( $currentModel['type'] === 'image' ) {
1288 if ( !$option ) {
1289 $this->core->log( "⚠️ (OpenAI) Image models require an option." );
1290 return null;
1291 }
1292 else {
1293 foreach ( $currentModel['options'] as $imageType ) {
1294 if ( $imageType['option'] == $option ) {
1295 return $imageType['price'] * $outUnits;
1296 }
1297 }
1298 }
1299 }
1300 else {
1301 if ( $finetune ) {
1302
1303 if ( isset( $currentModel['finetune']['price'] ) ) {
1304 $currentModel['price'] = $currentModel['finetune']['price'];
1305 }
1306 else if ( isset( $currentModel['finetune']['in'] ) ) {
1307 $currentModel['price'] = [
1308 'in' => $currentModel['finetune']['in'],
1309 'out' => $currentModel['finetune']['out']
1310 ];
1311 }
1312 }
1313 $inPrice = $currentModel['price'];
1314 $outPrice = $currentModel['price'];
1315 if ( is_array( $currentModel['price'] ) ) {
1316 $inPrice = $currentModel['price']['in'];
1317 $outPrice = $currentModel['price']['out'];
1318 }
1319 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1320 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1321 return $inTotalPrice + $outTotalPrice;
1322 }
1323 }
1324 }
1325 $this->core->log( "⚠️ (OpenAI) Invalid model ($modelFamily)." );
1326 return null;
1327 }
1328
1329 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1330 {
1331 $model = $query->model;
1332 $units = 0;
1333 $option = null;
1334
1335 $finetune = false;
1336 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1337 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1338 $finetune = true;
1339 }
1340 $inUnits = $reply->get_in_tokens( $query );
1341 $outUnits = $reply->get_out_tokens();
1342 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
1343 }
1344 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1345 /** @var Meow_MWAI_Query_Image $query */
1346 $units = $query->maxResults;
1347 $option = $query->resolution;
1348 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1349 }
1350 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1351 $model = 'whisper';
1352 $units = $reply->get_units();
1353 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1354 }
1355 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1356 $units = $reply->get_total_tokens();
1357 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1358 }
1359 $this->core->log( "⚠️ (OpenAI) Cannot calculate price for $model.");
1360 return null;
1361 }
1362
1363 public function get_incidents() {
1364 $url = 'https://status.openai.com/history.rss';
1365 $response = wp_remote_get( $url );
1366 if ( is_wp_error( $response ) ) {
1367 throw new Exception( $response->get_error_message() );
1368 }
1369 $response = wp_remote_retrieve_body( $response );
1370 $xml = simplexml_load_string( $response );
1371 $incidents = array();
1372 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1373 foreach ( $xml->channel->item as $item ) {
1374 $date = strtotime( $item->pubDate );
1375 if ( $date > $oneWeekAgo ) {
1376 $incidents[] = array(
1377 'title' => (string) $item->title,
1378 'description' => (string) $item->description,
1379 'date' => $date
1380 );
1381 }
1382 }
1383 return $incidents;
1384 }
1385 }
1386