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