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