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