PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.1.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.1.8
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
core.php 2 years ago factory.php 2 years ago openai.php 2 years ago openrouter.php 2 years ago
openai.php
1058 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 $endpoint = 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
17 // Streaming
18 private $streamTemporaryBuffer = "";
19 private $streamBuffer = "";
20 private $streamContent = "";
21 private $streamFunctionCall = null;
22 private $streamCallback = null;
23
24 public function __construct( $core, $env )
25 {
26 parent::__construct( $core, $env );
27 $this->set_environment();
28 }
29
30 protected function set_environment() {
31 $env = $this->env;
32 $this->apiKey = $env['apikey'];
33 if ( $this->envType === 'openai' ) {
34 $this->endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
35 }
36 else if ( $this->envType === 'azure' ) {
37 $this->endpoint = isset( $env['endpoint'] ) ? $env['endpoint'] : null;
38 $this->azureDeployments = isset( $env['deployments'] ) ? $env['deployments'] : [];
39 $this->azureDeployments[] = [ 'model' => 'dall-e', 'name' => 'dall-e' ];
40 }
41 else {
42 throw new Exception( 'Unknown environment type: ' . $this->envType );
43 }
44 }
45
46 private function get_azure_deployment_name( $model ) {
47 foreach ( $this->azureDeployments as $deployment ) {
48 if ( $deployment['model'] === $model && !empty( $deployment['name'] ) ) {
49 return $deployment['name'];
50 }
51 }
52 throw new Exception( 'Unknown deployment for model: ' . $model );
53 }
54
55 protected function get_service_name() {
56 return $this->envType === 'azure' ? 'Azure' : 'OpenAI';
57 }
58
59 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
60 function check_for_error( $data ) {
61 if ( strpos( $data, 'error' ) === false ) {
62 return;
63 }
64 if ( strpos( $data, 'data: ' ) === 0 ) {
65 $jsonPart = substr( $data, strlen( 'data: ' ) );
66 }
67 else {
68 $jsonPart = $data;
69 }
70 $json = json_decode( $jsonPart, true );
71 if ( json_last_error() === JSON_ERROR_NONE ) {
72 if ( isset( $json['error'] ) ) {
73 $error = $json['error'];
74 $code = $error['code'];
75 $message = $error['message'];
76 throw new Exception( "Error $code: $message" );
77 }
78 }
79 }
80
81 private function build_prompt( $query ) {
82 $prompt = "";
83 if ( $query->mode === 'chat' ) {
84 $prompt = $query->instructions . "\n\n";
85 foreach ( $query->messages as $message ) {
86 $role = $message['role'];
87 $content = $message['content'];
88 if ( $role === 'system' ) {
89 $prompt .= "$content\n\n";
90 }
91 if ( $role === 'user' ) {
92 $prompt .= "User: $content\n";
93 }
94 if ( $role === 'assistant' ) {
95 $prompt .= "AI: $content\n";
96 }
97 }
98 $prompt .= "AI: ";
99 }
100 else if ( $query->mode === 'completion' ) {
101 $prompt = $query->get_message();
102 }
103 return $prompt;
104 }
105
106 private function build_messages( $query ) {
107 $messages = [];
108
109 // First, we need to add the first message (the instructions).
110 if ( !empty( $query->instructions ) ) {
111 $messages[] = [ 'role' => 'system', 'content' => $query->instructions ];
112 }
113
114 // Then, if any, we need to add the 'messages', they are already formatted.
115 foreach ( $query->messages as $message ) {
116 $messages[] = $message;
117 }
118
119 // If there is a context, we need to add it.
120 if ( !empty( $query->context ) ) {
121 $messages[] = [ 'role' => 'system', 'content' => $query->context ];
122 }
123
124 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
125 $fileUrl = $query->get_file_url();
126 if ( !empty( $fileUrl ) ) {
127 $messages[] = [
128 'role' => 'user',
129 'content' => [
130 [
131 "type" => "text",
132 "text" => $query->get_message()
133 ],
134 [
135 "type" => "image_url",
136 "image_url" => [ "url" => $fileUrl ]
137 ]
138 ]
139 ];
140 }
141 else {
142 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
143 }
144
145 return $messages;
146 }
147
148 /*
149 This used to be in the core.php, but since it's relative to OpenAI, it's better to have it here.
150 */
151
152 public function stream_handler( $handle, $args, $url ) {
153 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
154 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
155
156 // Maybe we could get some info from headers, as for now, there is only the model.
157 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function( $curl, $headerLine ) {
158 // $line = trim( $headerLine );
159 // return strlen( $headerLine );
160 // });
161
162 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) {
163 $length = strlen( $data );
164
165 // FOR DEBUG:
166 // preg_match_all( '/"content":"(.*?)"/', $data, $matches );
167 // $contents = $matches[1];
168 // foreach ( $contents as $content ) {
169 // error_log( "Content: $content" );
170 // }
171
172 // Error Management
173 $this->check_for_error( $data );
174
175 // Bufferize the unfinished stream (if it's the case)
176 $this->streamTemporaryBuffer .= $data;
177 $this->streamBuffer .= $data;
178 $lines = explode( "\n", $this->streamTemporaryBuffer );
179 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
180 $this->streamTemporaryBuffer = array_pop( $lines );
181 }
182 else {
183 $this->streamTemporaryBuffer = "";
184 }
185
186 foreach ( $lines as $line ) {
187 if ( $line === "" ) {
188 continue;
189 }
190 if ( strpos( $line, 'data: ' ) === 0 ) {
191 $line = substr( $line, 6 );
192 $json = json_decode( $line, true );
193
194 if ( json_last_error() === JSON_ERROR_NONE ) {
195 $content = null;
196
197 // Get additional data from the JSON
198 if ( isset( $json['model'] ) ) {
199 $this->inModel = $json['model'];
200 }
201 if ( isset( $json['id'] ) ) {
202 $this->inId = $json['id'];
203 }
204
205 // Get the content
206 if ( isset( $json['choices'][0]['text'] ) ) {
207 $content = $json['choices'][0]['text'];
208 }
209 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
210 $content = $json['choices'][0]['delta']['content'];
211 }
212 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
213 $function_call = $json['choices'][0]['delta']['function_call'];
214 if ( empty( $this->streamFunctionCall ) ) {
215 $this->streamFunctionCall = [ 'name' => "", 'arguments' => "" ];
216 }
217 if ( isset( $function_call['name'] ) ) {
218 $this->streamFunctionCall['name'] .= $function_call['name'];
219 }
220 if ( isset( $function_call['arguments'] ) ) {
221 $this->streamFunctionCall['arguments'] .= $function_call['arguments'];
222 }
223 }
224 if ( $content !== null && $content !== "" ) {
225 $this->streamContent .= $content;
226 call_user_func( $this->streamCallback, $content );
227 }
228 }
229 else {
230 $this->streamTemporaryBuffer .= $line . "\n";
231 }
232 }
233 }
234 return $length;
235 });
236 }
237
238 protected function build_headers( $query ) {
239 if ( $query->apiKey ) {
240 $this->apiKey = $query->apiKey;
241 }
242 if ( empty( $this->apiKey ) ) {
243 throw new Exception( 'No API Key provided. Please visit the Settings.' );
244 }
245 $headers = array(
246 'Content-Type' => 'application/json',
247 'Authorization' => 'Bearer ' . $this->apiKey,
248 );
249 if ( $this->envType === 'azure' ) {
250 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $this->apiKey );
251 }
252 return $headers;
253 }
254
255 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
256 $body = null;
257 if ( !empty( $forms ) ) {
258 $boundary = wp_generate_password ( 24, false );
259 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
260 $body = $this->build_form_body( $forms, $boundary );
261 }
262 else if ( !empty( $json ) ) {
263 $body = json_encode( $json );
264 }
265 $options = array(
266 'headers' => $headers,
267 'method' => $method,
268 'timeout' => MWAI_TIMEOUT,
269 'body' => $body,
270 'sslverify' => false
271 );
272 return $options;
273 }
274
275 public function run_query( $url, $options, $isStream = false ) {
276 try {
277 $options['stream'] = $isStream;
278 if ( $isStream ) {
279 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
280 }
281 $res = wp_remote_get( $url, $options );
282
283 if ( is_wp_error( $res ) ) {
284 throw new Exception( $res->get_error_message() );
285 }
286
287 if ( $isStream ) {
288 return [ 'stream' => true ];
289 }
290
291 $response = wp_remote_retrieve_body( $res );
292 $headersRes = wp_remote_retrieve_headers( $res );
293 $headers = $headersRes->getAll();
294
295 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
296 // If so, we don't need to decode the response
297 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
298 $resContentType = $normalizedHeaders['content-type'] ?? '';
299 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
300 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
301 }
302
303 $data = json_decode( $response, true );
304 $this->handle_response_errors( $data );
305 return [ 'headers' => $headers, 'data' => $data ];
306 }
307 catch ( Exception $e ) {
308 error_log( $e->getMessage() );
309 throw $e;
310 }
311 }
312
313 private function get_audio( $url ) {
314 require_once( ABSPATH . 'wp-admin/includes/media.php' );
315 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
316 file_put_contents( $tmpFile, file_get_contents( $url ) );
317 $length = null;
318 $metadata = wp_read_audio_metadata( $tmpFile );
319 if ( isset( $metadata['length'] ) ) {
320 $length = $metadata['length'];
321 }
322 $data = file_get_contents( $tmpFile );
323 unlink( $tmpFile );
324 return [ 'data' => $data, 'length' => $length ];
325 }
326
327 public function run_transcribe_query( $query ) {
328 $modeEndpoint = $query->mode === 'translation' ? 'translations' : 'transcriptions';
329 $url = 'https://api.openai.com/v1/audio/' . $modeEndpoint;
330
331 // Check if the URL is valid.
332 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
333 throw new Exception( 'Invalid URL for transcription.' );
334 }
335
336 $audioData = $this->get_audio( $query->url );
337 $body = array(
338 'prompt' => $query->message,
339 'model' => $query->model,
340 'response_format' => 'text',
341 'file' => basename( $query->url ),
342 'data' => $audioData['data']
343 );
344 $headers = $this->build_headers( $query );
345 $options = $this->build_options( $headers, null, $body );
346
347 // Perform the request
348 try {
349 $res = $this->run_query( $url, $options );
350 $data = $res['data'];
351 if ( empty( $data ) ) {
352 throw new Exception( 'Invalid data for transcription.' );
353 }
354 $this->check_for_error( $data );
355 $usage = $this->core->record_audio_usage( $query->model, $audioData['length'] );
356 $reply = new Meow_MWAI_Reply( $query );
357 $reply->set_usage( $usage );
358 $reply->set_choices( $data );
359 return $reply;
360 }
361 catch ( Exception $e ) {
362 error_log( $e->getMessage() );
363 $service = $this->get_service_name();
364 throw new Exception( "From $service: " . $e->getMessage() );
365 }
366 }
367
368 public function run_embedding_query( $query ) {
369 $url = 'https://api.openai.com/v1/embeddings';
370 $body = array( 'input' => $query->message, 'model' => $query->model );
371 if ( $this->envType === 'azure' ) {
372 $deployment_name = $this->get_azure_deployment_name( $query->model );
373 $url = trailingslashit( $this->endpoint ) . 'openai/deployments/' .
374 $deployment_name . '/embeddings?' . $this->azureApiVersion;
375 $body = array( "input" => $query->message );
376 }
377 $headers = $this->build_headers( $query );
378 $options = $this->build_options( $headers, $body );
379
380 try {
381 $res = $this->run_query( $url, $options );
382 $data = $res['data'];
383 if ( empty( $data ) || !isset( $data['data'] ) ) {
384 throw new Exception( 'Invalid data for embedding.' );
385 }
386 $usage = $data['usage'];
387 $this->core->record_tokens_usage( $query->model, $usage['prompt_tokens'] );
388 $reply = new Meow_MWAI_Reply( $query );
389 $reply->set_usage( $usage );
390 $reply->set_choices( $data['data'] );
391 return $reply;
392 }
393 catch ( Exception $e ) {
394 error_log( $e->getMessage() );
395 $service = $this->get_service_name();
396 throw new Exception( "From $service: " . $e->getMessage() );
397 }
398 }
399
400 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
401 if ( !is_null( $streamCallback ) ) {
402 $this->streamCallback = $streamCallback;
403 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
404 }
405 if ( $query->mode !== 'chat' && $query->mode !== 'completion' ) {
406 throw new Exception( 'Unknown mode for query: ' . $query->mode );
407 }
408
409 $body = array(
410 "model" => $query->model,
411 "n" => $query->maxResults,
412 "max_tokens" => $query->maxTokens,
413 "temperature" => $query->temperature,
414 "stream" => !is_null( $streamCallback ),
415 );
416
417 if ( !empty( $query->stop ) ) {
418 $body['stop'] = $query->stop;
419 }
420
421 if ( !empty( $query->responseFormat ) ) {
422 if ( $query->responseFormat === 'json' ) {
423 $body['response_format'] = [ 'type' => 'json_object' ];
424 }
425 }
426
427 if ( !empty( $query->functions ) ) {
428 if ( strpos( $query->model, 'ft:' ) === 0 ) {
429 throw new Exception( 'OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
430 }
431 $body['functions'] = $query->functions;
432 $body['function_call'] = $query->functionCall;
433 }
434 if ( $query->mode === 'chat' ) {
435 $body['messages'] = $this->build_messages( $query );
436 }
437 else if ( $query->mode === 'completion' ) {
438 $body['prompt'] = $this->build_prompt( $query );
439 }
440
441 $url = $this->endpoint;
442 if ( $this->envType === 'azure' ) {
443 $deployment_name = $this->get_azure_deployment_name( $query->model );
444 $url = trailingslashit( $this->endpoint ) . 'openai/deployments/' . $deployment_name;
445 if ( $query->mode === 'chat' ) {
446 $url .= '/chat/completions?' . $this->azureApiVersion;
447 }
448 else if ($query->mode === 'completion') {
449 $url .= '/completions?' . $this->azureApiVersion;
450 }
451 }
452 else {
453 if ( $query->mode === 'chat' ) {
454 $url .= '/chat/completions';
455 }
456 else if ( $query->mode === 'completion' ) {
457 $url .= '/completions';
458 }
459 }
460
461 $headers = $this->build_headers( $query );
462 $options = $this->build_options( $headers, $body );
463
464 try {
465 $res = $this->run_query( $url, $options, $streamCallback );
466 $reply = new Meow_MWAI_Reply( $query );
467
468 $returned_id = null;
469 $returned_model = $this->inModel;
470 $returned_in_tokens = null;
471 $returned_out_tokens = null;
472 $returned_choices = [];
473
474 if ( !is_null( $streamCallback ) ) {
475 // Streamed data
476 if ( empty( $this->streamContent ) ) {
477 $json = json_decode( $this->streamBuffer, true );
478 if ( isset( $json['error']['message'] ) ) {
479 throw new Exception( $json['error']['message'] );
480 }
481 }
482 $returned_id = $this->inId;
483 $returned_model = $this->inModel ? $this->inModel : $query->model;
484 $returned_choices = [
485 [
486 'message' => [
487 'content' => $this->streamContent,
488 'function_call' => $this->streamFunctionCall
489 ]
490 ]
491 ];
492 }
493 else {
494 // Regular data
495 $data = $res['data'];
496 if ( empty( $data ) ) {
497 throw new Exception( 'No content received (res is null).' );
498 }
499 if ( !$data['model'] ) {
500 error_log( print_r( $data, 1 ) );
501 throw new Exception( 'Invalid response (no model information).' );
502 }
503 $returned_id = $data['id'];
504 $returned_model = $data['model'];
505 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ? $data['usage']['prompt_tokens'] : null;
506 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ? $data['usage']['completion_tokens'] : null;
507 $returned_choices = $data['choices'];
508 }
509
510 // Set the results.
511 $reply->set_choices( $returned_choices );
512 if ( !empty( $returned_id ) ) {
513 $reply->set_id( $returned_id );
514 }
515
516 // Handle tokens.
517 $this->handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens );
518
519 return $reply;
520 }
521 catch ( Exception $e ) {
522 error_log( $e->getMessage() );
523 $service = $this->get_service_name();
524 $message = "From $service: " . $e->getMessage();
525 throw new Exception( $message );
526 }
527 }
528
529 public function handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens ) {
530 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens : $reply->get_in_tokens( $query );
531 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens : $reply->get_out_tokens();
532 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
533 $reply->set_usage( $usage );
534 }
535
536 // Request to DALL-E API
537 public function run_images_query( $query ) {
538 $url = 'https://api.openai.com/v1/images/generations';
539 $model = $query->model;
540 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
541 $body = array(
542 "prompt" => $query->message,
543 "n" => $query->maxResults,
544 "size" => $resolution,
545 );
546 if ( $model === 'dall-e-3' ) {
547 $body['model'] = 'dall-e-3';
548 }
549 if ( $model === 'dall-e-3-hd' ) {
550 $body['model'] = 'dall-e-3';
551 $body['quality'] = 'hd';
552 }
553 if ( !empty( $query->style ) && strpos( $model, 'dall-e-3' ) === 0 ) {
554 $body['style'] = $query->style;
555 }
556 if ( $this->envType === 'azure' ) {
557 $deployment_name = $this->get_azure_deployment_name( $query->model );
558 $url = trailingslashit( $this->endpoint ) . 'openai/deployments/' .
559 $deployment_name . '/images/generations?' . $this->azureApiVersion;
560 }
561 $headers = $this->build_headers( $query );
562 $options = $this->build_options( $headers, $body );
563
564 try {
565 $res = $this->run_query( $url, $options );
566 $data = $res['data'];
567 $choices = [];
568 if ( $this->envType === 'azure' ) {
569 foreach ( $data['data'] as $entry ) {
570 $choices[] = [ 'url' => $entry['url'] ];
571 }
572 }
573 else {
574 $choices = $data['data'];
575 }
576
577 $reply = new Meow_MWAI_Reply( $query );
578 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
579 $reply->set_usage( $usage );
580 $reply->set_choices( $choices );
581 $reply->set_type( 'images' );
582
583 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
584 foreach ( $reply->results as &$result ) {
585 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
586 'query_envId' => $query->envId,
587 'query_session' => $query->session,
588 'query_model' => $query->model,
589 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
590 $fileUrl = $this->core->files->get_url( $fileId );
591 $result = $fileUrl;
592 }
593 }
594 $reply->result = $reply->results[0];
595 return $reply;
596 }
597 catch ( Exception $e ) {
598 error_log( $e->getMessage() );
599 $service = $this->get_service_name();
600 throw new Exception( "From $service: " . $e->getMessage() );
601 }
602 }
603
604 /*
605 This is the rest of the OpenAI API support, not related to the models directly.
606 */
607
608 // Check if there are errors in the response from OpenAI, and throw an exception if so.
609 public function handle_response_errors( $data ) {
610 if ( isset( $data['error'] ) ) {
611 $message = $data['error']['message'];
612 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
613 $message = str_replace( $matches[1], '', $message );
614 }
615 throw new Exception( $message );
616 }
617 }
618
619 public function list_files()
620 {
621 return $this->execute( 'GET', '/files' );
622 }
623
624 static function get_suffix_for_model($model)
625 {
626 // Legacy fine-tuned models
627 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
628 if ( count( $matches ) > 0 ) {
629 return $matches[1];
630 }
631
632 // New fine-tuned models
633 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
634 if (count($matches) > 0) {
635 return $matches[1];
636 }
637
638 return 'N/A';
639 }
640
641 static function get_finetune_base_model($model)
642 {
643 // New fine-tuned models
644 preg_match("/^ft:([^:]+):/", $model, $matches);
645 if (count($matches) > 0) {
646 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
647 return "gpt-3.5-turbo";
648 }
649 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
650 return "gpt-4";
651 }
652 return $matches[1];
653 }
654
655 // Legacy fine-tuned models
656 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
657 if ( count( $matches ) > 0 ) {
658 return $matches[1];
659 }
660
661 return null;
662 }
663
664 public function list_deleted_finetunes( $envId = null, $legacy = false )
665 {
666 $finetunes = $this->list_finetunes( $legacy );
667 $deleted = [];
668
669 foreach ( $finetunes as $finetune ) {
670 $name = $finetune['model'];
671 $isSucceeded = $finetune['status'] === 'succeeded';
672 if ( $isSucceeded ) {
673 try {
674 $finetune = $this->get_model( $name );
675 }
676 catch ( Exception $e ) {
677 $deleted[] = $name;
678 }
679 }
680 }
681 if ( $legacy ) {
682 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
683 }
684 else {
685 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
686 }
687 return $deleted;
688 }
689
690 // public function listModels() {
691 // $res = $this->execute( 'GET', '/models' );
692 // // TODO: Not used by the UI.
693 // throw new Exception( 'Not implemented yet.' );
694 // }
695
696 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
697 // retrieve all the models since Summer 2023, let's see! WIP.
698 public function list_finetunes( $legacy = false )
699 {
700 if ( $legacy ) {
701 $res = $this->execute( 'GET', '/fine-tunes' );
702 }
703 else {
704 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
705 }
706 $finetunes = $res['data'];
707
708 // Add suffix
709 $finetunes = array_map( function ( $finetune ) {
710 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
711 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
712 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
713 $finetune['base_model'] = $finetune['model'];
714 $finetune['model'] = $finetune['fine_tuned_model'];
715 unset( $finetune['object'] );
716 unset( $finetune['hyperparams'] );
717 unset( $finetune['result_files'] );
718 unset( $finetune['training_files'] );
719 unset( $finetune['validation_files'] );
720 unset( $finetune['created_at'] );
721 unset( $finetune['updated_at'] );
722 unset( $finetune['fine_tuned_model'] );
723 return $finetune;
724 }, $finetunes);
725
726 usort( $finetunes, function ( $a, $b ) {
727 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
728 });
729
730 if ( $legacy ) {
731 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
732 }
733 else {
734 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
735 }
736
737 return $finetunes;
738 }
739
740 public function moderate( $input ) {
741 $result = $this->execute('POST', '/moderations', [
742 'input' => $input
743 ]);
744 return $result;
745 }
746
747 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
748 {
749 $result = $this->execute('POST', '/files', null, [
750 'purpose' => $purpose,
751 'data' => $data,
752 'file' => $filename
753 ] );
754 return $result;
755 }
756
757 public function delete_file( $fileId )
758 {
759 return $this->execute( 'DELETE', '/files/' . $fileId );
760 }
761
762 public function get_model( $modelId )
763 {
764 return $this->execute( 'GET', '/models/' . $modelId );
765 }
766
767 public function cancel_finetune( $fineTuneId )
768 {
769 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
770 }
771
772 public function delete_finetune( $modelId )
773 {
774 return $this->execute( 'DELETE', '/models/' . $modelId );
775 }
776
777 public function download_file( $fileId, $newFile = null ) {
778 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
779 $fileInfo = json_decode( (string)$fileInfo, true );
780 $filename = $fileInfo['filename'];
781 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
782 if ( empty( $newFile ) ) {
783 include_once( ABSPATH . 'wp-admin/includes/file.php' );
784 $tempFile = wp_tempnam( $filename );
785 if ( !$tempFile ) {
786 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
787 }
788 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
789 $newFile = $tempFile . '.' . $extension;
790 }
791 else {
792 $newFile = $tempFile;
793 }
794 }
795 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
796 file_put_contents( $newFile, $data );
797 return $newFile;
798 }
799
800 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
801 {
802 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
803 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
804 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
805 (float)$hyperparams['learningRateMultiplier'] : null;
806 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
807 (float)$hyperparams['promptLossWeight'] : null;
808 $arguments = [
809 'training_file' => $fileId,
810 'model' => $model,
811 'suffix' => $suffix
812 ];
813 if ( $legacy ) {
814 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
815 }
816 else {
817 if ( $n_epochs ) {
818 $arguments['hyperparams'] = [];
819 $arguments['hyperparams']['n_epochs'] = $n_epochs;
820 }
821 if ( $batch_size ) {
822 if ( empty( $arguments['hyperparams'] ) ) {
823 $arguments['hyperparams'] = [];
824 }
825 $arguments['hyperparams']['batch_size'] = $batch_size;
826 }
827 if ( $learning_rate_multiplier ) {
828 if ( empty( $arguments['hyperparams'] ) ) {
829 $arguments['hyperparams'] = [];
830 }
831 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
832 }
833 if ( $prompt_loss_weight ) {
834 if ( empty( $arguments['hyperparams'] ) ) {
835 $arguments['hyperparams'] = [];
836 }
837 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
838 }
839 if ( $model === 'turbo' ) {
840 $arguments['model'] = 'gpt-3.5-turbo';
841 }
842 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
843 }
844 return $result;
845 }
846
847 /**
848 * Build the body of a form request.
849 * If the field name is 'file', then the field value is the filename of the file to upload.
850 * The file contents are taken from the 'data' field.
851 *
852 * @param array $fields
853 * @param string $boundary
854 * @return string
855 */
856 public function build_form_body( $fields, $boundary )
857 {
858 $body = '';
859 foreach ( $fields as $name => $value ) {
860 if ( $name == 'data' ) {
861 continue;
862 }
863 $body .= "--$boundary\r\n";
864 $body .= "Content-Disposition: form-data; name=\"$name\"";
865 if ( $name == 'file' ) {
866 $body .= "; filename=\"{$value}\"\r\n";
867 $body .= "Content-Type: application/json\r\n\r\n";
868 $body .= $fields['data'] . "\r\n";
869 }
870 else {
871 $body .= "\r\n\r\n$value\r\n";
872 }
873 }
874 $body .= "--$boundary--\r\n";
875 return $body;
876 }
877
878 /**
879 * Run a request to the OpenAI API.
880 * Fore more information about the $formFields, refer to the build_form_body method.
881 *
882 * @param string $method POST, PUT, GET, DELETE...
883 * @param string $url The API endpoint
884 * @param array $query The query parameters (json)
885 * @param array $formFields The form fields (multipart/form-data)
886 * @param bool $json Whether to return the response as json or not
887 * @return array
888 */
889 public function execute( $method, $url, $query = null, $formFields = null, $json = true, $extraHeaders = null )
890 {
891 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
892 $body = $query ? json_encode( $query ) : null;
893 if ( !empty( $formFields ) ) {
894 $boundary = wp_generate_password( 24, false );
895 $headers = [
896 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
897 'Authorization' => 'Bearer ' . $this->apiKey
898 ];
899 $body = $this->build_form_body( $formFields, $boundary );
900 }
901
902 // Maybe we should have headers always as an array... not sure why we have it as a string.
903 if ( !empty( $extraHeaders ) ) {
904 foreach ( $extraHeaders as $key => $value ) {
905 if ( is_array( $headers ) ) {
906 $headers[$key] = $value;
907 }
908 else {
909 $headers .= "$key: $value\r\n";
910 }
911 }
912 }
913
914 $url = 'https://api.openai.com/v1' . $url;
915 $options = [
916 "headers" => $headers,
917 "method" => $method,
918 "timeout" => MWAI_TIMEOUT,
919 "body" => $body,
920 "sslverify" => false
921 ];
922
923 try {
924 $response = wp_remote_request( $url, $options );
925 if ( is_wp_error( $response ) ) {
926 throw new Exception( $response->get_error_message() );
927 }
928 $response = wp_remote_retrieve_body( $response );
929 $data = $json ? json_decode( $response, true ) : $response;
930 $this->handle_response_errors( $data );
931 return $data;
932 }
933 catch ( Exception $e ) {
934 error_log( $e->getMessage() );
935 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
936 }
937 }
938
939 public function get_models() {
940 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
941 }
942
943 static public function get_models_static() {
944 return MWAI_OPENAI_MODELS;
945 }
946
947 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
948 {
949 // For fine-tuned models:
950 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
951 if ( !empty( $potentialBaseModel ) ) {
952 $modelFamily = $potentialBaseModel;
953 $finetune = true;
954 }
955
956 $models = $this->get_models();
957 foreach ( $models as $currentModel ) {
958 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
959 if ( $currentModel['type'] === 'image' ) {
960 if ( !$option ) {
961 error_log( "AI Engine: Image models require an option." );
962 return null;
963 }
964 else {
965 foreach ( $currentModel['options'] as $imageType ) {
966 if ( $imageType['option'] == $option ) {
967 return $imageType['price'] * $outUnits;
968 }
969 }
970 }
971 }
972 else {
973 if ( $finetune ) {
974
975 if ( isset( $currentModel['finetune']['price'] ) ) {
976 $currentModel['price'] = $currentModel['finetune']['price'];
977 }
978 else if ( isset( $currentModel['finetune']['in'] ) ) {
979 $currentModel['price'] = [
980 'in' => $currentModel['finetune']['in'],
981 'out' => $currentModel['finetune']['out']
982 ];
983 }
984 }
985 $inPrice = $currentModel['price'];
986 $outPrice = $currentModel['price'];
987 if ( is_array( $currentModel['price'] ) ) {
988 $inPrice = $currentModel['price']['in'];
989 $outPrice = $currentModel['price']['out'];
990 }
991 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
992 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
993 return $inTotalPrice + $outTotalPrice;
994 }
995 }
996 }
997 error_log( "AI Engine: Invalid model ($modelFamily)." );
998 return null;
999 }
1000
1001 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1002 {
1003 $model = $query->model;
1004 $units = 0;
1005 $option = null;
1006
1007 $finetune = false;
1008 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1009 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1010 $finetune = true;
1011 }
1012 $inUnits = $reply->get_in_tokens( $query );
1013 $outUnits = $reply->get_out_tokens();
1014 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
1015 }
1016 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1017 /** @var Meow_MWAI_Query_Image $query */
1018 $units = $query->maxResults;
1019 $option = $query->resolution;
1020 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1021 }
1022 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1023 $model = 'whisper';
1024 $units = $reply->get_units();
1025 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1026 }
1027 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1028 $units = $reply->get_total_tokens();
1029 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1030 }
1031 error_log("AI Engine: Cannot calculate price for $model.");
1032 return null;
1033 }
1034
1035 public function get_incidents() {
1036 $url = 'https://status.openai.com/history.rss';
1037 $response = wp_remote_get( $url );
1038 if ( is_wp_error( $response ) ) {
1039 throw new Exception( $response->get_error_message() );
1040 }
1041 $response = wp_remote_retrieve_body( $response );
1042 $xml = simplexml_load_string( $response );
1043 $incidents = array();
1044 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1045 foreach ( $xml->channel->item as $item ) {
1046 $date = strtotime( $item->pubDate );
1047 if ( $date > $oneWeekAgo ) {
1048 $incidents[] = array(
1049 'title' => (string) $item->title,
1050 'description' => (string) $item->description,
1051 'date' => $date
1052 );
1053 }
1054 }
1055 return $incidents;
1056 }
1057 }
1058