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