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