PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.3.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.3.0
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
1315 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( 'AI Engine: The model "' . $query->model . '" doesn\'t support Function Calling.' );
175 }
176 else if ( strpos( $query->model, 'ft:' ) === 0 ) {
177 error_log( 'AI Engine: 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( 'AI Engine: Invalid response (no model information):' );
693 error_log( print_r( $data, 1 ) );
694 throw new Exception( 'Invalid response (no model information).' );
695 }
696 $returned_id = $data['id'];
697 $returned_model = $data['model'];
698 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
699 $data['usage']['prompt_tokens'] : null;
700 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
701 $data['usage']['completion_tokens'] : null;
702 $returned_price = isset( $data['usage']['total_cost'] ) ?
703 $data['usage']['total_cost'] : null;
704 $returned_choices = $data['choices'];
705 }
706
707 // Set the results.
708 $reply->set_choices( $returned_choices );
709 if ( !empty( $returned_id ) ) {
710 $reply->set_id( $returned_id );
711 }
712 if ( !empty( $returned_id ) ) {
713 $reply->set_id( $returned_id );
714 }
715
716 // Handle tokens.
717 $this->handle_tokens_usage( $reply, $query, $returned_model,
718 $returned_in_tokens, $returned_out_tokens, $returned_price
719 );
720
721 return $reply;
722 }
723 catch ( Exception $e ) {
724 error_log( $e->getMessage() );
725 $service = $this->get_service_name();
726 $message = "From $service: " . $e->getMessage();
727 throw new Exception( $message );
728 }
729 finally {
730 if ( !is_null( $streamCallback ) ) {
731 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
732 }
733 }
734 }
735
736 public function handle_tokens_usage( $reply, $query, $returned_model,
737 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
738 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens :
739 $reply->get_in_tokens( $query );
740 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens :
741 $reply->get_out_tokens();
742 $returned_price = !is_null( $returned_price ) ? $returned_price :
743 $reply->get_price();
744 $usage = $this->core->record_tokens_usage(
745 $returned_model,
746 $returned_in_tokens,
747 $returned_out_tokens,
748 $returned_price
749 );
750 $reply->set_usage( $usage );
751 }
752
753 // Request to DALL-E API
754 public function run_images_query( $query ) {
755 $body = $this->build_body( $query );
756 $url = $this->build_url( $query );
757 $headers = $this->build_headers( $query );
758 $options = $this->build_options( $headers, $body );
759
760 try {
761 $res = $this->run_query( $url, $options );
762 $data = $res['data'];
763 $choices = [];
764 if ( $this->envType === 'azure' ) {
765 foreach ( $data['data'] as $entry ) {
766 $choices[] = [ 'url' => $entry['url'] ];
767 }
768 }
769 else {
770 $choices = $data['data'];
771 }
772
773 $reply = new Meow_MWAI_Reply( $query );
774 $model = $query->model;
775 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
776 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
777 $reply->set_usage( $usage );
778 $reply->set_choices( $choices );
779 $reply->set_type( 'images' );
780
781 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
782 foreach ( $reply->results as &$result ) {
783 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
784 'query_envId' => $query->envId,
785 'query_session' => $query->session,
786 'query_model' => $query->model,
787 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
788 $fileUrl = $this->core->files->get_url( $fileId );
789 $result = $fileUrl;
790 }
791 }
792 $reply->result = $reply->results[0];
793 return $reply;
794 }
795 catch ( Exception $e ) {
796 error_log( $e->getMessage() );
797 $service = $this->get_service_name();
798 throw new Exception( "From $service: " . $e->getMessage() );
799 }
800 }
801
802 /*
803 This is the rest of the OpenAI API support, not related to the models directly.
804 */
805
806 // Check if there are errors in the response from OpenAI, and throw an exception if so.
807 protected function handle_response_errors( $data ) {
808 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
809 $message = $data['error']['message'];
810 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
811 $message = str_replace( $matches[1], '', $message );
812 }
813 throw new Exception( $message );
814 }
815 }
816
817 public function list_files( $purposeFilter = null )
818 {
819 if ( empty( $purposeFilter ) ) {
820 return $this->execute( 'GET', '/files' );
821 }
822 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
823 }
824
825 static function get_suffix_for_model($model)
826 {
827 // Legacy fine-tuned models
828 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
829 if ( count( $matches ) > 0 ) {
830 return $matches[1];
831 }
832
833 // New fine-tuned models
834 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
835 if (count($matches) > 0) {
836 return $matches[1];
837 }
838
839 return 'N/A';
840 }
841
842 static function get_finetune_base_model($model)
843 {
844 // New fine-tuned models
845 preg_match("/^ft:([^:]+):/", $model, $matches);
846 if (count($matches) > 0) {
847 if ( preg_match( '/^gpt-3.5/', $matches[1] ) ) {
848 return "gpt-3.5-turbo";
849 }
850 else if ( preg_match( '/^gpt-4/', $matches[1] ) ) {
851 return "gpt-4";
852 }
853 return $matches[1];
854 }
855
856 // Legacy fine-tuned models
857 preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches );
858 if ( count( $matches ) > 0 ) {
859 return $matches[1];
860 }
861
862 return null;
863 }
864
865 public function list_deleted_finetunes( $envId = null, $legacy = false )
866 {
867 $finetunes = $this->list_finetunes( $legacy );
868 $deleted = [];
869
870 foreach ( $finetunes as $finetune ) {
871 $name = $finetune['model'];
872 $isSucceeded = $finetune['status'] === 'succeeded';
873 if ( $isSucceeded ) {
874 try {
875 $finetune = $this->get_model( $name );
876 }
877 catch ( Exception $e ) {
878 $deleted[] = $name;
879 }
880 }
881 }
882 if ( $legacy ) {
883 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
884 }
885 else {
886 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
887 }
888 return $deleted;
889 }
890
891 // public function listModels() {
892 // $res = $this->execute( 'GET', '/models' );
893 // // TODO: Not used by the UI.
894 // throw new Exception( 'Not implemented yet.' );
895 // }
896
897 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
898 // retrieve all the models since Summer 2023, let's see! WIP.
899 public function list_finetunes( $legacy = false )
900 {
901 if ( $legacy ) {
902 $res = $this->execute( 'GET', '/fine-tunes' );
903 }
904 else {
905 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
906 }
907 $finetunes = $res['data'];
908
909 // Add suffix
910 $finetunes = array_map( function ( $finetune ) {
911 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
912 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] );
913 if ( isset( $finetune['estimated_finish'] ) ) {
914 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] );
915 }
916 else {
917 $finetune['estimatedOn'] = null;
918 }
919 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
920 $finetune['base_model'] = $finetune['model'];
921 $finetune['model'] = $finetune['fine_tuned_model'];
922 unset( $finetune['object'] );
923 unset( $finetune['hyperparams'] );
924 unset( $finetune['result_files'] );
925 unset( $finetune['training_files'] );
926 unset( $finetune['validation_files'] );
927 unset( $finetune['created_at'] );
928 unset( $finetune['updated_at'] );
929 unset( $finetune['fine_tuned_model'] );
930 return $finetune;
931 }, $finetunes);
932
933 usort( $finetunes, function ( $a, $b ) {
934 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
935 });
936
937 if ( $legacy ) {
938 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
939 }
940 else {
941 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
942 }
943
944 return $finetunes;
945 }
946
947 public function moderate( $input ) {
948 $result = $this->execute('POST', '/moderations', [
949 'input' => $input
950 ]);
951 return $result;
952 }
953
954 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
955 {
956 $result = $this->execute('POST', '/files', null, [
957 'purpose' => $purpose,
958 'data' => $data,
959 'file' => $filename
960 ] );
961 return $result;
962 }
963
964 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
965 $expiryInDays = $expiry ? max( 1, ceil( $expiry / 86400 ) ) : 7;
966 $result = $this->execute( 'POST', '/vector_stores', [
967 'name' => !empty( $name ) ? $name : 'default',
968 'metadata' => $metadata,
969 'expires_after' => [
970 'anchor' => 'last_active_at',
971 'days' => $expiryInDays
972 ]
973 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
974 return $result['id'];
975 }
976
977 public function add_vector_store_file( $vectorStoreId, $fileId ) {
978 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
979 'file_id' => $fileId
980 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
981 return $result['id'];
982
983 }
984
985 public function delete_file( $fileId )
986 {
987 return $this->execute( 'DELETE', '/files/' . $fileId );
988 }
989
990 public function get_model( $modelId )
991 {
992 return $this->execute( 'GET', '/models/' . $modelId );
993 }
994
995 public function cancel_finetune( $fineTuneId )
996 {
997 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
998 }
999
1000 public function delete_finetune( $modelId )
1001 {
1002 return $this->execute( 'DELETE', '/models/' . $modelId );
1003 }
1004
1005 public function download_file( $fileId, $newFile = null ) {
1006 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1007 $fileInfo = json_decode( (string)$fileInfo, true );
1008 $filename = $fileInfo['filename'];
1009 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1010 if ( empty( $newFile ) ) {
1011 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1012 $tempFile = wp_tempnam( $filename );
1013 if ( !$tempFile ) {
1014 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1015 }
1016 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1017 $newFile = $tempFile . '.' . $extension;
1018 }
1019 else {
1020 $newFile = $tempFile;
1021 }
1022 }
1023 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1024 file_put_contents( $newFile, $data );
1025 return $newFile;
1026 }
1027
1028 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1029 {
1030 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1031 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1032 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1033 (float)$hyperparams['learningRateMultiplier'] : null;
1034 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1035 (float)$hyperparams['promptLossWeight'] : null;
1036 $arguments = [
1037 'training_file' => $fileId,
1038 'model' => $model,
1039 'suffix' => $suffix
1040 ];
1041 if ( $legacy ) {
1042 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1043 }
1044 else {
1045 if ( $n_epochs ) {
1046 $arguments['hyperparams'] = [];
1047 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1048 }
1049 if ( $batch_size ) {
1050 if ( empty( $arguments['hyperparams'] ) ) {
1051 $arguments['hyperparams'] = [];
1052 }
1053 $arguments['hyperparams']['batch_size'] = $batch_size;
1054 }
1055 if ( $learning_rate_multiplier ) {
1056 if ( empty( $arguments['hyperparams'] ) ) {
1057 $arguments['hyperparams'] = [];
1058 }
1059 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1060 }
1061 if ( $prompt_loss_weight ) {
1062 if ( empty( $arguments['hyperparams'] ) ) {
1063 $arguments['hyperparams'] = [];
1064 }
1065 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1066 }
1067 if ( $model === 'turbo' ) {
1068 $arguments['model'] = 'gpt-3.5-turbo';
1069 }
1070 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1071 }
1072 return $result;
1073 }
1074
1075 /**
1076 * Build the body of a form request.
1077 * If the field name is 'file', then the field value is the filename of the file to upload.
1078 * The file contents are taken from the 'data' field.
1079 *
1080 * @param array $fields
1081 * @param string $boundary
1082 * @return string
1083 */
1084 public function build_form_body( $fields, $boundary )
1085 {
1086 $body = '';
1087 foreach ( $fields as $name => $value ) {
1088 if ( $name == 'data' ) {
1089 continue;
1090 }
1091 $body .= "--$boundary\r\n";
1092 $body .= "Content-Disposition: form-data; name=\"$name\"";
1093 if ( $name == 'file' ) {
1094 $body .= "; filename=\"{$value}\"\r\n";
1095 $body .= "Content-Type: application/json\r\n\r\n";
1096 $body .= $fields['data'] . "\r\n";
1097 }
1098 else {
1099 $body .= "\r\n\r\n$value\r\n";
1100 }
1101 }
1102 $body .= "--$boundary--\r\n";
1103 return $body;
1104 }
1105
1106 /**
1107 * Run a request to the OpenAI API.
1108 * Fore more information about the $formFields, refer to the build_form_body method.
1109 *
1110 * @param string $method POST, PUT, GET, DELETE...
1111 * @param string $url The API endpoint
1112 * @param array $query The query parameters (json)
1113 * @param array $formFields The form fields (multipart/form-data)
1114 * @param bool $json Whether to return the response as json or not
1115 * @return array
1116 */
1117 public function execute( $method, $url, $query = null, $formFields = null,
1118 $json = true, $extraHeaders = null, $streamCallback = null )
1119 {
1120 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
1121 if ( $this->organizationId ) {
1122 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1123 }
1124 $body = $query ? json_encode( $query ) : null;
1125 if ( !empty( $formFields ) ) {
1126 $boundary = wp_generate_password( 24, false );
1127 $headers = [
1128 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
1129 'Authorization' => 'Bearer ' . $this->apiKey
1130 ];
1131 if ( $this->organizationId ) {
1132 $headers['OpenAI-Organization'] = $this->organizationId;
1133 }
1134 $body = $this->build_form_body( $formFields, $boundary );
1135 }
1136
1137 // Maybe we should have headers always as an array... not sure why we have it as a string.
1138 if ( !empty( $extraHeaders ) ) {
1139 foreach ( $extraHeaders as $key => $value ) {
1140 if ( is_array( $headers ) ) {
1141 $headers[$key] = $value;
1142 }
1143 else {
1144 $headers .= "$key: $value\r\n";
1145 }
1146 }
1147 }
1148
1149 // If it's a GET, body should be null, and we should append the query to the URL.
1150 if ( $method === 'GET' ) {
1151 if ( !empty( $query ) ) {
1152 $url .= '?' . http_build_query( $query );
1153 }
1154 $body = null;
1155 }
1156
1157 $url = 'https://api.openai.com/v1' . $url;
1158 $options = [
1159 "headers" => $headers,
1160 "method" => $method,
1161 "timeout" => MWAI_TIMEOUT,
1162 "body" => $body,
1163 "sslverify" => false
1164 ];
1165
1166 try {
1167 if ( !is_null( $streamCallback ) ) {
1168 $options['stream'] = true;
1169 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1170 // The stream handler calls the streamCallback every time there is content
1171 // TODO: For assistants, we should probably have a different stream handler to
1172 // handle the assistant's specific reply and perform the necessary actions.
1173 $this->streamCallback = $streamCallback;
1174 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1175 }
1176 $res = wp_remote_request( $url, $options );
1177 if ( is_wp_error( $res ) ) {
1178 throw new Exception( $res->get_error_message() );
1179 }
1180 $res = wp_remote_retrieve_body( $res );
1181 $data = $json ? json_decode( $res, true ) : $res;
1182 $this->handle_response_errors( $data );
1183 return $data;
1184 }
1185 catch ( Exception $e ) {
1186 error_log( $e->getMessage() );
1187 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
1188 }
1189 finally {
1190 if ( !is_null( $streamCallback ) ) {
1191 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1192 }
1193 }
1194 }
1195
1196 public function get_models() {
1197 return apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1198 }
1199
1200 static public function get_models_static() {
1201 return MWAI_OPENAI_MODELS;
1202 }
1203
1204 private function calculate_price( $modelFamily, $inUnits, $outUnits, $option = null, $finetune = false )
1205 {
1206 // For fine-tuned models:
1207 $potentialBaseModel = SELF::get_finetune_base_model( $modelFamily );
1208 if ( !empty( $potentialBaseModel ) ) {
1209 $modelFamily = $potentialBaseModel;
1210 $finetune = true;
1211 }
1212
1213 $models = $this->get_models();
1214 foreach ( $models as $currentModel ) {
1215 if ( $currentModel['model'] === $modelFamily || ( $finetune && $currentModel['family'] === $modelFamily ) ) {
1216 if ( $currentModel['type'] === 'image' ) {
1217 if ( !$option ) {
1218 error_log( "AI Engine: Image models require an option." );
1219 return null;
1220 }
1221 else {
1222 foreach ( $currentModel['options'] as $imageType ) {
1223 if ( $imageType['option'] == $option ) {
1224 return $imageType['price'] * $outUnits;
1225 }
1226 }
1227 }
1228 }
1229 else {
1230 if ( $finetune ) {
1231
1232 if ( isset( $currentModel['finetune']['price'] ) ) {
1233 $currentModel['price'] = $currentModel['finetune']['price'];
1234 }
1235 else if ( isset( $currentModel['finetune']['in'] ) ) {
1236 $currentModel['price'] = [
1237 'in' => $currentModel['finetune']['in'],
1238 'out' => $currentModel['finetune']['out']
1239 ];
1240 }
1241 }
1242 $inPrice = $currentModel['price'];
1243 $outPrice = $currentModel['price'];
1244 if ( is_array( $currentModel['price'] ) ) {
1245 $inPrice = $currentModel['price']['in'];
1246 $outPrice = $currentModel['price']['out'];
1247 }
1248 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1249 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1250 return $inTotalPrice + $outTotalPrice;
1251 }
1252 }
1253 }
1254 error_log( "AI Engine: Invalid model ($modelFamily)." );
1255 return null;
1256 }
1257
1258 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1259 {
1260 $model = $query->model;
1261 $units = 0;
1262 $option = null;
1263
1264 $finetune = false;
1265 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1266 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1267 $finetune = true;
1268 }
1269 $inUnits = $reply->get_in_tokens( $query );
1270 $outUnits = $reply->get_out_tokens();
1271 return $this->calculate_price( $model, $inUnits, $outUnits, $option, $finetune );
1272 }
1273 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1274 /** @var Meow_MWAI_Query_Image $query */
1275 $units = $query->maxResults;
1276 $option = $query->resolution;
1277 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1278 }
1279 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1280 $model = 'whisper';
1281 $units = $reply->get_units();
1282 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1283 }
1284 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1285 $units = $reply->get_total_tokens();
1286 return $this->calculate_price( $model, 0, $units, $option, $finetune );
1287 }
1288 error_log("AI Engine: Cannot calculate price for $model.");
1289 return null;
1290 }
1291
1292 public function get_incidents() {
1293 $url = 'https://status.openai.com/history.rss';
1294 $response = wp_remote_get( $url );
1295 if ( is_wp_error( $response ) ) {
1296 throw new Exception( $response->get_error_message() );
1297 }
1298 $response = wp_remote_retrieve_body( $response );
1299 $xml = simplexml_load_string( $response );
1300 $incidents = array();
1301 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1302 foreach ( $xml->channel->item as $item ) {
1303 $date = strtotime( $item->pubDate );
1304 if ( $date > $oneWeekAgo ) {
1305 $incidents[] = array(
1306 'title' => (string) $item->title,
1307 'description' => (string) $item->description,
1308 'date' => $date
1309 );
1310 }
1311 }
1312 return $incidents;
1313 }
1314 }
1315