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