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