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