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