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