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