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