PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 2.5.6
AI Engine – The Chatbot, AI Framework & MCP for WordPress v2.5.6
3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
anthropic.php 1 year ago core.php 1 year ago factory.php 1 year ago google.php 1 year ago huggingface.php 1 year ago openai.php 1 year ago openrouter.php 1 year ago replicate.php 1 year ago
openai.php
1405 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 $remote_upload = $this->core->get_option( 'image_remote_upload' );
113 if ( $remote_upload === 'url' ) {
114 $finalUrl = $query->attachedFile->get_url();
115 }
116 else {
117 $finalUrl = $query->attachedFile->get_inline_base64_url();
118 }
119 $messages[] = [
120 'role' => 'user',
121 'content' => [
122 [
123 "type" => "text",
124 "text" => $query->get_message()
125 ],
126 [
127 "type" => "image_url",
128 "image_url" => [
129 "url" => $finalUrl
130 ]
131 ]
132 ]
133 ];
134 }
135 else {
136 $messages[] = [ 'role' => 'user', 'content' => $query->get_message() ];
137 }
138
139 return $messages;
140 }
141
142 protected function build_body( $query, $streamCallback = null, $extra = null ) {
143 if ( $query instanceof Meow_MWAI_Query_Text ) {
144 $body = array(
145 "model" => $query->model,
146 "n" => $query->maxResults,
147 "max_tokens" => $query->maxTokens,
148 "temperature" => $query->temperature,
149 "stream" => !is_null( $streamCallback ),
150 );
151
152 if ( !empty( $query->stop ) ) {
153 $body['stop'] = $query->stop;
154 }
155
156 if ( !empty( $query->responseFormat ) ) {
157 if ( $query->responseFormat === 'json' ) {
158 $body['response_format'] = [ 'type' => 'json_object' ];
159 }
160 }
161
162
163 // Usage Data (only for OpenAI)
164 // https://cookbook.openai.com/examples/how_to_stream_completions#4-how-to-get-token-usage-data-for-streamed-chat-completion-response
165 if ( !empty( $streamCallback ) && $this->envType === 'openai' ) {
166 $body['stream_options'] = [
167 'include_usage' => true,
168 ];
169 }
170
171 if ( !empty( $query->functions ) ) {
172 $model = $this->retrieve_model_info( $query->model );
173 if ( !empty( $model['tags'] ) && !in_array( 'functions', $model['tags'] ) ) {
174 Meow_MWAI_Logging::warn( 'The model ' . $query->model . ' doesn\'t support Function Calling.' );
175 }
176 else if ( strpos( $query->model, 'ft:' ) === 0 ) {
177 Meow_MWAI_Logging::warn( 'OpenAI doesn\'t support Function Calling with fine-tuned models yet.' );
178 }
179 else {
180 $body['tools'] = [];
181 // Dynamic function: they will interactively enhance the completion (tools).
182 foreach ( $query->functions as $function ) {
183 $body['tools'][] = [
184 'type' => 'function',
185 'function' => $function->serializeForOpenAI()
186 ];
187 }
188 // Static functions: they will be executed at the end of the completion.
189 //$body['function_call'] = $query->functionCall;
190 }
191 }
192 $body['messages'] = $this->build_messages( $query );
193
194 // Add the feedback if it's a feedback query.
195 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
196 if ( !empty( $query->blocks ) ) {
197 foreach ( $query->blocks as $feedback_block ) {
198 $body['messages'][] = $feedback_block['rawMessage'];
199 foreach ( $feedback_block['feedbacks'] as $feedback ) {
200 $body['messages'][] = [
201 'tool_call_id' => $feedback['request']['toolId'],
202 "role" => "tool",
203 'name' => $feedback['request']['name'],
204 'content' => $feedback['reply']['value']
205 ];
206 }
207 }
208 }
209 return $body;
210 }
211
212 return $body;
213 }
214 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
215 $body = array(
216 'prompt' => $query->message,
217 'model' => $query->model,
218 'response_format' => 'text',
219 'file' => basename( $query->url ),
220 'data' => $extra
221 );
222 return $body;
223 }
224 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
225 $body = array( 'input' => $query->message, 'model' => $query->model );
226 if ( $this->envType === 'azure' ) {
227 $body = array( "input" => $query->message );
228 }
229 // Dimensions are only supported by v3 models
230 if ( !empty( $query->dimensions ) && strpos( $query->model, 'ada-002' ) === false ) {
231 $body['dimensions'] = $query->dimensions;
232 }
233 return $body;
234 }
235 else if ( $query instanceof Meow_MWAI_Query_Image ) {
236 $model = $query->model;
237 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
238 $body = array(
239 "prompt" => $query->message,
240 "n" => $query->maxResults,
241 "size" => $resolution,
242 );
243 if ( $model === 'dall-e-3' ) {
244 $body['model'] = 'dall-e-3';
245 }
246 if ( $model === 'dall-e-3-hd' ) {
247 $body['model'] = 'dall-e-3';
248 $body['quality'] = 'hd';
249 }
250 if ( !empty( $query->style ) && strpos( $model, 'dall-e-3' ) === 0 ) {
251 $body['style'] = $query->style;
252 }
253 return $body;
254 }
255 }
256
257 protected function build_url( $query, $endpoint = null ) {
258 $url = "";
259 $env = $this->env;
260 // This endpoint is basically OpenAI or Azure, but in the case this class
261 // is overriden, we can pass the endpoint directly (for OpenRouter or HuggingFace, for example).
262 if ( empty( $endpoint ) ) {
263 if ( $this->envType === 'openai' ) {
264 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
265 $this->organizationId = isset( $env['organizationId'] ) ? $env['organizationId'] : null;
266 }
267 else if ( $this->envType === 'azure' ) {
268 $endpoint = isset( $env['endpoint'] ) ? $env['endpoint'] : null;
269 }
270 else {
271 if ( empty( $this->envType ) ) {
272 throw new Exception( 'Endpoint is not defined, and this envType is not known.' );
273 }
274 throw new Exception( 'Endpoint is not defined, and this envType is not known: ' . $this->envType );
275 }
276 }
277 // Add the base API to the URL
278 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
279 if ( $this->envType === 'azure' ) {
280 $deployment_name = $this->get_azure_deployment_name( $query->model );
281 $url = trailingslashit( $endpoint ) . 'openai/deployments/' . $deployment_name;
282 $url .= '/chat/completions?' . $this->azureApiVersion;
283 }
284 else {
285 $url .= trailingslashit( $endpoint ) . 'chat/completions';
286 }
287 return $url;
288 }
289 else if ( $query instanceof Meow_MWAI_Query_Transcribe ) {
290 $modeEndpoint = $query->feature === 'translation' ? 'translations' : 'transcriptions';
291 $url .= trailingslashit( $endpoint ) . 'audio/' . $modeEndpoint;
292 return $url;
293 }
294 else if ( $query instanceof Meow_MWAI_Query_Embed ) {
295 $url .= trailingslashit( $endpoint ) . 'embeddings';
296 if ( $this->envType === 'azure' ) {
297 $deployment_name = $this->get_azure_deployment_name( $query->model );
298 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
299 $deployment_name . '/embeddings?' . $this->azureApiVersion;
300 }
301 return $url;
302 }
303 else if ( $query instanceof Meow_MWAI_Query_Image ) {
304 $url .= trailingslashit( $endpoint ) . 'images/generations';
305 if ( $this->envType === 'azure' ) {
306 $deployment_name = $this->get_azure_deployment_name( $query->model );
307 $url = trailingslashit( $endpoint ) . 'openai/deployments/' .
308 $deployment_name . '/images/generations?' . $this->azureApiVersion;
309 }
310 return $url;
311 }
312 throw new Exception( 'The query is not supported by build_url().' );
313 }
314
315 protected function build_headers( $query ) {
316 if ( $query->apiKey ) {
317 $this->apiKey = $query->apiKey;
318 }
319 if ( empty( $this->apiKey ) ) {
320 throw new Exception( 'No API Key provided. Please visit the Settings.' );
321 }
322 $headers = array(
323 'Content-Type' => 'application/json',
324 'Authorization' => 'Bearer ' . $this->apiKey,
325 );
326 if ( $this->organizationId ) {
327 $headers['OpenAI-Organization'] = $this->organizationId;
328 }
329 if ( $this->envType === 'azure' ) {
330 $headers = array( 'Content-Type' => 'application/json', 'api-key' => $this->apiKey );
331 }
332 return $headers;
333 }
334
335 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
336 $body = null;
337 if ( !empty( $forms ) ) {
338 $boundary = wp_generate_password ( 24, false );
339 $headers['Content-Type'] = 'multipart/form-data; boundary=' . $boundary;
340 $body = $this->build_form_body( $forms, $boundary );
341 }
342 else if ( !empty( $json ) ) {
343 $body = json_encode( $json );
344 }
345 $options = array(
346 'headers' => $headers,
347 'method' => $method,
348 'timeout' => MWAI_TIMEOUT,
349 'body' => $body,
350 'sslverify' => false
351 );
352 return $options;
353 }
354 // object: "thread.message.delta"
355 protected function stream_data_handler( $json ) {
356 $content = null;
357
358 // Get additional data from the JSON
359 if ( isset( $json['model'] ) ) {
360 $this->inModel = $json['model'];
361 }
362 if ( isset( $json['id'] ) ) {
363 $this->inId = $json['id'];
364 }
365
366 $object = $json['object'] ?? null;
367
368 if ( $object === 'thread.run' ) {
369 $this->inThreadId = $json['thread_id'];
370 if ( $json['status'] === 'failed' ) {
371 $error = $json['last_error']['message'] ?? 'The run failed.';
372 throw new Exception( $error );
373 }
374 }
375 else if ( $object === 'thread.run.step.delta' ) {
376 if ( $json['delta']['step_details']['type'] === 'tool_calls' ) {
377 foreach ( $json['delta']['step_details']['tool_calls'] as $tool_call ) {
378 $index = $tool_call['index'] ?? null;
379 $currentStreamToolCall = null;
380 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
381 $currentStreamToolCall = &$this->streamToolCalls[$index];
382 }
383 else {
384 $this->streamToolCalls[] = [
385 'id' => null,
386 'type' => null,
387 'function' => [ 'name' => "", 'arguments' => "" ],
388 'code_interpreter' => [ 'input' => "", 'outputs' => [] ],
389 ];
390 end( $this->streamToolCalls );
391 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
392 }
393 if ( !empty( $tool_call['id'] ) ) {
394 $currentStreamToolCall['id'] = $tool_call['id'];
395 }
396 if ( !empty( $tool_call['type'] ) ) {
397 $currentStreamToolCall['type'] = $tool_call['type'];
398 }
399 if ( isset( $tool_call['function'] ) ) {
400 $function = $tool_call['function'];
401 if ( isset( $function['name'] ) ) {
402 $currentStreamToolCall['function']['name'] .= $function['name'];
403 }
404 if ( isset( $function['arguments'] ) ) {
405 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
406 }
407 }
408 if ( isset( $tool_call['code_interpreter'] ) ) {
409 $code_interpreter = $tool_call['code_interpreter'];
410 if ( isset( $code_interpreter['input'] ) ) {
411 $currentStreamToolCall['code_interpreter']['input'] .= $code_interpreter['input'];
412 }
413 if ( isset( $code_interpreter['outputs'] ) ) {
414 $currentStreamToolCall['code_interpreter']['outputs'] = $code_interpreter['outputs'];
415 }
416 }
417 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
418 }
419 }
420 }
421 else if ( $object === 'thread.message.delta' ) {
422 $delta = $json['delta']['content'][0] ?? null;
423 if ( $delta ) {
424 switch ( $delta['type'] ?? null ) {
425 case 'text':
426 if ( !empty( $delta['value'] ) && is_string( $delta['value'] ) ) {
427 $content = $delta['value'];
428 }
429 else if ( !empty( $delta['text'] ) && is_string( $delta['text'] ) ) {
430 $content = $delta['text'];
431 }
432 else if ( !empty( $delta['text'] ) && is_array( $delta['text'] ) ) {
433 $text = $delta['text'];
434 if ( !empty( $text['annotations'] ) ) {
435 $this->streamAnnotations = array_merge( $this->streamAnnotations, $text['annotations'] );
436 }
437 if ( !empty( $text['value'] ) ) {
438 $content = $text['value'];
439 }
440 }
441 else {
442 error_log( 'AI Engine: Unknown text format: ' . json_encode( $delta ) );
443 }
444 break;
445 case 'image':
446 $content = $delta['url'];
447 break;
448 case 'image_file':
449 $fileId = $delta['image_file']['file_id'];
450 $content = "<!-- IMG #" . $fileId . " -->";
451 $this->streamImageIds[] = $fileId;
452 break;
453 case 'function_call':
454 if ( empty( $this->streamFunctionCall ) ) {
455 $this->streamFunctionCall = [ 'name' => "", 'arguments' => [] ];
456 }
457 $this->streamFunctionCall['name'] = $delta['function_call']['name'] ?? $this->streamFunctionCall['name'];
458 if ( isset( $delta['function_call']['arguments'] ) ) {
459 $args = json_decode( $delta['function_call']['arguments'], true );
460 $this->streamFunctionCall['arguments'] = $args ?? [];
461 }
462 break;
463 case 'tool_call':
464 $tool_call = $delta['tool_call'];
465 $index = $tool_call['index'] ?? null;
466 $currentStreamToolCall = null;
467 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
468 $currentStreamToolCall = &$this->streamToolCalls[$index];
469 }
470 else {
471 $this->streamToolCalls[] = [
472 'id' => null,
473 'type' => null,
474 'function' => [ 'name' => "", 'arguments' => "" ]
475 ];
476 end( $this->streamToolCalls );
477 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
478 }
479 break;
480 }
481 }
482 }
483 else if ( $object === 'thread.run.step' ) {
484 //$type = $json['step'];
485 // Could be tool_calls, means an OpenAI Assistant is doing something.
486 }
487 else {
488 if ( isset( $json['choices'][0]['text'] ) ) {
489 $content = $json['choices'][0]['text'];
490 }
491 else if ( isset( $json['choices'][0]['delta']['content'] ) ) {
492 $content = $json['choices'][0]['delta']['content'];
493 }
494 else if ( isset( $json['choices'][0]['delta']['function_call'] ) ) {
495 if ( empty( $this->streamFunctionCall ) ) {
496 $this->streamFunctionCall = [ 'name' => "", 'arguments' => [] ];
497 }
498 $this->streamFunctionCall['name'] = $json['choices'][0]['delta']['function_call']['name'] ?? $this->streamFunctionCall['name'];
499 if ( isset( $json['choices'][0]['delta']['function_call']['arguments'] ) ) {
500 $args = json_decode( $json['choices'][0]['delta']['function_call']['arguments'], true );
501 $this->streamFunctionCall['arguments'] = $args ?? [];
502 }
503 }
504 else if ( isset( $json['choices'][0]['delta']['tool_calls'] ) ) {
505 foreach ( $json['choices'][0]['delta']['tool_calls'] as $tool_call ) {
506 $index = $tool_call['index'] ?? null;
507 $currentStreamToolCall = null;
508 if ( $index !== null && isset( $this->streamToolCalls[$index] ) ) {
509 $currentStreamToolCall = &$this->streamToolCalls[$index];
510 }
511 else {
512 $this->streamToolCalls[] = [
513 'id' => null,
514 'type' => null,
515 'function' => [ 'name' => "", 'arguments' => "" ]
516 ];
517 end( $this->streamToolCalls );
518 $currentStreamToolCall = &$this->streamToolCalls[ key( $this->streamToolCalls ) ];
519 }
520 if ( !empty( $tool_call['id'] ) ) {
521 $currentStreamToolCall['id'] = $tool_call['id'];
522 }
523 if ( !empty( $tool_call['type'] ) ) {
524 $currentStreamToolCall['type'] = $tool_call['type'];
525 }
526 if ( isset( $tool_call['function'] ) ) {
527 $function = $tool_call['function'];
528 if ( isset( $function['name'] ) ) {
529 $currentStreamToolCall['function']['name'] .= $function['name'];
530 }
531 if ( isset( $function['arguments'] ) ) {
532 $currentStreamToolCall['function']['arguments'] .= $function['arguments'];
533 }
534 }
535 $this->streamLastMessage['tool_calls'] = $this->streamToolCalls;
536 }
537 }
538 else if ( isset( $json['choices'][0]['delta']['role'] ) ) {
539 $this->streamLastMessage = [
540 'role' => $json['choices'][0]['delta']['role'],
541 'content' => null
542 ];
543 }
544 }
545
546 if ( !empty( $json['usage'] ) && isset( $json['usage']['prompt_tokens'] )
547 && isset( $json['usage']['completion_tokens'] ) ) {
548 $this->streamInTokens = $json['usage']['prompt_tokens'];
549 $this->streamOutTokens = $json['usage']['completion_tokens'];
550 }
551
552 // If content is an array, let's try to convert it into a string. Normally, there would be a 'value' key.
553 if ( is_array( $content ) ) {
554 if ( isset( $content['value'] ) ) {
555 $content = $content['value'];
556 }
557 else {
558 throw new Exception( 'AI Engine: Could not read this: ' . json_encode( $content ) );
559 }
560 }
561
562 // Avoid some endings
563 $endings = [ "", "</s>" ];
564 if ( in_array( $content, $endings ) ) {
565 $content = null;
566 }
567
568 return ( $content === '0' || !empty( $content ) ) ? $content : null;
569 }
570
571 public function run_query( $url, $options, $isStream = false ) {
572 try {
573 $options['stream'] = $isStream;
574 if ( $isStream ) {
575 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
576 }
577 $res = wp_remote_get( $url, $options );
578
579 if ( is_wp_error( $res ) ) {
580 throw new Exception( $res->get_error_message() );
581 }
582
583 $responseCode = wp_remote_retrieve_response_code( $res );
584 if ( $responseCode === 404 ) {
585 throw new Exception( 'The model\'s API URL was not found: ' . $url );
586 }
587 if ( $responseCode === 400 ) {
588 $message = wp_remote_retrieve_body( $res );
589 if ( empty( $message ) ) {
590 $message = wp_remote_retrieve_response_message( $res );
591 }
592 if ( empty( $message ) ) {
593 $message = 'Bad Request';
594 }
595 throw new Exception( $message );
596 }
597
598 if ( $isStream ) {
599 return [ 'stream' => true ];
600 }
601
602 $response = wp_remote_retrieve_body( $res );
603 $headersRes = wp_remote_retrieve_headers( $res );
604 $headers = $headersRes->getAll();
605
606 // Check if Content-Type is 'multipart/form-data' or 'text/plain'
607 // If so, we don't need to decode the response
608 $normalizedHeaders = array_change_key_case( $headers, CASE_LOWER );
609 $resContentType = $normalizedHeaders['content-type'] ?? '';
610 if ( strpos( $resContentType, 'multipart/form-data' ) !== false || strpos( $resContentType, 'text/plain' ) !== false ) {
611 return [ 'stream' => false, 'headers' => $headers, 'data' => $response ];
612 }
613
614 $data = json_decode( $response, true );
615 $this->handle_response_errors( $data );
616 return [ 'headers' => $headers, 'data' => $data ];
617 }
618 catch ( Exception $e ) {
619 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
620 throw $e;
621 }
622 finally {
623 if ( $isStream && file_exists( $options['filename'] ) ) {
624 unlink( $options['filename'] );
625 }
626 }
627 }
628
629 private function get_audio( $url ) {
630 require_once( ABSPATH . 'wp-admin/includes/media.php' );
631 $tmpFile = tempnam( sys_get_temp_dir(), 'audio_' );
632 file_put_contents( $tmpFile, file_get_contents( $url ) );
633 $length = null;
634 $metadata = wp_read_audio_metadata( $tmpFile );
635 if ( isset( $metadata['length'] ) ) {
636 $length = $metadata['length'];
637 }
638 $data = file_get_contents( $tmpFile );
639 unlink( $tmpFile );
640 return [ 'data' => $data, 'length' => $length ];
641 }
642
643 public function run_transcribe_query( $query ) {
644 // Check if the URL is valid.
645 if ( !filter_var( $query->url, FILTER_VALIDATE_URL ) ) {
646 throw new Exception( 'Invalid URL for transcription.' );
647 }
648
649 $audioData = $this->get_audio( $query->url );
650 $body = $this->build_body( $query, null, $audioData['data'] );
651 $url = $this->build_url( $query );
652 $headers = $this->build_headers( $query );
653 $options = $this->build_options( $headers, null, $body );
654
655 // Perform the request
656 try {
657 $res = $this->run_query( $url, $options );
658 $data = $res['data'];
659 if ( empty( $data ) ) {
660 throw new Exception( 'Invalid data for transcription.' );
661 }
662 $usage = $this->core->record_audio_usage( $query->model, $audioData['length'] );
663 $reply = new Meow_MWAI_Reply( $query );
664 $reply->set_usage( $usage );
665 $reply->set_choices( $data );
666 return $reply;
667 }
668 catch ( Exception $e ) {
669 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
670 $service = $this->get_service_name();
671 throw new Exception( "From $service: " . $e->getMessage() );
672 }
673 }
674
675 public function run_embedding_query( $query ) {
676 $body = $this->build_body( $query );
677 $url = $this->build_url( $query );
678 $headers = $this->build_headers( $query );
679 $options = $this->build_options( $headers, $body );
680
681 try {
682 $res = $this->run_query( $url, $options );
683 $data = $res['data'];
684 if ( empty( $data ) || !isset( $data['data'] ) ) {
685 throw new Exception( 'Invalid data for embedding.' );
686 }
687 $usage = $data['usage'];
688 $this->core->record_tokens_usage( $query->model, $usage['prompt_tokens'] );
689 $reply = new Meow_MWAI_Reply( $query );
690 $reply->set_usage( $usage );
691 $reply->set_choices( $data['data'] );
692 return $reply;
693 }
694 catch ( Exception $e ) {
695 $message = $e->getMessage();
696 $error = $this->try_decode_error( $message );
697 if ( !is_null( $error ) ) {
698 $message = $error;
699 }
700 Meow_MWAI_Logging::error( 'OpenAI: ' . $message );
701 $service = $this->get_service_name();
702 throw new Exception( "From $service: " . $message );
703 }
704 }
705
706 public function try_decode_error( $data ) {
707 $json = json_decode( $data, true );
708 if ( isset( $json['error']['message'] ) ) {
709 return $json['error']['message'];
710 }
711 return null;
712 }
713
714 public function run_completion_query( $query, $streamCallback = null ) : Meow_MWAI_Reply {
715 $isStreaming = !is_null( $streamCallback );
716
717 if ( $isStreaming ) {
718 $this->streamCallback = $streamCallback;
719 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
720 }
721
722 $this->reset_stream();
723 $body = $this->build_body( $query, $streamCallback );
724 $url = $this->build_url( $query );
725 $headers = $this->build_headers( $query );
726 $options = $this->build_options( $headers, $body );
727
728 try {
729 $res = $this->run_query( $url, $options, $streamCallback );
730 $reply = new Meow_MWAI_Reply( $query );
731
732 $returned_id = null;
733 $returned_model = $this->inModel;
734 $returned_in_tokens = null;
735 $returned_out_tokens = null;
736 $returned_price = null;
737 $returned_choices = [];
738
739 // Streaming Mode
740 if ( $isStreaming ) {
741 if ( empty( $this->streamContent ) ) {
742 $error = $this->try_decode_error( $this->streamBuffer );
743 if ( !is_null( $error ) ) {
744 throw new Exception( $error );
745 }
746 }
747 $returned_id = $this->inId;
748 $returned_model = $this->inModel ? $this->inModel : $query->model;
749 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
750 if ( !empty( $this->streamFunctionCall ) ) {
751 $message['function_call'] = $this->streamFunctionCall;
752 }
753 if ( !empty( $this->streamToolCalls ) ) {
754 $message['tool_calls'] = $this->streamToolCalls;
755 }
756 if ( !is_null( $this->streamInTokens ) ) {
757 $returned_in_tokens = $this->streamInTokens;
758 }
759 if ( !is_null( $this->streamOutTokens ) ) {
760 $returned_out_tokens = $this->streamOutTokens;
761 }
762 $returned_choices = [ [ 'message' => $message ] ];
763 }
764 // Standard Mode
765 else {
766 $data = $res['data'];
767 if ( empty( $data ) ) {
768 throw new Exception( 'No content received (res is null).' );
769 }
770 if ( !$data['model'] ) {
771 Meow_MWAI_Logging::error( 'OpenAI: Invalid response (no model information):' );
772 Meow_MWAI_Logging::error( print_r( $data, 1 ) );
773 throw new Exception( 'Invalid response (no model information).' );
774 }
775 $returned_id = $data['id'];
776 $returned_model = $data['model'];
777 $returned_in_tokens = isset( $data['usage']['prompt_tokens'] ) ?
778 $data['usage']['prompt_tokens'] : null;
779 $returned_out_tokens = isset( $data['usage']['completion_tokens'] ) ?
780 $data['usage']['completion_tokens'] : null;
781 $returned_price = isset( $data['usage']['total_cost'] ) ?
782 $data['usage']['total_cost'] : null;
783 $returned_choices = $data['choices'];
784 }
785
786 // Set the results.
787 $reply->set_choices( $returned_choices );
788 if ( !empty( $returned_id ) ) {
789 $reply->set_id( $returned_id );
790 }
791 if ( !empty( $returned_id ) ) {
792 $reply->set_id( $returned_id );
793 }
794
795 // Handle tokens.
796 $this->handle_tokens_usage( $reply, $query, $returned_model,
797 $returned_in_tokens, $returned_out_tokens, $returned_price
798 );
799
800 return $reply;
801 }
802 catch ( Exception $e ) {
803 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
804 $service = $this->get_service_name();
805 $message = "From $service: " . $e->getMessage();
806 throw new Exception( $message );
807 }
808 finally {
809 if ( !is_null( $streamCallback ) ) {
810 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
811 }
812 }
813 }
814
815 public function handle_tokens_usage( $reply, $query, $returned_model,
816 $returned_in_tokens, $returned_out_tokens, $returned_price = null ) {
817 $returned_in_tokens = !is_null( $returned_in_tokens ) ? $returned_in_tokens :
818 $reply->get_in_tokens( $query );
819 $returned_out_tokens = !is_null( $returned_out_tokens ) ? $returned_out_tokens :
820 $reply->get_out_tokens();
821 $returned_price = !is_null( $returned_price ) ? $returned_price :
822 $reply->get_price();
823 $usage = $this->core->record_tokens_usage(
824 $returned_model,
825 $returned_in_tokens,
826 $returned_out_tokens,
827 $returned_price
828 );
829 $reply->set_usage( $usage );
830 }
831
832 // Request to DALL-E API
833 public function run_image_query( $query ) {
834 $body = $this->build_body( $query );
835 $url = $this->build_url( $query );
836 $headers = $this->build_headers( $query );
837 $options = $this->build_options( $headers, $body );
838
839 try {
840 $res = $this->run_query( $url, $options );
841 $data = $res['data'];
842 $choices = [];
843 if ( $this->envType === 'azure' ) {
844 foreach ( $data['data'] as $entry ) {
845 $choices[] = [ 'url' => $entry['url'] ];
846 }
847 }
848 else {
849 $choices = $data['data'];
850 }
851
852 $reply = new Meow_MWAI_Reply( $query );
853 $model = $query->model;
854 $resolution = !empty( $query->resolution ) ? $query->resolution : '1024x1024';
855 $usage = $this->core->record_images_usage( $model, $resolution, $query->maxResults );
856 $reply->set_usage( $usage );
857 $reply->set_choices( $choices );
858 $reply->set_type( 'images' );
859
860 if ( $query->localDownload === 'uploads' || $query->localDownload === 'library' ) {
861 foreach ( $reply->results as &$result ) {
862 $fileId = $this->core->files->upload_file( $result, null, 'generated', [
863 'query_envId' => $query->envId,
864 'query_session' => $query->session,
865 'query_model' => $query->model,
866 ], $query->envId, $query->localDownload, $query->localDownloadExpiry );
867 $fileUrl = $this->core->files->get_url( $fileId );
868 $result = $fileUrl;
869 }
870 }
871 $reply->result = $reply->results[0];
872 return $reply;
873 }
874 catch ( Exception $e ) {
875 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
876 $service = $this->get_service_name();
877 throw new Exception( "From $service: " . $e->getMessage() );
878 }
879 }
880
881 /*
882 This is the rest of the OpenAI API support, not related to the models directly.
883 */
884
885 // Check if there are errors in the response from OpenAI, and throw an exception if so.
886 protected function handle_response_errors( $data ) {
887 if ( isset( $data['error'] ) && !empty( $data['error'] ) ) {
888 $message = $data['error']['message'];
889 if ( preg_match( '/API key provided(: .*)\./', $message, $matches ) ) {
890 $message = str_replace( $matches[1], '', $message );
891 }
892 throw new Exception( $message );
893 }
894 }
895
896 public function list_files( $purposeFilter = null )
897 {
898 if ( empty( $purposeFilter ) ) {
899 return $this->execute( 'GET', '/files' );
900 }
901 return $this->execute( 'GET', '/files', [ 'purpose' => $purposeFilter ] );
902 }
903
904 static function get_suffix_for_model($model)
905 {
906 // Legacy fine-tuned models
907 preg_match( "/:([a-zA-Z0-9\-]{1,40})-([0-9]{4})-([0-9]{2})-([0-9]{2})/", $model, $matches);
908 if ( count( $matches ) > 0 ) {
909 return $matches[1];
910 }
911
912 // New fine-tuned models
913 preg_match("/:([^:]+)(?=:[^:]+$)/", $model, $matches);
914 if (count($matches) > 0) {
915 return $matches[1];
916 }
917
918 return 'N/A';
919 }
920
921 static function get_model_without_release_date( $model )
922 {
923 if ( empty( $model ) ) {
924 return null;
925 }
926 return preg_replace( '/-\d{4}-\d{2}-\d{2}$/', '', $model );
927 }
928
929 public function list_deleted_finetunes( $envId = null, $legacy = false )
930 {
931 $finetunes = $this->list_finetunes( $legacy );
932 $deleted = [];
933
934 foreach ( $finetunes as $finetune ) {
935 $name = $finetune['model'];
936 $isSucceeded = $finetune['status'] === 'succeeded';
937 if ( $isSucceeded ) {
938 try {
939 $finetune = $this->get_model( $name );
940 }
941 catch ( Exception $e ) {
942 $deleted[] = $name;
943 }
944 }
945 }
946 if ( $legacy ) {
947 $this->core->update_ai_env( $this->envId, 'legacy_finetunes_deleted', $deleted );
948 }
949 else {
950 $this->core->update_ai_env( $this->envId, 'finetunes_deleted', $deleted );
951 }
952 return $deleted;
953 }
954
955 // TODO: This was used to retrieve the fine-tuned models, but not sure this is how we should
956 // retrieve all the models since Summer 2023, let's see! WIP.
957 public function list_finetunes( $legacy = false )
958 {
959 if ( $legacy ) {
960 $res = $this->execute( 'GET', '/fine-tunes' );
961 }
962 else {
963 $res = $this->execute( 'GET', '/fine_tuning/jobs' );
964 }
965 $finetunes = $res['data'];
966
967 // Add suffix
968 $finetunes = array_map( function ( $finetune ) {
969 if ( isset( $finetune['user_provided_suffix'] ) ) {
970 $finetune['suffix'] = $finetune['user_provided_suffix'];
971 }
972 else {
973 $finetune['suffix'] = SELF::get_suffix_for_model( $finetune['fine_tuned_model'] );
974 }
975 $finetune['createdOn'] = date( 'Y-m-d H:i:s', $finetune['created_at'] ) . ' UTC';
976 if ( isset( $finetune['estimated_finish'] ) ) {
977 $finetune['estimatedOn'] = date( 'Y-m-d H:i:s', $finetune['estimated_finish'] ) . ' UTC';
978 }
979 else {
980 $finetune['estimatedOn'] = null;
981 }
982 //$finetune['updatedOn'] = date( 'Y-m-d H:i:s', $finetune['updated_at'] );
983 $finetune['base_model'] = $finetune['model'];
984 $finetune['model'] = $finetune['fine_tuned_model'];
985 unset( $finetune['object'] );
986 unset( $finetune['hyperparams'] );
987 unset( $finetune['result_files'] );
988 unset( $finetune['training_files'] );
989 unset( $finetune['validation_files'] );
990 unset( $finetune['created_at'] );
991 unset( $finetune['updated_at'] );
992 unset( $finetune['fine_tuned_model'] );
993 return $finetune;
994 }, $finetunes);
995
996 usort( $finetunes, function ( $a, $b ) {
997 return strtotime( $b['createdOn'] ) - strtotime( $a['createdOn'] );
998 });
999
1000 if ( $legacy ) {
1001 $this->core->update_ai_env( $this->envId, 'legacy_finetunes', $finetunes );
1002 }
1003 else {
1004 $this->core->update_ai_env( $this->envId, 'finetunes', $finetunes );
1005 }
1006
1007 return $finetunes;
1008 }
1009
1010 public function moderate( $input ) {
1011 $result = $this->execute('POST', '/moderations', [
1012 'input' => $input
1013 ]);
1014 return $result;
1015 }
1016
1017 public function upload_file( $filename, $data, $purpose = 'fine-tune' )
1018 {
1019 $result = $this->execute('POST', '/files', null, [
1020 'purpose' => $purpose,
1021 'data' => $data,
1022 'file' => $filename
1023 ] );
1024 return $result;
1025 }
1026
1027 public function create_vector_store( $name = null, $expiry = null, $metadata = null ) {
1028 $body = [
1029 'name' => !empty( $name ) ? $name : 'default',
1030 'metadata' => $metadata
1031 ];
1032 if ( $expiry !== 'never' ) {
1033 if ( is_string( $expiry ) ) {
1034 error_log( 'AI Engine: Expiry is a string, setting it to 7 days.' );
1035 $expiry = 7;
1036 }
1037 $expiryInDays = $expiry ? max( 1, ceil( (int)$expiry / 86400 ) ) : 7;
1038 if ( $expiry && is_numeric( $expiry ) ) {
1039 $body['expires_after'] = [
1040 'anchor' => 'last_active_at',
1041 'days' => $expiryInDays
1042 ];
1043 }
1044 }
1045 $result = $this->execute( 'POST', '/vector_stores', $body, null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1046 return $result['id'];
1047 }
1048
1049 public function add_vector_store_file( $vectorStoreId, $fileId ) {
1050 $result = $this->execute( 'POST', '/vector_stores/' . $vectorStoreId . '/files', [
1051 'file_id' => $fileId
1052 ], null, true, [ 'OpenAI-Beta' => 'assistants=v2' ] );
1053 return $result['id'];
1054
1055 }
1056
1057 public function delete_file( $fileId )
1058 {
1059 return $this->execute( 'DELETE', '/files/' . $fileId );
1060 }
1061
1062 public function get_model( $modelId )
1063 {
1064 return $this->execute( 'GET', '/models/' . $modelId );
1065 }
1066
1067 public function cancel_finetune( $fineTuneId )
1068 {
1069 return $this->execute( 'POST', '/fine-tunes/' . $fineTuneId . '/cancel' );
1070 }
1071
1072 public function delete_finetune( $modelId )
1073 {
1074 return $this->execute( 'DELETE', '/models/' . $modelId );
1075 }
1076
1077 public function download_file( $fileId, $newFile = null ) {
1078 $fileInfo = $this->execute( 'GET', '/files/' . $fileId, null, null, false );
1079 $fileInfo = json_decode( (string)$fileInfo, true );
1080 if ( empty( $fileInfo ) ) {
1081 throw new Exception( 'AI Engine: File (' . ( $fileId ?? 'N/A' ) . ') not found.' );
1082 }
1083 $filename = $fileInfo['filename'];
1084 $extension = pathinfo( $filename, PATHINFO_EXTENSION );
1085 if ( empty( $newFile ) ) {
1086 include_once( ABSPATH . 'wp-admin/includes/file.php' );
1087 $tempFile = wp_tempnam( $filename );
1088 if ( !$tempFile ) {
1089 $tempFile = tempnam( sys_get_temp_dir(), 'download_' );
1090 }
1091 if ( pathinfo( $tempFile, PATHINFO_EXTENSION ) != $extension ) {
1092 $newFile = $tempFile . '.' . $extension;
1093 }
1094 else {
1095 $newFile = $tempFile;
1096 }
1097 }
1098 $data = $this->execute( 'GET', '/files/' . $fileId . '/content', null, null, false );
1099 file_put_contents( $newFile, $data );
1100 return $newFile;
1101 }
1102
1103 public function run_finetune( $fileId, $model, $suffix, $hyperparams = [], $legacy = false )
1104 {
1105 $n_epochs = isset( $hyperparams['nEpochs'] ) ? (int)$hyperparams['nEpochs'] : null;
1106 $batch_size = isset( $hyperparams['batchSize'] ) ? (int)$hyperparams['batchSize'] : null;
1107 $learning_rate_multiplier = isset( $hyperparams['learningRateMultiplier'] ) ?
1108 (float)$hyperparams['learningRateMultiplier'] : null;
1109 $prompt_loss_weight = isset( $hyperparams['promptLossWeight'] ) ?
1110 (float)$hyperparams['promptLossWeight'] : null;
1111 $arguments = [
1112 'training_file' => $fileId,
1113 'model' => $model,
1114 'suffix' => $suffix
1115 ];
1116 if ( $legacy ) {
1117 $result = $this->execute( 'POST', '/fine-tunes', $arguments );
1118 }
1119 else {
1120 if ( $n_epochs ) {
1121 $arguments['hyperparams'] = [];
1122 $arguments['hyperparams']['n_epochs'] = $n_epochs;
1123 }
1124 if ( $batch_size ) {
1125 if ( empty( $arguments['hyperparams'] ) ) {
1126 $arguments['hyperparams'] = [];
1127 }
1128 $arguments['hyperparams']['batch_size'] = $batch_size;
1129 }
1130 if ( $learning_rate_multiplier ) {
1131 if ( empty( $arguments['hyperparams'] ) ) {
1132 $arguments['hyperparams'] = [];
1133 }
1134 $arguments['hyperparams']['learning_rate_multiplier'] = $learning_rate_multiplier;
1135 }
1136 if ( $prompt_loss_weight ) {
1137 if ( empty( $arguments['hyperparams'] ) ) {
1138 $arguments['hyperparams'] = [];
1139 }
1140 $arguments['hyperparams']['prompt_loss_weight'] = $prompt_loss_weight;
1141 }
1142 if ( $model === 'turbo' ) {
1143 $arguments['model'] = 'gpt-3.5-turbo';
1144 }
1145 $result = $this->execute( 'POST', '/fine_tuning/jobs', $arguments );
1146 }
1147 return $result;
1148 }
1149
1150 /**
1151 * Build the body of a form request.
1152 * If the field name is 'file', then the field value is the filename of the file to upload.
1153 * The file contents are taken from the 'data' field.
1154 *
1155 * @param array $fields
1156 * @param string $boundary
1157 * @return string
1158 */
1159 public function build_form_body( $fields, $boundary )
1160 {
1161 $body = '';
1162 foreach ( $fields as $name => $value ) {
1163 if ( $name == 'data' ) {
1164 continue;
1165 }
1166 $body .= "--$boundary\r\n";
1167 $body .= "Content-Disposition: form-data; name=\"$name\"";
1168 if ( $name == 'file' ) {
1169 $body .= "; filename=\"{$value}\"\r\n";
1170 $body .= "Content-Type: application/json\r\n\r\n";
1171 $body .= $fields['data'] . "\r\n";
1172 }
1173 else {
1174 $body .= "\r\n\r\n$value\r\n";
1175 }
1176 }
1177 $body .= "--$boundary--\r\n";
1178 return $body;
1179 }
1180
1181 /**
1182 * Run a request to the OpenAI API.
1183 * Fore more information about the $formFields, refer to the build_form_body method.
1184 *
1185 * @param string $method POST, PUT, GET, DELETE...
1186 * @param string $url The API endpoint
1187 * @param array $query The query parameters (json)
1188 * @param array $formFields The form fields (multipart/form-data)
1189 * @param bool $json Whether to return the response as json or not
1190 * @return array
1191 */
1192 public function execute( $method, $url, $query = null, $formFields = null,
1193 $json = true, $extraHeaders = null, $streamCallback = null )
1194 {
1195 $headers = "Content-Type: application/json\r\n" . "Authorization: Bearer " . $this->apiKey . "\r\n";
1196 if ( $this->organizationId ) {
1197 $headers .= "OpenAI-Organization: " . $this->organizationId . "\r\n";
1198 }
1199 $body = $query ? json_encode( $query ) : null;
1200 if ( !empty( $formFields ) ) {
1201 $boundary = wp_generate_password( 24, false );
1202 $headers = [
1203 'Content-Type' => 'multipart/form-data; boundary=' . $boundary,
1204 'Authorization' => 'Bearer ' . $this->apiKey
1205 ];
1206 if ( $this->organizationId ) {
1207 $headers['OpenAI-Organization'] = $this->organizationId;
1208 }
1209 $body = $this->build_form_body( $formFields, $boundary );
1210 }
1211
1212 // Maybe we should have headers always as an array... not sure why we have it as a string.
1213 if ( !empty( $extraHeaders ) ) {
1214 foreach ( $extraHeaders as $key => $value ) {
1215 if ( is_array( $headers ) ) {
1216 $headers[$key] = $value;
1217 }
1218 else {
1219 $headers .= "$key: $value\r\n";
1220 }
1221 }
1222 }
1223
1224 // If it's a GET, body should be null, and we should append the query to the URL.
1225 if ( $method === 'GET' ) {
1226 if ( !empty( $query ) ) {
1227 $url .= '?' . http_build_query( $query );
1228 }
1229 $body = null;
1230 }
1231
1232 $url = 'https://api.openai.com/v1' . $url;
1233 $options = [
1234 "headers" => $headers,
1235 "method" => $method,
1236 "timeout" => MWAI_TIMEOUT,
1237 "body" => $body,
1238 "sslverify" => false
1239 ];
1240
1241 try {
1242 if ( !is_null( $streamCallback ) ) {
1243 $options['stream'] = true;
1244 $options['filename'] = tempnam( sys_get_temp_dir(), 'mwai-stream-' );
1245 // The stream handler calls the streamCallback every time there is content
1246 // TODO: For assistants, we should probably have a different stream handler to
1247 // handle the assistant's specific reply and perform the necessary actions.
1248 $this->streamCallback = $streamCallback;
1249 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1250 }
1251 $res = wp_remote_request( $url, $options );
1252 if ( is_wp_error( $res ) ) {
1253 throw new Exception( $res->get_error_message() );
1254 }
1255 $res = wp_remote_retrieve_body( $res );
1256 $data = $json ? json_decode( $res, true ) : $res;
1257 $this->handle_response_errors( $data );
1258 return $data;
1259 }
1260 catch ( Exception $e ) {
1261 Meow_MWAI_Logging::error( 'OpenAI: ' . $e->getMessage() );
1262 throw new Exception( 'From OpenAI: ' . $e->getMessage() );
1263 }
1264 finally {
1265 if ( !is_null( $streamCallback ) ) {
1266 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1267 }
1268 if ( !empty( $options['stream'] ) && file_exists( $options['filename'] ) ) {
1269 unlink( $options['filename'] );
1270 }
1271 }
1272 }
1273
1274 public function get_models() {
1275 $models = apply_filters( 'mwai_openai_models', MWAI_OPENAI_MODELS );
1276 $finetunes = !empty( $this->env['finetunes'] ) ? $this->env['finetunes'] : [];
1277 foreach ( $finetunes as $finetune ) {
1278 if ( $finetune['status'] !== 'succeeded' ) {
1279 continue;
1280 }
1281 $baseModel = SELF::get_model_without_release_date( $finetune['base_model'] );
1282 if ( !empty( $baseModel ) ) {
1283 $model = null;
1284 foreach ( $models as $currentModel ) {
1285 if ( $currentModel['model'] === $baseModel ) {
1286 $model = $currentModel;
1287 break;
1288 }
1289 }
1290 if ( !empty( $model ) ) {
1291 $model['model'] = $finetune['model'];
1292 $model['name'] = $finetune['suffix'];
1293 $models[] = $model;
1294 }
1295 }
1296 }
1297 return $models;
1298 }
1299
1300 static public function get_models_static() {
1301 return MWAI_OPENAI_MODELS;
1302 }
1303
1304 private function calculate_price( $modelFamily, $inUnits, $outUnits, $resolution = null, $finetune = false )
1305 {
1306 $modelFamily = SELF::get_model_without_release_date( $modelFamily );
1307 $models = $this->get_models();
1308 foreach ( $models as $currentModel ) {
1309 if ( $currentModel['model'] === $modelFamily ) {
1310 if ( $currentModel['type'] === 'image' ) {
1311 if ( !$resolution ) {
1312 Meow_MWAI_Logging::warn( "(OpenAI) Image models require a resolution." );
1313 return null;
1314 }
1315 else {
1316 foreach ( $currentModel['resolutions'] as $r ) {
1317 if ( $r['name'] == $resolution ) {
1318 return $r['price'] * $outUnits;
1319 }
1320 }
1321 }
1322 }
1323 else {
1324 if ( $finetune ) {
1325 if ( isset( $currentModel['finetune']['price'] ) ) {
1326 $currentModel['price'] = $currentModel['finetune']['price'];
1327 }
1328 else if ( isset( $currentModel['finetune']['in'] ) ) {
1329 $currentModel['price'] = [
1330 'in' => $currentModel['finetune']['in'],
1331 'out' => $currentModel['finetune']['out']
1332 ];
1333 }
1334 }
1335 $inPrice = $currentModel['price'];
1336 $outPrice = $currentModel['price'];
1337 if ( is_array( $currentModel['price'] ) ) {
1338 $inPrice = $currentModel['price']['in'];
1339 $outPrice = $currentModel['price']['out'];
1340 }
1341 $inTotalPrice = $inPrice * $currentModel['unit'] * $inUnits;
1342 $outTotalPrice = $outPrice * $currentModel['unit'] * $outUnits;
1343 return $inTotalPrice + $outTotalPrice;
1344 }
1345 }
1346 }
1347 Meow_MWAI_Logging::warn( "(OpenAI) Invalid model ($modelFamily)." );
1348 return null;
1349 }
1350
1351 public function get_price( Meow_MWAI_Query_Base $query, Meow_MWAI_Reply $reply )
1352 {
1353 $model = $query->model;
1354 $units = 0;
1355 $finetune = false;
1356 if ( is_a( $query, 'Meow_MWAI_Query_Text' ) || is_a( $query, 'Meow_MWAI_Query_Assistant' ) ) {
1357 if ( preg_match('/^([a-zA-Z]{0,32}):/', $model, $matches ) ) {
1358 $finetune = true;
1359 }
1360 $inUnits = $reply->get_in_tokens( $query );
1361 $outUnits = $reply->get_out_tokens();
1362 return $this->calculate_price( $model, $inUnits, $outUnits, null, $finetune );
1363 }
1364 else if ( is_a( $query, 'Meow_MWAI_Query_Image' ) ) {
1365 $units = $query->maxResults;
1366 $resolution = $query->resolution;
1367 return $this->calculate_price( $model, 0, $units, $resolution, $finetune );
1368 }
1369 else if ( is_a( $query, 'Meow_MWAI_Query_Transcribe' ) ) {
1370 $model = 'whisper';
1371 $units = $reply->get_units();
1372 return $this->calculate_price( $model, 0, $units, null, $finetune );
1373 }
1374 else if ( is_a( $query, 'Meow_MWAI_Query_Embed' ) ) {
1375 $units = $reply->get_total_tokens();
1376 return $this->calculate_price( $model, 0, $units, null, $finetune );
1377 }
1378 Meow_MWAI_Logging::warn( "(OpenAI) Cannot calculate price for $model." );
1379 return null;
1380 }
1381
1382 public function get_incidents() {
1383 $url = 'https://status.openai.com/history.rss';
1384 $response = wp_remote_get( $url );
1385 if ( is_wp_error( $response ) ) {
1386 throw new Exception( $response->get_error_message() );
1387 }
1388 $response = wp_remote_retrieve_body( $response );
1389 $xml = simplexml_load_string( $response );
1390 $incidents = array();
1391 $oneWeekAgo = time() - 5 * 24 * 60 * 60;
1392 foreach ( $xml->channel->item as $item ) {
1393 $date = strtotime( $item->pubDate );
1394 if ( $date > $oneWeekAgo ) {
1395 $incidents[] = array(
1396 'title' => (string) $item->title,
1397 'description' => (string) $item->description,
1398 'date' => $date
1399 );
1400 }
1401 }
1402 return $incidents;
1403 }
1404 }
1405