PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 1.9.95
AI Engine – The Chatbot, AI Framework & MCP for WordPress v1.9.95
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
core.php 2 years ago openai.php 2 years ago
openai.php
977 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 set_environment 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->set_environment( $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->set_environment( $envId );
52 }
53 }
54
55 function set_environment( $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 build_headers( $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 build_options( $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->build_form_body( $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 run_query( $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->handle_response_errors( $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 apply_query_parameters( $query ) {
270 $this->set_environment( $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->set_environment( $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 get_audio( $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 run_transcribe_query( $query ) {
311 $this->apply_query_parameters( $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->get_audio( $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->build_headers( $query );
331 $options = $this->build_options( $headers, null, $body );
332
333 // Perform the request
334 try {
335 $res = $this->run_query( $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->record_audio_usage( $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 run_embedding_query( $query ) {
355 $this->apply_query_parameters( $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->build_headers( $query );
366 $options = $this->build_options( $headers, $body );
367
368 // Perform the request
369 try {
370 $res = $this->run_query( $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 run_completion_query( $query, $streamCallback = null ) {
390 $this->apply_query_parameters( $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 "n" => $query->maxResults,
403 "max_tokens" => $query->maxTokens,
404 "temperature" => $query->temperature,
405 "stream" => !is_null( $streamCallback ),
406 );
407
408 if ( !empty( $query->stop ) ) {
409 $body['stop'] = $query->stop;
410 }
411
412 if ( !empty( $query->functions ) ) {
413 if ( strpos( $query->model, 'ft:' ) === 0 ) {
414 throw new Exception( 'OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
415 }
416 $body['functions'] = $query->functions;
417 $body['function_call'] = $query->functionCall;
418 }
419 if ( $query->mode === 'chat' ) {
420 $body['messages'] = $query->messages;
421 }
422 else if ( $query->mode === 'completion' ) {
423 $body['prompt'] = $query->getPrompt();
424 }
425 $url = $this->localService === 'azure' ? trailingslashit( $this->defaultAzureEndpoint ) .
426 'openai/deployments/' . $this->defaultAzureDeployment : $this->defaultOpenAiEndpoint;
427 if ( $query->mode === 'chat' ) {
428 $url .= $this->localService === 'azure' ? '/chat/completions?' . $this->azureApiVersion : '/chat/completions';
429 }
430 else if ($query->mode === 'completion') {
431 $url .= $this->localService === 'azure' ? '/completions?' . $this->azureApiVersion : '/completions';
432 }
433 $headers = $this->build_headers( $query );
434 $options = $this->build_options( $headers, $body );
435
436 try {
437 $res = $this->run_query( $url, $options, $streamCallback );
438 $reply = new Meow_MWAI_Reply( $query );
439
440 // Streamed data
441 $prompt_tokens = $query->getPromptTokens();
442 if ( !is_null( $streamCallback ) ) {
443 $data = [
444 'model' => $query->model,
445 'usage' => [
446 'prompt_tokens' => $prompt_tokens,
447 'completion_tokens' => $this->streamedTokens
448 ],
449 'choices' => [
450 [
451 'message' => [
452 'content' => $this->streamContent,
453 'function_call' => $this->streamFunctionCall
454 ]
455 ]
456 ],
457 ];
458 }
459 // Regular data
460 else {
461 $data = $res['data'];
462 if ( !$data['model'] ) {
463 error_log( print_r( $data, 1 ) );
464 throw new Exception( "Got an unexpected response from OpenAI. Check your PHP Error Logs." );
465 }
466 }
467
468 try {
469 $usage = $this->core->recordTokensUsage(
470 $data['model'],
471 $data['usage']['prompt_tokens'],
472 $data['usage']['completion_tokens']
473 );
474 }
475 catch ( Exception $e ) {
476 error_log( $e->getMessage() );
477 }
478 $reply->setUsage( $usage );
479 $reply->setChoices( $data['choices'] );
480 return $reply;
481 }
482 catch ( Exception $e ) {
483 error_log( $e->getMessage() );
484 $service = $this->localService === 'azure' ? 'Azure' : 'OpenAI';
485 throw new Exception( $e->getMessage() . " ($service)" );
486 }
487 }
488
489 // Request to DALL-E API
490 public function run_images_query( $query ) {
491 $this->apply_query_parameters( $query );
492
493 // Prepare the request
494 $url = 'https://api.openai.com/v1/images/generations';
495 $body = array(
496 "prompt" => $query->prompt,
497 "n" => $query->maxResults,
498 "size" => '1024x1024',
499 );
500 if ( $this->localService === 'azure' ) {
501 $url = trailingslashit( $this->defaultAzureEndpoint ) . 'dalle/text-to-image?api-version=2022-08-03-preview';
502 $body = array(
503 "caption" => $query->prompt,
504 //"n" => $query->maxResults,
505 "resolution" => '1024x1024',
506 );
507 }
508 $headers = $this->build_headers( $query );
509 $options = $this->build_options( $headers, $body );
510
511 // Perform the request
512 try {
513 $res = $this->run_query( $url, $options );
514 $data = $res['data'];
515 $choices = [];
516
517 if ( $this->localService === 'azure' ) {
518 if ( !isset( $res['headers']['operation-location'] ) || !isset( $res['headers']['retry-after'] ) ) {
519 throw new Exception( 'Invalid response from Azure.' );
520 }
521 $operationLocation = $res['headers']['operation-location'];
522 $retryAfter = (int)$res['headers']['retry-after'];
523 $status = $data['status'];
524 $options = $this->build_options( $headers, null );
525 $options['method'] = 'GET';
526 while ( $status !== 'Succeeded' ) {
527 sleep( $retryAfter );
528 $res = $this->run_query( $operationLocation, $options );
529 $data = $res['data'];
530 $status = $data['status'];
531 }
532 $result = $data['result'];
533 $contentUrl = $result['contentUrl'];
534 $choices = [ [ 'url' => $contentUrl ] ];
535
536 }
537 else {
538 // OpenAI returns an array of URLs
539 $choices = $data['data'];
540 }
541
542 $reply = new Meow_MWAI_Reply( $query );
543 $usage = $this->core->record_images_usage( "dall-e", "1024x1024", $query->maxResults );
544 $reply->setUsage( $usage );
545 $reply->setChoices( $choices );
546 $reply->setType( 'images' );
547 return $reply;
548 }
549 catch ( Exception $e ) {
550 error_log( $e->getMessage() );
551 $service = $this->localService === 'azure' ? 'Azure' : 'OpenAI';
552 throw new Exception( $e->getMessage() . " ($service)" );
553 }
554 }
555
556
557 /*
558 This is the rest of the OpenAI API support, not related to the models directly.
559 */
560
561 // Check if there are errors in the response from OpenAI, and throw an exception if so.
562 public function handle_response_errors( $data ) {
563 if ( isset( $data['error'] ) ) {
564 $message = $data['error']['message'];
565 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
566 $message = str_replace( $matches[1], '', $message );
567 }
568 throw new Exception( $message );
569 }
570 }
571
572 public function list_files()
573 {
574 return $this->run( 'GET', '/files' );
575 }
576
577 static function get_suffix_for_model($model)
578 {
579 // Legacy fine-tuned models
580 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
581 if ( count( $matches ) > 0 ) {
582 return $matches[1];
583 }
584
585 // New fine-tuned models
586 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
587 if (count($matches) > 0) {
588 return $matches[1];
589 }
590
591 return 'N/A';
592 }
593
594 static function get_finetune_base_model($model)
595 {
596 // New fine-tuned models
597 preg_match("/^ft:([^:]+):/", $model, $matches);
598 if (count($matches) > 0) {
599 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
600 return "gpt-3.5-turbo";
601 }
602 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
603 return "gpt-4";
604 }
605 return $matches[1];
606 }
607
608 // Legacy fine-tuned models
609 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
610 if ( count( $matches ) > 0 ) {
611 return $matches[1];
612 }
613
614 return null;
615 }
616
617 public function list_deleted_finetunes( $envId = null, $legacy = false )
618 {
619 $finetunes = $this->list_finetunes( $legacy );
620 $deleted = [];
621
622 foreach ( $finetunes as $finetune ) {
623 $name = $finetune['model'];
624 $isSucceeded = $finetune['status'] === 'succeeded';
625 if ( $isSucceeded ) {
626 try {
627 $finetune = $this->get_model( $name );
628 }
629 catch ( Exception $e ) {
630 $deleted[] = $name;
631 }
632 }
633 }
634 if ( $legacy ) {
635 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'legacy_finetunes_deleted', $deleted );
636 }
637 else {
638 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'finetunes_deleted', $deleted );
639 }
640 return $deleted;
641 }
642
643 // public function listModels() {
644 // $res = $this->run( 'GET', '/models' );
645 // // TODO: Not used by the UI.
646 // throw new Exception( 'Not implemented yet.' );
647 // }
648
649 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
650 // retrieve all the models since Summer 2023, let's see! WIP.
651 public function list_finetunes( $legacy = false )
652 {
653 if ( $legacy ) {
654 $res = $this->run( 'GET', '/fine-tunes' );
655 }
656 else {
657 $res = $this->run( 'GET', '/fine_tuning/jobs' );
658 }
659 $finetunes = $res['data'];
660
661 // Add suffix
662 $finetunes = array_map( function ( $finetune ) {
663 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
664 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
665 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
666 $finetune['base_model'] = $finetune['model'];
667 $finetune['model'] = $finetune['fine_tuned_model'];
668 unset( $finetune['object'] );
669 unset( $finetune['hyperparams'] );
670 unset( $finetune['result_files'] );
671 unset( $finetune['training_files'] );
672 unset( $finetune['validation_files'] );
673 unset( $finetune['created_at'] );
674 unset( $finetune['updated_at'] );
675 unset( $finetune['fine_tuned_model'] );
676 return $finetune;
677 }, $finetunes);
678
679 usort( $finetunes, function ( $a, $b ) {
680 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
681 });
682
683 if ( $legacy ) {
684 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'legacy_finetunes', $finetunes );
685 }
686 else {
687 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'finetunes', $finetunes );
688 }
689
690 return $finetunes;
691 }
692
693 public function moderate( $input ) {
694 $result = $this->run('POST', '/moderations', [
695 'input' => $input
696 ]);
697 return $result;
698 }
699
700 public function upload_file( $filename, $data )
701 {
702 $result = $this->run('POST', '/files', null, [
703 'purpose' => 'fine-tune',
704 'data' => $data,
705 'file' => $filename
706 ] );
707 return $result;
708 }
709
710 public function delete_file( $fileId )
711 {
712 return $this->run('DELETE', '/files/' . $fileId);
713 }
714
715 public function get_model( $modelId )
716 {
717 return $this->run('GET', '/models/' . $modelId);
718 }
719
720 public function cancel_finetune( $fineTuneId )
721 {
722 return $this->run('POST', '/fine-tunes/' . $fineTuneId . '/cancel');
723 }
724
725 public function delete_finetune( $modelId )
726 {
727 return $this->run('DELETE', '/models/' . $modelId);
728 }
729
730 public function download_file( $fileId )
731 {
732 return $this->run('GET', '/files/' . $fileId . '/content', null, null, false);
733 }
734
735 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
736 {
737 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
738 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
739 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
740 (float)$hyperparams['learningRateMultiplier'] : null;
741 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
742 (float)$hyperparams['promptLossWeight'] : null;
743 $arguments = [
744 'training_file' => $fileId,
745 'model' => $model,
746 'suffix' => $suffix
747 ];
748 if ( $legacy ) {
749 $result = $this->run( 'POST', '/fine-tunes', $arguments );
750 }
751 else {
752 if ( $n_epochs ) {
753 $arguments['hyperparams'] = [];
754 $arguments['hyperparams']['n_epochs'] = $n_epochs;
755 }
756 if ( $batch_size ) {
757 if ( empty( $arguments['hyperparams'] ) ) {
758 $arguments['hyperparams'] = [];
759 }
760 $arguments['hyperparams']['batch_size'] = $batch_size;
761 }
762 if ( $learning_rate_multiplier ) {
763 if ( empty( $arguments['hyperparams'] ) ) {
764 $arguments['hyperparams'] = [];
765 }
766 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
767 }
768 if ( $prompt_loss_weight ) {
769 if ( empty( $arguments['hyperparams'] ) ) {
770 $arguments['hyperparams'] = [];
771 }
772 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
773 }
774 if ( $model === 'turbo' ) {
775 $arguments['model'] = 'gpt-3.5-turbo';
776 }
777 $result = $this->run( 'POST', '/fine_tuning/jobs', $arguments );
778 }
779 return $result;
780 }
781
782 /**
783 * Build the body of a form request.
784 * If the field name is 'file', then the field value is the filename of the file to upload.
785 * The file contents are taken from the 'data' field.
786 *
787 * @param array $fields
788 * @param string $boundary
789 * @return string
790 */
791 public function build_form_body( $fields, $boundary )
792 {
793 $body = '';
794 foreach ( $fields as $name => $value ) {
795 if ( $name == 'data' ) {
796 continue;
797 }
798 $body .= "--$boundary\r\n";
799 $body .= "Content-Disposition: form-data; name=\"$name\"";
800 if ( $name == 'file' ) {
801 $body .= "; filename=\"{$value}\"\r\n";
802 $body .= "Content-Type: application/json\r\n\r\n";
803 $body .= $fields['data'] . "\r\n";
804 }
805 else {
806 $body .= "\r\n\r\n$value\r\n";
807 }
808 }
809 $body .= "--$boundary--\r\n";
810 return $body;
811 }
812
813 /**
814 * Run a request to the OpenAI API.
815 * Fore more information about the $formFields, refer to the build_form_body method.
816 *
817 * @param string $method POST, PUT, GET, DELETE...
818 * @param string $url The API endpoint
819 * @param array $query The query parameters (json)
820 * @param array $formFields The form fields (multipart/form-data)
821 * @param bool $json Whether to return the response as json or not
822 * @return array
823 */
824 public function run( $method, $url, $query = null, $formFields = null, $json = true )
825 {
826 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->defaultApiKey . "\r\n";
827 $body = $query ? json_encode( $query ) : null;
828 if ( !empty( $formFields ) ) {
829 $boundary = wp_generate_password (24, false );
830 $headers = [
831 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
832 'Authorization' => 'Bearer ' . $this->defaultApiKey
833 ];
834 $body = $this->build_form_body( $formFields, $boundary );
835 }
836
837 $url = 'https://api.openai.com/v1' . $url;
838 $options = [
839 "headers" => $headers,
840 "method" => $method,
841 "timeout" => MWAI_TIMEOUT,
842 "body" => $body,
843 "sslverify" => false
844 ];
845
846 try {
847 $response = wp_remote_request( $url, $options );
848 if ( is_wp_error( $response ) ) {
849 throw new Exception( $response->get_error_message() );
850 }
851 $response = wp_remote_retrieve_body( $response );
852 $data = $json ? json_decode( $response, true ) : $response;
853 $this->handle_response_errors( $data );
854 return $data;
855 }
856 catch ( Exception $e ) {
857 error_log( $e->getMessage() );
858 throw new Exception( $e->getMessage() . " (OpenAI)" );
859 }
860 }
861
862 static public function get_openai_models() {
863 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
864 }
865
866 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
867 {
868 // For fine-tuned models:
869 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
870 if ( !empty( $potentialBaseModel ) ) {
871 $modelFamily = $potentialBaseModel;
872 $finetune = true;
873 }
874
875 $openai_models = Meow_MWAI_Engines_OpenAI::get_openai_models();
876 foreach ( $openai_models as $currentModel ) {
877 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
878 if ( $currentModel['type'] === 'image' ) {
879 if ( !$option ) {
880 error_log( "AI Engine: Image models require an option." );
881 return null;
882 }
883 else {
884 foreach ( $currentModel['options'] as $imageType ) {
885 if ( $imageType['option'] == $option ) {
886 return $imageType['price'] * $outUnits;
887 }
888 }
889 }
890 }
891 else {
892 if ( $finetune ) {
893
894 if ( isset( $currentModel['finetune']['price'] ) ) {
895 $currentModel['price'] = $currentModel['finetune']['price'];
896 }
897 else if ( isset( $currentModel['finetune']['in'] ) ) {
898 $currentModel['price'] = [
899 'in' => $currentModel['finetune']['in'],
900 'out' => $currentModel['finetune']['out']
901 ];
902 }
903 }
904 $inPrice = $currentModel['price'];
905 $outPrice = $currentModel['price'];
906 if ( is_array( $currentModel['price'] ) ) {
907 $inPrice = $currentModel['price']['in'];
908 $outPrice = $currentModel['price']['out'];
909 }
910 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
911 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
912 return $inTotalPrice + $outTotalPrice;
913 }
914 }
915 }
916 error_log( "AI Engine: Invalid model ($modelFamily)." );
917 return null;
918 }
919
920 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
921 {
922 $model = $query->model;
923 $units = 0;
924 $option = null;
925
926 $finetune = false;
927 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) ) {
928 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
929 $finetune = true;
930 }
931 $inUnits = $reply->getPromptTokens();
932 $outUnits = $reply->getCompletionTokens();
933 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
934 }
935 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
936 $model = 'dall-e';
937 $units = $query->maxResults;
938 $option = "1024x1024";
939 return $this->calculate_price( $model, 0, $units, $option, $finetune );
940 }
941 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
942 $model = 'whisper';
943 $units = $reply->getUnits();
944 return $this->calculate_price( $model, 0, $units, $option, $finetune );
945 }
946 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
947 $units = $reply->getTotalTokens();
948 return $this->calculate_price( $model, 0, $units, $option, $finetune );
949 }
950 error_log("AI Engine: Cannot calculate price for $model.");
951 return null;
952 }
953
954 public function get_incidents() {
955 $url = 'https://status.openai.com/history.rss';
956 $response = wp_remote_get( $url );
957 if ( is_wp_error( $response ) ) {
958 throw new Exception( $response->get_error_message() );
959 }
960 $response = wp_remote_retrieve_body( $response );
961 $xml = simplexml_load_string( $response );
962 $incidents = array();
963 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
964 foreach ( $xml->channel->item as $item ) {
965 $date = strtotime( $item->pubDate );
966 if ( $date > $oneWeekAgo ) {
967 $incidents[] = array(
968 'title' => (string) $item->title,
969 'description' => (string) $item->description,
970 'date' => $date
971 );
972 }
973 }
974 return $incidents;
975 }
976 }
977