PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.2.1
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.2.1
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
core.php 2 years ago 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
1137 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 $streamTemporaryBuffer = "";
19 private $streamBuffer = "";
20 private $streamContent = "";
21 private $streamFunctionCall = null;
22 private $streamCallback = null;
23
24 public function __construct( $core, $env )
25 {
26 parent::__construct( $core, $env );
27 $this->set_environment();
28 }
29
30 protected function set_environment() {
31 $env = $this->env;
32 $this->apiKey = $env['apikey'];
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 // Check for a JSON-formatted error in the data, and throw an exception if it's the case.
53 function check_for_error( $data ) {
54 if ( strpos( $data, 'error' ) === false ) {
55 return;
56 }
57 if ( strpos( $data, 'data: ' ) === 0 ) {
58 $jsonPart = substr( $data, strlen( 'data: ' ) );
59 }
60 else {
61 $jsonPart = $data;
62 }
63 $json = json_decode( $jsonPart, true );
64 if ( json_last_error() === JSON_ERROR_NONE ) {
65 if ( isset( $json['error'] ) ) {
66 $error = $json['error'];
67 if ( isset( $error['message'] ) && isset( $error['code'] ) ) {
68 $code = $error['code'];
69 $message = $error['message'];
70 throw new Exception( "Error $code: $message" );
71 }
72 if ( is_string( $error ) ) {
73 throw new Exception( "Error: $error" );
74 }
75 throw new Exception( "Unknown error." );
76 }
77 }
78 }
79
80 private function build_prompt( $query ) {
81 $prompt = "";
82 if ( $query->mode === 'chat' ) {
83 $prompt = $query->instructions . "\n\n";
84 foreach ( $query->messages as $message ) {
85 $role = $message['role'];
86 $content = $message['content'];
87 if ( $role === 'system' ) {
88 $prompt .= "$content\n\n";
89 }
90 if ( $role === 'user' ) {
91 $prompt .= "User: $content\n";
92 }
93 if ( $role === 'assistant' ) {
94 $prompt .= "AI: $content\n";
95 }
96 }
97 $prompt .= "AI: ";
98 }
99 else if ( $query->mode === 'completion' ) {
100 $prompt = $query->get_message();
101 }
102 return $prompt;
103 }
104
105 private function build_messages( $query ) {
106 $messages = [];
107
108 // First, we need to add the first message (the instructions).
109 if ( !empty( $query->instructions ) ) {
110 $messages[] = [ 'role' => 'system', 'content' => $query->instructions ];
111 }
112
113 // Then, if any, we need to add the 'messages', they are already formatted.
114 foreach ( $query->messages as $message ) {
115 $messages[] = $message;
116 }
117
118 // If there is a context, we need to add it.
119 if ( !empty( $query->context ) ) {
120 $messages[] = [ 'role' => 'system', 'content' => $query->context ];
121 }
122
123 // Finally, we need to add the message, but if there is an image, we need to add it as a system message.
124 $fileUrl = $query->get_file_url();
125 if ( !empty( $fileUrl ) ) {
126 $messages[] = [
127 'role' => 'user',
128 'content' => [
129 [
130 "type" => "text",
131 "text" => $query->get_message()
132 ],
133 [
134 "type" => "image_url",
135 "image_url" => [ "url" => $fileUrl ]
136 ]
137 ]
138 ];
139 }
140 else {
141 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
142 }
143
144 return $messages;
145 }
146
147 /*
148 This used to be in the core.php, but since it's relative to OpenAI, it's better to have it here.
149 */
150
151 public function stream_handler( $handle, $args, $url ) {
152 curl_setopt( $handle, CURLOPT_SSL_VERIFYPEER, false );
153 curl_setopt( $handle, CURLOPT_SSL_VERIFYHOST, false );
154
155 // Maybe we could get some info from headers, as for now, there is only the model.
156 // curl_setopt( $handle, CURLOPT_HEADERFUNCTION, function( $curl, $headerLine ) {
157 // $line = trim( $headerLine );
158 // return strlen( $headerLine );
159 // });
160
161 curl_setopt( $handle, CURLOPT_WRITEFUNCTION, function ( $curl, $data ) {
162 $length = strlen( $data );
163
164 // FOR DEBUG:
165 // preg_match_all( '/"content":"(.*?)"/', $data, $matches );
166 // $contents = $matches[1];
167 // foreach ( $contents as $content ) {
168 // error_log( "Content: $content" );
169 // }
170
171 // Error Management
172 $this->check_for_error( $data );
173
174 // Bufferize the unfinished stream (if it's the case)
175 $this->streamTemporaryBuffer .= $data;
176 $this->streamBuffer .= $data;
177 $lines = explode( "\n", $this->streamTemporaryBuffer );
178 if ( substr( $this->streamTemporaryBuffer, -1 ) !== "\n" ) {
179 $this->streamTemporaryBuffer = array_pop( $lines );
180 }
181 else {
182 $this->streamTemporaryBuffer = "";
183 }
184
185 foreach ( $lines as $line ) {
186 if ( $line === "" ) {
187 continue;
188 }
189 if ( strpos( $line, 'data:' ) === 0 ) {
190 $line = substr( $line, 5 );
191 $json = json_decode( $line, true );
192
193 if ( json_last_error() === JSON_ERROR_NONE ) {
194 $content = null;
195
196 // Get additional data from the JSON
197 if ( isset( $json['model'] ) ) {
198 $this->inModel = $json['model'];
199 }
200 if ( isset( $json['id'] ) ) {
201 $this->inId = $json['id'];
202 }
203
204 // Get the content
205 if ( isset( $json['choices'][0]['text'] ) ) {
206 $content = $json['choices'][0]['text'];
207 }
208 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
209 $content = $json['choices'][0]['delta']['content'];
210 }
211 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
212 $function_call = $json['choices'][0]['delta']['function_call'];
213 if ( empty( $this->streamFunctionCall ) ) {
214 $this->streamFunctionCall = [ 'name' => "", 'arguments' => "" ];
215 }
216 if ( isset( $function_call['name'] ) ) {
217 $this->streamFunctionCall['name'] .= $function_call['name'];
218 }
219 if ( isset( $function_call['arguments'] ) ) {
220 $this->streamFunctionCall['arguments'] .= $function_call['arguments'];
221 }
222 }
223 if ( $content !== null && $content !== "" && $content !== "<|im_end|>" ) {
224 $this->streamContent .= $content;
225 call_user_func( $this->streamCallback, $content );
226 }
227 }
228 else {
229 $this->streamTemporaryBuffer .= $line . "\n";
230 }
231 }
232 }
233 return $length;
234 });
235 }
236
237 protected function build_body( $query, $streamCallback = null ) {
238 if ( $query instanceof Meow_MWAI_Query_Text ) {
239 $body = array(
240 "model" => $query->model,
241 "n" => $query->maxResults,
242 "max_tokens" => $query->maxTokens,
243 "temperature" => $query->temperature,
244 "stream" => !is_null( $streamCallback ),
245 );
246
247 if ( !empty( $query->stop ) ) {
248 $body['stop'] = $query->stop;
249 }
250
251 if ( !empty( $query->responseFormat ) ) {
252 if ( $query->responseFormat === 'json' ) {
253 $body['response_format'] = [ 'type' => 'json_object' ];
254 }
255 }
256
257 if ( !empty( $query->functions ) ) {
258 if ( strpos( $query->model, 'ft:' ) === 0 ) {
259 throw new Exception( 'OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
260 }
261 $body['functions'] = $query->functions;
262 $body['function_call'] = $query->functionCall;
263 }
264 if ( $query->mode === 'chat' ) {
265 $body['messages'] = $this->build_messages( $query );
266 }
267 else if ( $query->mode === 'completion' ) {
268 $body['prompt'] = $this->build_prompt( $query );
269 }
270 return $body;
271 }
272 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
273 $audioData = $this->get_audio( $query->url );
274 $body = array(
275 'prompt' => $query->message,
276 'model' => $query->model,
277 'response_format' => 'text',
278 'file' => basename( $query->url ),
279 'data' => $audioData['data']
280 );
281 return $body;
282 }
283 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
284 $body = array( 'input' => $query->message, 'model' => $query->model );
285 if ( $this->envType === 'azure' ) {
286 $body = array( "input" => $query->message );
287 }
288 return $body;
289 }
290 else if ( $query instanceof Meow_MWAI_Query_Image ) {
291 $model = $query->model;
292 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
293 $body = array(
294 "prompt" => $query->message,
295 "n" => $query->maxResults,
296 "size" => $resolution,
297 );
298 if ( $model === 'dall-e-3' ) {
299 $body['model'] = 'dall-e-3';
300 }
301 if ( $model === 'dall-e-3-hd' ) {
302 $body['model'] = 'dall-e-3';
303 $body['quality'] = 'hd';
304 }
305 if ( !empty( $query->style ) && strpos( $model, 'dall-e-3' ) === 0 ) {
306 $body['style'] = $query->style;
307 }
308 return $body;
309 }
310 }
311
312 protected function build_url( $query, $endpoint = null ) {
313 $url = "";
314 $env = $this->env;
315 // This endpoint is basically OpenAI or Azure, but in the case this class
316 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
317 if ( empty( $endpoint ) ) {
318 if ( $this->envType === 'openai' ) {
319 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
320 $this->organizationId = isset( $env['organizationId'] ) ? $env['organizationId'] : null;
321 }
322 else if ( $this->envType === 'azure' ) {
323 $endpoint = isset( $env['endpoint'] ) ? $env['endpoint'] : null;
324 }
325 else {
326 throw new Exception( 'Endpoing is not defined, and this envType is not known: ' . $this->envType );
327 }
328 }
329 // Add the base API to the URL
330 if ( $query instanceof Meow_MWAI_Query_Text ) {
331 if ( $this->envType === 'azure' ) {
332 $deployment_name = $this->get_azure_deployment_name( $query->model );
333 $url = trailingslashit( $endpoint ) . 'openai/deployments/' . $deployment_name;
334 if ( $query->mode === 'chat' ) {
335 $url .= '/chat/completions?' . $this->azureApiVersion;
336 }
337 else if ($query->mode === 'completion') {
338 $url .= '/completions?' . $this->azureApiVersion;
339 }
340 }
341 else {
342 if ( $query->mode === 'chat' ) {
343 $url .= trailingslashit( $endpoint ) . 'chat/completions';
344 }
345 else if ( $query->mode === 'completion' ) {
346 $url .= trailingslashit( $endpoint ) . 'completions';
347 }
348 }
349 return $url;
350 }
351 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
352 $modeEndpoint = $query->mode === 'translation' ? 'translations' : 'transcriptions';
353 $url .= trailingslashit( $endpoint ) . 'audio/' . $modeEndpoint;
354 return $url;
355 }
356 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
357 $url .= trailingslashit( $endpoint ) . 'embeddings';
358 if ( $this->envType === 'azure' ) {
359 $deployment_name = $this->get_azure_deployment_name( $query->model );
360 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
361 $deployment_name . '/embeddings?' . $this->azureApiVersion;
362 }
363 return $url;
364 }
365 else if ( $query instanceof Meow_MWAI_Query_Image ) {
366 $url .= trailingslashit( $endpoint ) . 'images/generations';
367 if ( $this->envType === 'azure' ) {
368 $deployment_name = $this->get_azure_deployment_name( $query->model );
369 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
370 $deployment_name . '/images/generations?' . $this->azureApiVersion;
371 }
372 return $url;
373 }
374 throw new Exception( 'The query is not supported by build_url().' );
375 }
376
377 protected function build_headers( $query ) {
378 if ( $query->apiKey ) {
379 $this->apiKey = $query->apiKey;
380 }
381 if ( empty( $this->apiKey ) ) {
382 throw new Exception( 'No API Key provided. Please visit the Settings.' );
383 }
384 $headers = array(
385 'Content-Type' => 'application/json',
386 'Authorization' => 'Bearer ' . $this->apiKey,
387 );
388 if ( $this->organizationId ) {
389 $headers['OpenAI-Organization'] = $this->organizationId;
390 }
391 if ( $this->envType === 'azure' ) {
392 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $this->apiKey );
393 }
394 return $headers;
395 }
396
397 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
398 $body = null;
399 if ( !empty( $forms ) ) {
400 $boundary = wp_generate_password ( 24, false );
401 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
402 $body = $this->build_form_body( $forms, $boundary );
403 }
404 else if ( !empty( $json ) ) {
405 $body = json_encode( $json );
406 }
407 $options = array(
408 'headers' => $headers,
409 'method' => $method,
410 'timeout' => MWAI_TIMEOUT,
411 'body' => $body,
412 'sslverify' => false
413 );
414 return $options;
415 }
416
417 public function run_query( $url, $options, $isStream = false ) {
418 try {
419 $options['stream'] = $isStream;
420 if ( $isStream ) {
421 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
422 }
423 $res = wp_remote_get( $url, $options );
424
425 if ( is_wp_error( $res ) ) {
426 throw new Exception( $res->get_error_message() );
427 }
428
429 $responseCode = wp_remote_retrieve_response_code( $res );
430 if ( $responseCode === 404 ) {
431 throw new Exception( 'The model\'s API URL was not found.' );
432 }
433 if ( $responseCode === 400 ) {
434 $message = wp_remote_retrieve_body( $res );
435 if ( empty( $message ) ) {
436 $message = wp_remote_retrieve_response_message( $res );
437 }
438 if ( empty( $message ) ) {
439 $message = 'Bad Request';
440 }
441 throw new Exception( $message );
442 }
443
444 if ( $isStream ) {
445 return [ 'stream' => true ];
446 }
447
448 $response = wp_remote_retrieve_body( $res );
449 $headersRes = wp_remote_retrieve_headers( $res );
450 $headers = $headersRes->getAll();
451
452 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
453 // If so, we don't need to decode the response
454 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
455 $resContentType = $normalizedHeaders['content-type'] ?? '';
456 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
457 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
458 }
459
460 $data = json_decode( $response, true );
461 $this->handle_response_errors( $data );
462 return [ 'headers' => $headers, 'data' => $data ];
463 }
464 catch ( Exception $e ) {
465 error_log( $e->getMessage() );
466 throw $e;
467 }
468 }
469
470 private function get_audio( $url ) {
471 require_once( ABSPATH . 'wp-admin/includes/media.php' );
472 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
473 file_put_contents( $tmpFile, file_get_contents( $url ) );
474 $length = null;
475 $metadata = wp_read_audio_metadata( $tmpFile );
476 if ( isset( $metadata['length'] ) ) {
477 $length = $metadata['length'];
478 }
479 $data = file_get_contents( $tmpFile );
480 unlink( $tmpFile );
481 return [ 'data' => $data, 'length' => $length ];
482 }
483
484 public function run_transcribe_query( $query ) {
485 // Check if the URL is valid.
486 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
487 throw new Exception( 'Invalid URL for transcription.' );
488 }
489
490 $body = $this->build_body( $query );
491 $url = $this->build_url( $query );
492 $headers = $this->build_headers( $query );
493 $options = $this->build_options( $headers, null, $body );
494
495 // Perform the request
496 try {
497 $res = $this->run_query( $url, $options );
498 $data = $res['data'];
499 if ( empty( $data ) ) {
500 throw new Exception( 'Invalid data for transcription.' );
501 }
502 $this->check_for_error( $data );
503 $usage = $this->core->record_audio_usage( $query->model, $audioData['length'] );
504 $reply = new Meow_MWAI_Reply( $query );
505 $reply->set_usage( $usage );
506 $reply->set_choices( $data );
507 return $reply;
508 }
509 catch ( Exception $e ) {
510 error_log( $e->getMessage() );
511 $service = $this->get_service_name();
512 throw new Exception( "From $service: " . $e->getMessage() );
513 }
514 }
515
516 public function run_embedding_query( $query ) {
517 $body = $this->build_body( $query );
518 $url = $this->build_url( $query );
519 $headers = $this->build_headers( $query );
520 $options = $this->build_options( $headers, $body );
521
522 try {
523 $res = $this->run_query( $url, $options );
524 $data = $res['data'];
525 if ( empty( $data ) || !isset( $data['data'] ) ) {
526 throw new Exception( 'Invalid data for embedding.' );
527 }
528 $usage = $data['usage'];
529 $this->core->record_tokens_usage( $query->model, $usage['prompt_tokens'] );
530 $reply = new Meow_MWAI_Reply( $query );
531 $reply->set_usage( $usage );
532 $reply->set_choices( $data['data'] );
533 return $reply;
534 }
535 catch ( Exception $e ) {
536 error_log( $e->getMessage() );
537 $service = $this->get_service_name();
538 throw new Exception( "From $service: " . $e->getMessage() );
539 }
540 }
541
542 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
543 if ( !is_null( $streamCallback ) ) {
544 $this->streamCallback = $streamCallback;
545 add_action( 'http_api_curl', array( $this, 'stream_handler' ), 10, 3 );
546 }
547 if ( $query->mode !== 'chat' && $query->mode !== 'completion' ) {
548 throw new Exception( 'Unknown mode for query: ' . $query->mode );
549 }
550
551 $body = $this->build_body( $query, $streamCallback );
552 $url = $this->build_url( $query );
553 $headers = $this->build_headers( $query );
554 $options = $this->build_options( $headers, $body );
555
556 try {
557 $res = $this->run_query( $url, $options, $streamCallback );
558 $reply = new Meow_MWAI_Reply( $query );
559
560 $returned_id = null;
561 $returned_model = $this->inModel;
562 $returned_in_tokens = null;
563 $returned_out_tokens = null;
564 $returned_choices = [];
565
566 if ( !is_null( $streamCallback ) ) {
567 // Streamed data
568 if ( empty( $this->streamContent ) ) {
569 $json = json_decode( $this->streamBuffer, true );
570 if ( isset( $json['error']['message'] ) ) {
571 throw new Exception( $json['error']['message'] );
572 }
573 }
574 $returned_id = $this->inId;
575 $returned_model = $this->inModel ? $this->inModel : $query->model;
576 $returned_choices = [
577 [
578 'message' => [
579 'content' => $this->streamContent,
580 'function_call' => $this->streamFunctionCall
581 ]
582 ]
583 ];
584 }
585 else {
586 // Regular data
587 $data = $res['data'];
588 if ( empty( $data ) ) {
589 throw new Exception( 'No content received (res is null).' );
590 }
591 if ( !$data['model'] ) {
592 error_log( print_r( $data, 1 ) );
593 throw new Exception( 'Invalid response (no model information).' );
594 }
595 $returned_id = $data['id'];
596 $returned_model = $data['model'];
597 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ? $data['usage']['prompt_tokens'] : null;
598 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ? $data['usage']['completion_tokens'] : null;
599 $returned_choices = $data['choices'];
600 }
601
602 // Set the results.
603 $reply->set_choices( $returned_choices );
604 if ( !empty( $returned_id ) ) {
605 $reply->set_id( $returned_id );
606 }
607
608 // Handle tokens.
609 $this->handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens );
610
611 return $reply;
612 }
613 catch ( Exception $e ) {
614 error_log( $e->getMessage() );
615 $service = $this->get_service_name();
616 $message = "From $service: " . $e->getMessage();
617 throw new Exception( $message );
618 }
619 }
620
621 public function handle_tokens_usage( $reply, $query, $returned_model, $returned_in_tokens, $returned_out_tokens ) {
622 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens : $reply->get_in_tokens( $query );
623 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens : $reply->get_out_tokens();
624 $usage = $this->core->record_tokens_usage( $returned_model, $returned_in_tokens, $returned_out_tokens );
625 $reply->set_usage( $usage );
626 }
627
628 // Request to DALL-E API
629 public function run_images_query( $query ) {
630 $body = $this->build_body( $query );
631 $url = $this->build_url( $query );
632 $headers = $this->build_headers( $query );
633 $options = $this->build_options( $headers, $body );
634
635 try {
636 $res = $this->run_query( $url, $options );
637 $data = $res['data'];
638 $choices = [];
639 if ( $this->envType === 'azure' ) {
640 foreach ( $data['data'] as $entry ) {
641 $choices[] = [ 'url' => $entry['url'] ];
642 }
643 }
644 else {
645 $choices = $data['data'];
646 }
647
648 $reply = new Meow_MWAI_Reply( $query );
649 $model = $query->model;
650 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
651 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
652 $reply->set_usage( $usage );
653 $reply->set_choices( $choices );
654 $reply->set_type( 'images' );
655
656 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
657 foreach ( $reply->results as &$result ) {
658 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
659 'query_envId' => $query->envId,
660 'query_session' => $query->session,
661 'query_model' => $query->model,
662 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
663 $fileUrl = $this->core->files->get_url( $fileId );
664 $result = $fileUrl;
665 }
666 }
667 $reply->result = $reply->results[0];
668 return $reply;
669 }
670 catch ( Exception $e ) {
671 error_log( $e->getMessage() );
672 $service = $this->get_service_name();
673 throw new Exception( "From $service: " . $e->getMessage() );
674 }
675 }
676
677 /*
678 This is the rest of the OpenAI API support, not related to the models directly.
679 */
680
681 // Check if there are errors in the response from OpenAI, and throw an exception if so.
682 protected function handle_response_errors( $data ) {
683 if ( isset( $data['error'] ) ) {
684 $message = $data['error']['message'];
685 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
686 $message = str_replace( $matches[1], '', $message );
687 }
688 throw new Exception( $message );
689 }
690 }
691
692 public function list_files()
693 {
694 return $this->execute( 'GET', '/files' );
695 }
696
697 static function get_suffix_for_model($model)
698 {
699 // Legacy fine-tuned models
700 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
701 if ( count( $matches ) > 0 ) {
702 return $matches[1];
703 }
704
705 // New fine-tuned models
706 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
707 if (count($matches) > 0) {
708 return $matches[1];
709 }
710
711 return 'N/A';
712 }
713
714 static function get_finetune_base_model($model)
715 {
716 // New fine-tuned models
717 preg_match("/^ft:([^:]+):/", $model, $matches);
718 if (count($matches) > 0) {
719 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
720 return "gpt-3.5-turbo";
721 }
722 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
723 return "gpt-4";
724 }
725 return $matches[1];
726 }
727
728 // Legacy fine-tuned models
729 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
730 if ( count( $matches ) > 0 ) {
731 return $matches[1];
732 }
733
734 return null;
735 }
736
737 public function list_deleted_finetunes( $envId = null, $legacy = false )
738 {
739 $finetunes = $this->list_finetunes( $legacy );
740 $deleted = [];
741
742 foreach ( $finetunes as $finetune ) {
743 $name = $finetune['model'];
744 $isSucceeded = $finetune['status'] === 'succeeded';
745 if ( $isSucceeded ) {
746 try {
747 $finetune = $this->get_model( $name );
748 }
749 catch ( Exception $e ) {
750 $deleted[] = $name;
751 }
752 }
753 }
754 if ( $legacy ) {
755 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
756 }
757 else {
758 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
759 }
760 return $deleted;
761 }
762
763 // public function listModels() {
764 // $res = $this->execute( 'GET', '/models' );
765 // // TODO: Not used by the UI.
766 // throw new Exception( 'Not implemented yet.' );
767 // }
768
769 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
770 // retrieve all the models since Summer 2023, let's see! WIP.
771 public function list_finetunes( $legacy = false )
772 {
773 if ( $legacy ) {
774 $res = $this->execute( 'GET', '/fine-tunes' );
775 }
776 else {
777 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
778 }
779 $finetunes = $res['data'];
780
781 // Add suffix
782 $finetunes = array_map( function ( $finetune ) {
783 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
784 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
785 $finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
786 $finetune['base_model'] = $finetune['model'];
787 $finetune['model'] = $finetune['fine_tuned_model'];
788 unset( $finetune['object'] );
789 unset( $finetune['hyperparams'] );
790 unset( $finetune['result_files'] );
791 unset( $finetune['training_files'] );
792 unset( $finetune['validation_files'] );
793 unset( $finetune['created_at'] );
794 unset( $finetune['updated_at'] );
795 unset( $finetune['fine_tuned_model'] );
796 return $finetune;
797 }, $finetunes);
798
799 usort( $finetunes, function ( $a, $b ) {
800 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
801 });
802
803 if ( $legacy ) {
804 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
805 }
806 else {
807 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
808 }
809
810 return $finetunes;
811 }
812
813 public function moderate( $input ) {
814 $result = $this->execute('POST', '/moderations', [
815 'input' => $input
816 ]);
817 return $result;
818 }
819
820 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
821 {
822 $result = $this->execute('POST', '/files', null, [
823 'purpose' => $purpose,
824 'data' => $data,
825 'file' => $filename
826 ] );
827 return $result;
828 }
829
830 public function delete_file( $fileId )
831 {
832 return $this->execute( 'DELETE', '/files/' . $fileId );
833 }
834
835 public function get_model( $modelId )
836 {
837 return $this->execute( 'GET', '/models/' . $modelId );
838 }
839
840 public function cancel_finetune( $fineTuneId )
841 {
842 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
843 }
844
845 public function delete_finetune( $modelId )
846 {
847 return $this->execute( 'DELETE', '/models/' . $modelId );
848 }
849
850 public function download_file( $fileId, $newFile = null ) {
851 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
852 $fileInfo = json_decode( (string)$fileInfo, true );
853 $filename = $fileInfo['filename'];
854 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
855 if ( empty( $newFile ) ) {
856 include_once( ABSPATH . 'wp-admin/includes/file.php' );
857 $tempFile = wp_tempnam( $filename );
858 if ( !$tempFile ) {
859 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
860 }
861 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
862 $newFile = $tempFile . '.' . $extension;
863 }
864 else {
865 $newFile = $tempFile;
866 }
867 }
868 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
869 file_put_contents( $newFile, $data );
870 return $newFile;
871 }
872
873 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
874 {
875 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
876 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
877 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
878 (float)$hyperparams['learningRateMultiplier'] : null;
879 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
880 (float)$hyperparams['promptLossWeight'] : null;
881 $arguments = [
882 'training_file' => $fileId,
883 'model' => $model,
884 'suffix' => $suffix
885 ];
886 if ( $legacy ) {
887 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
888 }
889 else {
890 if ( $n_epochs ) {
891 $arguments['hyperparams'] = [];
892 $arguments['hyperparams']['n_epochs'] = $n_epochs;
893 }
894 if ( $batch_size ) {
895 if ( empty( $arguments['hyperparams'] ) ) {
896 $arguments['hyperparams'] = [];
897 }
898 $arguments['hyperparams']['batch_size'] = $batch_size;
899 }
900 if ( $learning_rate_multiplier ) {
901 if ( empty( $arguments['hyperparams'] ) ) {
902 $arguments['hyperparams'] = [];
903 }
904 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
905 }
906 if ( $prompt_loss_weight ) {
907 if ( empty( $arguments['hyperparams'] ) ) {
908 $arguments['hyperparams'] = [];
909 }
910 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
911 }
912 if ( $model === 'turbo' ) {
913 $arguments['model'] = 'gpt-3.5-turbo';
914 }
915 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
916 }
917 return $result;
918 }
919
920 /**
921 * Build the body of a form request.
922 * If the field name is 'file', then the field value is the filename of the file to upload.
923 * The file contents are taken from the 'data' field.
924 *
925 * @param array $fields
926 * @param string $boundary
927 * @return string
928 */
929 public function build_form_body( $fields, $boundary )
930 {
931 $body = '';
932 foreach ( $fields as $name => $value ) {
933 if ( $name == 'data' ) {
934 continue;
935 }
936 $body .= "--$boundary\r\n";
937 $body .= "Content-Disposition: form-data; name=\"$name\"";
938 if ( $name == 'file' ) {
939 $body .= "; filename=\"{$value}\"\r\n";
940 $body .= "Content-Type: application/json\r\n\r\n";
941 $body .= $fields['data'] . "\r\n";
942 }
943 else {
944 $body .= "\r\n\r\n$value\r\n";
945 }
946 }
947 $body .= "--$boundary--\r\n";
948 return $body;
949 }
950
951 /**
952 * Run a request to the OpenAI API.
953 * Fore more information about the $formFields, refer to the build_form_body method.
954 *
955 * @param string $method POST, PUT, GET, DELETE...
956 * @param string $url The API endpoint
957 * @param array $query The query parameters (json)
958 * @param array $formFields The form fields (multipart/form-data)
959 * @param bool $json Whether to return the response as json or not
960 * @return array
961 */
962 public function execute( $method, $url, $query = null, $formFields = null, $json = true, $extraHeaders = null )
963 {
964 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
965 if ( $this->organizationId ) {
966 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
967 }
968 $body = $query ? json_encode( $query ) : null;
969 if ( !empty( $formFields ) ) {
970 $boundary = wp_generate_password( 24, false );
971 $headers = [
972 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
973 'Authorization' => 'Bearer ' . $this->apiKey
974 ];
975 if ( $this->organizationId ) {
976 $headers['OpenAI-Organization'] = $this->organizationId;
977 }
978 $body = $this->build_form_body( $formFields, $boundary );
979 }
980
981 // Maybe we should have headers always as an array... not sure why we have it as a string.
982 if ( !empty( $extraHeaders ) ) {
983 foreach ( $extraHeaders as $key => $value ) {
984 if ( is_array( $headers ) ) {
985 $headers[$key] = $value;
986 }
987 else {
988 $headers .= "$key: $value\r\n";
989 }
990 }
991 }
992
993 $url = 'https://api.openai.com/v1' . $url;
994 $options = [
995 "headers" => $headers,
996 "method" => $method,
997 "timeout" => MWAI_TIMEOUT,
998 "body" => $body,
999 "sslverify" => false
1000 ];
1001
1002 try {
1003 $response = wp_remote_request( $url, $options );
1004 if ( is_wp_error( $response ) ) {
1005 throw new Exception( $response->get_error_message() );
1006 }
1007 $response = wp_remote_retrieve_body( $response );
1008 $data = $json ? json_decode( $response, true ) : $response;
1009 $this->handle_response_errors( $data );
1010 return $data;
1011 }
1012 catch ( Exception $e ) {
1013 error_log( $e->getMessage() );
1014 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
1015 }
1016 }
1017
1018 public function get_models() {
1019 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1020 }
1021
1022 static public function get_models_static() {
1023 return MWAI_OPENAI_MODELS;
1024 }
1025
1026 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
1027 {
1028 // For fine-tuned models:
1029 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
1030 if ( !empty( $potentialBaseModel ) ) {
1031 $modelFamily = $potentialBaseModel;
1032 $finetune = true;
1033 }
1034
1035 $models = $this->get_models();
1036 foreach ( $models as $currentModel ) {
1037 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
1038 if ( $currentModel['type'] === 'image' ) {
1039 if ( !$option ) {
1040 error_log( "AI Engine: Image models require an option." );
1041 return null;
1042 }
1043 else {
1044 foreach ( $currentModel['options'] as $imageType ) {
1045 if ( $imageType['option'] == $option ) {
1046 return $imageType['price'] * $outUnits;
1047 }
1048 }
1049 }
1050 }
1051 else {
1052 if ( $finetune ) {
1053
1054 if ( isset( $currentModel['finetune']['price'] ) ) {
1055 $currentModel['price'] = $currentModel['finetune']['price'];
1056 }
1057 else if ( isset( $currentModel['finetune']['in'] ) ) {
1058 $currentModel['price'] = [
1059 'in' => $currentModel['finetune']['in'],
1060 'out' => $currentModel['finetune']['out']
1061 ];
1062 }
1063 }
1064 $inPrice = $currentModel['price'];
1065 $outPrice = $currentModel['price'];
1066 if ( is_array( $currentModel['price'] ) ) {
1067 $inPrice = $currentModel['price']['in'];
1068 $outPrice = $currentModel['price']['out'];
1069 }
1070 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1071 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1072 return $inTotalPrice + $outTotalPrice;
1073 }
1074 }
1075 }
1076 error_log( "AI Engine: Invalid model ($modelFamily)." );
1077 return null;
1078 }
1079
1080 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1081 {
1082 $model = $query->model;
1083 $units = 0;
1084 $option = null;
1085
1086 $finetune = false;
1087 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1088 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1089 $finetune = true;
1090 }
1091 $inUnits = $reply->get_in_tokens( $query );
1092 $outUnits = $reply->get_out_tokens();
1093 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
1094 }
1095 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1096 /** @var Meow_MWAI_Query_Image $query */
1097 $units = $query->maxResults;
1098 $option = $query->resolution;
1099 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1100 }
1101 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1102 $model = 'whisper';
1103 $units = $reply->get_units();
1104 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1105 }
1106 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1107 $units = $reply->get_total_tokens();
1108 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1109 }
1110 error_log("AI Engine: Cannot calculate price for $model.");
1111 return null;
1112 }
1113
1114 public function get_incidents() {
1115 $url = 'https://status.openai.com/history.rss';
1116 $response = wp_remote_get( $url );
1117 if ( is_wp_error( $response ) ) {
1118 throw new Exception( $response->get_error_message() );
1119 }
1120 $response = wp_remote_retrieve_body( $response );
1121 $xml = simplexml_load_string( $response );
1122 $incidents = array();
1123 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1124 foreach ( $xml->channel->item as $item ) {
1125 $date = strtotime( $item->pubDate );
1126 if ( $date > $oneWeekAgo ) {
1127 $incidents[] = array(
1128 'title' => (string) $item->title,
1129 'description' => (string) $item->description,
1130 'date' => $date
1131 );
1132 }
1133 }
1134 return $incidents;
1135 }
1136 }
1137