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