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