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