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