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