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