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