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