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