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