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