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