PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.1.5
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.1.5
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
core.php 2 years ago factory.php 2 years ago openai.php 2 years ago openrouter.php 2 years ago
openai.php
1066 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 // Transfer the images to the local server if needed.
584 $localDownload = $this->core->get_option( 'image_local_download' );
585 if ( $localDownload === 'uploads' || $localDownload === 'library' ) {
586 foreach ( $reply->results as &$result ) {
587 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
588 'query_envId' => $query->envId,
589 'query_session' => $query->session,
590 'query_model' => $query->model,
591 ], $query->envId );
592 $fileUrl = $this->core->files->get_url( $fileId );
593 $result = $fileUrl;
594 }
595 }
596
597 // Convert the URLs into Markdown.
598 $reply->result = "";
599 foreach ( $reply->results as $result ) {
600 $reply->result .= "![Image]($result)\n";
601 }
602
603 return $reply;
604 }
605 catch ( Exception $e ) {
606 error_log( $e->getMessage() );
607 $service = $this->get_service_name();
608 throw new Exception( "From $service: " . $e->getMessage() );
609 }
610 }
611
612 /*
613 This is the rest of the OpenAI API support, not related to the models directly.
614 */
615
616 // Check if there are errors in the response from OpenAI, and throw an exception if so.
617 public function handle_response_errors( $data ) {
618 if ( isset( $data['error'] ) ) {
619 $message = $data['error']['message'];
620 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
621 $message = str_replace( $matches[1], '', $message );
622 }
623 throw new Exception( $message );
624 }
625 }
626
627 public function list_files()
628 {
629 return $this->execute( 'GET', '/files' );
630 }
631
632 static function get_suffix_for_model($model)
633 {
634 // Legacy fine-tuned models
635 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
636 if ( count( $matches ) > 0 ) {
637 return $matches[1];
638 }
639
640 // New fine-tuned models
641 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
642 if (count($matches) > 0) {
643 return $matches[1];
644 }
645
646 return 'N/A';
647 }
648
649 static function get_finetune_base_model($model)
650 {
651 // New fine-tuned models
652 preg_match("/^ft:([^:]+):/", $model, $matches);
653 if (count($matches) > 0) {
654 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
655 return "gpt-3.5-turbo";
656 }
657 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
658 return "gpt-4";
659 }
660 return $matches[1];
661 }
662
663 // Legacy fine-tuned models
664 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
665 if ( count( $matches ) > 0 ) {
666 return $matches[1];
667 }
668
669 return null;
670 }
671
672 public function list_deleted_finetunes( $envId = null, $legacy = false )
673 {
674 $finetunes = $this->list_finetunes( $legacy );
675 $deleted = [];
676
677 foreach ( $finetunes as $finetune ) {
678 $name = $finetune['model'];
679 $isSucceeded = $finetune['status'] === 'succeeded';
680 if ( $isSucceeded ) {
681 try {
682 $finetune = $this->get_model( $name );
683 }
684 catch ( Exception $e ) {
685 $deleted[] = $name;
686 }
687 }
688 }
689 if ( $legacy ) {
690 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
691 }
692 else {
693 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
694 }
695 return $deleted;
696 }
697
698 // public function listModels() {
699 // $res = $this->execute( 'GET', '/models' );
700 // // TODO: Not used by the UI.
701 // throw new Exception( 'Not implemented yet.' );
702 // }
703
704 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
705 // retrieve all the models since Summer 2023, let's see! WIP.
706 public function list_finetunes( $legacy = false )
707 {
708 if ( $legacy ) {
709 $res = $this->execute( 'GET', '/fine-tunes' );
710 }
711 else {
712 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
713 }
714 $finetunes = $res['data'];
715
716 // Add suffix
717 $finetunes = array_map( function ( $finetune ) {
718 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
719 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
720 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
721 $finetune['base_model'] = $finetune['model'];
722 $finetune['model'] = $finetune['fine_tuned_model'];
723 unset( $finetune['object'] );
724 unset( $finetune['hyperparams'] );
725 unset( $finetune['result_files'] );
726 unset( $finetune['training_files'] );
727 unset( $finetune['validation_files'] );
728 unset( $finetune['created_at'] );
729 unset( $finetune['updated_at'] );
730 unset( $finetune['fine_tuned_model'] );
731 return $finetune;
732 }, $finetunes);
733
734 usort( $finetunes, function ( $a, $b ) {
735 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
736 });
737
738 if ( $legacy ) {
739 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
740 }
741 else {
742 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
743 }
744
745 return $finetunes;
746 }
747
748 public function moderate( $input ) {
749 $result = $this->execute('POST', '/moderations', [
750 'input' => $input
751 ]);
752 return $result;
753 }
754
755 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
756 {
757 $result = $this->execute('POST', '/files', null, [
758 'purpose' => $purpose,
759 'data' => $data,
760 'file' => $filename
761 ] );
762 return $result;
763 }
764
765 public function delete_file( $fileId )
766 {
767 return $this->execute( 'DELETE', '/files/' . $fileId );
768 }
769
770 public function get_model( $modelId )
771 {
772 return $this->execute( 'GET', '/models/' . $modelId );
773 }
774
775 public function cancel_finetune( $fineTuneId )
776 {
777 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
778 }
779
780 public function delete_finetune( $modelId )
781 {
782 return $this->execute( 'DELETE', '/models/' . $modelId );
783 }
784
785 public function download_file( $fileId, $newFile = null ) {
786 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
787 $fileInfo = json_decode( (string)$fileInfo, true );
788 $filename = $fileInfo['filename'];
789 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
790 if ( empty( $newFile ) ) {
791 include_once( ABSPATH . 'wp-admin/includes/file.php' );
792 $tempFile = wp_tempnam( $filename );
793 if ( !$tempFile ) {
794 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
795 }
796 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
797 $newFile = $tempFile . '.' . $extension;
798 }
799 else {
800 $newFile = $tempFile;
801 }
802 }
803 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
804 file_put_contents( $newFile, $data );
805 return $newFile;
806 }
807
808 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
809 {
810 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
811 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
812 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
813 (float)$hyperparams['learningRateMultiplier'] : null;
814 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
815 (float)$hyperparams['promptLossWeight'] : null;
816 $arguments = [
817 'training_file' => $fileId,
818 'model' => $model,
819 'suffix' => $suffix
820 ];
821 if ( $legacy ) {
822 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
823 }
824 else {
825 if ( $n_epochs ) {
826 $arguments['hyperparams'] = [];
827 $arguments['hyperparams']['n_epochs'] = $n_epochs;
828 }
829 if ( $batch_size ) {
830 if ( empty( $arguments['hyperparams'] ) ) {
831 $arguments['hyperparams'] = [];
832 }
833 $arguments['hyperparams']['batch_size'] = $batch_size;
834 }
835 if ( $learning_rate_multiplier ) {
836 if ( empty( $arguments['hyperparams'] ) ) {
837 $arguments['hyperparams'] = [];
838 }
839 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
840 }
841 if ( $prompt_loss_weight ) {
842 if ( empty( $arguments['hyperparams'] ) ) {
843 $arguments['hyperparams'] = [];
844 }
845 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
846 }
847 if ( $model === 'turbo' ) {
848 $arguments['model'] = 'gpt-3.5-turbo';
849 }
850 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
851 }
852 return $result;
853 }
854
855 /**
856 * Build the body of a form request.
857 * If the field name is 'file', then the field value is the filename of the file to upload.
858 * The file contents are taken from the 'data' field.
859 *
860 * @param array $fields
861 * @param string $boundary
862 * @return string
863 */
864 public function build_form_body( $fields, $boundary )
865 {
866 $body = '';
867 foreach ( $fields as $name => $value ) {
868 if ( $name == 'data' ) {
869 continue;
870 }
871 $body .= "--$boundary\r\n";
872 $body .= "Content-Disposition: form-data; name=\"$name\"";
873 if ( $name == 'file' ) {
874 $body .= "; filename=\"{$value}\"\r\n";
875 $body .= "Content-Type: application/json\r\n\r\n";
876 $body .= $fields['data'] . "\r\n";
877 }
878 else {
879 $body .= "\r\n\r\n$value\r\n";
880 }
881 }
882 $body .= "--$boundary--\r\n";
883 return $body;
884 }
885
886 /**
887 * Run a request to the OpenAI API.
888 * Fore more information about the $formFields, refer to the build_form_body method.
889 *
890 * @param string $method POST, PUT, GET, DELETE...
891 * @param string $url The API endpoint
892 * @param array $query The query parameters (json)
893 * @param array $formFields The form fields (multipart/form-data)
894 * @param bool $json Whether to return the response as json or not
895 * @return array
896 */
897 public function execute( $method, $url, $query = null, $formFields = null, $json = true, $extraHeaders = null )
898 {
899 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
900 $body = $query ? json_encode( $query ) : null;
901 if ( !empty( $formFields ) ) {
902 $boundary = wp_generate_password( 24, false );
903 $headers = [
904 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
905 'Authorization' => 'Bearer ' . $this->apiKey
906 ];
907 $body = $this->build_form_body( $formFields, $boundary );
908 }
909
910 // Maybe we should have headers always as an array... not sure why we have it as a string.
911 if ( !empty( $extraHeaders ) ) {
912 foreach ( $extraHeaders as $key => $value ) {
913 if ( is_array( $headers ) ) {
914 $headers[$key] = $value;
915 }
916 else {
917 $headers .= "$key: $value\r\n";
918 }
919 }
920 }
921
922 $url = 'https://api.openai.com/v1' . $url;
923 $options = [
924 "headers" => $headers,
925 "method" => $method,
926 "timeout" => MWAI_TIMEOUT,
927 "body" => $body,
928 "sslverify" => false
929 ];
930
931 try {
932 $response = wp_remote_request( $url, $options );
933 if ( is_wp_error( $response ) ) {
934 throw new Exception( $response->get_error_message() );
935 }
936 $response = wp_remote_retrieve_body( $response );
937 $data = $json ? json_decode( $response, true ) : $response;
938 $this->handle_response_errors( $data );
939 return $data;
940 }
941 catch ( Exception $e ) {
942 error_log( $e->getMessage() );
943 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
944 }
945 }
946
947 public function get_models() {
948 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
949 }
950
951 static public function get_models_static() {
952 return MWAI_OPENAI_MODELS;
953 }
954
955 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
956 {
957 // For fine-tuned models:
958 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
959 if ( !empty( $potentialBaseModel ) ) {
960 $modelFamily = $potentialBaseModel;
961 $finetune = true;
962 }
963
964 $models = $this->get_models();
965 foreach ( $models as $currentModel ) {
966 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
967 if ( $currentModel['type'] === 'image' ) {
968 if ( !$option ) {
969 error_log( "AI Engine: Image models require an option." );
970 return null;
971 }
972 else {
973 foreach ( $currentModel['options'] as $imageType ) {
974 if ( $imageType['option'] == $option ) {
975 return $imageType['price'] * $outUnits;
976 }
977 }
978 }
979 }
980 else {
981 if ( $finetune ) {
982
983 if ( isset( $currentModel['finetune']['price'] ) ) {
984 $currentModel['price'] = $currentModel['finetune']['price'];
985 }
986 else if ( isset( $currentModel['finetune']['in'] ) ) {
987 $currentModel['price'] = [
988 'in' => $currentModel['finetune']['in'],
989 'out' => $currentModel['finetune']['out']
990 ];
991 }
992 }
993 $inPrice = $currentModel['price'];
994 $outPrice = $currentModel['price'];
995 if ( is_array( $currentModel['price'] ) ) {
996 $inPrice = $currentModel['price']['in'];
997 $outPrice = $currentModel['price']['out'];
998 }
999 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1000 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1001 return $inTotalPrice + $outTotalPrice;
1002 }
1003 }
1004 }
1005 error_log( "AI Engine: Invalid model ($modelFamily)." );
1006 return null;
1007 }
1008
1009 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1010 {
1011 $model = $query->model;
1012 $units = 0;
1013 $option = null;
1014
1015 $finetune = false;
1016 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1017 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1018 $finetune = true;
1019 }
1020 $inUnits = $reply->get_in_tokens( $query );
1021 $outUnits = $reply->get_out_tokens();
1022 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
1023 }
1024 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1025 /** @var Meow_MWAI_Query_Image $query */
1026 $units = $query->maxResults;
1027 $option = $query->resolution;
1028 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1029 }
1030 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1031 $model = 'whisper';
1032 $units = $reply->get_units();
1033 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1034 }
1035 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1036 $units = $reply->get_total_tokens();
1037 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1038 }
1039 error_log("AI Engine: Cannot calculate price for $model.");
1040 return null;
1041 }
1042
1043 public function get_incidents() {
1044 $url = 'https://status.openai.com/history.rss';
1045 $response = wp_remote_get( $url );
1046 if ( is_wp_error( $response ) ) {
1047 throw new Exception( $response->get_error_message() );
1048 }
1049 $response = wp_remote_retrieve_body( $response );
1050 $xml = simplexml_load_string( $response );
1051 $incidents = array();
1052 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1053 foreach ( $xml->channel->item as $item ) {
1054 $date = strtotime( $item->pubDate );
1055 if ( $date > $oneWeekAgo ) {
1056 $incidents[] = array(
1057 'title' => (string) $item->title,
1058 'description' => (string) $item->description,
1059 'date' => $date
1060 );
1061 }
1062 }
1063 return $incidents;
1064 }
1065 }
1066