PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 1.9.84
AI Engine – The Chatbot, AI Framework & MCP for WordPress v1.9.84
3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
core.php 3 years ago openai.php 2 years ago
openai.php
923 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-06-01-preview';
17
18 // Streaming
19 private $streamTemporaryBuffer = "";
20 private $streamContent = "";
21 private $streamFunctionCall = null;
22 private $streamParameter = "";
23 private $streamCallback = null;
24 private $streamedTokens = 0;
25
26 public function __construct( $core )
27 {
28 $this->core = $core;
29 $this->localService = $this->core->get_option( 'openai_service' );
30 $this->localApiKey = $this->core->get_option( 'openai_apikey' );
31 $this->localAzureEndpoint = $this->core->get_option( 'openai_azure_endpoint' );
32 $this->localAzureApiKey = $this->core->get_option( 'openai_azure_apikey' );
33 $this->localAzureDeployments = $this->core->get_option( 'openai_azure_deployments' );
34 $this->localAzureDeployments[] = [ 'model' => 'dall-e', 'name' => 'dall-e' ];
35 $this->localAzureDeployments[] = [];
36 }
37
38
39 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
40 function check_for_error( $data ) {
41 if ( strpos( $data, '"error"' ) !== false ) {
42 $json = json_decode( $data, true );
43 if ( json_last_error() === JSON_ERROR_NONE ) {
44 $error = $json['error'];
45 $code = $error['code'];
46 $message = $error['message'];
47 throw new Exception( "Error $code: $message" );
48 }
49 }
50 }
51
52 /*
53 This used to be in the core.php, but since it's relative to OpenAI, it's better to have it here.
54 */
55
56 public function stream_handler( $handle, $args, $url ) {
57 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
58 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
59
60 // Maybe we could get some info from headers, as for now, there is only the model.
61 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function( $curl, $headerLine ) {
62 // $line = trim( $headerLine );
63 // return strlen( $headerLine );
64 // });
65
66 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) {
67 $length = strlen( $data );
68
69 // FOR DEBUG:
70 // preg_match_all( '/"content":"(.*?)"/', $data, $matches );
71 // $contents = $matches[1];
72 // foreach ( $contents as $content ) {
73 // error_log( "Content: $content" );
74 // }
75
76 // Error Management
77 $this->check_for_error( $data );
78
79 // Bufferize the unfinished stream (if it's the case)
80 $this->streamTemporaryBuffer .= $data;
81 $lines = explode( "\n", $this->streamTemporaryBuffer );
82 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
83 $this->streamTemporaryBuffer = array_pop( $lines );
84 }
85 else {
86 $this->streamTemporaryBuffer = "";
87 }
88
89 foreach ( $lines as $line ) {
90 if ( $line === "" ) {
91 continue;
92 }
93 if ( strpos($line, 'data: ' ) === 0 ) {
94 $line = substr( $line, 6 );
95 $json = json_decode( $line, true );
96
97 if ( json_last_error() === JSON_ERROR_NONE ) {
98 $content = null;
99 if ( isset( $json['choices'][0]['text'] ) ) {
100 $content = $json['choices'][0]['text'];
101 }
102 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
103 $content = $json['choices'][0]['delta']['content'];
104 }
105 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
106 $function_call = $json['choices'][0]['delta']['function_call'];
107 if ( empty( $this->streamFunctionCall ) ) {
108 $this->streamFunctionCall = [ 'name' => "", 'arguments' => "" ];
109 }
110 if ( isset( $function_call['name'] ) ) {
111 $this->streamFunctionCall['name'] .= $function_call['name'];
112 }
113 if ( isset( $function_call['arguments'] ) ) {
114 $this->streamFunctionCall['arguments'] .= $function_call['arguments'];
115 }
116 }
117 if ( $content !== null && $content !== "" ) {
118 $this->streamedTokens += count( explode( " ", $content ) );
119 $this->streamContent .= $content;
120 call_user_func( $this->streamCallback, $content );
121 }
122 }
123 else {
124 $this->streamTemporaryBuffer .= $line . "\n";
125 }
126 }
127 }
128 return $length;
129 });
130 }
131
132 private function buildHeaders( $query ) {
133 $headers = array(
134 'Content-Type' => 'application/json',
135 'Authorization' => 'Bearer ' . $query->apiKey,
136 );
137 if ( $query->service === 'azure' ) {
138 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $query->azureApiKey );
139 }
140 return $headers;
141 }
142
143 private function buildOptions( $headers, $json = null, $forms = null ) {
144
145 // Build body
146 $body = null;
147 if ( !empty( $forms ) ) {
148 $boundary = wp_generate_password ( 24, false );
149 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
150 $body = $this->buildFormBody( $forms, $boundary );
151 }
152 else if ( !empty( $json ) ) {
153 $body = json_encode( $json );
154 }
155
156 // Build options
157 $options = array(
158 'headers' => $headers,
159 'method' => 'POST',
160 'timeout' => MWAI_TIMEOUT,
161 'body' => $body,
162 'sslverify' => false
163 );
164
165 return $options;
166 }
167
168 public function runQuery( $url, $options, $isStream = false ) {
169 try {
170 $options['stream'] = $isStream;
171 if ( $isStream ) {
172 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
173 }
174 $res = wp_remote_get( $url, $options );
175
176 if ( is_wp_error( $res ) ) {
177 throw new Exception( $res->get_error_message() );
178 }
179
180 if ( $isStream ) {
181 return [ 'stream' => true ];
182 }
183
184 $response = wp_remote_retrieve_body( $res );
185 $headersRes = wp_remote_retrieve_headers( $res );
186 $headers = $headersRes->getAll();
187
188 // If Headers contains multipart/form-data then we don't need to decode the response
189 if ( strpos( $options['headers']['Content-Type'], 'multipart/form-data' ) !== false ) {
190 return [
191 'stream' => false,
192 'headers' => $headers,
193 'data' => $response
194 ];
195 }
196
197 $data = json_decode( $response, true );
198 $this->handleResponseErrors( $data );
199
200 return [
201 'headers' => $headers,
202 'data' => $data
203 ];
204 }
205 catch ( Exception $e ) {
206 error_log( $e->getMessage() );
207 throw $e;
208 }
209 }
210
211 private function applyQueryParameters( $query ) {
212 if ( empty( $query->service ) ) {
213 $query->service = $this->localService;
214 }
215
216 // OpenAI will be used by default for everything
217 if ( empty( $query->apiKey ) ) {
218 $query->apiKey = $this->localApiKey;
219 }
220
221 // But if the service is set to Azure and the deployments/models are available,
222 // then we will use Azure instead.
223 if ( $query->service === 'azure' && !empty( $this->localAzureDeployments ) ) {
224 $found = false;
225 foreach ( $this->localAzureDeployments as $deployment ) {
226 if ( $deployment['model'] === $query->model && !empty( $deployment['name'] ) ) {
227 $query->azureDeployment = $deployment['name'];
228 if ( empty( $query->azureEndpoint ) ) {
229 $query->azureEndpoint = $this->localAzureEndpoint;
230 }
231 if ( empty( $query->azureApiKey ) ) {
232 $query->azureApiKey = $this->localAzureApiKey;
233 }
234 $found = true;
235 break;
236 }
237 }
238 if ( !$found ) {
239 //error_log( 'Azure deployment not found for model: ' . $query->model );
240 $query->service = 'openai';
241 }
242 }
243 }
244
245 private function getAudio( $url ) {
246 require_once( ABSPATH . 'wp-admin/includes/media.php' );
247 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
248 file_put_contents( $tmpFile, file_get_contents( $url ) );
249 $length = null;
250 $metadata = wp_read_audio_metadata( $tmpFile );
251 if ( isset( $metadata['length'] ) ) {
252 $length = $metadata['length'];
253 }
254 $data = file_get_contents( $tmpFile );
255 unlink( $tmpFile );
256 return [ 'data' => $data, 'length' => $length ];
257 }
258
259 public function runTranscribeQuery( $query ) {
260 $this->applyQueryParameters( $query );
261
262 // Prepare the request.
263 $modeEndpoint = $query->mode === 'translation' ? 'translations' : 'transcriptions';
264 $url = 'https://api.openai.com/v1/audio/' . $modeEndpoint;
265
266 // Check if the URL is valid.
267 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
268 throw new Exception( 'Invalid URL for transcription.' );
269 }
270
271 $audioData = $this->getAudio( $query->url );
272 $body = array(
273 'prompt' => $query->prompt,
274 'model' => $query->model,
275 'response_format' => 'text',
276 'file' => basename( $query->url ),
277 'data' => $audioData['data']
278 );
279 $headers = $this->buildHeaders( $query );
280 $options = $this->buildOptions( $headers, null, $body );
281
282 // Perform the request
283 try {
284 $res = $this->runQuery( $url, $options );
285 $data = $res['data'];
286 if ( empty( $data ) ) {
287 throw new Exception( 'Invalid data for transcription.' );
288 }
289 $this->check_for_error( $data );
290 $usage = $this->core->recordAudioUsage( $query->model, $audioData['length'] );
291 $reply = new Meow_MWAI_Reply( $query );
292 $reply->setUsage( $usage );
293 $reply->setChoices( $data );
294 return $reply;
295 }
296 catch ( Exception $e ) {
297 error_log( $e->getMessage() );
298 $service = $query->service === 'azure' ? 'Azure' : 'OpenAI';
299 throw new Exception( $e->getMessage() . " ($service)" );
300 }
301 }
302
303 public function runEmbeddingQuery( $query ) {
304 $this->applyQueryParameters( $query );
305
306 // Prepare the request
307 $url = 'https://api.openai.com/v1/embeddings';
308 $body = array( 'input' => $query->prompt, 'model' => $query->model );
309 if ( $query->service === 'azure' ) {
310 $url = trailingslashit( $query->azureEndpoint ) . 'openai/deployments/' .
311 $query->azureDeployment . '/embeddings?' . $this->azureApiVersion;
312 $body = array( "input" => $query->prompt );
313 }
314 $headers = $this->buildHeaders( $query );
315 $options = $this->buildOptions( $headers, $body );
316
317 // Perform the request
318 try {
319 $res = $this->runQuery( $url, $options );
320 $data = $res['data'];
321 if ( empty( $data ) || !isset( $data['data'] ) ) {
322 throw new Exception( 'Invalid data for embedding.' );
323 }
324 $usage = $data['usage'];
325 $this->core->recordTokensUsage( $query->model, $usage['prompt_tokens'] );
326 $reply = new Meow_MWAI_Reply( $query );
327 $reply->setUsage( $usage );
328 $reply->setChoices( $data['data'] );
329 return $reply;
330 }
331 catch ( Exception $e ) {
332 error_log( $e->getMessage() );
333 $service = $query->service === 'azure' ? 'Azure' : 'OpenAI';
334 throw new Exception( $e->getMessage() . " ($service)" );
335 }
336 }
337
338 public function runCompletionQuery( $query, $streamCallback = null ) {
339 $this->applyQueryParameters( $query );
340 if ( !is_null( $streamCallback ) ) {
341 $this->streamCallback = $streamCallback;
342 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
343 }
344 if ( $query->mode !== 'chat' && $query->mode !== 'completion' ) {
345 throw new Exception( 'Unknown mode for query: ' . $query->mode );
346 }
347
348 // Prepare the request
349 $body = array(
350 "model" => $query->model,
351 "stop" => $query->stop,
352 "n" => $query->maxResults,
353 "max_tokens" => $query->maxTokens,
354 "temperature" => $query->temperature,
355 "stream" => !is_null( $streamCallback ),
356 );
357 if ( !empty( $query->functions ) ) {
358 if ( strpos( $query->model, 'ft:' ) === 0 ) {
359 throw new Exception( 'OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
360 }
361 $body['functions'] = $query->functions;
362 $body['function_call'] = $query->functionCall;
363 }
364 if ( $query->mode === 'chat' ) {
365 $body['messages'] = $query->messages;
366 }
367 else if ( $query->mode === 'completion' ) {
368 $body['prompt'] = $query->getPrompt();
369 }
370 $url = $query->service === 'azure' ? trailingslashit( $query->azureEndpoint ) .
371 'openai/deployments/' . $query->azureDeployment : $this->openaiEndpoint;
372 if ( $query->mode === 'chat' ) {
373 $url .= $query->service === 'azure' ? '/chat/completions?' . $this->azureApiVersion : '/chat/completions';
374 }
375 else if ($query->mode === 'completion') {
376 $url .= $query->service === 'azure' ? '/completions?' . $this->azureApiVersion : '/completions';
377 }
378 $headers = $this->buildHeaders( $query );
379 $options = $this->buildOptions( $headers, $body );
380
381 try {
382 $res = $this->runQuery( $url, $options, $streamCallback );
383 $reply = new Meow_MWAI_Reply( $query );
384
385 // Streamed data
386 if ( !is_null( $streamCallback ) ) {
387 $data = [
388 'model' => $query->model,
389 'usage' => [
390 'prompt_tokens' => $query->getPromptTokens(),
391 'completion_tokens' => $this->streamedTokens
392 ],
393 'choices' => [
394 [
395 'message' => [
396 'content' => $this->streamContent,
397 'function_call' => $this->streamFunctionCall
398 ]
399 ]
400 ],
401 ];
402 }
403 // Regular data
404 else {
405 $data = $res['data'];
406 if ( !$data['model'] ) {
407 error_log( print_r( $data, 1 ) );
408 throw new Exception( "Got an unexpected response from OpenAI. Check your PHP Error Logs." );
409 }
410 }
411
412 try {
413 $usage = $this->core->recordTokensUsage(
414 $data['model'],
415 $data['usage']['prompt_tokens'],
416 $data['usage']['completion_tokens']
417 );
418 }
419 catch ( Exception $e ) {
420 error_log( $e->getMessage() );
421 }
422 $reply->setUsage( $usage );
423 $reply->setChoices( $data['choices'] );
424 return $reply;
425 }
426 catch ( Exception $e ) {
427 error_log( $e->getMessage() );
428 $service = $query->service === 'azure' ? 'Azure' : 'OpenAI';
429 throw new Exception( $e->getMessage() . " ($service)" );
430 }
431 }
432
433 // Request to DALL-E API
434 public function runImagesQuery( $query ) {
435 $this->applyQueryParameters( $query );
436
437 // Prepare the request
438 $url = 'https://api.openai.com/v1/images/generations';
439 $body = array(
440 "prompt" => $query->prompt,
441 "n" => $query->maxResults,
442 "size" => '1024x1024',
443 );
444 if ( $query->service === 'azure' ) {
445 //$url = trailingslashit( $query->azureEndpoint ) . 'dalle/text-to-image?' . $this->azureApiVersion;
446 $url = trailingslashit( $query->azureEndpoint ) . 'dalle/text-to-image?api-version=2022-08-03-preview';
447 $body = array(
448 "caption" => $query->prompt,
449 //"n" => $query->maxResults,
450 "resolution" => '1024x1024',
451 );
452 }
453 $headers = $this->buildHeaders( $query );
454 $options = $this->buildOptions( $headers, $body );
455
456 // Perform the request
457 try {
458 $res = $this->runQuery( $url, $options );
459 $data = $res['data'];
460 $choices = [];
461
462 if ( $query->service === 'azure' ) {
463 if ( !isset( $res['headers']['operation-location'] ) || !isset( $res['headers']['retry-after'] ) ) {
464 throw new Exception( 'Invalid response from Azure.' );
465 }
466 $operationLocation = $res['headers']['operation-location'];
467 $retryAfter = (int)$res['headers']['retry-after'];
468 $status = $data['status'];
469 $options = $this->buildOptions( $headers, null );
470 $options['method'] = 'GET';
471 while ( $status !== 'Succeeded' ) {
472 sleep( $retryAfter );
473 $res = $this->runQuery( $operationLocation, $options );
474 $data = $res['data'];
475 $status = $data['status'];
476 }
477 $result = $data['result'];
478 $contentUrl = $result['contentUrl'];
479 $choices = [ [ 'url' => $contentUrl ] ];
480
481 }
482 else {
483 // OpenAI returns an array of URLs
484 $choices = $data['data'];
485 }
486
487 $reply = new Meow_MWAI_Reply( $query );
488 $usage = $this->core->recordImagesUsage( "dall-e", "1024x1024", $query->maxResults );
489 $reply->setUsage( $usage );
490 $reply->setChoices( $choices );
491 $reply->setType( 'images' );
492 return $reply;
493 }
494 catch ( Exception $e ) {
495 error_log( $e->getMessage() );
496 $service = $query->service === 'azure' ? 'Azure' : 'OpenAI';
497 throw new Exception( $e->getMessage() . " ($service)" );
498 }
499 }
500
501
502 /*
503 This is the rest of the OpenAI API support, not related to the models directly.
504 */
505
506 // Check if there are errors in the response from OpenAI, and throw an exception if so.
507 public function handleResponseErrors( $data ) {
508 if ( isset( $data['error'] ) ) {
509 $message = $data['error']['message'];
510 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
511 $message = str_replace( $matches[1], '', $message );
512 }
513 throw new Exception( $message );
514 }
515 }
516
517 public function listFiles()
518 {
519 return $this->run( 'GET', '/files' );
520 }
521
522 static function getSuffixForModel($model)
523 {
524 // Legacy fine-tuned models
525 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
526 if ( count( $matches ) > 0 ) {
527 return $matches[1];
528 }
529
530 // New fine-tuned models
531 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
532 if (count($matches) > 0) {
533 return $matches[1];
534 }
535
536 return 'N/A';
537 }
538
539 static function getBaseModelForFinetune($model)
540 {
541 // New fine-tuned models
542 preg_match("/^ft:([^:]+):/", $model, $matches);
543 if (count($matches) > 0) {
544 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
545 return "gpt-3.5-turbo";
546 }
547 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
548 return "gpt-4";
549 }
550 return $matches[1];
551 }
552
553 // Legacy fine-tuned models
554 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
555 if ( count( $matches ) > 0 ) {
556 return $matches[1];
557 }
558
559 return null;
560 }
561
562 public function listDeletedFineTunes( $legacy = false )
563 {
564 $finetunes = $this->listFineTunes( $legacy );
565 $deleted = [];
566
567 foreach ( $finetunes as $finetune ) {
568 $name = $finetune['model'];
569 $isSucceeded = $finetune['status'] === 'succeeded';
570 if ( $isSucceeded ) {
571 try {
572 $finetune = $this->getModel( $name );
573 }
574 catch ( Exception $e ) {
575 $deleted[] = $name;
576 }
577 }
578 }
579 if ( $legacy ) {
580 $this->core->update_option( 'openai_legacy_finetunes_deleted', $deleted );
581 }
582 else {
583 $this->core->update_option( 'openai_finetunes_deleted', $deleted );
584 }
585 return $deleted;
586 }
587
588 public function listModels() {
589 $res = $this->run( 'GET', '/models' );
590 // TODO: Not used by the UI.
591 throw new Exception( 'Not implemented yet.' );
592 }
593
594 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
595 // retrieve all the models since Summer 2023, let's see! WIP.
596 public function listFineTunes( $legacy = false )
597 {
598 if ( $legacy ) {
599 $res = $this->run( 'GET', '/fine-tunes' );
600 }
601 else {
602 $res = $this->run( 'GET', '/fine_tuning/jobs' );
603 }
604 $finetunes = $res['data'];
605
606 // Add suffix
607 $finetunes = array_map( function ( $finetune ) {
608 $finetune['suffix'] = SELF::getSuffixForModel( $finetune['fine_tuned_model'] );
609 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
610 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
611 $finetune['base_model'] = $finetune['model'];
612 $finetune['model'] = $finetune['fine_tuned_model'];
613 unset( $finetune['object'] );
614 unset( $finetune['hyperparams'] );
615 unset( $finetune['result_files'] );
616 unset( $finetune['training_files'] );
617 unset( $finetune['validation_files'] );
618 unset( $finetune['created_at'] );
619 unset( $finetune['updated_at'] );
620 unset( $finetune['fine_tuned_model'] );
621 return $finetune;
622 }, $finetunes);
623
624 usort( $finetunes, function ( $a, $b ) {
625 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
626 });
627
628 if ( $legacy ) {
629 $this->core->update_option( 'openai_legacy_finetunes', $finetunes );
630 }
631 else {
632 $this->core->update_option( 'openai_finetunes', $finetunes );
633 }
634
635 return $finetunes;
636 }
637
638 public function moderate( $input ) {
639 $result = $this->run('POST', '/moderations', [
640 'input' => $input
641 ]);
642 return $result;
643 }
644
645 public function uploadFile( $filename, $data )
646 {
647 $result = $this->run('POST', '/files', null, [
648 'purpose' => 'fine-tune',
649 'data' => $data,
650 'file' => $filename
651 ] );
652 return $result;
653 }
654
655 public function deleteFile( $fileId )
656 {
657 return $this->run('DELETE', '/files/' . $fileId);
658 }
659
660 public function getModel( $modelId )
661 {
662 return $this->run('GET', '/models/' . $modelId);
663 }
664
665 public function cancelFineTune( $fineTuneId )
666 {
667 return $this->run('POST', '/fine-tunes/' . $fineTuneId . '/cancel');
668 }
669
670 public function deleteFineTune( $modelId )
671 {
672 return $this->run('DELETE', '/models/' . $modelId);
673 }
674
675 public function downloadFile( $fileId )
676 {
677 return $this->run('GET', '/files/' . $fileId . '/content', null, null, false);
678 }
679
680 public function fineTuneFile( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
681 {
682 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
683 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
684 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
685 (float)$hyperparams['learningRateMultiplier'] : null;
686 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
687 (float)$hyperparams['promptLossWeight'] : null;
688 $arguments = [
689 'training_file' => $fileId,
690 'model' => $model,
691 'suffix' => $suffix
692 ];
693 if ( $legacy ) {
694 $result = $this->run( 'POST', '/fine-tunes', $arguments );
695 }
696 else {
697 if ( $n_epochs ) {
698 $arguments['hyperparams'] = [];
699 $arguments['hyperparams']['n_epochs'] = $n_epochs;
700 }
701 if ( $batch_size ) {
702 if ( empty( $arguments['hyperparams'] ) ) {
703 $arguments['hyperparams'] = [];
704 }
705 $arguments['hyperparams']['batch_size'] = $batch_size;
706 }
707 if ( $learning_rate_multiplier ) {
708 if ( empty( $arguments['hyperparams'] ) ) {
709 $arguments['hyperparams'] = [];
710 }
711 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
712 }
713 if ( $prompt_loss_weight ) {
714 if ( empty( $arguments['hyperparams'] ) ) {
715 $arguments['hyperparams'] = [];
716 }
717 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
718 }
719 if ( $model === 'turbo' ) {
720 $arguments['model'] = 'gpt-3.5-turbo';
721 }
722 $result = $this->run( 'POST', '/fine_tuning/jobs', $arguments );
723 }
724 return $result;
725 }
726
727 /**
728 * Build the body of a form request.
729 * If the field name is 'file', then the field value is the filename of the file to upload.
730 * The file contents are taken from the 'data' field.
731 *
732 * @param array $fields
733 * @param string $boundary
734 * @return string
735 */
736 public function buildFormBody( $fields, $boundary )
737 {
738 $body = '';
739 foreach ( $fields as $name => $value ) {
740 if ( $name == 'data' ) {
741 continue;
742 }
743 $body .= "--$boundary\r\n";
744 $body .= "Content-Disposition: form-data; name=\"$name\"";
745 if ( $name == 'file' ) {
746 $body .= "; filename=\"{$value}\"\r\n";
747 $body .= "Content-Type: application/json\r\n\r\n";
748 $body .= $fields['data'] . "\r\n";
749 }
750 else {
751 $body .= "\r\n\r\n$value\r\n";
752 }
753 }
754 $body .= "--$boundary--\r\n";
755 return $body;
756 }
757
758 /**
759 * Run a request to the OpenAI API.
760 * Fore more information about the $formFields, refer to the buildFormBody method.
761 *
762 * @param string $method POST, PUT, GET, DELETE...
763 * @param string $url The API endpoint
764 * @param array $query The query parameters (json)
765 * @param array $formFields The form fields (multipart/form-data)
766 * @param bool $json Whether to return the response as json or not
767 * @return array
768 */
769 public function run( $method, $url, $query = null, $formFields = null, $json = true )
770 {
771 $apiKey = $this->localApiKey;
772 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $apiKey . "\r\n";
773 $body = $query ? json_encode( $query ) : null;
774 if ( !empty( $formFields ) ) {
775 $boundary = wp_generate_password (24, false );
776 $headers = [
777 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
778 'Authorization' => 'Bearer ' . $this->localApiKey,
779 ];
780 $body = $this->buildFormBody( $formFields, $boundary );
781 }
782
783 $url = 'https://api.openai.com/v1' . $url;
784 $options = [
785 "headers" => $headers,
786 "method" => $method,
787 "timeout" => MWAI_TIMEOUT,
788 "body" => $body,
789 "sslverify" => false
790 ];
791
792 try {
793 $response = wp_remote_request( $url, $options );
794 if ( is_wp_error( $response ) ) {
795 throw new Exception( $response->get_error_message() );
796 }
797 $response = wp_remote_retrieve_body( $response );
798 $data = $json ? json_decode( $response, true ) : $response;
799 $this->handleResponseErrors( $data );
800 return $data;
801 }
802 catch ( Exception $e ) {
803 error_log( $e->getMessage() );
804 throw new Exception( $e->getMessage() . " (OpenAI)" );
805 }
806 }
807
808 static public function get_openai_models() {
809 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
810 }
811
812 private function calculatePrice( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
813 {
814 // For fine-tuned models:
815 $potentialBaseModel = SELF::getBaseModelForFinetune( $modelFamily );
816 if ( !empty( $potentialBaseModel ) ) {
817 $modelFamily = $potentialBaseModel;
818 $finetune = true;
819 }
820
821 $openai_models = Meow_MWAI_Engines_OpenAI::get_openai_models();
822 foreach ( $openai_models as $currentModel ) {
823 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
824 if ( $currentModel['type'] === 'image' ) {
825 if ( !$option ) {
826 error_log( "AI Engine: Image models require an option." );
827 return null;
828 }
829 else {
830 foreach ( $currentModel['options'] as $imageType ) {
831 if ( $imageType['option'] == $option ) {
832 return $imageType['price'] * $outUnits;
833 }
834 }
835 }
836 }
837 else {
838 if ( $finetune ) {
839
840 if ( isset( $currentModel['finetune']['price'] ) ) {
841 $currentModel['price'] = $currentModel['finetune']['price'];
842 }
843 else if ( isset( $currentModel['finetune']['in'] ) ) {
844 $currentModel['price'] = [
845 'in' => $currentModel['finetune']['in'],
846 'out' => $currentModel['finetune']['out']
847 ];
848 }
849 }
850 $inPrice = $currentModel['price'];
851 $outPrice = $currentModel['price'];
852 if ( is_array( $currentModel['price'] ) ) {
853 $inPrice = $currentModel['price']['in'];
854 $outPrice = $currentModel['price']['out'];
855 }
856 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
857 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
858 return $inTotalPrice + $outTotalPrice;
859 }
860 }
861 }
862 error_log( "AI Engine: Invalid model ($modelFamily)." );
863 return null;
864 }
865
866 public function getPrice( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
867 {
868 $model = $query->model;
869 $units = 0;
870 $option = null;
871
872 $finetune = false;
873 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) ) {
874 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
875 $finetune = true;
876 }
877 $inUnits = $reply->getPromptTokens();
878 $outUnits = $reply->getCompletionTokens();
879 return $this->calculatePrice( $model, $inUnits, $outUnits, $option, $finetune );
880 }
881 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
882 $model = 'dall-e';
883 $units = $query->maxResults;
884 $option = "1024x1024";
885 return $this->calculatePrice( $model, 0, $units, $option, $finetune );
886 }
887 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
888 $model = 'whisper';
889 $units = $reply->getUnits();
890 return $this->calculatePrice( $model, 0, $units, $option, $finetune );
891 }
892 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
893 $units = $reply->getTotalTokens();
894 return $this->calculatePrice( $model, 0, $units, $option, $finetune );
895 }
896 error_log("AI Engine: Cannot calculate price for $model.");
897 return null;
898 }
899
900 public function getIncidents() {
901 $url = 'https://status.openai.com/history.rss';
902 $response = wp_remote_get( $url );
903 if ( is_wp_error( $response ) ) {
904 throw new Exception( $response->get_error_message() );
905 }
906 $response = wp_remote_retrieve_body( $response );
907 $xml = simplexml_load_string( $response );
908 $incidents = array();
909 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
910 foreach ( $xml->channel->item as $item ) {
911 $date = strtotime( $item->pubDate );
912 if ( $date > $oneWeekAgo ) {
913 $incidents[] = array(
914 'title' => (string) $item->title,
915 'description' => (string) $item->description,
916 'date' => $date
917 );
918 }
919 }
920 return $incidents;
921 }
922 }
923