PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 1.8.9
AI Engine – The Chatbot, AI Framework & MCP for WordPress v1.8.9
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 3 years ago openai.php 2 years ago
openai.php
832 lines
1 <?php
2
3 class Meow_MWAI_Engines_OpenAI
4 {
5 private $core = null;
6 private $localApiKey = null;
7 private $localService = null;
8
9 // OpenAI Server
10 private $openaiEndpoint = 'https://api.openai.com/v1';
11
12 // Azure Server
13 private $localAzureEndpoint = null;
14 private $localAzureApiKey = null;
15 private $localAzureDeployments = null;
16 private $azureApiVersion = 'api-version=2023-03-15-preview';
17
18 // Streaming
19 private $streamTemporaryBuffer = "";
20 private $streamContent = "";
21 private $streamFunctionCall = null;
22 private $streamParameter = "";
23 private $streamCallback = null;
24 private $streamedTokens = 0;
25
26 public function __construct( $core )
27 {
28 $this->core = $core;
29 $this->localService = $this->core->get_option( 'openai_service' );
30 $this->localApiKey = $this->core->get_option( 'openai_apikey' );
31 $this->localAzureEndpoint = $this->core->get_option( 'openai_azure_endpoint' );
32 $this->localAzureApiKey = $this->core->get_option( 'openai_azure_apikey' );
33 $this->localAzureDeployments = $this->core->get_option( 'openai_azure_deployments' );
34 $this->localAzureDeployments[] = [ 'model' => 'dall-e', 'name' => 'dall-e' ];
35 }
36
37
38 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
39 function check_for_error( $data ) {
40 if ( strpos( $data, '"error"' ) !== false ) {
41 $json = json_decode( $data, true );
42 if ( json_last_error() === JSON_ERROR_NONE ) {
43 $error = $json['error'];
44 $code = $error['code'];
45 $message = $error['message'];
46 throw new Exception( "Error $code: $message" );
47 }
48 }
49 }
50
51 /*
52 This used to be in the core.php, but since it's relative to OpenAI, it's better to have it here.
53 */
54
55 public function stream_handler( $handle, $args, $url ) {
56 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
57 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
58
59 // Maybe we could get some info from headers, as for now, there is only the model.
60 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function( $curl, $headerLine ) {
61 // $line = trim( $headerLine );
62 // return strlen( $headerLine );
63 // });
64
65 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) {
66 $length = strlen( $data );
67
68 // FOR DEBUG:
69 // preg_match_all( '/"content":"(.*?)"/', $data, $matches );
70 // $contents = $matches[1];
71 // foreach ( $contents as $content ) {
72 // error_log( "Content: $content" );
73 // }
74
75 // Error Management
76 $this->check_for_error( $data );
77
78 // Bufferize the unfinished stream (if it's the case)
79 $this->streamTemporaryBuffer .= $data;
80 $lines = explode( "\n", $this->streamTemporaryBuffer );
81 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
82 $this->streamTemporaryBuffer = array_pop( $lines );
83 }
84 else {
85 $this->streamTemporaryBuffer = "";
86 }
87
88 foreach ( $lines as $line ) {
89 if ( $line === "" ) {
90 continue;
91 }
92 if ( strpos($line, 'data: ' ) === 0 ) {
93 $line = substr( $line, 6 );
94 $json = json_decode( $line, true );
95
96 if ( json_last_error() === JSON_ERROR_NONE ) {
97 $content = null;
98 if ( isset( $json['choices'][0]['text'] ) ) {
99 $content = $json['choices'][0]['text'];
100 }
101 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
102 $content = $json['choices'][0]['delta']['content'];
103 }
104 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
105 $function_call = $json['choices'][0]['delta']['function_call'];
106 if ( empty( $this->streamFunctionCall ) ) {
107 $this->streamFunctionCall = [ 'name' => "", 'arguments' => "" ];
108 }
109 if ( isset( $function_call['name'] ) ) {
110 $this->streamFunctionCall['name'] .= $function_call['name'];
111 }
112 if ( isset( $function_call['arguments'] ) ) {
113 $this->streamFunctionCall['arguments'] .= $function_call['arguments'];
114 }
115 }
116 if ( $content !== null && $content !== "" ) {
117 $this->streamedTokens += count( explode( " ", $content ) );
118 $this->streamContent .= $content;
119 call_user_func( $this->streamCallback, $content );
120 }
121 }
122 else {
123 $this->streamTemporaryBuffer .= $line . "\n";
124 }
125 }
126 }
127 return $length;
128 });
129 }
130
131 private function buildHeaders( $query ) {
132 $headers = array(
133 'Content-Type' => 'application/json',
134 'Authorization' => 'Bearer ' . $query->apiKey,
135 );
136 if ( $query->service === 'azure' ) {
137 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $query->azureApiKey );
138 }
139 return $headers;
140 }
141
142 private function buildOptions( $headers, $json = null, $forms = null ) {
143
144 // Build body
145 $body = null;
146 if ( !empty( $forms ) ) {
147 $boundary = wp_generate_password ( 24, false );
148 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
149 $body = $this->buildFormBody( $forms, $boundary );
150 }
151 else if ( !empty( $json ) ) {
152 $body = json_encode( $json );
153 }
154
155 // Build options
156 $options = array(
157 'headers' => $headers,
158 'method' => 'POST',
159 'timeout' => MWAI_TIMEOUT,
160 'body' => $body,
161 'sslverify' => false
162 );
163
164 return $options;
165 }
166
167 public function runQuery( $url, $options, $isStream = false ) {
168 try {
169 $options['stream'] = $isStream;
170 if ( $isStream ) {
171 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
172 }
173 $res = wp_remote_get( $url, $options );
174
175 if ( is_wp_error( $res ) ) {
176 throw new Exception( $res->get_error_message() );
177 }
178
179 if ( $isStream ) {
180 return [ 'stream' => true ];
181 }
182
183 $response = wp_remote_retrieve_body( $res );
184 $headersRes = wp_remote_retrieve_headers( $res );
185 $headers = $headersRes->getAll();
186
187 // If Headers contains multipart/form-data then we don't need to decode the response
188 if ( strpos( $options['headers']['Content-Type'], 'multipart/form-data' ) !== false ) {
189 return [
190 'stream' => false,
191 'headers' => $headers,
192 'data' => $response
193 ];
194 }
195
196 $data = json_decode( $response, true );
197 $this->handleResponseErrors( $data );
198
199 return [
200 'headers' => $headers,
201 'data' => $data
202 ];
203 }
204 catch ( Exception $e ) {
205 error_log( $e->getMessage() );
206 throw $e;
207 }
208 }
209
210 private function applyQueryParameters( $query ) {
211 if ( empty( $query->service ) ) {
212 $query->service = $this->localService;
213 }
214
215 // OpenAI will be used by default for everything
216 if ( empty( $query->apiKey ) ) {
217 $query->apiKey = $this->localApiKey;
218 }
219
220 // But if the service is set to Azure and the deployments/models are available,
221 // then we will use Azure instead.
222 if ( $query->service === 'azure' && !empty( $this->localAzureDeployments ) ) {
223 $found = false;
224 foreach ( $this->localAzureDeployments as $deployment ) {
225 if ( $deployment['model'] === $query->model && !empty( $deployment['name'] ) ) {
226 $query->azureDeployment = $deployment['name'];
227 if ( empty( $query->azureEndpoint ) ) {
228 $query->azureEndpoint = $this->localAzureEndpoint;
229 }
230 if ( empty( $query->azureApiKey ) ) {
231 $query->azureApiKey = $this->localAzureApiKey;
232 }
233 $found = true;
234 break;
235 }
236 }
237 if ( !$found ) {
238 error_log( 'Azure deployment not found for model: ' . $query->model );
239 $query->service = 'openai';
240 }
241 }
242 }
243
244 private function getAudio( $url ) {
245 require_once( ABSPATH . 'wp-admin/includes/media.php' );
246 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
247 file_put_contents( $tmpFile, file_get_contents( $url ) );
248 $length = null;
249 $metadata = wp_read_audio_metadata( $tmpFile );
250 if ( isset( $metadata['length'] ) ) {
251 $length = $metadata['length'];
252 }
253 $data = file_get_contents( $tmpFile );
254 unlink( $tmpFile );
255 return [ 'data' => $data, 'length' => $length ];
256 }
257
258 public function runTranscribeQuery( $query ) {
259 $this->applyQueryParameters( $query );
260
261 // Prepare the request.
262 $modeEndpoint = $query->mode === 'translation' ? 'translations' : 'transcriptions';
263 $url = 'https://api.openai.com/v1/audio/' . $modeEndpoint;
264
265 // Check if the URL is valid.
266 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
267 throw new Exception( 'Invalid URL for transcription.' );
268 }
269
270 $audioData = $this->getAudio( $query->url );
271 $body = array(
272 'prompt' => $query->prompt,
273 'model' => $query->model,
274 'response_format' => 'text',
275 'file' => basename( $query->url ),
276 'data' => $audioData['data']
277 );
278 $headers = $this->buildHeaders( $query );
279 $options = $this->buildOptions( $headers, null, $body );
280
281 // Perform the request
282 try {
283 $res = $this->runQuery( $url, $options );
284 $data = $res['data'];
285 if ( empty( $data ) ) {
286 throw new Exception( 'Invalid data for transcription.' );
287 }
288 $this->check_for_error( $data );
289 $usage = $this->core->recordAudioUsage( $query->model, $audioData['length'] );
290 $reply = new Meow_MWAI_Reply( $query );
291 $reply->setUsage( $usage );
292 $reply->setChoices( $data );
293 return $reply;
294 }
295 catch ( Exception $e ) {
296 error_log( $e->getMessage() );
297 throw new Exception( $e->getMessage() . " (OpenAI)" );
298 }
299 }
300
301 public function runEmbeddingQuery( $query ) {
302 $this->applyQueryParameters( $query );
303
304 // Prepare the request
305 $url = 'https://api.openai.com/v1/embeddings';
306 $body = array( 'input' => $query->prompt, 'model' => $query->model );
307 if ( $query->service === 'azure' ) {
308 $url = trailingslashit( $query->azureEndpoint ) . 'openai/deployments/' .
309 $query->azureDeployment . '/embeddings?' . $this->azureApiVersion;
310 $body = array( "input" => $query->prompt );
311 }
312 $headers = $this->buildHeaders( $query );
313 $options = $this->buildOptions( $headers, $body );
314
315 // Perform the request
316 try {
317 $res = $this->runQuery( $url, $options );
318 $data = $res['data'];
319 if ( empty( $data ) || !isset( $data['data'] ) ) {
320 throw new Exception( 'Invalid data for embedding.' );
321 }
322 $usage = $data['usage'];
323 $this->core->recordTokensUsage( $query->model, $usage['prompt_tokens'] );
324 $reply = new Meow_MWAI_Reply( $query );
325 $reply->setUsage( $usage );
326 $reply->setChoices( $data['data'] );
327 return $reply;
328 }
329 catch ( Exception $e ) {
330 error_log( $e->getMessage() );
331 $service = $query->service === 'azure' ? 'Azure' : 'OpenAI';
332 throw new Exception( $e->getMessage() . " ($service)" );
333 }
334 }
335
336 public function runCompletionQuery( $query, $streamCallback = null ) {
337 $this->applyQueryParameters( $query );
338 if ( !is_null( $streamCallback ) ) {
339 $this->streamCallback = $streamCallback;
340 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
341 }
342 if ( $query->mode !== 'chat' && $query->mode !== 'completion' ) {
343 throw new Exception( 'Unknown mode for query: ' . $query->mode );
344 }
345
346 // Prepare the request
347 $body = array(
348 "model" => $query->model,
349 "stop" => $query->stop,
350 "n" => $query->maxResults,
351 "max_tokens" => $query->maxTokens,
352 "temperature" => $query->temperature,
353 "stream" => !is_null( $streamCallback ),
354 );
355 if ( !empty( $query->functions ) ) {
356 $body['functions'] = $query->functions;
357 $body['function_call'] = $query->functionCall;
358 }
359 if ( $query->mode === 'chat' ) {
360 $body['messages'] = $query->messages;
361 }
362 else if ( $query->mode === 'completion' ) {
363 $body['prompt'] = $query->getPrompt();
364 }
365 $url = $query->service === 'azure' ? trailingslashit( $query->azureEndpoint ) .
366 'openai/deployments/' . $query->azureDeployment : $this->openaiEndpoint;
367 if ( $query->mode === 'chat' ) {
368 $url .= $query->service === 'azure' ? '/chat/completions?' . $this->azureApiVersion : '/chat/completions';
369 }
370 else if ($query->mode === 'completion') {
371 $url .= $query->service === 'azure' ? '/completions?' . $this->azureApiVersion : '/completions';
372 }
373 $headers = $this->buildHeaders( $query );
374 $options = $this->buildOptions( $headers, $body );
375
376 try {
377 $res = $this->runQuery( $url, $options, $streamCallback );
378 $reply = new Meow_MWAI_Reply( $query );
379
380 // Streamed data
381 if ( !is_null( $streamCallback ) ) {
382 $data = [
383 'model' => $query->model,
384 'usage' => [
385 'prompt_tokens' => $query->getPromptTokens(),
386 'completion_tokens' => $this->streamedTokens
387 ],
388 'choices' => [
389 [
390 'message' => [
391 'content' => $this->streamContent,
392 'function_call' => $this->streamFunctionCall
393 ]
394 ]
395 ],
396 ];
397 }
398 // Regular data
399 else {
400 $data = $res['data'];
401 if ( !$data['model'] ) {
402 error_log( print_r( $data, 1 ) );
403 throw new Exception( "Got an unexpected response from OpenAI. Check your PHP Error Logs." );
404 }
405 }
406
407 try {
408 $usage = $this->core->recordTokensUsage(
409 $data['model'],
410 $data['usage']['prompt_tokens'],
411 $data['usage']['completion_tokens']
412 );
413 }
414 catch ( Exception $e ) {
415 error_log( $e->getMessage() );
416 }
417 $reply->setUsage( $usage );
418 $reply->setChoices( $data['choices'] );
419 return $reply;
420 }
421 catch ( Exception $e ) {
422 error_log( $e->getMessage() );
423 $service = $query->service === 'azure' ? 'Azure' : 'OpenAI';
424 throw new Exception( $e->getMessage() . " ($service)" );
425 }
426 }
427
428 // Request to DALL-E API
429 public function runImagesQuery( $query ) {
430 $this->applyQueryParameters( $query );
431
432 // Prepare the request
433 $url = 'https://api.openai.com/v1/images/generations';
434 $body = array(
435 "prompt" => $query->prompt,
436 "n" => $query->maxResults,
437 "size" => '1024x1024',
438 );
439 if ( $query->service === 'azure' ) {
440 //$url = trailingslashit( $query->azureEndpoint ) . 'dalle/text-to-image?' . $this->azureApiVersion;
441 $url = trailingslashit( $query->azureEndpoint ) . 'dalle/text-to-image?api-version=2022-08-03-preview';
442 $body = array(
443 "caption" => $query->prompt,
444 //"n" => $query->maxResults,
445 "resolution" => '1024x1024',
446 );
447 }
448 $headers = $this->buildHeaders( $query );
449 $options = $this->buildOptions( $headers, $body );
450
451 // Perform the request
452 try {
453 $res = $this->runQuery( $url, $options );
454 $data = $res['data'];
455 $choices = [];
456
457 if ( $query->service === 'azure' ) {
458 if ( !isset( $res['headers']['operation-location'] ) || !isset( $res['headers']['retry-after'] ) ) {
459 throw new Exception( 'Invalid response from Azure.' );
460 }
461 $operationLocation = $res['headers']['operation-location'];
462 $retryAfter = (int)$res['headers']['retry-after'];
463 $status = $data['status'];
464 $options = $this->buildOptions( $headers, null );
465 $options['method'] = 'GET';
466 while ( $status !== 'Succeeded' ) {
467 sleep( $retryAfter );
468 $res = $this->runQuery( $operationLocation, $options );
469 $data = $res['data'];
470 $status = $data['status'];
471 }
472 $result = $data['result'];
473 $contentUrl = $result['contentUrl'];
474 $choices = [ [ 'url' => $contentUrl ] ];
475
476 }
477 else {
478 // OpenAI returns an array of URLs
479 $choices = $data['data'];
480 }
481
482 $reply = new Meow_MWAI_Reply( $query );
483 $usage = $this->core->recordImagesUsage( "dall-e", "1024x1024", $query->maxResults );
484 $reply->setUsage( $usage );
485 $reply->setChoices( $choices );
486 $reply->setType( 'images' );
487 return $reply;
488 }
489 catch ( Exception $e ) {
490 error_log( $e->getMessage() );
491 throw new Exception( $e->getMessage() . " (OpenAI)" );
492 }
493 }
494
495
496 /*
497 This is the rest of the OpenAI API support, not related to the models directly.
498 */
499
500 // Check if there are errors in the response from OpenAI, and throw an exception if so.
501 public function handleResponseErrors( $data ) {
502 if ( isset( $data['error'] ) ) {
503 $message = $data['error']['message'];
504 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
505 $message = str_replace( $matches[1], '', $message );
506 }
507 throw new Exception( $message );
508 }
509 }
510
511 public function listFiles()
512 {
513 return $this->run( 'GET', '/files' );
514 }
515
516 function getSuffixForModel($model)
517 {
518 preg_match("/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
519 if ( count( $matches ) > 0 ) {
520 return $matches[1];
521 }
522 return 'N/A';
523 }
524
525 function getBaseModel($model)
526 {
527 preg_match("/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
528 if (count($matches) > 0) {
529 return $matches[1];
530 }
531 return 'N/A';
532 }
533
534 public function listDeletedFineTunes()
535 {
536 $finetunes = $this->listFineTunes();
537 $deleted = [];
538
539 foreach ( $finetunes as $finetune ) {
540 $name = $finetune['model'];
541 $isSucceeded = $finetune['status'] === 'succeeded';
542 if ( $isSucceeded ) {
543 try {
544 $finetune = $this->getModel( $name );
545 }
546 catch ( Exception $e ) {
547 $deleted[] = $name;
548 }
549 }
550 }
551
552 $this->core->update_option( 'openai_finetunes_deleted', $deleted );
553 return $deleted;
554 }
555
556 public function listFineTunes()
557 {
558 $res = $this->run( 'GET', '/fine-tunes' );
559 $finetunes = $res['data'];
560
561 // Add suffix
562 $finetunes = array_map( function ( $finetune ) {
563 $finetune['suffix'] = $this->getSuffixForModel( $finetune['fine_tuned_model'] );
564 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
565 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
566 $finetune['base_model'] = $finetune['model'];
567 $finetune['model'] = $finetune['fine_tuned_model'];
568 unset( $finetune['object'] );
569 unset( $finetune['hyperparams'] );
570 unset( $finetune['result_files'] );
571 unset( $finetune['training_files'] );
572 unset( $finetune['validation_files'] );
573 unset( $finetune['created_at'] );
574 unset( $finetune['updated_at'] );
575 unset( $finetune['fine_tuned_model'] );
576 return $finetune;
577 }, $finetunes);
578
579 usort( $finetunes, function ( $a, $b ) {
580 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
581 });
582
583 $this->core->update_option( 'openai_finetunes', $finetunes );
584 return $finetunes;
585 }
586
587 public function moderate( $input ) {
588 $result = $this->run('POST', '/moderations', [
589 'input' => $input
590 ]);
591 return $result;
592 }
593
594 public function uploadFile( $filename, $data )
595 {
596 $result = $this->run('POST', '/files', null, [
597 'purpose' => 'fine-tune',
598 'data' => $data,
599 'file' => $filename
600 ] );
601 return $result;
602 }
603
604 public function deleteFile( $fileId )
605 {
606 return $this->run('DELETE', '/files/' . $fileId);
607 }
608
609 public function getModel( $modelId )
610 {
611 return $this->run('GET', '/models/' . $modelId);
612 }
613
614 public function cancelFineTune( $fineTuneId )
615 {
616 return $this->run('POST', '/fine-tunes/' . $fineTuneId . '/cancel');
617 }
618
619 public function deleteFineTune( $modelId )
620 {
621 return $this->run('DELETE', '/models/' . $modelId);
622 }
623
624 public function downloadFile( $fileId )
625 {
626 return $this->run('GET', '/files/' . $fileId . '/content', null, null, false);
627 }
628
629 public function fineTuneFile( $fileId, $model, $suffix, $hyperparams = [] )
630 {
631 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : 4;
632 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
633 $arguments = [
634 'training_file' => $fileId,
635 'model' => $model,
636 'suffix' => $suffix,
637 'n_epochs' => $n_epochs
638 ];
639 if ( $batch_size ) {
640 $arguments['batch_size'] = $batch_size;
641 }
642 $result = $this->run('POST', '/fine-tunes', $arguments);
643 return $result;
644 }
645
646 /**
647 * Build the body of a form request.
648 * If the field name is 'file', then the field value is the filename of the file to upload.
649 * The file contents are taken from the 'data' field.
650 *
651 * @param array $fields
652 * @param string $boundary
653 * @return string
654 */
655 public function buildFormBody( $fields, $boundary )
656 {
657 $body = '';
658 foreach ( $fields as $name => $value ) {
659 if ( $name == 'data' ) {
660 continue;
661 }
662 $body .= "--$boundary\r\n";
663 $body .= "Content-Disposition: form-data; name=\"$name\"";
664 if ( $name == 'file' ) {
665 $body .= "; filename=\"{$value}\"\r\n";
666 $body .= "Content-Type: application/json\r\n\r\n";
667 $body .= $fields['data'] . "\r\n";
668 }
669 else {
670 $body .= "\r\n\r\n$value\r\n";
671 }
672 }
673 $body .= "--$boundary--\r\n";
674 return $body;
675 }
676
677 /**
678 * Run a request to the OpenAI API.
679 * Fore more information about the $formFields, refer to the buildFormBody method.
680 *
681 * @param string $method POST, PUT, GET, DELETE...
682 * @param string $url The API endpoint
683 * @param array $query The query parameters (json)
684 * @param array $formFields The form fields (multipart/form-data)
685 * @param bool $json Whether to return the response as json or not
686 * @return array
687 */
688 public function run( $method, $url, $query = null, $formFields = null, $json = true )
689 {
690 $apiKey = $this->localApiKey;
691 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $apiKey . "\r\n";
692 $body = $query ? json_encode( $query ) : null;
693 if ( !empty( $formFields ) ) {
694 $boundary = wp_generate_password (24, false );
695 $headers = [
696 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
697 'Authorization' => 'Bearer ' . $this->localApiKey,
698 ];
699 $body = $this->buildFormBody( $formFields, $boundary );
700 }
701
702 $url = 'https://api.openai.com/v1' . $url;
703 $options = [
704 "headers" => $headers,
705 "method" => $method,
706 "timeout" => MWAI_TIMEOUT,
707 "body" => $body,
708 "sslverify" => false
709 ];
710
711 try {
712 $response = wp_remote_request( $url, $options );
713 if ( is_wp_error( $response ) ) {
714 throw new Exception( $response->get_error_message() );
715 }
716 $response = wp_remote_retrieve_body( $response );
717 $data = $json ? json_decode( $response, true ) : $response;
718 $this->handleResponseErrors( $data );
719 return $data;
720 }
721 catch ( Exception $e ) {
722 error_log( $e->getMessage() );
723 throw new Exception( $e->getMessage() . " (OpenAI)" );
724 }
725 }
726
727 static public function get_openai_models() {
728 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
729 }
730
731 private function calculatePrice( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
732 {
733 // Finetuned models => We need to modify the model to the family of the model.
734 if ( $finetune && preg_match('/^([a-zA-Z]{0,32}):/', $modelFamily, $matches ) ) {
735 $modelFamily = $matches[1];
736 $finetune = true;
737 }
738
739 $openai_models = Meow_MWAI_Engines_OpenAI::get_openai_models();
740 foreach ( $openai_models as $currentModel ) {
741 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
742 if ( $currentModel['type'] === 'image' ) {
743 if ( !$option ) {
744 error_log( "AI Engine: Image models require an option." );
745 return null;
746 }
747 else {
748 foreach ( $currentModel['options'] as $imageType ) {
749 if ( $imageType['option'] == $option ) {
750 return $imageType['price'] * $outUnits;
751 }
752 }
753 }
754 }
755 else {
756 if ( $finetune ) {
757 $currentModel['price'] = $currentModel['finetune']['price'];
758 }
759 $inPrice = $currentModel['price'];
760 $outPrice = $currentModel['price'];
761 if ( is_array( $currentModel['price'] ) ) {
762 $inPrice = $currentModel['price']['in'];
763 $outPrice = $currentModel['price']['out'];
764 }
765 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
766 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
767 return $inTotalPrice + $outTotalPrice;
768 }
769 }
770 }
771 error_log( "AI Engine: Invalid model ($modelFamily)." );
772 return null;
773 }
774
775 public function getPrice( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
776 {
777 $model = $query->model;
778 $units = 0;
779 $option = null;
780
781 $finetune = false;
782 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) ) {
783 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
784 $finetune = true;
785 }
786 $inUnits = $reply->getPromptTokens();
787 $outUnits = $reply->getCompletionTokens();
788 return $this->calculatePrice( $model, $inUnits, $outUnits, $option, $finetune );
789 }
790 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
791 $model = 'dall-e';
792 $units = $query->maxResults;
793 $option = "1024x1024";
794 return $this->calculatePrice( $model, 0, $units, $option, $finetune );
795 }
796 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
797 $model = 'whisper';
798 $units = $reply->getUnits();
799 return $this->calculatePrice( $model, 0, $units, $option, $finetune );
800 }
801 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
802 $units = $reply->getTotalTokens();
803 return $this->calculatePrice( $model, 0, $units, $option, $finetune );
804 }
805 error_log("AI Engine: Cannot calculate price for $model.");
806 return null;
807 }
808
809 public function getIncidents() {
810 $url = 'https://status.openai.com/history.rss';
811 $response = wp_remote_get( $url );
812 if ( is_wp_error( $response ) ) {
813 throw new Exception( $response->get_error_message() );
814 }
815 $response = wp_remote_retrieve_body( $response );
816 $xml = simplexml_load_string( $response );
817 $incidents = array();
818 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
819 foreach ( $xml->channel->item as $item ) {
820 $date = strtotime( $item->pubDate );
821 if ( $date > $oneWeekAgo ) {
822 $incidents[] = array(
823 'title' => (string) $item->title,
824 'description' => (string) $item->description,
825 'date' => $date
826 );
827 }
828 }
829 return $incidents;
830 }
831 }
832