PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.6.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.6.2
3.7.4 3.7.3 3.7.2 3.7.1 3.7.0 3.6.9 3.6.8 3.6.7 3.6.6 3.6.4 3.6.5 3.6.3 3.6.2 3.6.1 3.6.0 3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
anthropic.php 1 year ago core.php 2 years ago factory.php 2 years ago google.php 2 years ago huggingface.php 2 years ago openai.php 1 year ago openrouter.php 2 years ago replicate.php 2 years ago
openai.php
1439 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-03-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 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
654 throw $e;
655 }
656 finally {
657 if ( $isStream && file_exists( $options['filename'] ) ) {
658 unlink( $options['filename'] );
659 }
660 }
661 }
662
663 private function get_audio( $url ) {
664 require_once( ABSPATH . 'wp-admin/includes/media.php' );
665 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
666 file_put_contents( $tmpFile, file_get_contents( $url ) );
667 $length = null;
668 $metadata = wp_read_audio_metadata( $tmpFile );
669 if ( isset( $metadata['length'] ) ) {
670 $length = $metadata['length'];
671 }
672 $data = file_get_contents( $tmpFile );
673 unlink( $tmpFile );
674 return [ 'data' => $data, 'length' => $length ];
675 }
676
677 public function run_transcribe_query( $query ) {
678 // Check if the URL is valid.
679 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
680 throw new Exception( 'Invalid URL for transcription.' );
681 }
682
683 $audioData = $this->get_audio( $query->url );
684 $body = $this->build_body( $query, null, $audioData['data'] );
685 $url = $this->build_url( $query );
686 $headers = $this->build_headers( $query );
687 $options = $this->build_options( $headers, null, $body );
688
689 // Perform the request
690 try {
691 $res = $this->run_query( $url, $options );
692 $data = $res['data'];
693 if ( empty( $data ) ) {
694 throw new Exception( 'Invalid data for transcription.' );
695 }
696 $usage = $this->core->record_audio_usage( $query->model, $audioData['length'] );
697 $reply = new Meow_MWAI_Reply( $query );
698 $reply->set_usage( $usage );
699 $reply->set_choices( $data );
700 return $reply;
701 }
702 catch ( Exception $e ) {
703 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
704 $service = $this->get_service_name();
705 throw new Exception( "From $service: " . $e->getMessage() );
706 }
707 }
708
709 public function run_embedding_query( $query ) {
710 $body = $this->build_body( $query );
711 $url = $this->build_url( $query );
712 $headers = $this->build_headers( $query );
713 $options = $this->build_options( $headers, $body );
714
715 try {
716 $res = $this->run_query( $url, $options );
717 $data = $res['data'];
718 if ( empty( $data ) || !isset( $data['data'] ) ) {
719 throw new Exception( 'Invalid data for embedding.' );
720 }
721 $usage = $data['usage'];
722 $this->core->record_tokens_usage( $query->model, $usage['prompt_tokens'] );
723 $reply = new Meow_MWAI_Reply( $query );
724 $reply->set_usage( $usage );
725 $reply->set_choices( $data['data'] );
726 return $reply;
727 }
728 catch ( Exception $e ) {
729 $message = $e->getMessage();
730 $error = $this->try_decode_error( $message );
731 if ( !is_null( $error ) ) {
732 $message = $error;
733 }
734 Meow_MWAI_Logging::error( 'OpenAI: ' . $message );
735 $service = $this->get_service_name();
736 throw new Exception( "From $service: " . $message );
737 }
738 }
739
740 public function try_decode_error( $data ) {
741 $json = json_decode( $data, true );
742 if ( isset( $json['error']['message'] ) ) {
743 return $json['error']['message'];
744 }
745 return null;
746 }
747
748 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
749 $isStreaming = !is_null( $streamCallback );
750
751 if ( $isStreaming ) {
752 $this->streamCallback = $streamCallback;
753 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
754 }
755
756 $this->reset_stream();
757 $body = $this->build_body( $query, $streamCallback );
758 $url = $this->build_url( $query );
759 $headers = $this->build_headers( $query );
760 $options = $this->build_options( $headers, $body );
761
762 try {
763 $res = $this->run_query( $url, $options, $streamCallback );
764 $reply = new Meow_MWAI_Reply( $query );
765
766 $returned_id = null;
767 $returned_model = $this->inModel;
768 $returned_in_tokens = null;
769 $returned_out_tokens = null;
770 $returned_price = null;
771 $returned_choices = [];
772
773 // Streaming Mode
774 if ( $isStreaming ) {
775 if ( empty( $this->streamContent ) ) {
776 $error = $this->try_decode_error( $this->streamBuffer );
777 if ( !is_null( $error ) ) {
778 throw new Exception( $error );
779 }
780 }
781 $returned_id = $this->inId;
782 $returned_model = $this->inModel ? $this->inModel : $query->model;
783 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
784 if ( !empty( $this->streamFunctionCall ) ) {
785 $message['function_call'] = $this->streamFunctionCall;
786 }
787 if ( !empty( $this->streamToolCalls ) ) {
788 $message['tool_calls'] = $this->streamToolCalls;
789 }
790 if ( !is_null( $this->streamInTokens ) ) {
791 $returned_in_tokens = $this->streamInTokens;
792 }
793 if ( !is_null( $this->streamOutTokens ) ) {
794 $returned_out_tokens = $this->streamOutTokens;
795 }
796 $returned_choices = [ [ 'message' => $message ] ];
797 }
798 // Standard Mode
799 else {
800 $data = $res['data'];
801 if ( empty( $data ) ) {
802 throw new Exception( 'No content received (res is null).' );
803 }
804 if ( !$data['model'] ) {
805 Meow_MWAI_Logging::error( 'OpenAI: Invalid response (no model information):' );
806 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
807 throw new Exception( 'Invalid response (no model information).' );
808 }
809 $returned_id = $data['id'];
810 $returned_model = $data['model'];
811 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
812 $data['usage']['prompt_tokens'] : null;
813 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
814 $data['usage']['completion_tokens'] : null;
815 $returned_price = isset( $data['usage']['total_cost'] ) ?
816 $data['usage']['total_cost'] : null;
817 $returned_choices = $data['choices'];
818 }
819
820 // Set the results.
821 $reply->set_choices( $returned_choices );
822 if ( !empty( $returned_id ) ) {
823 $reply->set_id( $returned_id );
824 }
825 if ( !empty( $returned_id ) ) {
826 $reply->set_id( $returned_id );
827 }
828
829 // Handle tokens.
830 $this->handle_tokens_usage( $reply, $query, $returned_model,
831 $returned_in_tokens, $returned_out_tokens, $returned_price
832 );
833
834 return $reply;
835 }
836 catch ( Exception $e ) {
837 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
838 $service = $this->get_service_name();
839 $message = "From $service: " . $e->getMessage();
840 throw new Exception( $message );
841 }
842 finally {
843 if ( !is_null( $streamCallback ) ) {
844 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
845 }
846 }
847 }
848
849 public function handle_tokens_usage( $reply, $query, $returned_model,
850 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
851 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens :
852 $reply->get_in_tokens( $query );
853 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens :
854 $reply->get_out_tokens();
855 $returned_price = !is_null( $returned_price ) ? $returned_price :
856 $reply->get_price();
857 $usage = $this->core->record_tokens_usage(
858 $returned_model,
859 $returned_in_tokens,
860 $returned_out_tokens,
861 $returned_price
862 );
863 $reply->set_usage( $usage );
864 }
865
866 // Request to DALL-E API
867 public function run_image_query( $query ) {
868 $body = $this->build_body( $query );
869 $url = $this->build_url( $query );
870 $headers = $this->build_headers( $query );
871 $options = $this->build_options( $headers, $body );
872
873 try {
874 $res = $this->run_query( $url, $options );
875 $data = $res['data'];
876 $choices = [];
877 if ( $this->envType === 'azure' ) {
878 foreach ( $data['data'] as $entry ) {
879 $choices[] = [ 'url' => $entry['url'] ];
880 }
881 }
882 else {
883 $choices = $data['data'];
884 }
885
886 $reply = new Meow_MWAI_Reply( $query );
887 $model = $query->model;
888 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
889 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
890 $reply->set_usage( $usage );
891 $reply->set_choices( $choices );
892 $reply->set_type( 'images' );
893
894 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
895 foreach ( $reply->results as &$result ) {
896 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
897 'query_envId' => $query->envId,
898 'query_session' => $query->session,
899 'query_model' => $query->model,
900 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
901 $fileUrl = $this->core->files->get_url( $fileId );
902 $result = $fileUrl;
903 }
904 }
905 $reply->result = $reply->results[0];
906 return $reply;
907 }
908 catch ( Exception $e ) {
909 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
910 $service = $this->get_service_name();
911 throw new Exception( "From $service: " . $e->getMessage() );
912 }
913 }
914
915 /*
916 This is the rest of the OpenAI API support, not related to the models directly.
917 */
918
919 // Check if there are errors in the response from OpenAI, and throw an exception if so.
920 protected function handle_response_errors( $data ) {
921 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
922 $message = $data['error']['message'];
923 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
924 $message = str_replace( $matches[1], '', $message );
925 }
926 throw new Exception( $message );
927 }
928 }
929
930 public function list_files( $purposeFilter = null )
931 {
932 if ( empty( $purposeFilter ) ) {
933 return $this->execute( 'GET', '/files' );
934 }
935 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
936 }
937
938 static function get_suffix_for_model($model)
939 {
940 // Legacy fine-tuned models
941 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
942 if ( count( $matches ) > 0 ) {
943 return $matches[1];
944 }
945
946 // New fine-tuned models
947 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
948 if (count($matches) > 0) {
949 return $matches[1];
950 }
951
952 return 'N/A';
953 }
954
955 static function get_model_without_release_date( $model )
956 {
957 if ( empty( $model ) ) {
958 return null;
959 }
960 return preg_replace( '/-\d{4}-\d{2}-\d{2}$/', '', $model );
961 }
962
963 public function list_deleted_finetunes( $envId = null, $legacy = false )
964 {
965 $finetunes = $this->list_finetunes( $legacy );
966 $deleted = [];
967
968 foreach ( $finetunes as $finetune ) {
969 $name = $finetune['model'];
970 $isSucceeded = $finetune['status'] === 'succeeded';
971 if ( $isSucceeded ) {
972 try {
973 $finetune = $this->get_model( $name );
974 }
975 catch ( Exception $e ) {
976 $deleted[] = $name;
977 }
978 }
979 }
980 if ( $legacy ) {
981 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
982 }
983 else {
984 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
985 }
986 return $deleted;
987 }
988
989 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
990 // retrieve all the models since Summer 2023, let's see! WIP.
991 public function list_finetunes( $legacy = false )
992 {
993 if ( $legacy ) {
994 $res = $this->execute( 'GET', '/fine-tunes' );
995 }
996 else {
997 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
998 }
999 $finetunes = $res['data'];
1000
1001 // Add suffix
1002 $finetunes = array_map( function ( $finetune ) {
1003 if ( isset( $finetune['user_provided_suffix'] ) ) {
1004 $finetune['suffix'] = $finetune['user_provided_suffix'];
1005 }
1006 else {
1007 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
1008 }
1009 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] ) . ' UTC';
1010 if ( isset( $finetune['estimated_finish'] ) ) {
1011 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] ) . ' UTC';
1012 }
1013 else {
1014 $finetune['estimatedOn'] = null;
1015 }
1016 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
1017 $finetune['base_model'] = $finetune['model'];
1018 $finetune['model'] = $finetune['fine_tuned_model'];
1019 unset( $finetune['object'] );
1020 unset( $finetune['hyperparams'] );
1021 unset( $finetune['result_files'] );
1022 unset( $finetune['training_files'] );
1023 unset( $finetune['validation_files'] );
1024 unset( $finetune['created_at'] );
1025 unset( $finetune['updated_at'] );
1026 unset( $finetune['fine_tuned_model'] );
1027 return $finetune;
1028 }, $finetunes);
1029
1030 usort( $finetunes, function ( $a, $b ) {
1031 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
1032 });
1033
1034 if ( $legacy ) {
1035 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
1036 }
1037 else {
1038 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
1039 }
1040
1041 return $finetunes;
1042 }
1043
1044 public function moderate( $input ) {
1045 $result = $this->execute('POST', '/moderations', [
1046 'input' => $input
1047 ]);
1048 return $result;
1049 }
1050
1051 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
1052 {
1053 $result = $this->execute('POST', '/files', null, [
1054 'purpose' => $purpose,
1055 'data' => $data,
1056 'file' => $filename
1057 ] );
1058 return $result;
1059 }
1060
1061 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
1062 $body = [
1063 'name' => !empty( $name ) ? $name : 'default',
1064 'metadata' => $metadata
1065 ];
1066 if ( $expiry !== 'never' ) {
1067 if ( is_string( $expiry ) ) {
1068 error_log( 'AI Engine: Expiry is a string, setting it to 7 days.' );
1069 $expiry = 7;
1070 }
1071 $expiryInDays = $expiry ? max( 1, ceil( (int)$expiry / 86400 ) ) : 7;
1072 if ( $expiry && is_numeric( $expiry ) ) {
1073 $body['expires_after'] = [
1074 'anchor' => 'last_active_at',
1075 'days' => $expiryInDays
1076 ];
1077 }
1078 }
1079 $result = $this->execute( 'POST', '/vector_stores', $body, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1080 return $result['id'];
1081 }
1082
1083 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1084 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1085 'file_id' => $fileId
1086 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1087 return $result['id'];
1088
1089 }
1090
1091 public function delete_file( $fileId )
1092 {
1093 return $this->execute( 'DELETE', '/files/' . $fileId );
1094 }
1095
1096 public function get_model( $modelId )
1097 {
1098 return $this->execute( 'GET', '/models/' . $modelId );
1099 }
1100
1101 public function cancel_finetune( $fineTuneId )
1102 {
1103 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1104 }
1105
1106 public function delete_finetune( $modelId )
1107 {
1108 return $this->execute( 'DELETE', '/models/' . $modelId );
1109 }
1110
1111 public function download_file( $fileId, $newFile = null ) {
1112 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1113 $fileInfo = json_decode( (string)$fileInfo, true );
1114 if ( empty( $fileInfo ) ) {
1115 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1116 }
1117 $filename = $fileInfo['filename'];
1118 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1119 if ( empty( $newFile ) ) {
1120 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1121 $tempFile = wp_tempnam( $filename );
1122 if ( !$tempFile ) {
1123 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1124 }
1125 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1126 $newFile = $tempFile . '.' . $extension;
1127 }
1128 else {
1129 $newFile = $tempFile;
1130 }
1131 }
1132 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1133 file_put_contents( $newFile, $data );
1134 return $newFile;
1135 }
1136
1137 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1138 {
1139 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1140 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1141 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1142 (float)$hyperparams['learningRateMultiplier'] : null;
1143 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1144 (float)$hyperparams['promptLossWeight'] : null;
1145 $arguments = [
1146 'training_file' => $fileId,
1147 'model' => $model,
1148 'suffix' => $suffix
1149 ];
1150 if ( $legacy ) {
1151 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1152 }
1153 else {
1154 if ( $n_epochs ) {
1155 $arguments['hyperparams'] = [];
1156 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1157 }
1158 if ( $batch_size ) {
1159 if ( empty( $arguments['hyperparams'] ) ) {
1160 $arguments['hyperparams'] = [];
1161 }
1162 $arguments['hyperparams']['batch_size'] = $batch_size;
1163 }
1164 if ( $learning_rate_multiplier ) {
1165 if ( empty( $arguments['hyperparams'] ) ) {
1166 $arguments['hyperparams'] = [];
1167 }
1168 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1169 }
1170 if ( $prompt_loss_weight ) {
1171 if ( empty( $arguments['hyperparams'] ) ) {
1172 $arguments['hyperparams'] = [];
1173 }
1174 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1175 }
1176 if ( $model === 'turbo' ) {
1177 $arguments['model'] = 'gpt-3.5-turbo';
1178 }
1179 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1180 }
1181 return $result;
1182 }
1183
1184 /**
1185 * Build the body of a form request.
1186 * If the field name is 'file', then the field value is the filename of the file to upload.
1187 * The file contents are taken from the 'data' field.
1188 *
1189 * @param array $fields
1190 * @param string $boundary
1191 * @return string
1192 */
1193 public function build_form_body( $fields, $boundary )
1194 {
1195 $body = '';
1196 foreach ( $fields as $name => $value ) {
1197 if ( $name == 'data' ) {
1198 continue;
1199 }
1200 $body .= "--$boundary\r\n";
1201 $body .= "Content-Disposition: form-data; name=\"$name\"";
1202 if ( $name == 'file' ) {
1203 $body .= "; filename=\"{$value}\"\r\n";
1204 $body .= "Content-Type: application/json\r\n\r\n";
1205 $body .= $fields['data'] . "\r\n";
1206 }
1207 else {
1208 $body .= "\r\n\r\n$value\r\n";
1209 }
1210 }
1211 $body .= "--$boundary--\r\n";
1212 return $body;
1213 }
1214
1215 /**
1216 * Run a request to the OpenAI API.
1217 * Fore more information about the $formFields, refer to the build_form_body method.
1218 *
1219 * @param string $method POST, PUT, GET, DELETE...
1220 * @param string $url The API endpoint
1221 * @param array $query The query parameters (json)
1222 * @param array $formFields The form fields (multipart/form-data)
1223 * @param bool $json Whether to return the response as json or not
1224 * @return array
1225 */
1226 public function execute( $method, $url, $query = null, $formFields = null,
1227 $json = true, $extraHeaders = null, $streamCallback = null )
1228 {
1229 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
1230 if ( $this->organizationId ) {
1231 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1232 }
1233 $body = $query ? json_encode( $query ) : null;
1234 if ( !empty( $formFields ) ) {
1235 $boundary = wp_generate_password( 24, false );
1236 $headers = [
1237 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
1238 'Authorization' => 'Bearer ' . $this->apiKey
1239 ];
1240 if ( $this->organizationId ) {
1241 $headers['OpenAI-Organization'] = $this->organizationId;
1242 }
1243 $body = $this->build_form_body( $formFields, $boundary );
1244 }
1245
1246 // Maybe we should have headers always as an array... not sure why we have it as a string.
1247 if ( !empty( $extraHeaders ) ) {
1248 foreach ( $extraHeaders as $key => $value ) {
1249 if ( is_array( $headers ) ) {
1250 $headers[$key] = $value;
1251 }
1252 else {
1253 $headers .= "$key: $value\r\n";
1254 }
1255 }
1256 }
1257
1258 // If it's a GET, body should be null, and we should append the query to the URL.
1259 if ( $method === 'GET' ) {
1260 if ( !empty( $query ) ) {
1261 $url .= '?' . http_build_query( $query );
1262 }
1263 $body = null;
1264 }
1265
1266 $url = 'https://api.openai.com/v1' . $url;
1267 $options = [
1268 "headers" => $headers,
1269 "method" => $method,
1270 "timeout" => MWAI_TIMEOUT,
1271 "body" => $body,
1272 "sslverify" => false
1273 ];
1274
1275 try {
1276 if ( !is_null( $streamCallback ) ) {
1277 $options['stream'] = true;
1278 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1279 // The stream handler calls the streamCallback every time there is content
1280 // TODO: For assistants, we should probably have a different stream handler to
1281 // handle the assistant's specific reply and perform the necessary actions.
1282 $this->streamCallback = $streamCallback;
1283 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1284 }
1285 $res = wp_remote_request( $url, $options );
1286 if ( is_wp_error( $res ) ) {
1287 throw new Exception( $res->get_error_message() );
1288 }
1289 $res = wp_remote_retrieve_body( $res );
1290 $data = $json ? json_decode( $res, true ) : $res;
1291 $this->handle_response_errors( $data );
1292 return $data;
1293 }
1294 catch ( Exception $e ) {
1295 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
1296 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
1297 }
1298 finally {
1299 if ( !is_null( $streamCallback ) ) {
1300 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1301 }
1302 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1303 unlink( $options['filename'] );
1304 }
1305 }
1306 }
1307
1308 public function get_models() {
1309 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1310 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1311 foreach ( $finetunes as $finetune ) {
1312 if ( $finetune['status'] !== 'succeeded' ) {
1313 continue;
1314 }
1315 $baseModel = SELF::get_model_without_release_date( $finetune['base_model'] );
1316 if ( !empty( $baseModel ) ) {
1317 $model = null;
1318 foreach ( $models as $currentModel ) {
1319 if ( $currentModel['model'] === $baseModel ) {
1320 $model = $currentModel;
1321 break;
1322 }
1323 }
1324 if ( !empty( $model ) ) {
1325 $model['model'] = $finetune['model'];
1326 $model['name'] = $finetune['suffix'];
1327 $models[] = $model;
1328 }
1329 }
1330 }
1331 return $models;
1332 }
1333
1334 static public function get_models_static() {
1335 return MWAI_OPENAI_MODELS;
1336 }
1337
1338 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false )
1339 {
1340 $modelFamily = SELF::get_model_without_release_date( $modelFamily );
1341 $models = $this->get_models();
1342 foreach ( $models as $currentModel ) {
1343 if ( $currentModel['model'] === $modelFamily ) {
1344 if ( $currentModel['type'] === 'image' ) {
1345 if ( !$resolution ) {
1346 Meow_MWAI_Logging::warn( "(OpenAI) Image models require a resolution." );
1347 return null;
1348 }
1349 else {
1350 foreach ( $currentModel['resolutions'] as $r ) {
1351 if ( $r['name'] == $resolution ) {
1352 return $r['price'] * $outUnits;
1353 }
1354 }
1355 }
1356 }
1357 else {
1358 if ( $finetune ) {
1359 if ( isset( $currentModel['finetune']['price'] ) ) {
1360 $currentModel['price'] = $currentModel['finetune']['price'];
1361 }
1362 else if ( isset( $currentModel['finetune']['in'] ) ) {
1363 $currentModel['price'] = [
1364 'in' => $currentModel['finetune']['in'],
1365 'out' => $currentModel['finetune']['out']
1366 ];
1367 }
1368 }
1369 $inPrice = $currentModel['price'];
1370 $outPrice = $currentModel['price'];
1371 if ( is_array( $currentModel['price'] ) ) {
1372 $inPrice = $currentModel['price']['in'];
1373 $outPrice = $currentModel['price']['out'];
1374 }
1375 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1376 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1377 return $inTotalPrice + $outTotalPrice;
1378 }
1379 }
1380 }
1381 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1382 return null;
1383 }
1384
1385 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1386 {
1387 $model = $query->model;
1388 $units = 0;
1389 $finetune = false;
1390 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1391 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1392 $finetune = true;
1393 }
1394 $inUnits = $reply->get_in_tokens( $query );
1395 $outUnits = $reply->get_out_tokens();
1396 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1397 }
1398 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1399 $units = $query->maxResults;
1400 $resolution = $query->resolution;
1401 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1402 }
1403 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1404 $model = 'whisper';
1405 $units = $reply->get_units();
1406 return $this->calculate_price( $model, 0, $units, null, $finetune );
1407 }
1408 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1409 $units = $reply->get_total_tokens();
1410 return $this->calculate_price( $model, 0, $units, null, $finetune );
1411 }
1412 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1413 return null;
1414 }
1415
1416 public function get_incidents() {
1417 $url = 'https://status.openai.com/history.rss';
1418 $response = wp_remote_get( $url );
1419 if ( is_wp_error( $response ) ) {
1420 throw new Exception( $response->get_error_message() );
1421 }
1422 $response = wp_remote_retrieve_body( $response );
1423 $xml = simplexml_load_string( $response );
1424 $incidents = array();
1425 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1426 foreach ( $xml->channel->item as $item ) {
1427 $date = strtotime( $item->pubDate );
1428 if ( $date > $oneWeekAgo ) {
1429 $incidents[] = array(
1430 'title' => (string) $item->title,
1431 'description' => (string) $item->description,
1432 'date' => $date
1433 );
1434 }
1435 }
1436 return $incidents;
1437 }
1438 }
1439