PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 1.7.8
AI Engine – The Chatbot, AI Framework & MCP for WordPress v1.7.8
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
826 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, $units, $option = null, $finetune = false )
703 {
704 foreach ( MWAI_OPENAI_MODELS as $currentModel ) {
705 if ( $currentModel['family'] === $modelFamily ) {
706 if ( $currentModel['type'] === 'image' ) {
707 if ( !$option ) {
708 error_log( "AI Engine: Image models require an option." );
709 return null;
710 }
711 else {
712 foreach ( $currentModel['options'] as $imageType ) {
713 if ( $imageType['option'] == $option ) {
714 return $imageType['price'] * $units;
715 }
716 }
717 }
718 }
719 else {
720 if ( $finetune ) {
721 // The price is doubled for finetuned models.
722 return $currentModel['finetune']['price'] * $currentModel['unit'] * $units * 2;
723 }
724 return $currentModel['price'] * $currentModel['unit'] * $units;
725 }
726 }
727 }
728 error_log( "AI Engine: Invalid family ($modelFamily)." );
729 return null;
730 }
731
732 public function getPrice( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
733 {
734 $model = $query->model;
735 $family = null;
736 $units = 0;
737 $option = null;
738 $currentModel = null;
739 $priceRules = null;
740
741 $finetune = false;
742 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) ) {
743 // Finetuned models
744 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
745 $family = $matches[1];
746 $finetune = true;
747 }
748 // Standard models
749 else {
750 foreach ( MWAI_OPENAI_MODELS as $currentModel ) {
751 if ( $currentModel['model'] == $model ) {
752 $family = $currentModel['family'];
753 $priceRules = isset( $currentModel['priceRules'] ) ? $currentModel['priceRules'] : null;
754 break;
755 }
756 }
757 }
758 if ( empty( $family ) ) {
759 error_log("AI Engine: Cannot find the base model for $model.");
760 return null;
761 }
762 if ( !empty( $priceRules ) ) {
763 if ( $priceRules === "completion_x2" ) {
764 $units = $reply->getPromptTokens();
765 $units += $reply->getCompletionTokens() * 2;
766 return $this->calculatePrice( $family, $units, $option, $finetune );
767 }
768 else {
769 error_log("AI Engine: Unknown price rules ($priceRules) for $model.");
770 return null;
771 }
772 }
773 else {
774 $units = $reply->getTotalTokens();
775 return $this->calculatePrice( $family, $units, $option, $finetune );
776 }
777 }
778 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
779 $family = 'dall-e';
780 $units = $query->maxResults;
781 $option = "1024x1024";
782 return $this->calculatePrice( $family, $units, $option, $finetune );
783 }
784 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
785 $family = 'whisper';
786 $units = $reply->getUnits();
787 return $this->calculatePrice( $family, $units, $option, $finetune );
788 }
789 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
790 foreach ( MWAI_OPENAI_MODELS as $currentModel ) {
791 if ( $currentModel['model'] == $model ) {
792 $family = $currentModel['family'];
793 break;
794 }
795 }
796 $units = $reply->getTotalTokens();
797 return $this->calculatePrice( $family, $units, $option, $finetune );
798 }
799 error_log("AI Engine: Cannot calculate price for $model.");
800 return null;
801 }
802
803 public function getIncidents() {
804 $url = 'https://status.openai.com/history.rss';
805 $response = wp_remote_get( $url );
806 if ( is_wp_error( $response ) ) {
807 throw new Exception( $response->get_error_message() );
808 }
809 $response = wp_remote_retrieve_body( $response );
810 $xml = simplexml_load_string( $response );
811 $incidents = array();
812 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
813 foreach ( $xml->channel->item as $item ) {
814 $date = strtotime( $item->pubDate );
815 if ( $date > $oneWeekAgo ) {
816 $incidents[] = array(
817 'title' => (string) $item->title,
818 'description' => (string) $item->description,
819 'date' => $date
820 );
821 }
822 }
823 return $incidents;
824 }
825 }
826