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