PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.61
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.61
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
1075 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_choices = [];
503
504 if ( !is_null( $streamCallback ) ) {
505 // Streamed data
506 if ( empty( $this->streamContent ) ) {
507 $error = $this->try_decode_error( $this->streamBuffer );
508 if ( !is_null( $error ) ) {
509 throw new Exception( $error );
510 }
511 }
512 $returned_id = $this->inId;
513 $returned_model = $this->inModel ? $this->inModel : $query->model;
514 $returned_choices = [
515 [
516 'message' => [
517 'content' => $this->streamContent,
518 'function_call' => $this->streamFunctionCall
519 ]
520 ]
521 ];
522 }
523 else {
524 // Regular data
525 $data = $res['data'];
526 if ( empty( $data ) ) {
527 throw new Exception( 'No content received (res is null).' );
528 }
529 if ( !$data['model'] ) {
530 error_log( print_r( $data, 1 ) );
531 throw new Exception( 'Invalid response (no model information).' );
532 }
533 $returned_id = $data['id'];
534 $returned_model = $data['model'];
535 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ? $data['usage']['prompt_tokens'] : null;
536 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ? $data['usage']['completion_tokens'] : null;
537 $returned_choices = $data['choices'];
538 }
539
540 // Set the results.
541 $reply->set_choices( $returned_choices );
542 if ( !empty( $returned_id ) ) {
543 $reply->set_id( $returned_id );
544 }
545
546 // Handle tokens.
547 $this->handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens );
548
549 return $reply;
550 }
551 catch ( Exception $e ) {
552 error_log( $e->getMessage() );
553 $service = $this->get_service_name();
554 $message = "From $service: " . $e->getMessage();
555 throw new Exception( $message );
556 }
557 }
558
559 public function handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens ) {
560 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens : $reply->get_in_tokens( $query );
561 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens : $reply->get_out_tokens();
562 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
563 $reply->set_usage( $usage );
564 }
565
566 // Request to DALL-E API
567 public function run_images_query( $query ) {
568 $body = $this->build_body( $query );
569 $url = $this->build_url( $query );
570 $headers = $this->build_headers( $query );
571 $options = $this->build_options( $headers, $body );
572
573 try {
574 $res = $this->run_query( $url, $options );
575 $data = $res['data'];
576 $choices = [];
577 if ( $this->envType === 'azure' ) {
578 foreach ( $data['data'] as $entry ) {
579 $choices[] = [ 'url' => $entry['url'] ];
580 }
581 }
582 else {
583 $choices = $data['data'];
584 }
585
586 $reply = new Meow_MWAI_Reply( $query );
587 $model = $query->model;
588 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
589 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
590 $reply->set_usage( $usage );
591 $reply->set_choices( $choices );
592 $reply->set_type( 'images' );
593
594 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
595 foreach ( $reply->results as &$result ) {
596 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
597 'query_envId' => $query->envId,
598 'query_session' => $query->session,
599 'query_model' => $query->model,
600 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
601 $fileUrl = $this->core->files->get_url( $fileId );
602 $result = $fileUrl;
603 }
604 }
605 $reply->result = $reply->results[0];
606 return $reply;
607 }
608 catch ( Exception $e ) {
609 error_log( $e->getMessage() );
610 $service = $this->get_service_name();
611 throw new Exception( "From $service: " . $e->getMessage() );
612 }
613 }
614
615 /*
616 This is the rest of the OpenAI API support, not related to the models directly.
617 */
618
619 // Check if there are errors in the response from OpenAI, and throw an exception if so.
620 protected function handle_response_errors( $data ) {
621 if ( isset( $data['error'] ) ) {
622 $message = $data['error']['message'];
623 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
624 $message = str_replace( $matches[1], '', $message );
625 }
626 throw new Exception( $message );
627 }
628 }
629
630 public function list_files()
631 {
632 return $this->execute( 'GET', '/files' );
633 }
634
635 static function get_suffix_for_model($model)
636 {
637 // Legacy fine-tuned models
638 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
639 if ( count( $matches ) > 0 ) {
640 return $matches[1];
641 }
642
643 // New fine-tuned models
644 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
645 if (count($matches) > 0) {
646 return $matches[1];
647 }
648
649 return 'N/A';
650 }
651
652 static function get_finetune_base_model($model)
653 {
654 // New fine-tuned models
655 preg_match("/^ft:([^:]+):/", $model, $matches);
656 if (count($matches) > 0) {
657 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
658 return "gpt-3.5-turbo";
659 }
660 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
661 return "gpt-4";
662 }
663 return $matches[1];
664 }
665
666 // Legacy fine-tuned models
667 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
668 if ( count( $matches ) > 0 ) {
669 return $matches[1];
670 }
671
672 return null;
673 }
674
675 public function list_deleted_finetunes( $envId = null, $legacy = false )
676 {
677 $finetunes = $this->list_finetunes( $legacy );
678 $deleted = [];
679
680 foreach ( $finetunes as $finetune ) {
681 $name = $finetune['model'];
682 $isSucceeded = $finetune['status'] === 'succeeded';
683 if ( $isSucceeded ) {
684 try {
685 $finetune = $this->get_model( $name );
686 }
687 catch ( Exception $e ) {
688 $deleted[] = $name;
689 }
690 }
691 }
692 if ( $legacy ) {
693 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
694 }
695 else {
696 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
697 }
698 return $deleted;
699 }
700
701 // public function listModels() {
702 // $res = $this->execute( 'GET', '/models' );
703 // // TODO: Not used by the UI.
704 // throw new Exception( 'Not implemented yet.' );
705 // }
706
707 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
708 // retrieve all the models since Summer 2023, let's see! WIP.
709 public function list_finetunes( $legacy = false )
710 {
711 if ( $legacy ) {
712 $res = $this->execute( 'GET', '/fine-tunes' );
713 }
714 else {
715 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
716 }
717 $finetunes = $res['data'];
718
719 // Add suffix
720 $finetunes = array_map( function ( $finetune ) {
721 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
722 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
723 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
724 $finetune['base_model'] = $finetune['model'];
725 $finetune['model'] = $finetune['fine_tuned_model'];
726 unset( $finetune['object'] );
727 unset( $finetune['hyperparams'] );
728 unset( $finetune['result_files'] );
729 unset( $finetune['training_files'] );
730 unset( $finetune['validation_files'] );
731 unset( $finetune['created_at'] );
732 unset( $finetune['updated_at'] );
733 unset( $finetune['fine_tuned_model'] );
734 return $finetune;
735 }, $finetunes);
736
737 usort( $finetunes, function ( $a, $b ) {
738 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
739 });
740
741 if ( $legacy ) {
742 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
743 }
744 else {
745 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
746 }
747
748 return $finetunes;
749 }
750
751 public function moderate( $input ) {
752 $result = $this->execute('POST', '/moderations', [
753 'input' => $input
754 ]);
755 return $result;
756 }
757
758 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
759 {
760 $result = $this->execute('POST', '/files', null, [
761 'purpose' => $purpose,
762 'data' => $data,
763 'file' => $filename
764 ] );
765 return $result;
766 }
767
768 public function delete_file( $fileId )
769 {
770 return $this->execute( 'DELETE', '/files/' . $fileId );
771 }
772
773 public function get_model( $modelId )
774 {
775 return $this->execute( 'GET', '/models/' . $modelId );
776 }
777
778 public function cancel_finetune( $fineTuneId )
779 {
780 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
781 }
782
783 public function delete_finetune( $modelId )
784 {
785 return $this->execute( 'DELETE', '/models/' . $modelId );
786 }
787
788 public function download_file( $fileId, $newFile = null ) {
789 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
790 $fileInfo = json_decode( (string)$fileInfo, true );
791 $filename = $fileInfo['filename'];
792 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
793 if ( empty( $newFile ) ) {
794 include_once( ABSPATH . 'wp-admin/includes/file.php' );
795 $tempFile = wp_tempnam( $filename );
796 if ( !$tempFile ) {
797 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
798 }
799 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
800 $newFile = $tempFile . '.' . $extension;
801 }
802 else {
803 $newFile = $tempFile;
804 }
805 }
806 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
807 file_put_contents( $newFile, $data );
808 return $newFile;
809 }
810
811 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
812 {
813 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
814 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
815 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
816 (float)$hyperparams['learningRateMultiplier'] : null;
817 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
818 (float)$hyperparams['promptLossWeight'] : null;
819 $arguments = [
820 'training_file' => $fileId,
821 'model' => $model,
822 'suffix' => $suffix
823 ];
824 if ( $legacy ) {
825 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
826 }
827 else {
828 if ( $n_epochs ) {
829 $arguments['hyperparams'] = [];
830 $arguments['hyperparams']['n_epochs'] = $n_epochs;
831 }
832 if ( $batch_size ) {
833 if ( empty( $arguments['hyperparams'] ) ) {
834 $arguments['hyperparams'] = [];
835 }
836 $arguments['hyperparams']['batch_size'] = $batch_size;
837 }
838 if ( $learning_rate_multiplier ) {
839 if ( empty( $arguments['hyperparams'] ) ) {
840 $arguments['hyperparams'] = [];
841 }
842 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
843 }
844 if ( $prompt_loss_weight ) {
845 if ( empty( $arguments['hyperparams'] ) ) {
846 $arguments['hyperparams'] = [];
847 }
848 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
849 }
850 if ( $model === 'turbo' ) {
851 $arguments['model'] = 'gpt-3.5-turbo';
852 }
853 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
854 }
855 return $result;
856 }
857
858 /**
859 * Build the body of a form request.
860 * If the field name is 'file', then the field value is the filename of the file to upload.
861 * The file contents are taken from the 'data' field.
862 *
863 * @param array $fields
864 * @param string $boundary
865 * @return string
866 */
867 public function build_form_body( $fields, $boundary )
868 {
869 $body = '';
870 foreach ( $fields as $name => $value ) {
871 if ( $name == 'data' ) {
872 continue;
873 }
874 $body .= "--$boundary\r\n";
875 $body .= "Content-Disposition: form-data; name=\"$name\"";
876 if ( $name == 'file' ) {
877 $body .= "; filename=\"{$value}\"\r\n";
878 $body .= "Content-Type: application/json\r\n\r\n";
879 $body .= $fields['data'] . "\r\n";
880 }
881 else {
882 $body .= "\r\n\r\n$value\r\n";
883 }
884 }
885 $body .= "--$boundary--\r\n";
886 return $body;
887 }
888
889 /**
890 * Run a request to the OpenAI API.
891 * Fore more information about the $formFields, refer to the build_form_body method.
892 *
893 * @param string $method POST, PUT, GET, DELETE...
894 * @param string $url The API endpoint
895 * @param array $query The query parameters (json)
896 * @param array $formFields The form fields (multipart/form-data)
897 * @param bool $json Whether to return the response as json or not
898 * @return array
899 */
900 public function execute( $method, $url, $query = null, $formFields = null, $json = true, $extraHeaders = null )
901 {
902 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
903 if ( $this->organizationId ) {
904 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
905 }
906 $body = $query ? json_encode( $query ) : null;
907 if ( !empty( $formFields ) ) {
908 $boundary = wp_generate_password( 24, false );
909 $headers = [
910 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
911 'Authorization' => 'Bearer ' . $this->apiKey
912 ];
913 if ( $this->organizationId ) {
914 $headers['OpenAI-Organization'] = $this->organizationId;
915 }
916 $body = $this->build_form_body( $formFields, $boundary );
917 }
918
919 // Maybe we should have headers always as an array... not sure why we have it as a string.
920 if ( !empty( $extraHeaders ) ) {
921 foreach ( $extraHeaders as $key => $value ) {
922 if ( is_array( $headers ) ) {
923 $headers[$key] = $value;
924 }
925 else {
926 $headers .= "$key: $value\r\n";
927 }
928 }
929 }
930
931 $url = 'https://api.openai.com/v1' . $url;
932 $options = [
933 "headers" => $headers,
934 "method" => $method,
935 "timeout" => MWAI_TIMEOUT,
936 "body" => $body,
937 "sslverify" => false
938 ];
939
940 try {
941 $response = wp_remote_request( $url, $options );
942 if ( is_wp_error( $response ) ) {
943 throw new Exception( $response->get_error_message() );
944 }
945 $response = wp_remote_retrieve_body( $response );
946 $data = $json ? json_decode( $response, true ) : $response;
947 $this->handle_response_errors( $data );
948 return $data;
949 }
950 catch ( Exception $e ) {
951 error_log( $e->getMessage() );
952 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
953 }
954 }
955
956 public function get_models() {
957 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
958 }
959
960 static public function get_models_static() {
961 return MWAI_OPENAI_MODELS;
962 }
963
964 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
965 {
966 // For fine-tuned models:
967 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
968 if ( !empty( $potentialBaseModel ) ) {
969 $modelFamily = $potentialBaseModel;
970 $finetune = true;
971 }
972
973 $models = $this->get_models();
974 foreach ( $models as $currentModel ) {
975 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
976 if ( $currentModel['type'] === 'image' ) {
977 if ( !$option ) {
978 error_log( "AI Engine: Image models require an option." );
979 return null;
980 }
981 else {
982 foreach ( $currentModel['options'] as $imageType ) {
983 if ( $imageType['option'] == $option ) {
984 return $imageType['price'] * $outUnits;
985 }
986 }
987 }
988 }
989 else {
990 if ( $finetune ) {
991
992 if ( isset( $currentModel['finetune']['price'] ) ) {
993 $currentModel['price'] = $currentModel['finetune']['price'];
994 }
995 else if ( isset( $currentModel['finetune']['in'] ) ) {
996 $currentModel['price'] = [
997 'in' => $currentModel['finetune']['in'],
998 'out' => $currentModel['finetune']['out']
999 ];
1000 }
1001 }
1002 $inPrice = $currentModel['price'];
1003 $outPrice = $currentModel['price'];
1004 if ( is_array( $currentModel['price'] ) ) {
1005 $inPrice = $currentModel['price']['in'];
1006 $outPrice = $currentModel['price']['out'];
1007 }
1008 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1009 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1010 return $inTotalPrice + $outTotalPrice;
1011 }
1012 }
1013 }
1014 error_log( "AI Engine: Invalid model ($modelFamily)." );
1015 return null;
1016 }
1017
1018 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1019 {
1020 $model = $query->model;
1021 $units = 0;
1022 $option = null;
1023
1024 $finetune = false;
1025 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1026 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1027 $finetune = true;
1028 }
1029 $inUnits = $reply->get_in_tokens( $query );
1030 $outUnits = $reply->get_out_tokens();
1031 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
1032 }
1033 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1034 /** @var Meow_MWAI_Query_Image $query */
1035 $units = $query->maxResults;
1036 $option = $query->resolution;
1037 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1038 }
1039 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1040 $model = 'whisper';
1041 $units = $reply->get_units();
1042 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1043 }
1044 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1045 $units = $reply->get_total_tokens();
1046 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1047 }
1048 error_log("AI Engine: Cannot calculate price for $model.");
1049 return null;
1050 }
1051
1052 public function get_incidents() {
1053 $url = 'https://status.openai.com/history.rss';
1054 $response = wp_remote_get( $url );
1055 if ( is_wp_error( $response ) ) {
1056 throw new Exception( $response->get_error_message() );
1057 }
1058 $response = wp_remote_retrieve_body( $response );
1059 $xml = simplexml_load_string( $response );
1060 $incidents = array();
1061 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1062 foreach ( $xml->channel->item as $item ) {
1063 $date = strtotime( $item->pubDate );
1064 if ( $date > $oneWeekAgo ) {
1065 $incidents[] = array(
1066 'title' => (string) $item->title,
1067 'description' => (string) $item->description,
1068 'date' => $date
1069 );
1070 }
1071 }
1072 return $incidents;
1073 }
1074 }
1075