PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 1.9.94
AI Engine – The Chatbot, AI Framework & MCP for WordPress v1.9.94
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
976 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 if ( !is_null( $streamCallback ) ) {
442 $data = [
443 'model' => $query->model,
444 'usage' => [
445 'prompt_tokens' => $query->getPromptTokens(),
446 'completion_tokens' => $this->streamedTokens
447 ],
448 'choices' => [
449 [
450 'message' => [
451 'content' => $this->streamContent,
452 'function_call' => $this->streamFunctionCall
453 ]
454 ]
455 ],
456 ];
457 }
458 // Regular data
459 else {
460 $data = $res['data'];
461 if ( !$data['model'] ) {
462 error_log( print_r( $data, 1 ) );
463 throw new Exception( "Got an unexpected response from OpenAI. Check your PHP Error Logs." );
464 }
465 }
466
467 try {
468 $usage = $this->core->recordTokensUsage(
469 $data['model'],
470 $data['usage']['prompt_tokens'],
471 $data['usage']['completion_tokens']
472 );
473 }
474 catch ( Exception $e ) {
475 error_log( $e->getMessage() );
476 }
477 $reply->setUsage( $usage );
478 $reply->setChoices( $data['choices'] );
479 return $reply;
480 }
481 catch ( Exception $e ) {
482 error_log( $e->getMessage() );
483 $service = $this->localService === 'azure' ? 'Azure' : 'OpenAI';
484 throw new Exception( $e->getMessage() . " ($service)" );
485 }
486 }
487
488 // Request to DALL-E API
489 public function run_images_query( $query ) {
490 $this->apply_query_parameters( $query );
491
492 // Prepare the request
493 $url = 'https://api.openai.com/v1/images/generations';
494 $body = array(
495 "prompt" => $query->prompt,
496 "n" => $query->maxResults,
497 "size" => '1024x1024',
498 );
499 if ( $this->localService === 'azure' ) {
500 $url = trailingslashit( $this->defaultAzureEndpoint ) . 'dalle/text-to-image?api-version=2022-08-03-preview';
501 $body = array(
502 "caption" => $query->prompt,
503 //"n" => $query->maxResults,
504 "resolution" => '1024x1024',
505 );
506 }
507 $headers = $this->build_headers( $query );
508 $options = $this->build_options( $headers, $body );
509
510 // Perform the request
511 try {
512 $res = $this->run_query( $url, $options );
513 $data = $res['data'];
514 $choices = [];
515
516 if ( $this->localService === 'azure' ) {
517 if ( !isset( $res['headers']['operation-location'] ) || !isset( $res['headers']['retry-after'] ) ) {
518 throw new Exception( 'Invalid response from Azure.' );
519 }
520 $operationLocation = $res['headers']['operation-location'];
521 $retryAfter = (int)$res['headers']['retry-after'];
522 $status = $data['status'];
523 $options = $this->build_options( $headers, null );
524 $options['method'] = 'GET';
525 while ( $status !== 'Succeeded' ) {
526 sleep( $retryAfter );
527 $res = $this->run_query( $operationLocation, $options );
528 $data = $res['data'];
529 $status = $data['status'];
530 }
531 $result = $data['result'];
532 $contentUrl = $result['contentUrl'];
533 $choices = [ [ 'url' => $contentUrl ] ];
534
535 }
536 else {
537 // OpenAI returns an array of URLs
538 $choices = $data['data'];
539 }
540
541 $reply = new Meow_MWAI_Reply( $query );
542 $usage = $this->core->record_images_usage( "dall-e", "1024x1024", $query->maxResults );
543 $reply->setUsage( $usage );
544 $reply->setChoices( $choices );
545 $reply->setType( 'images' );
546 return $reply;
547 }
548 catch ( Exception $e ) {
549 error_log( $e->getMessage() );
550 $service = $this->localService === 'azure' ? 'Azure' : 'OpenAI';
551 throw new Exception( $e->getMessage() . " ($service)" );
552 }
553 }
554
555
556 /*
557 This is the rest of the OpenAI API support, not related to the models directly.
558 */
559
560 // Check if there are errors in the response from OpenAI, and throw an exception if so.
561 public function handle_response_errors( $data ) {
562 if ( isset( $data['error'] ) ) {
563 $message = $data['error']['message'];
564 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
565 $message = str_replace( $matches[1], '', $message );
566 }
567 throw new Exception( $message );
568 }
569 }
570
571 public function list_files()
572 {
573 return $this->run( 'GET', '/files' );
574 }
575
576 static function get_suffix_for_model($model)
577 {
578 // Legacy fine-tuned models
579 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
580 if ( count( $matches ) > 0 ) {
581 return $matches[1];
582 }
583
584 // New fine-tuned models
585 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
586 if (count($matches) > 0) {
587 return $matches[1];
588 }
589
590 return 'N/A';
591 }
592
593 static function get_finetune_base_model($model)
594 {
595 // New fine-tuned models
596 preg_match("/^ft:([^:]+):/", $model, $matches);
597 if (count($matches) > 0) {
598 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
599 return "gpt-3.5-turbo";
600 }
601 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
602 return "gpt-4";
603 }
604 return $matches[1];
605 }
606
607 // Legacy fine-tuned models
608 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
609 if ( count( $matches ) > 0 ) {
610 return $matches[1];
611 }
612
613 return null;
614 }
615
616 public function list_deleted_finetunes( $envId = null, $legacy = false )
617 {
618 $finetunes = $this->list_finetunes( $legacy );
619 $deleted = [];
620
621 foreach ( $finetunes as $finetune ) {
622 $name = $finetune['model'];
623 $isSucceeded = $finetune['status'] === 'succeeded';
624 if ( $isSucceeded ) {
625 try {
626 $finetune = $this->get_model( $name );
627 }
628 catch ( Exception $e ) {
629 $deleted[] = $name;
630 }
631 }
632 }
633 if ( $legacy ) {
634 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'legacy_finetunes_deleted', $deleted );
635 }
636 else {
637 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'finetunes_deleted', $deleted );
638 }
639 return $deleted;
640 }
641
642 // public function listModels() {
643 // $res = $this->run( 'GET', '/models' );
644 // // TODO: Not used by the UI.
645 // throw new Exception( 'Not implemented yet.' );
646 // }
647
648 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
649 // retrieve all the models since Summer 2023, let's see! WIP.
650 public function list_finetunes( $legacy = false )
651 {
652 if ( $legacy ) {
653 $res = $this->run( 'GET', '/fine-tunes' );
654 }
655 else {
656 $res = $this->run( 'GET', '/fine_tuning/jobs' );
657 }
658 $finetunes = $res['data'];
659
660 // Add suffix
661 $finetunes = array_map( function ( $finetune ) {
662 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
663 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
664 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
665 $finetune['base_model'] = $finetune['model'];
666 $finetune['model'] = $finetune['fine_tuned_model'];
667 unset( $finetune['object'] );
668 unset( $finetune['hyperparams'] );
669 unset( $finetune['result_files'] );
670 unset( $finetune['training_files'] );
671 unset( $finetune['validation_files'] );
672 unset( $finetune['created_at'] );
673 unset( $finetune['updated_at'] );
674 unset( $finetune['fine_tuned_model'] );
675 return $finetune;
676 }, $finetunes);
677
678 usort( $finetunes, function ( $a, $b ) {
679 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
680 });
681
682 if ( $legacy ) {
683 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'legacy_finetunes', $finetunes );
684 }
685 else {
686 $this->core->update_ai_env( $this->defaultOpenAiEnvId, 'finetunes', $finetunes );
687 }
688
689 return $finetunes;
690 }
691
692 public function moderate( $input ) {
693 $result = $this->run('POST', '/moderations', [
694 'input' => $input
695 ]);
696 return $result;
697 }
698
699 public function upload_file( $filename, $data )
700 {
701 $result = $this->run('POST', '/files', null, [
702 'purpose' => 'fine-tune',
703 'data' => $data,
704 'file' => $filename
705 ] );
706 return $result;
707 }
708
709 public function delete_file( $fileId )
710 {
711 return $this->run('DELETE', '/files/' . $fileId);
712 }
713
714 public function get_model( $modelId )
715 {
716 return $this->run('GET', '/models/' . $modelId);
717 }
718
719 public function cancel_finetune( $fineTuneId )
720 {
721 return $this->run('POST', '/fine-tunes/' . $fineTuneId . '/cancel');
722 }
723
724 public function delete_finetune( $modelId )
725 {
726 return $this->run('DELETE', '/models/' . $modelId);
727 }
728
729 public function download_file( $fileId )
730 {
731 return $this->run('GET', '/files/' . $fileId . '/content', null, null, false);
732 }
733
734 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
735 {
736 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
737 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
738 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
739 (float)$hyperparams['learningRateMultiplier'] : null;
740 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
741 (float)$hyperparams['promptLossWeight'] : null;
742 $arguments = [
743 'training_file' => $fileId,
744 'model' => $model,
745 'suffix' => $suffix
746 ];
747 if ( $legacy ) {
748 $result = $this->run( 'POST', '/fine-tunes', $arguments );
749 }
750 else {
751 if ( $n_epochs ) {
752 $arguments['hyperparams'] = [];
753 $arguments['hyperparams']['n_epochs'] = $n_epochs;
754 }
755 if ( $batch_size ) {
756 if ( empty( $arguments['hyperparams'] ) ) {
757 $arguments['hyperparams'] = [];
758 }
759 $arguments['hyperparams']['batch_size'] = $batch_size;
760 }
761 if ( $learning_rate_multiplier ) {
762 if ( empty( $arguments['hyperparams'] ) ) {
763 $arguments['hyperparams'] = [];
764 }
765 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
766 }
767 if ( $prompt_loss_weight ) {
768 if ( empty( $arguments['hyperparams'] ) ) {
769 $arguments['hyperparams'] = [];
770 }
771 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
772 }
773 if ( $model === 'turbo' ) {
774 $arguments['model'] = 'gpt-3.5-turbo';
775 }
776 $result = $this->run( 'POST', '/fine_tuning/jobs', $arguments );
777 }
778 return $result;
779 }
780
781 /**
782 * Build the body of a form request.
783 * If the field name is 'file', then the field value is the filename of the file to upload.
784 * The file contents are taken from the 'data' field.
785 *
786 * @param array $fields
787 * @param string $boundary
788 * @return string
789 */
790 public function build_form_body( $fields, $boundary )
791 {
792 $body = '';
793 foreach ( $fields as $name => $value ) {
794 if ( $name == 'data' ) {
795 continue;
796 }
797 $body .= "--$boundary\r\n";
798 $body .= "Content-Disposition: form-data; name=\"$name\"";
799 if ( $name == 'file' ) {
800 $body .= "; filename=\"{$value}\"\r\n";
801 $body .= "Content-Type: application/json\r\n\r\n";
802 $body .= $fields['data'] . "\r\n";
803 }
804 else {
805 $body .= "\r\n\r\n$value\r\n";
806 }
807 }
808 $body .= "--$boundary--\r\n";
809 return $body;
810 }
811
812 /**
813 * Run a request to the OpenAI API.
814 * Fore more information about the $formFields, refer to the build_form_body method.
815 *
816 * @param string $method POST, PUT, GET, DELETE...
817 * @param string $url The API endpoint
818 * @param array $query The query parameters (json)
819 * @param array $formFields The form fields (multipart/form-data)
820 * @param bool $json Whether to return the response as json or not
821 * @return array
822 */
823 public function run( $method, $url, $query = null, $formFields = null, $json = true )
824 {
825 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->defaultApiKey . "\r\n";
826 $body = $query ? json_encode( $query ) : null;
827 if ( !empty( $formFields ) ) {
828 $boundary = wp_generate_password (24, false );
829 $headers = [
830 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
831 'Authorization' => 'Bearer ' . $this->defaultApiKey
832 ];
833 $body = $this->build_form_body( $formFields, $boundary );
834 }
835
836 $url = 'https://api.openai.com/v1' . $url;
837 $options = [
838 "headers" => $headers,
839 "method" => $method,
840 "timeout" => MWAI_TIMEOUT,
841 "body" => $body,
842 "sslverify" => false
843 ];
844
845 try {
846 $response = wp_remote_request( $url, $options );
847 if ( is_wp_error( $response ) ) {
848 throw new Exception( $response->get_error_message() );
849 }
850 $response = wp_remote_retrieve_body( $response );
851 $data = $json ? json_decode( $response, true ) : $response;
852 $this->handle_response_errors( $data );
853 return $data;
854 }
855 catch ( Exception $e ) {
856 error_log( $e->getMessage() );
857 throw new Exception( $e->getMessage() . " (OpenAI)" );
858 }
859 }
860
861 static public function get_openai_models() {
862 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
863 }
864
865 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
866 {
867 // For fine-tuned models:
868 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
869 if ( !empty( $potentialBaseModel ) ) {
870 $modelFamily = $potentialBaseModel;
871 $finetune = true;
872 }
873
874 $openai_models = Meow_MWAI_Engines_OpenAI::get_openai_models();
875 foreach ( $openai_models as $currentModel ) {
876 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
877 if ( $currentModel['type'] === 'image' ) {
878 if ( !$option ) {
879 error_log( "AI Engine: Image models require an option." );
880 return null;
881 }
882 else {
883 foreach ( $currentModel['options'] as $imageType ) {
884 if ( $imageType['option'] == $option ) {
885 return $imageType['price'] * $outUnits;
886 }
887 }
888 }
889 }
890 else {
891 if ( $finetune ) {
892
893 if ( isset( $currentModel['finetune']['price'] ) ) {
894 $currentModel['price'] = $currentModel['finetune']['price'];
895 }
896 else if ( isset( $currentModel['finetune']['in'] ) ) {
897 $currentModel['price'] = [
898 'in' => $currentModel['finetune']['in'],
899 'out' => $currentModel['finetune']['out']
900 ];
901 }
902 }
903 $inPrice = $currentModel['price'];
904 $outPrice = $currentModel['price'];
905 if ( is_array( $currentModel['price'] ) ) {
906 $inPrice = $currentModel['price']['in'];
907 $outPrice = $currentModel['price']['out'];
908 }
909 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
910 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
911 return $inTotalPrice + $outTotalPrice;
912 }
913 }
914 }
915 error_log( "AI Engine: Invalid model ($modelFamily)." );
916 return null;
917 }
918
919 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
920 {
921 $model = $query->model;
922 $units = 0;
923 $option = null;
924
925 $finetune = false;
926 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) ) {
927 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
928 $finetune = true;
929 }
930 $inUnits = $reply->getPromptTokens();
931 $outUnits = $reply->getCompletionTokens();
932 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
933 }
934 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
935 $model = 'dall-e';
936 $units = $query->maxResults;
937 $option = "1024x1024";
938 return $this->calculate_price( $model, 0, $units, $option, $finetune );
939 }
940 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
941 $model = 'whisper';
942 $units = $reply->getUnits();
943 return $this->calculate_price( $model, 0, $units, $option, $finetune );
944 }
945 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
946 $units = $reply->getTotalTokens();
947 return $this->calculate_price( $model, 0, $units, $option, $finetune );
948 }
949 error_log("AI Engine: Cannot calculate price for $model.");
950 return null;
951 }
952
953 public function get_incidents() {
954 $url = 'https://status.openai.com/history.rss';
955 $response = wp_remote_get( $url );
956 if ( is_wp_error( $response ) ) {
957 throw new Exception( $response->get_error_message() );
958 }
959 $response = wp_remote_retrieve_body( $response );
960 $xml = simplexml_load_string( $response );
961 $incidents = array();
962 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
963 foreach ( $xml->channel->item as $item ) {
964 $date = strtotime( $item->pubDate );
965 if ( $date > $oneWeekAgo ) {
966 $incidents[] = array(
967 'title' => (string) $item->title,
968 'description' => (string) $item->description,
969 'date' => $date
970 );
971 }
972 }
973 return $incidents;
974 }
975 }
976