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