PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.2
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.2
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
traits 1 year ago anthropic.php 10 months ago chatml.php 10 months ago core.php 11 months ago factory.php 1 year ago google.php 10 months ago hugging-face.php 1 year ago open-router.php 1 year ago openai.php 10 months ago perplexity.php 1 year ago replicate.php 1 year ago
openai.php
2419 lines
1 <?php
2
3 /**
4 * OpenAI Engine implementation.
5 *
6 * This engine supports both the standard Chat Completions API and the new Responses API.
7 * The Responses API is used automatically for models that support it (models with the 'responses' tag).
8 *
9 * Key differences when using the Responses API:
10 * - Function calls and results use specific message types instead of role-based messages
11 * - MCP (Model Context Protocol) tools are executed remotely by OpenAI
12 * - Different streaming event structure
13 *
14 * @see https://platform.openai.com/docs/api-reference/responses
15 */
16 class Meow_MWAI_Engines_OpenAI extends Meow_MWAI_Engines_ChatML {
17 // Static
18 private static $creating = false;
19
20 // Responses API specific properties
21 protected $previousResponseId = null;
22 protected $conversationState = [];
23 protected $mcpToolNames = [];
24 protected $mcpServerCount = 0;
25 protected $mcpTotalToolCount = 0;
26 protected $emittedFunctionResults = [];
27
28 // Code interpreter content (separate from main content)
29 protected $streamContentCode = '';
30 protected $streamContainerId = null;
31 protected $streamCodeInterpreterFiles = []; // Track files created by code interpreter
32 protected $currentQuery = null;
33 protected $streamImages = [];
34 protected $seenCallIds = []; // Track seen call IDs to prevent duplicates
35 protected $lastRequestBody = null; // For debugging
36 protected $contentStarted = false; // Track if content streaming has started
37 // IMPORTANT: OpenAI Responses API sends the same function call in both:
38 // 1. response.output_item.done - when individual function call completes
39 // 2. response.completed - with all function calls in the final response
40 // We must deduplicate to avoid processing the same function twice
41
42 public static function create( $core, $env ) {
43 self::$creating = true;
44 if ( class_exists( 'MeowPro_MWAI_OpenAI' ) ) {
45 $instance = new MeowPro_MWAI_OpenAI( $core, $env );
46 }
47 else {
48 $instance = new self( $core, $env );
49 }
50 self::$creating = false;
51 return $instance;
52 }
53
54 public function __construct( $core, $env ) {
55 $isOwnClass = get_class( $this ) === 'Meow_MWAI_Engines_OpenAI';
56 if ( $isOwnClass && !self::$creating ) {
57 throw new \Exception( 'Please use the create() method to instantiate the Meow_MWAI_Engines_OpenAI class.' );
58 }
59 parent::__construct( $core, $env );
60 $this->set_environment();
61 }
62
63 public function reset_stream() {
64 parent::reset_stream();
65 $this->mcpServerCount = 0;
66 $this->mcpTotalToolCount = 0;
67 $this->emittedFunctionResults = [];
68 $this->streamImages = [];
69 $this->seenCallIds = [];
70 }
71
72 /**
73 * Check if a model should use the new Responses API
74 */
75 protected function should_use_responses_api( $model ) {
76 // First check if Responses API is enabled in settings
77 $options = $this->core->get_all_options();
78 $responsesApiEnabled = $options['ai_responses_api'] ?? true;
79
80 if ( !$responsesApiEnabled ) {
81 return false;
82 }
83
84 // Azure doesn't support Responses API yet
85 if ( $this->envType === 'azure' ) {
86 return false;
87 }
88
89 // Check if the model has the 'responses' tag
90 $modelInfo = $this->retrieve_model_info( $model );
91 if ( $modelInfo && !empty( $modelInfo['tags'] ) ) {
92 return in_array( 'responses', $modelInfo['tags'] );
93 }
94
95 return false;
96 }
97
98 /**
99 * Set conversation state for stateful responses
100 */
101 public function set_previous_response_id( $responseId ) {
102 $this->previousResponseId = $responseId;
103 }
104
105 /**
106 * Get conversation state
107 */
108 public function get_conversation_state() {
109 return $this->conversationState;
110 }
111
112 /**
113 * Build body for Responses API
114 */
115 protected function build_responses_body( $query, $streamCallback = null ) {
116 $body = [
117 'model' => $query->model,
118 'stream' => !is_null( $streamCallback ),
119 ];
120
121 // Handle different query types for Responses API
122 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
123 // Use simplified instructions + input format for basic queries
124 if ( !empty( $query->instructions ) ) {
125 $body['instructions'] = $query->instructions;
126 }
127
128 // Determine history strategy
129 $historyStrategy = $query->historyStrategy;
130
131 // Treat empty string as null for automatic mode
132 if ( empty( $historyStrategy ) ) {
133 $historyStrategy = null;
134 }
135
136 // If historyStrategy is null (automatic), use response_id when previousResponseId is available
137 if ( $historyStrategy === null && !empty( $query->previousResponseId ) ) {
138 $historyStrategy = 'response_id';
139 }
140
141 // Debug logging for all queries when using Responses API
142 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
143
144 if ( $queries_debug ) {
145 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
146 error_log( '[AI Engine] Feedback query blocks: ' . count( $query->blocks ?? [] ) );
147 }
148 }
149
150 // Handle based on history strategy
151 // For Responses API, feedback queries MUST use previous_response_id to maintain conversation state
152 if ( $historyStrategy === 'response_id' && !empty( $query->previousResponseId ) ) {
153 // Use ResponseIdManager to validate the response ID
154 if ( $this->core->responseIdManager->is_valid_for_responses_api( $query->previousResponseId ) ) {
155 // Use incremental mode with previous_response_id
156 $body['previous_response_id'] = $query->previousResponseId;
157
158 // Debug logging
159 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
160 if ( $queries_debug ) {
161 error_log( '[AI Engine Queries] Using previous_response_id: ' . $query->previousResponseId );
162 }
163 }
164 else {
165 // Log warning if queries debug is enabled
166 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
167 if ( $queries_debug ) {
168 error_log( '[AI Engine Queries] Warning: ' .
169 Meow_MWAI_FunctionCallException::invalid_response_id(
170 $query->previousResponseId,
171 'Responses API',
172 'resp'
173 )->getMessage() );
174 }
175 // Fall through to full history mode
176 $historyStrategy = 'full_history';
177 }
178
179 }
180
181 // If we're still in response_id mode after validation, use incremental input
182 if ( $historyStrategy === 'response_id' && !empty( $body['previous_response_id'] ) ) {
183 // Check if this is a feedback query (function call response)
184 if ( $query instanceof Meow_MWAI_Query_Feedback && !empty( $query->blocks ) ) {
185 // For feedback queries with previous_response_id, we need to include:
186 // 1. The function_call from the model
187 // 2. The function_call_output with the result
188 $body['input'] = $this->build_feedback_input_for_responses_api( $query );
189
190 // Debug: Log the feedback input structure
191 if ( $queries_debug ) {
192 error_log( '[AI Engine Queries] Feedback input structure: ' . json_encode( $body['input'], JSON_PRETTY_PRINT ) );
193 }
194 }
195 else {
196 // Regular user message
197 $content = [
198 [
199 'type' => 'input_text',
200 'text' => $query->get_message()
201 ]
202 ];
203
204 // Check for attached file/image
205 if ( $query->attachedFile ) {
206 $imageUrl = $query->image_remote_upload === 'url'
207 ? $query->attachedFile->get_url()
208 : $query->attachedFile->get_inline_base64_url();
209
210 $content[] = [
211 'type' => 'input_image',
212 'image_url' => $imageUrl
213 ];
214 }
215
216 $body['input'] = [
217 [
218 'role' => 'user',
219 'content' => $content
220 ]
221 ];
222
223 // Add context if present
224 if ( !empty( $query->context ) ) {
225 // Prepend context as a separate input_text in the same message
226 array_unshift( $body['input'][0]['content'], [
227 'type' => 'input_text',
228 'text' => $query->context . "\n\n"
229 ] );
230 }
231 }
232 }
233 else {
234 // Use full history mode (internal) or when no previous_response_id
235
236 // Build input - always use array format for Responses API
237 if ( !empty( $query->messages ) || $query->attachedFile || $query instanceof Meow_MWAI_Query_Feedback ) {
238 $body['input'] = $this->build_responses_input_array( $query );
239 }
240 else {
241 // Even for simple text, Responses API expects message format
242 $body['input'] = [
243 [
244 'role' => 'user',
245 'content' => [
246 [
247 'type' => 'input_text',
248 'text' => $query->get_message()
249 ]
250 ]
251 ]
252 ];
253 }
254
255 // Add context if present
256 if ( !empty( $query->context ) ) {
257 if ( isset( $body['input'] ) && is_string( $body['input'] ) ) {
258 $body['input'] = $query->context . "\n\n" . $body['input'];
259 }
260 else {
261 // Add context as system message
262 array_unshift( $body['input'], [
263 'role' => 'system',
264 'content' => $query->context
265 ] );
266 }
267 }
268 }
269
270 // Parameters
271 if ( !empty( $query->maxTokens ) ) {
272 $body['max_output_tokens'] = $query->maxTokens;
273 }
274
275 // Handle temperature parameter - GPT-5 models don't support it
276 if ( !empty( $query->temperature ) && $query->temperature !== 1 ) {
277 // Check if this is a GPT-5 model (gpt-5, gpt-5-mini, gpt-5-nano)
278 if ( strpos( $query->model, 'gpt-5' ) !== 0 ) {
279 $body['temperature'] = $query->temperature;
280 }
281 // For GPT-5 models, skip the temperature parameter entirely
282 }
283
284 // Handle GPT-5 specific parameters: reasoning and verbosity
285 if ( strpos( $query->model, 'gpt-5' ) === 0 ) {
286 // Add reasoning parameter as an object (Responses API expects object)
287 // { reasoning: { effort: 'minimal|low|medium|high' } }
288 if ( !empty( $query->reasoning ) ) {
289 $body['reasoning'] = [ 'effort' => $query->reasoning ];
290 }
291 // Add verbosity parameter if set (inside text object)
292 if ( !empty( $query->verbosity ) ) {
293 if ( !isset( $body['text'] ) || !is_array( $body['text'] ) ) {
294 $body['text'] = [];
295 }
296 $body['text']['verbosity'] = $query->verbosity;
297 }
298 }
299
300 // Note: The Responses API does not support the 'n' parameter for multiple results
301 // Unlike the Chat Completions API, Responses API generates one response at a time
302 // If multiple results are needed, separate requests must be made
303 // Reference: https://platform.openai.com/docs/api-reference/responses
304 if ( !empty( $query->maxResults ) && $query->maxResults > 1 ) {
305 Meow_MWAI_Logging::warn( 'Responses API does not support multiple results (n parameter). Only one result will be generated.' );
306 }
307
308 if ( !empty( $query->stop ) ) {
309 $body['stop'] = $query->stop;
310 }
311
312 if ( !empty( $query->responseFormat ) && $query->responseFormat === 'json' ) {
313 // Responses API uses 'text.format' instead of 'response_format'
314 if ( !isset( $body['text'] ) || !is_array( $body['text'] ) ) {
315 $body['text'] = [];
316 }
317 $body['text']['format'] = [ 'type' => 'json_object' ];
318 }
319
320 // Function calling - convert to tools
321 // IMPORTANT: Tools must be included in ALL requests, even when using previous_response_id
322 // The API needs to know which functions are available throughout the entire conversation
323 if ( !empty( $query->functions ) ) {
324 $body['tools'] = $this->build_responses_tools( $query->functions );
325 // IMPORTANT: Enable parallel tool calls to allow multiple function calls in one response
326 // TODO: OpenAI's Responses API has a bug where it only returns ONE function call even when
327 // parallel_tool_calls=true is set and multiple functions are clearly needed. This works correctly
328 // with the Chat Completions API. Monitor OpenAI's updates and test again in the future.
329 // Issue discovered: August 2025 - Only getDeskTemperature is called when both desk AND outdoor are requested.
330 $body['parallel_tool_calls'] = true;
331 }
332
333
334 // Add MCP servers if available
335 if ( isset( $query->mcpServers ) && is_array( $query->mcpServers ) && !empty( $query->mcpServers ) ) {
336 $mcp_envs = $this->core->get_option( 'mcp_envs' );
337 $this->mcpServerCount = count( $query->mcpServers );
338
339 foreach ( $query->mcpServers as $mcpServer ) {
340 if ( isset( $mcpServer['id'] ) ) {
341 // Find the full MCP server configuration by ID
342 foreach ( $mcp_envs as $env ) {
343 if ( $env['id'] === $mcpServer['id'] ) {
344 // Sanitize server label for OpenAI requirements
345 $server_label = $env['name'] . '_' . $env['id'];
346 // Remove spaces and special characters
347 $server_label = preg_replace( '/[^a-zA-Z0-9_]/', '', $server_label );
348 // Replace double or tripe underscores with single underscore
349 $server_label = preg_replace( '/_{2,}/', '_', $server_label );
350 // Ensure it starts with a letter
351 if ( !preg_match( '/^[a-zA-Z]/', $server_label ) ) {
352 $server_label = 'mcp_' . $server_label;
353 }
354
355 $mcp_tool = [
356 'type' => 'mcp',
357 'server_label' => $server_label,
358 'server_url' => $env['url'],
359 'require_approval' => 'never'
360 ];
361
362 // Add authorization header if available
363 if ( !empty( $env['token'] ) ) {
364 $mcp_tool['headers'] = [
365 'Authorization' => 'Bearer ' . $env['token']
366 ];
367 }
368
369 // Add to tools array
370 if ( !isset( $body['tools'] ) ) {
371 $body['tools'] = [];
372 }
373 $body['tools'][] = $mcp_tool;
374
375 break;
376 }
377 }
378 }
379 }
380 }
381
382 // Add tool_choice parameter if tools are present
383 if ( !empty( $body['tools'] ) ) {
384 // Default to 'auto' to let the model choose
385 $body['tool_choice'] = 'auto';
386 }
387
388 // Add tools (web_search, image_generation, code_interpreter) if specified
389 if ( !empty( $query->tools ) && is_array( $query->tools ) ) {
390
391 // Ensure tools array exists
392 if ( !isset( $body['tools'] ) ) {
393 $body['tools'] = [];
394 }
395
396 // Add each enabled tool
397 foreach ( $query->tools as $tool ) {
398 if ( in_array( $tool, ['web_search', 'image_generation', 'code_interpreter'] ) ) {
399 $toolConfig = [ 'type' => $tool ];
400
401 // Image generation requires partial_images when streaming
402 if ( $tool === 'image_generation' && !empty( $streamCallback ) ) {
403 $toolConfig['partial_images'] = 1;
404 }
405
406 // Code interpreter requires container configuration
407 if ( $tool === 'code_interpreter' ) {
408 $toolConfig['container'] = [ 'type' => 'auto' ];
409 // Add file_ids if available in the query
410 if ( !empty( $query->fileIds ) && is_array( $query->fileIds ) ) {
411 $toolConfig['container']['file_ids'] = $query->fileIds;
412 }
413 // Code interpreter tool configured
414 }
415
416 $body['tools'][] = $toolConfig;
417 Meow_MWAI_Logging::log( 'Responses API: Added tool ' . $tool . ' to request' );
418 }
419 }
420 }
421
422 // Add file_search tool if OpenAI Vector Store is configured
423 if ( !empty( $query->embeddingsEnvId ) ) {
424 Meow_MWAI_Logging::log( 'Responses API: Checking embeddings environment - embeddingsEnvId: ' . $query->embeddingsEnvId );
425
426 $embeddingsEnv = $this->core->get_embeddings_env( $query->embeddingsEnvId );
427
428 if ( $embeddingsEnv && $embeddingsEnv['type'] === 'openai-vector-store' ) {
429 Meow_MWAI_Logging::log( 'Responses API: Found OpenAI Vector Store environment' );
430
431 // Check if the OpenAI environment matches
432 $openai_env_id = $embeddingsEnv['openai_env_id'] ?? null;
433
434 Meow_MWAI_Logging::log( 'Responses API: Comparing environments - embeddings OpenAI env: ' . ( $openai_env_id ?? 'null' ) . ', current env: ' . $this->envId );
435
436 if ( $openai_env_id === $this->envId && !empty( $embeddingsEnv['store_id'] ) ) {
437 // Ensure tools array exists
438 if ( !isset( $body['tools'] ) ) {
439 $body['tools'] = [];
440 }
441
442 // Add file_search tool with vector store ID
443 $body['tools'][] = [
444 'type' => 'file_search',
445 'vector_store_ids' => [ $embeddingsEnv['store_id'] ]
446 ];
447
448 Meow_MWAI_Logging::log( 'Responses API: Added file_search tool with vector store: ' . $embeddingsEnv['store_id'] );
449 } else {
450 if ( $openai_env_id !== $this->envId ) {
451 Meow_MWAI_Logging::log( 'Responses API: Environment mismatch - file_search tool not added' );
452 }
453 if ( empty( $embeddingsEnv['store_id'] ) ) {
454 Meow_MWAI_Logging::log( 'Responses API: No store_id configured - file_search tool not added' );
455 }
456 }
457 } else {
458 Meow_MWAI_Logging::log( 'Responses API: Embeddings environment is not OpenAI Vector Store type (type: ' . ( $embeddingsEnv['type'] ?? 'null' ) . ')' );
459 }
460 } else {
461 Meow_MWAI_Logging::log( 'Responses API: No embeddingsEnvId in query - file_search tool not added' );
462 }
463
464 // Note: Responses API doesn't support stream_options parameter
465 // Usage tracking is handled differently in the streaming response
466 }
467 else if ( $query instanceof Meow_MWAI_Query_Image ) {
468 // For image generation, we can use the integrated approach
469 if ( $query->model === 'gpt-image-1' ) {
470 $body['tools'] = [[
471 'type' => 'image_generation'
472 ]];
473 $body['input'] = $query->get_message();
474 }
475 else {
476 // Fallback to old API for DALL-E models
477 return $this->build_body( $query, $streamCallback );
478 }
479 }
480
481 // Debug logging for feedback queries
482 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
483 Meow_MWAI_Logging::log( 'Responses API: Feedback query body: ' . json_encode( $body ) );
484 }
485
486 // Ensure parallel_tool_calls is set when we have tools
487 if ( !empty( $body['tools'] ) && !isset( $body['parallel_tool_calls'] ) ) {
488 $body['parallel_tool_calls'] = true;
489 }
490
491 return $body;
492 }
493
494 /**
495 * Build tool messages for feedback when using previous_response_id
496 */
497 protected function build_tool_messages_for_feedback( $query ) {
498 $messages = [];
499
500 if ( $query instanceof Meow_MWAI_Query_Feedback && !empty( $query->blocks ) ) {
501 foreach ( $query->blocks as $block ) {
502 if ( isset( $block['feedbacks'] ) ) {
503 foreach ( $block['feedbacks'] as $feedback ) {
504 // Get the tool call ID from the original request
505 $toolId = $feedback['request']['toolId'] ?? null;
506
507 if ( $toolId ) {
508 // According to Responses API spec, tool results should use role:"tool"
509 $toolMessage = [
510 'role' => 'tool',
511 'tool_call_id' => $toolId,
512 'content' => [
513 [
514 'type' => 'tool_result',
515 'tool_result' => (string) ( $feedback['reply']['value'] ?? '' )
516 ]
517 ]
518 ];
519 $messages[] = $toolMessage;
520
521 Meow_MWAI_Logging::log( 'Responses API: Added tool result with tool_call_id ' . $toolId . ' - Message: ' . json_encode( $toolMessage ) );
522 }
523 }
524 }
525 }
526 }
527
528 return $messages;
529 }
530
531 /**
532 * Build input array for complex message structures
533 */
534 protected function build_responses_input_array( $query ) {
535 // Use the MessageBuilder service for streamlined message building
536 $messages = $this->core->messageBuilder->build_responses_api_messages( $query );
537
538 // Note: Function result events are now emitted centrally in core.php
539 // when the function is actually executed
540
541 // Debug logging
542 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
543 if ( $queries_debug && $query instanceof Meow_MWAI_Query_Feedback ) {
544 error_log( '[AI Engine Queries] Feedback query messages order:' );
545 foreach ( $messages as $idx => $msg ) {
546 if ( isset( $msg['type'] ) ) {
547 $log_msg = ' [' . $idx . '] ' . $msg['type'];
548 if ( $msg['type'] === 'function_call' ) {
549 $log_msg .= ' - ' . ( $msg['name'] ?? 'unknown' ) . ' (call_id: ' . ( $msg['call_id'] ?? 'none' ) . ')';
550 }
551 elseif ( $msg['type'] === 'function_call_output' ) {
552 $log_msg .= ' (call_id: ' . ( $msg['call_id'] ?? 'none' ) . ', output: ' . substr( $msg['output'] ?? '', 0, 50 ) . ')';
553 }
554 error_log( '[AI Engine Queries]' . $log_msg );
555 }
556 elseif ( isset( $msg['role'] ) ) {
557 $content_preview = '';
558 if ( isset( $msg['content'] ) ) {
559 if ( is_string( $msg['content'] ) ) {
560 $content_preview = ' - "' . substr( $msg['content'], 0, 50 ) . '"';
561 }
562 elseif ( is_array( $msg['content'] ) && isset( $msg['content'][0]['text'] ) ) {
563 $content_preview = ' - "' . substr( $msg['content'][0]['text'], 0, 50 ) . '"';
564 }
565 elseif ( is_array( $msg['content'] ) && isset( $msg['content'][0]['type'] ) && $msg['content'][0]['type'] === 'input_text' ) {
566 $content_preview = ' - "' . substr( $msg['content'][0]['text'] ?? '', 0, 50 ) . '"';
567 }
568 }
569 error_log( '[AI Engine Queries] [' . $idx . '] ' . $msg['role'] . $content_preview );
570 }
571 }
572 }
573
574 return $messages;
575 }
576
577 /**
578 * Convert functions to Responses API tools format
579 */
580 protected function build_responses_tools( $functions ) {
581 $tools = [];
582
583 foreach ( $functions as $function ) {
584 $functionData = $function->serializeForOpenAI();
585
586 // Ensure the function data has all required fields
587 if ( !isset( $functionData['name'] ) || empty( $functionData['name'] ) ) {
588 Meow_MWAI_Logging::warn( 'Function missing required name field' );
589 continue;
590 }
591
592 // Responses API expects a flatter structure
593 $parameters = $functionData['parameters'] ?? null;
594
595 // Ensure parameters has the correct structure
596 if ( !$parameters ) {
597 $parameters = [
598 'type' => 'object',
599 'properties' => new stdClass(),
600 'required' => []
601 ];
602 }
603 else {
604 // Ensure properties is an object, not an array when empty
605 if ( isset( $parameters['properties'] ) &&
606 is_array( $parameters['properties'] ) &&
607 empty( $parameters['properties'] ) ) {
608 $parameters['properties'] = new stdClass();
609 }
610 }
611
612 $tool = [
613 'type' => 'function',
614 'name' => $functionData['name'],
615 'description' => $functionData['description'] ?? '',
616 'parameters' => $parameters,
617 'strict' => false // Set to false for now, can be made configurable later
618 ];
619
620 $tools[] = $tool;
621 }
622
623 return $tools;
624 }
625
626 /**
627 * Build feedback input for Responses API when using previous_response_id.
628 *
629 * The Responses API requires a very specific format for function results:
630 * 1. Echo the exact function_call message from the model
631 * 2. Provide the function_call_output with matching call_id
632 *
633 * This method extracts these from the feedback blocks and formats them correctly.
634 *
635 * @param Meow_MWAI_Query_Feedback $query The feedback query containing function results
636 * @return array Array of messages in Responses API format
637 */
638 protected function build_feedback_input_for_responses_api( $query ) {
639 // Use the MessageBuilder service for streamlined message building
640 $messages = $this->core->messageBuilder->build_feedback_only_messages( $query );
641
642 // For Responses API, the input should be wrapped in a specific structure
643 // According to OpenAI docs, function results should be sent as an array of messages
644 return $messages;
645 }
646
647 /**
648 * Build URL for Responses API
649 */
650 protected function build_responses_url() {
651 if ( $this->envType === 'azure' ) {
652 // Azure uses a different URL structure
653 $endpoint = isset( $this->env['endpoint'] ) ? $this->env['endpoint'] : null;
654 $url = trailingslashit( $endpoint ) . 'openai/responses?' . $this->azureApiVersion;
655 }
656 else {
657 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
658 $url = trailingslashit( $endpoint ) . 'responses';
659 }
660
661 return $url;
662 }
663
664 /**
665 * Handle Responses API streaming data
666 */
667 protected function responses_stream_data_handler( $json ) {
668 $content = null;
669 static $currentItemType = null; // Track the current output item type
670 // Load event helper
671 if ( !class_exists( 'Meow_MWAI_Event' ) ) {
672 require_once MWAI_PATH . '/classes/event.php';
673 }
674
675 // Get response metadata
676 if ( isset( $json['id'] ) ) {
677 $this->inId = $json['id'];
678 Meow_MWAI_Logging::log( 'Responses API Streaming: Found response ID in stream: ' . $this->inId );
679 }
680 if ( isset( $json['model'] ) ) {
681 $this->inModel = $json['model'];
682 }
683
684 // Handle different event types for Responses API
685 $eventType = $json['type'] ?? null;
686
687
688 // Debug streaming events
689 if ( isset( $_GET['debug_mcp'] ) ) {
690 error_log( 'AI_ENGINE_DEBUG: Streaming type: ' . ( $eventType ?? 'no_type' ) . ' - Data: ' . json_encode( $json ) );
691 }
692
693 switch ( $eventType ) {
694 // ===== LIFECYCLE EVENTS =====
695
696 case 'response.created':
697 // Emitted when a response object is created - contains initial response metadata
698 $response = $json['response'] ?? [];
699 $this->inId = $response['id'] ?? null;
700 $this->inModel = $response['model'] ?? null;
701 if ( $this->inId ) {
702 }
703 break;
704
705 case 'response.queued':
706 // Response is queued and waiting to start processing
707 // We can log this for debugging purposes
708 Meow_MWAI_Logging::log( 'Responses API: Response queued for processing' );
709 break;
710
711 case 'response.in_progress':
712 // Emitted repeatedly while the response is being generated
713 // Contains partial response state but typically not used for streaming text
714 break;
715
716 case 'response.completed':
717 // Response is fully generated - extract any function calls from completed output
718 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
719 error_log( '[AI Engine Queries] Current streamToolCalls count: ' . count( $this->streamToolCalls ) );
720 }
721
722 $response = $json['response'] ?? [];
723 $outputs = $response['output'] ?? [];
724
725 foreach ( $outputs as $idx => $output ) {
726 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
727 error_log( '[AI Engine Queries] Output ' . $idx . ' type: ' . ( $output['type'] ?? 'unknown' ) . ', status: ' . ( $output['status'] ?? 'no-status' ) );
728 }
729
730 if ( isset( $output['type'] ) && $output['type'] === 'function_call' &&
731 isset( $output['status'] ) && $output['status'] === 'completed' ) {
732 // Note: Responses API uses 'call_id' not 'id' for function calls
733 $callId = $output['call_id'] ?? $output['id'] ?? null;
734 $functionName = $output['name'] ?? '';
735
736 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
737 error_log( '[AI Engine Queries] Processing function_call: ' . $functionName . ' (id: ' . $callId . ')' );
738 }
739
740 // IMPORTANT: Deduplicate function calls
741 // OpenAI sends the same function call in both response.output_item.done
742 // and response.completed events. We track call IDs to avoid duplicates.
743 if ( in_array( $callId, $this->seenCallIds, true ) ) {
744 // Skip duplicate - already processed in response.output_item.done
745 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
746 error_log( '[AI Engine Queries] Skipping duplicate call ID: ' . $callId );
747 }
748 continue;
749 }
750
751 // First time seeing this call ID - add it
752 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
753 error_log( '[AI Engine Queries] response.completed adding tool call: ' . $functionName . ' (id: ' . $callId . ')' );
754 }
755 $this->seenCallIds[] = $callId;
756 $this->streamToolCalls[] = [
757 'id' => $callId,
758 'type' => 'function',
759 'function' => [
760 'name' => $functionName,
761 'arguments' => $output['arguments'] ?? '{}'
762 ]
763 ];
764 }
765 }
766 break;
767
768 case 'response.incomplete':
769 // Response stopped before completion (e.g., max_tokens reached)
770 $details = $json['response']['incomplete_details'] ?? [];
771 Meow_MWAI_Logging::warn( 'Responses API: Response incomplete - ' . json_encode( $details ) );
772 break;
773
774 case 'response.failed':
775 // Response generation failed
776 $error = $json['response']['error'] ?? [];
777 $message = $error['message'] ?? 'Response generation failed';
778 throw new Exception( $message );
779
780 // ===== OUTPUT ITEM EVENTS =====
781
782 case 'response.output_item.added':
783 // New output item added (e.g., message, function_call, etc.)
784 // Track the type of the current output item
785 if ( isset( $json['item'] ) && isset( $json['item']['type'] ) ) {
786 $item = $json['item'];
787 $itemType = $item['type'];
788 $currentItemType = $itemType;
789
790 // Code interpreter items are handled in event processing
791
792 // Don't emit events here for web search or image generation - wait for more specific events
793 // This prevents duplicate events
794
795 // If it's an MCP call, store the tool name
796 if ( $itemType === 'mcp_call' && isset( $item['id'] ) && isset( $item['name'] ) ) {
797 $this->mcpToolNames[$item['id']] = $item['name'];
798 Meow_MWAI_Logging::log( 'Responses API: MCP tool call added - ' . $item['name'] . ' (id: ' . $item['id'] . ')' );
799
800 if ( $this->currentDebugMode ) {
801 $event = Meow_MWAI_Event::mcp_calling( $item['name'], $item['id'] )
802 ->set_metadata( 'name', $item['name'] )
803 ->set_metadata( 'server_label', $item['server_label'] ?? null );
804 call_user_func( $this->streamCallback, $event );
805 }
806 }
807 }
808 break;
809
810 case 'response.output_item.done':
811 // Output item completed - check for MCP approval requests or tool lists
812 if ( isset( $json['item'] ) && isset( $json['item']['type'] ) ) {
813 $item = $json['item'];
814 $itemType = $item['type'];
815
816 // Reset current item type when we complete a message item
817 if ( $itemType === 'message' ) {
818 $currentItemType = null;
819 }
820
821 if ( $itemType === 'function_call' ) {
822 // Regular function call completed - send event
823 if ( $this->currentDebugMode && $this->streamCallback ) {
824 $event = Meow_MWAI_Event::function_calling( $item['name'] ?? 'unknown', json_decode( $item['arguments'] ?? '{}', true ) )
825 ->set_metadata( 'call_id', $item['call_id'] ?? null );
826 call_user_func( $this->streamCallback, $event );
827 }
828
829 // Add to streamToolCalls for execution
830 // Note: Responses API uses 'call_id' not 'id' for function calls
831 $callId = $item['call_id'] ?? $item['id'] ?? null;
832 $functionName = $item['name'] ?? '';
833
834 // Add to our deduplication tracking
835 // We process function calls here as they complete individually during streaming
836 // The response.completed event will also try to add them, so we track IDs
837 if ( !in_array( $callId, $this->seenCallIds, true ) ) {
838 $this->seenCallIds[] = $callId;
839
840 $this->streamToolCalls[] = [
841 'id' => $callId,
842 'type' => 'function',
843 'function' => [
844 'name' => $functionName,
845 'arguments' => $item['arguments'] ?? '{}'
846 ]
847 ];
848 }
849 }
850 elseif ( $itemType === 'mcp_approval_request' ) {
851 // IMPORTANT: MCP (Model Context Protocol) tools are executed remotely by OpenAI
852 // Unlike regular function calls, MCP tools do NOT need local execution
853 // Therefore, we should NOT add them to streamToolCalls array
854 // This prevents creation of unnecessary feedback queries and second response cycles
855 Meow_MWAI_Logging::log( 'Responses API: MCP approval request for ' . $item['name'] . ' from server ' . $item['server_label'] . ' (handled remotely)' );
856 }
857 elseif ( $item['type'] === 'mcp_call' ) {
858 // IMPORTANT: MCP calls are already executed remotely by OpenAI's infrastructure
859 // The result is included in the same response stream
860 // We must NOT add these to streamToolCalls to avoid duplicate execution attempts
861 Meow_MWAI_Logging::log( 'Responses API: MCP call completed - ' . $item['name'] . ' (already executed remotely)' );
862
863 // Send event for completed MCP call when debug is enabled
864 if ( $this->currentDebugMode && isset( $item['name'] ) ) {
865 $args = json_decode( $item['arguments'] ?? '{}', true );
866 $output = $item['output'] ?? null;
867
868 // Skip the tool_call event for MCP calls since we already sent mcp_tool_call
869 // This prevents duplicate events in the UI
870
871 // Then send a separate event for the tool result
872 if ( $output ) {
873 // Format the output preview
874 $outputPreview = is_array( $output ) ? json_encode( $output ) : (string) $output;
875 if ( strlen( $outputPreview ) > 100 ) {
876 $outputPreview = substr( $outputPreview, 0, 100 ) . '...';
877 }
878
879 $resultEvent = Meow_MWAI_Event::mcp_result( $item['name'] )
880 ->set_metadata( 'output', $output );
881 call_user_func( $this->streamCallback, $resultEvent );
882 }
883
884 // Don't return content since we've already sent events
885 $content = null;
886 }
887 }
888 elseif ( $itemType === 'web_search_call' ) {
889 // Web search completed - don't emit event here
890 // The event will be emitted by the response.web_search_call.completed handler
891 // This prevents duplicate events
892 Meow_MWAI_Logging::log( 'Responses API: Web search output item completed (event handled by specific handler)' );
893 }
894 elseif ( $itemType === 'code_interpreter_call' ) {
895 // Code interpreter completed
896 Meow_MWAI_Logging::log( 'Responses API: Code interpreter output item completed' );
897
898 // Store container ID if available
899 if ( isset( $item['container_id'] ) ) {
900 $this->streamContainerId = $item['container_id'];
901 Meow_MWAI_Logging::log( 'Responses API: Found container_id in streaming: ' . $this->streamContainerId );
902 }
903
904 // Check for files in the result
905 if ( isset( $item['result'] ) ) {
906 $result = $item['result'];
907
908 // Look for files in the result
909 if ( isset( $result['files'] ) ) {
910 // Store these files
911 if ( !isset( $this->streamCodeInterpreterFiles ) ) {
912 $this->streamCodeInterpreterFiles = [];
913 }
914
915 foreach ( $result['files'] as $file ) {
916 $this->streamCodeInterpreterFiles[] = $file;
917 Meow_MWAI_Logging::log( 'Responses API: Captured file from result: ' . ( $file['filename'] ?? $file['id'] ?? 'unknown' ) );
918 }
919 }
920
921 // Handle standard output
922 if ( isset( $result['stdout'] ) && !empty( $result['stdout'] ) ) {
923 // Add code output to the response content
924 $content = "\n```\n" . $result['stdout'] . "\n```\n";
925 Meow_MWAI_Logging::log( 'Responses API: Code interpreter stdout: ' . substr( $result['stdout'], 0, 100 ) );
926 }
927 }
928 }
929 elseif ( $itemType === 'image_generation_call' ) {
930 // Image generation completed
931 Meow_MWAI_Logging::log( 'Responses API: Image generation output item completed' );
932
933 // Extract the base64 image from the result
934 if ( isset( $item['result'] ) ) {
935 $base64Image = $item['result'];
936
937 // Store the image for later processing
938 if ( !isset( $this->streamImages ) ) {
939 $this->streamImages = [];
940 }
941
942 $this->streamImages[] = $base64Image;
943
944 Meow_MWAI_Logging::log( 'Responses API: Stored generated image (base64 length: ' . strlen( $base64Image ) . ')' );
945 }
946 }
947 elseif ( $item['type'] === 'mcp_list_tools' ) {
948 // MCP tools list discovered
949 $server_label = $item['server_label'] ?? 'unknown';
950 $tools_count = isset( $item['tools'] ) ? count( $item['tools'] ) : 0;
951 $this->mcpTotalToolCount += $tools_count;
952 Meow_MWAI_Logging::log( 'Responses API: MCP tools list from server ' . $server_label . ' containing ' . $tools_count . ' tools' );
953
954 // Send event for tools discovery using the aggregated format
955 if ( $this->currentDebugMode ) {
956 $serverCount = $this->mcpServerCount > 0 ? $this->mcpServerCount : 1;
957 $event = Meow_MWAI_Event::mcp_discovery( $serverCount, $this->mcpTotalToolCount );
958 call_user_func( $this->streamCallback, $event );
959 }
960
961 // Log first few tools for debugging
962 if ( isset( $item['tools'] ) && is_array( $item['tools'] ) ) {
963 $sample_tools = array_slice( $item['tools'], 0, 3 );
964 foreach ( $sample_tools as $tool ) {
965 Meow_MWAI_Logging::log( 'Responses API: MCP tool "' . ( $tool['name'] ?? 'unnamed' ) . '": ' . ( $tool['description'] ?? 'no description' ) );
966 }
967 if ( $tools_count > 3 ) {
968 Meow_MWAI_Logging::log( 'Responses API: ... and ' . ( $tools_count - 3 ) . ' more tools' );
969 }
970 }
971 }
972 }
973 break;
974
975 // ===== CONTENT PART EVENTS =====
976
977 case 'response.content_part.added':
978 // New content part added to an output item
979 // Indicates start of a new content section (text, image, etc.)
980 // Check if this is MCP-related content that shouldn't be shown
981 if ( isset( $json['part']['type'] ) ) {
982 $partType = $json['part']['type'];
983
984 // Just log the part type for debugging
985 // We can use this info later if needed
986 }
987 break;
988
989 case 'response.content_part.done':
990 // Content part is finalized
991 // No more deltas will be sent for this content part
992 break;
993
994 // ===== TEXT STREAMING EVENTS =====
995
996 case 'response.output_text.delta':
997 // Streaming text chunk for the current content part
998 if ( isset( $json['delta'] ) ) {
999 // Send a status event for the first content chunk
1000 if ( $this->currentDebugMode && !$this->contentStarted ) {
1001 $this->contentStarted = true;
1002 $statusEvent = Meow_MWAI_Event::generating_response();
1003 call_user_func( $this->streamCallback, $statusEvent );
1004 }
1005 $content = $json['delta'];
1006 }
1007 break;
1008
1009 case 'response.output_text.done':
1010 // Final text for the content part
1011 // Contains the complete accumulated text
1012 // Don't send response_completed here - ChatbotContext adds "Request completed"
1013 $this->contentStarted = false;
1014 break;
1015
1016 case 'response.refusal.delta':
1017 // Streaming refusal message chunk
1018 // Model is refusing to generate the requested content
1019 if ( isset( $json['delta'] ) ) {
1020 // We might want to stream refusals as regular content
1021 $content = $json['delta'];
1022 }
1023 break;
1024
1025 case 'response.refusal.done':
1026 // Final refusal message
1027 // Contains the complete refusal reason
1028 break;
1029
1030 case 'response.function_call_arguments.delta':
1031 // Streaming JSON arguments for a function call
1032 // We don't stream these to UI as they're not human-readable
1033 break;
1034
1035 case 'response.function_call_arguments.done':
1036 // Complete function call arguments
1037 // Already handled in response.output_item.done for function_call type
1038 break;
1039
1040 // ===== FILE & WEB SEARCH EVENTS =====
1041
1042 case 'response.file_search_call.in_progress':
1043 // File search started
1044 Meow_MWAI_Logging::log( 'Responses API: File search in progress' );
1045 break;
1046
1047 case 'response.file_search_call.searching':
1048 // Actively searching files
1049 break;
1050
1051 case 'response.file_search_call.completed':
1052 // File search finished
1053 break;
1054
1055 case 'response.web_search_call.in_progress':
1056 // Web search started - only emit one event at the start
1057 Meow_MWAI_Logging::log( 'Responses API: Web search in progress' );
1058 if ( $this->currentDebugMode && $this->streamCallback ) {
1059 $event = Meow_MWAI_Event::status( 'Searching the web...' );
1060 call_user_func( $this->streamCallback, $event );
1061 }
1062 break;
1063
1064 case 'response.web_search_call.searching':
1065 // Actively searching - don't emit duplicate events
1066 if ( isset( $json['query'] ) ) {
1067 Meow_MWAI_Logging::log( 'Responses API: Searching for: ' . $json['query'] );
1068 }
1069 break;
1070
1071 case 'response.web_search_call.completed':
1072 // Web search finished
1073 Meow_MWAI_Logging::log( 'Responses API: Web search completed' );
1074
1075 // The completed event doesn't contain results, just metadata
1076 // Results are likely embedded in the model's response text
1077 if ( $this->currentDebugMode && $this->streamCallback ) {
1078 $message = 'Web search completed';
1079 $event = Meow_MWAI_Event::status( $message );
1080 call_user_func( $this->streamCallback, $event );
1081 }
1082 break;
1083
1084 // ===== IMAGE GENERATION EVENTS =====
1085
1086 case 'response.image_generation_call.in_progress':
1087 // Image generation started
1088 Meow_MWAI_Logging::log( 'Responses API: Image generation in progress' );
1089 if ( $this->currentDebugMode && $this->streamCallback ) {
1090 $event = Meow_MWAI_Event::status( 'Generating image...' );
1091 call_user_func( $this->streamCallback, $event );
1092 }
1093 break;
1094
1095 case 'response.image_generation_call.generating':
1096 // Image is being generated
1097 break;
1098
1099 case 'response.image_generation_call.partial_image':
1100 // Partial image data (base64)
1101 // Could be used for progressive image display
1102 if ( isset( $json['partial_image_b64'] ) ) {
1103 Meow_MWAI_Logging::log( 'Responses API: Received partial image index ' . ( $json['partial_image_index'] ?? 'unknown' ) );
1104 // For now, we don't display partial images, but we could in the future
1105 }
1106 break;
1107
1108 case 'response.image_generation_call.completed':
1109 // Image generation finished
1110 Meow_MWAI_Logging::log( 'Responses API: Image generation completed' );
1111
1112 // Note: The actual image data comes in response.output_item.done event
1113 // This event just signals completion
1114
1115 if ( $this->currentDebugMode && $this->streamCallback ) {
1116 $event = Meow_MWAI_Event::status( 'Image generated.' );
1117 call_user_func( $this->streamCallback, $event );
1118 }
1119 break;
1120
1121 // ===== CODE INTERPRETER EVENTS =====
1122
1123 case 'response.code_interpreter_call.in_progress':
1124 // Code interpreter started
1125
1126 // Check for container_id in the event
1127 if ( isset( $json['container_id'] ) ) {
1128 $this->streamContainerId = $json['container_id'];
1129 error_log( '[AI Engine] Found container_id in code_interpreter_call.in_progress: ' . $this->streamContainerId );
1130 }
1131
1132 // Also check in item if present
1133 if ( isset( $json['item']['container_id'] ) ) {
1134 $this->streamContainerId = $json['item']['container_id'];
1135 error_log( '[AI Engine] Found container_id in item: ' . $this->streamContainerId );
1136 }
1137
1138 // Container ID captured if available
1139 break;
1140
1141 case 'response.code_interpreter_call.running':
1142 // Code is being executed
1143 // Check for container_id here too
1144 if ( isset( $json['container_id'] ) ) {
1145 $this->streamContainerId = $json['container_id'];
1146 Meow_MWAI_Logging::log( 'Responses API: Found container_id in running event: ' . $this->streamContainerId );
1147 }
1148 break;
1149
1150 case 'response.code_interpreter_call.stdout':
1151 // Standard output from code execution
1152 if ( isset( $json['stdout'] ) ) {
1153 Meow_MWAI_Logging::log( 'Responses API: Code output - ' . substr( $json['stdout'], 0, 100 ) );
1154 }
1155 break;
1156
1157 case 'response.code_interpreter_call.stderr':
1158 // Standard error from code execution
1159 if ( isset( $json['stderr'] ) ) {
1160 Meow_MWAI_Logging::log( 'Responses API: Code error - ' . $json['stderr'] );
1161 }
1162 break;
1163
1164 case 'response.code_interpreter_call.completed':
1165 // Code interpreter finished - files are now ready for download
1166 Meow_MWAI_Logging::log( 'Responses API: Code interpreter completed' );
1167
1168 // Check for container_id in completed event
1169 if ( isset( $json['container_id'] ) ) {
1170 $this->streamContainerId = $json['container_id'];
1171 Meow_MWAI_Logging::log( 'Responses API: Container ID: ' . $this->streamContainerId );
1172 }
1173
1174 // Mark that code interpreter has completed
1175 $this->codeInterpreterCompleted = true;
1176
1177 // Send CODE event to client
1178 if ( $this->currentDebugMode && $this->streamCallback ) {
1179 $codeEvent = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['CODE'] ) )
1180 ->set_content( 'Code execution completed.' );
1181 call_user_func( $this->streamCallback, $codeEvent );
1182 }
1183 break;
1184
1185 case 'response.code_interpreter_call_code.delta':
1186 // Streaming code being written/executed by the code interpreter
1187 // This should NOT be added to the main content
1188 if ( isset( $json['delta'] ) ) {
1189 // Send CODE event only for the first code delta
1190 if ( empty( $this->streamContentCode ) && $this->currentDebugMode && $this->streamCallback ) {
1191 $codeEvent = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['CODE'] ) )
1192 ->set_content( 'Writing code...' );
1193 call_user_func( $this->streamCallback, $codeEvent );
1194 }
1195
1196 // Accumulate code in streamContentCode instead of content
1197 $this->streamContentCode .= $json['delta'];
1198
1199 Meow_MWAI_Logging::log( 'Responses API: Code interpreter code delta - ' . substr( $json['delta'], 0, 100 ) );
1200 }
1201 // Important: Don't return any content here so it's not added to streamContent
1202 return null;
1203
1204 case 'response.code_interpreter_call_code.done':
1205 // Code interpreter code writing completed
1206 if ( !empty( $this->streamContentCode ) && $this->currentDebugMode && $this->streamCallback ) {
1207 $lines = substr_count( $this->streamContentCode, "\n" ) + 1;
1208
1209 // Send the complete code as a collapsed CODE event
1210 // Set summary as content (shown when collapsed) and full code as metadata (shown when expanded)
1211 $codeEvent = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['CODE'] ) )
1212 ->set_content( "Wrote Python code ($lines lines)" )
1213 ->set_visibility( MWAI_STREAM_VISIBILITY['COLLAPSED'] )
1214 ->set_metadata( 'full_code', $this->streamContentCode );
1215 call_user_func( $this->streamCallback, $codeEvent );
1216
1217 Meow_MWAI_Logging::log( 'Responses API: Code interpreter code completed - ' . strlen( $this->streamContentCode ) . ' bytes' );
1218 }
1219 break;
1220
1221 case 'response.code_interpreter_file_citation':
1222 case 'code_interpreter_file_citation':
1223 // Code interpreter has created or cited a file
1224 // This event contains the file_id for files generated during code execution
1225 if ( isset( $json['file_id'] ) ) {
1226 if ( !isset( $this->streamCodeInterpreterFiles ) ) {
1227 $this->streamCodeInterpreterFiles = [];
1228 }
1229 $file_info = [
1230 'file_id' => $json['file_id'],
1231 'filename' => $json['filename'] ?? null,
1232 'file_type' => $json['file_type'] ?? null,
1233 'path' => isset( $json['path'] ) ? $json['path'] : ( isset( $json['filename'] ) ? '/mnt/data/' . $json['filename'] : null )
1234 ];
1235 $this->streamCodeInterpreterFiles[] = $file_info;
1236 error_log( '[AI Engine] File citation captured: ' . json_encode( $file_info ) );
1237 Meow_MWAI_Logging::log( 'Responses API: Code interpreter file citation - file_id: ' . $json['file_id'] );
1238 }
1239 break;
1240
1241 // ===== MCP (Model Context Protocol) EVENTS =====
1242
1243 case 'response.mcp_call.in_progress':
1244 // MCP tool call is running
1245 $itemId = $json['item_id'] ?? null;
1246 $toolName = isset( $this->mcpToolNames[$itemId] ) ? $this->mcpToolNames[$itemId] : 'unknown';
1247
1248 Meow_MWAI_Logging::log( 'Responses API: MCP tool call in progress - ' . $toolName );
1249 break;
1250
1251 case 'response.mcp_call.arguments.delta':
1252 case 'response.mcp_call_arguments.delta':
1253 // Streaming arguments for MCP tool call
1254 // Don't stream these JSON arguments to the UI
1255 // These contain the function parameters like {"post_type":"post",...}
1256 break;
1257
1258 case 'response.mcp_call.arguments.done':
1259 case 'response.mcp_call_arguments.done':
1260 // Complete arguments for MCP tool call
1261 break;
1262
1263 case 'response.mcp_call.completed':
1264 // MCP tool call succeeded
1265 break;
1266
1267 case 'response.mcp_call.failed':
1268 // MCP tool call failed
1269 $error = $json['error'] ?? [];
1270 Meow_MWAI_Logging::error( 'Responses API: MCP tool call failed - ' . ( $error['message'] ?? 'Unknown error' ) );
1271 break;
1272
1273 case 'response.mcp_list_tools.in_progress':
1274 // Listing MCP tools has started
1275 Meow_MWAI_Logging::log( 'Responses API: MCP tools discovery in progress' );
1276 break;
1277
1278 case 'response.mcp_list_tools.completed':
1279 // MCP tools listing completed successfully
1280 break;
1281
1282 case 'response.mcp_list_tools.failed':
1283 // MCP tools listing failed
1284 $error = $json['error'] ?? [];
1285 $message = 'MCP tools listing failed: ' . ( $error['message'] ?? 'Unknown error' );
1286 Meow_MWAI_Logging::error( 'Responses API: ' . $message );
1287 throw new Exception( $message );
1288 break;
1289
1290 // ===== REASONING EVENTS (for o1/o3 models) =====
1291
1292 case 'response.reasoning.delta':
1293 // Streaming reasoning text chunk
1294 // Internal reasoning process of the model
1295 break;
1296
1297 case 'response.reasoning.done':
1298 // Complete reasoning text
1299 break;
1300
1301 case 'response.reasoning_summary_part.added':
1302 // New reasoning summary part added
1303 break;
1304
1305 case 'response.reasoning_summary_part.done':
1306 // Reasoning summary part completed
1307 break;
1308
1309 case 'response.reasoning_summary_text.delta':
1310 // Streaming reasoning summary text
1311 break;
1312
1313 case 'response.reasoning_summary_text.done':
1314 // Complete reasoning summary
1315 break;
1316
1317 // ===== ANNOTATION EVENTS =====
1318
1319 case 'response.output_text_annotation.added':
1320 case 'response.output_text.annotation.added':
1321 // Text annotation added - check for container file citations
1322 if ( isset( $json['annotation'] ) ) {
1323 $annotation = $json['annotation'];
1324
1325 // Check if this is a container file citation
1326 if ( isset( $annotation['type'] ) && $annotation['type'] === 'container_file_citation' ) {
1327 // Initialize files array if needed
1328 if ( !isset( $this->streamCodeInterpreterFiles ) ) {
1329 $this->streamCodeInterpreterFiles = [];
1330 }
1331
1332 // Extract file information
1333 $fileInfo = [
1334 'file_id' => $annotation['file_id'] ?? null,
1335 'filename' => $annotation['filename'] ?? null,
1336 'container_id' => $annotation['container_id'] ?? null
1337 ];
1338
1339 // Store the file info if we have a file_id
1340 if ( $fileInfo['file_id'] ) {
1341 $this->streamCodeInterpreterFiles[] = $fileInfo;
1342
1343 // Also store container ID if available
1344 if ( $fileInfo['container_id'] && !$this->streamContainerId ) {
1345 $this->streamContainerId = $fileInfo['container_id'];
1346 }
1347
1348 Meow_MWAI_Logging::log( 'Responses API: File citation - ' . $fileInfo['filename'] . ' (' . $fileInfo['file_id'] . ')' );
1349 }
1350 }
1351 }
1352 break;
1353
1354 case 'response.completed':
1355 // Response fully completed - function calls are already handled in response.output_item.done
1356 break;
1357
1358 // ===== ERROR EVENTS =====
1359
1360 case 'error':
1361 // Generic error event
1362 $error = $json['error'] ?? $json;
1363 $message = $error['message'] ?? 'Unknown error occurred';
1364 $code = $error['code'] ?? null;
1365 if ( $code ) {
1366 $message .= " (Code: $code)";
1367 }
1368 throw new Exception( $message );
1369
1370 default:
1371 // Unknown event type - log for debugging
1372 Meow_MWAI_Logging::log( 'Responses API: Unknown event type: ' . $eventType );
1373
1374 // Check if this might be a different streaming format
1375 if ( isset( $json['delta'] ) && is_string( $json['delta'] ) ) {
1376 $content = $json['delta'];
1377 }
1378 elseif ( isset( $json['content'] ) && is_string( $json['content'] ) ) {
1379 $content = $json['content'];
1380 }
1381 }
1382
1383 // Handle usage data
1384 $usage = $json['usage'] ?? [];
1385 if ( isset( $usage['input_tokens'], $usage['output_tokens'] ) ) {
1386 $this->streamInTokens = (int) $usage['input_tokens'];
1387 $this->streamOutTokens = (int) $usage['output_tokens'];
1388 if ( isset( $usage['cost'] ) ) {
1389 $this->streamCost = (float) $usage['cost'];
1390 }
1391 }
1392
1393 return $content;
1394 }
1395
1396 /**
1397 * Override stream data handler to support both APIs
1398 */
1399 protected function stream_data_handler( $json ) {
1400 // Check if this is a Responses API event (uses 'type' field)
1401 if ( isset( $json['type'] ) && strpos( $json['type'], 'response.' ) === 0 ) {
1402 return $this->responses_stream_data_handler( $json );
1403 }
1404
1405 // Fallback to ChatML handler
1406 return parent::stream_data_handler( $json );
1407 }
1408
1409 /**
1410 * Override reset to include OpenAI-specific state
1411 */
1412 protected function reset_request_state() {
1413 parent::reset_request_state();
1414
1415 // Reset OpenAI-specific state
1416 $this->streamImages = [];
1417 $this->streamContentCode = '';
1418 $this->streamContainerId = null;
1419 $this->streamCodeInterpreterFiles = [];
1420 $this->codeInterpreterCompleted = false;
1421 }
1422
1423 /**
1424 * Override run_completion_query to route to appropriate API
1425 */
1426 public function run_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
1427 // Reset request-specific state to prevent leakage between requests
1428 $this->reset_request_state();
1429
1430 // Store current query for should_use_responses_api check
1431 $this->currentQuery = $query;
1432
1433 // Check if this is a GPT-5 model
1434 $isGpt5Model = strpos( $query->model, 'gpt-5' ) === 0;
1435
1436 // Debug: Always log which API we're using
1437 $useResponsesApi = $this->should_use_responses_api( $query->model );
1438
1439 // GPT-5 models MUST use Responses API
1440 if ( $isGpt5Model && !$useResponsesApi ) {
1441 $options = $this->core->get_all_options();
1442 $responsesApiEnabled = $options['ai_responses_api'] ?? true;
1443
1444 if ( !$responsesApiEnabled ) {
1445 throw new Exception( 'GPT-5 models require the Responses API to be enabled. Please enable "Use Responses API" in AI Engine settings.' );
1446 }
1447
1448 // If Responses API is enabled but model doesn't have the tag, force it
1449 return $this->run_responses_completion_query( $query, $streamCallback );
1450 }
1451
1452 // Check if we should use Responses API
1453 if ( $useResponsesApi ) {
1454 return $this->run_responses_completion_query( $query, $streamCallback );
1455 }
1456
1457 // Fallback to ChatML implementation
1458 return parent::run_completion_query( $query, $streamCallback );
1459 }
1460
1461 /**
1462 * Run completion query using Responses API
1463 */
1464 protected function run_responses_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
1465 // Check if we have functions that might require feedback
1466 $hasFunctions = !empty( $query->functions );
1467
1468
1469 $isStreaming = !is_null( $streamCallback );
1470
1471 // Initialize debug mode
1472 $this->init_debug_mode( $query );
1473
1474 if ( $isStreaming ) {
1475 $this->streamCallback = $streamCallback;
1476 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1477 }
1478
1479 $this->reset_stream();
1480 $body = $this->build_responses_body( $query, $streamCallback );
1481 $url = $this->build_responses_url();
1482 $headers = $this->build_headers( $query );
1483 $options = $this->build_options( $headers, $body );
1484
1485 // Store the request body for debugging
1486 $this->lastRequestBody = $body;
1487
1488 // Debug log for Responses API
1489 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
1490 if ( $queries_debug ) {
1491 error_log( '[AI Engine Queries] Using Responses API' );
1492 error_log( '[AI Engine Queries] Request URL: ' . $url );
1493 error_log( '[AI Engine Queries] Request Body: ' . json_encode( $body, JSON_PRETTY_PRINT ) );
1494
1495 // Log specific tool information
1496 if ( isset( $body['tools'] ) && is_array( $body['tools'] ) ) {
1497 error_log( '[AI Engine Queries] Tools included in request:' );
1498 foreach ( $body['tools'] as $index => $tool ) {
1499 $toolInfo = 'Tool ' . $index . ': type=' . ( $tool['type'] ?? 'unknown' );
1500 if ( $tool['type'] === 'file_search' && isset( $tool['vector_store_ids'] ) ) {
1501 $toolInfo .= ', vector_store_ids=' . json_encode( $tool['vector_store_ids'] );
1502 }
1503 error_log( '[AI Engine Queries] - ' . $toolInfo );
1504 }
1505 } else {
1506 error_log( '[AI Engine Queries] No tools included in request' );
1507 }
1508 }
1509
1510 // Emit "Request sent" event for feedback queries
1511 if ( $this->currentDebugMode && !empty( $streamCallback ) &&
1512 ( $query instanceof Meow_MWAI_Query_Feedback || $query instanceof Meow_MWAI_Query_AssistFeedback ) ) {
1513 $event = Meow_MWAI_Event::request_sent()
1514 ->set_metadata( 'is_feedback', true )
1515 ->set_metadata( 'feedback_count', count( $query->blocks ) );
1516 call_user_func( $streamCallback, $event );
1517 }
1518
1519 try {
1520 // Log the input being sent for feedback queries
1521 if ( $queries_debug && $query instanceof Meow_MWAI_Query_Feedback && isset( $body['input'] ) ) {
1522 error_log( '[AI Engine Queries] Sending feedback with ' . count( $body['input'] ) . ' messages to Responses API' );
1523 error_log( '[AI Engine Queries] Previous Response ID: ' . ( $body['previous_response_id'] ?? 'none' ) );
1524 foreach ( $body['input'] as $idx => $msg ) {
1525 $msgType = is_array( $msg ) && isset( $msg['type'] ) ? $msg['type'] : 'unknown';
1526 $callId = is_array( $msg ) && isset( $msg['call_id'] ) ? $msg['call_id'] : 'no-id';
1527 error_log( '[AI Engine Queries] Message ' . $idx . ': type=' . $msgType . ', call_id=' . $callId );
1528 if ( $msgType === 'function_call' && isset( $msg['name'] ) ) {
1529 error_log( '[AI Engine Queries] Function name: ' . $msg['name'] );
1530 }
1531 if ( $msgType === 'function_call_output' && isset( $msg['output'] ) ) {
1532 error_log( '[AI Engine Queries] Output: ' . substr( $msg['output'], 0, 50 ) . '...' );
1533 }
1534 }
1535 }
1536
1537 $res = $this->run_query( $url, $options, $streamCallback );
1538 $reply = new Meow_MWAI_Reply( $query );
1539
1540 $returned_id = null;
1541 $returned_model = $this->inModel;
1542 $returned_in_tokens = null;
1543 $returned_out_tokens = null;
1544 $returned_price = null;
1545 $returned_choices = [];
1546
1547 // Streaming Mode
1548 if ( $isStreaming ) {
1549 if ( empty( $this->streamContent ) ) {
1550 $error = $this->try_decode_error( $this->streamBuffer );
1551 if ( !is_null( $error ) ) {
1552 throw new Exception( $error );
1553 }
1554 }
1555
1556 $returned_id = $this->inId;
1557 $returned_model = $this->inModel ? $this->inModel : $query->model;
1558 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
1559
1560 // Store code interpreter code if any
1561 if ( !empty( $this->streamContentCode ) ) {
1562 $reply->contentCode = $this->streamContentCode;
1563 Meow_MWAI_Logging::log( 'Responses API: Stored ' . strlen( $this->streamContentCode ) . ' bytes of code interpreter code' );
1564 }
1565
1566 // REMOVED - We'll handle files after streaming completes, not here
1567
1568 if ( !empty( $this->streamToolCalls ) ) {
1569 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1570 error_log( '[AI Engine Queries] Responses API: Found ' . count( $this->streamToolCalls ) . ' tool calls in streaming response' );
1571 foreach ( $this->streamToolCalls as $idx => $toolCall ) {
1572 error_log( '[AI Engine Queries] Tool call ' . $idx . ': ' . $toolCall['function']['name'] . ' (id: ' . $toolCall['id'] . ')' );
1573 }
1574 }
1575 $message['tool_calls'] = $this->streamToolCalls;
1576 }
1577
1578 if ( !is_null( $this->streamInTokens ) ) {
1579 $returned_in_tokens = $this->streamInTokens;
1580 }
1581 if ( !is_null( $this->streamOutTokens ) ) {
1582 $returned_out_tokens = $this->streamOutTokens;
1583 }
1584 if ( !is_null( $this->streamCost ) ) {
1585 $returned_price = $this->streamCost;
1586 }
1587
1588 // Handle code interpreter sandbox files ONLY if code interpreter has completed
1589 if ( !empty( $this->streamContainerId ) && !empty( $this->streamContent ) && $this->codeInterpreterCompleted ) {
1590 // Check for sandbox links before processing
1591 if ( strpos( $this->streamContent, 'sandbox:' ) !== false ) {
1592 // Pass file citations if available
1593 $fileCitations = isset( $this->streamCodeInterpreterFiles ) ? $this->streamCodeInterpreterFiles : [];
1594
1595 // Download files and replace sandbox links
1596 $this->streamContent = $this->handle_code_interpreter_sandbox_files(
1597 $this->streamContent,
1598 $this->streamContainerId,
1599 $query,
1600 $fileCitations,
1601 true // streaming mode
1602 );
1603 }
1604
1605 // Update the message content with replaced links
1606 $message['content'] = $this->streamContent;
1607 }
1608
1609 $returned_choices = [ [ 'message' => $message ] ];
1610
1611 // Add generated images to the content if any
1612 if ( !empty( $this->streamImages ) ) {
1613 // Add images as additional choices with b64_json format
1614 foreach ( $this->streamImages as $base64Image ) {
1615 $returned_choices[] = [ 'b64_json' => $base64Image ];
1616 }
1617 Meow_MWAI_Logging::log( 'Responses API: Added ' . count( $this->streamImages ) . ' images to choices (streaming)' );
1618 }
1619
1620 // Log streaming response data if queries debug is enabled
1621 if ( $queries_debug ) {
1622 error_log( '[AI Engine Queries] Streaming Response Collected:' );
1623 $streaming_data = [
1624 'id' => $returned_id,
1625 'model' => $returned_model,
1626 'content_length' => strlen( $this->streamContent ),
1627 'content_preview' => substr( $this->streamContent, 0, 200 ) . ( strlen( $this->streamContent ) > 200 ? '...' : '' ),
1628 'tool_calls' => !empty( $this->streamToolCalls ) ? count( $this->streamToolCalls ) . ' tool calls' : 'none',
1629 'usage' => [
1630 'input_tokens' => $returned_in_tokens,
1631 'output_tokens' => $returned_out_tokens,
1632 'cost' => $returned_price
1633 ]
1634 ];
1635
1636 // Log tool calls details if present
1637 if ( !empty( $this->streamToolCalls ) ) {
1638 $streaming_data['tool_calls_details'] = [];
1639 foreach ( $this->streamToolCalls as $tool_call ) {
1640 $streaming_data['tool_calls_details'][] = [
1641 'id' => $tool_call['id'] ?? 'unknown',
1642 'name' => $tool_call['function']['name'] ?? 'unknown',
1643 'arguments' => substr( $tool_call['function']['arguments'] ?? '{}', 0, 100 ) . '...'
1644 ];
1645 }
1646 }
1647
1648 error_log( json_encode( $streaming_data, JSON_PRETTY_PRINT ) );
1649 }
1650 }
1651 // Standard Mode
1652 else {
1653 $data = $res['data'];
1654 if ( empty( $data ) ) {
1655 throw new Exception( 'No content received (res is null).' );
1656 }
1657
1658 // Debug logging for non-streaming mode
1659 if ( $queries_debug ) {
1660 error_log( '[AI Engine Queries] Full response structure (non-streaming):' );
1661 error_log( json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
1662
1663 // Look for container_id in the response
1664 $this->search_for_container_id_recursive( $data, '' );
1665 }
1666
1667 // Ensure $data is an array
1668 if ( !is_array( $data ) ) {
1669 $error_message = is_string( $data ) ? $data : 'Invalid response format';
1670 throw new Exception( 'Responses API error: ' . $error_message );
1671 }
1672
1673 // Handle Responses API response format
1674 $returned_id = $data['id'] ?? null;
1675 $returned_model = $data['model'] ?? $query->model;
1676
1677 // Extract content from Responses API format
1678 $content = '';
1679 $tool_calls = [];
1680 $images = [];
1681
1682
1683 if ( isset( $data['output'] ) && is_array( $data['output'] ) ) {
1684
1685 foreach ( $data['output'] as $idx => $output_item ) {
1686 if ( isset( $output_item['type'] ) && $output_item['type'] === 'message' && isset( $output_item['content'] ) ) {
1687 // Handle message content array - this is the actual text content
1688 if ( is_array( $output_item['content'] ) ) {
1689 foreach ( $output_item['content'] as $content_item ) {
1690 // The actual text is in content_item['text'] for type 'output_text'
1691 if ( isset( $content_item['type'] ) && $content_item['type'] === 'output_text' && isset( $content_item['text'] ) ) {
1692 $content .= $content_item['text'];
1693 }
1694 // Fallback checks for other possible structures
1695 elseif ( isset( $content_item['content'] ) && is_string( $content_item['content'] ) ) {
1696 $content .= $content_item['content'];
1697 }
1698 elseif ( is_string( $content_item ) ) {
1699 $content .= $content_item;
1700 }
1701 }
1702 }
1703 }
1704 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'function_call' ) {
1705 // Responses API returns function_call type with call_id
1706 $callId = $output_item['call_id'] ?? $output_item['id'] ?? null;
1707 $functionName = $output_item['name'] ?? '';
1708 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1709 error_log( '[AI Engine Queries] Found function_call: ' . $functionName . ' (call_id: ' . $callId . ')' );
1710 }
1711
1712 $tool_calls[] = [
1713 'id' => $callId,
1714 'type' => 'function',
1715 'function' => [
1716 'name' => $functionName,
1717 'arguments' => $output_item['arguments'] ?? '{}'
1718 ]
1719 ];
1720 }
1721 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'code_interpreter_call' ) {
1722 // Handle code interpreter calls - both with and without results
1723
1724 // Store container ID if available (this is the primary location)
1725 if ( isset( $output_item['container_id'] ) ) {
1726 $codeInterpreterContainerId = $output_item['container_id'];
1727 Meow_MWAI_Logging::log( 'Responses API: Found container_id for code interpreter: ' . $codeInterpreterContainerId );
1728 }
1729
1730 // Log the entire output_item structure for debugging
1731 if ( $queries_debug ) {
1732 error_log( '[AI Engine Queries] Code interpreter output_item structure:' );
1733 error_log( json_encode( $output_item, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
1734 }
1735
1736 // Handle results if they exist
1737 if ( isset( $output_item['result'] ) ) {
1738 $result = $output_item['result'];
1739
1740 // Also check for container_id in the result itself (backup location)
1741 if ( isset( $result['container_id'] ) && !isset( $codeInterpreterContainerId ) ) {
1742 $codeInterpreterContainerId = $result['container_id'];
1743 Meow_MWAI_Logging::log( 'Responses API: Found container_id in result: ' . $codeInterpreterContainerId );
1744 }
1745
1746 // Append stdout to content if available
1747 if ( isset( $result['stdout'] ) && !empty( $result['stdout'] ) ) {
1748 $content .= "\n```\n" . $result['stdout'] . "\n```\n";
1749 Meow_MWAI_Logging::log( 'Responses API: Found code interpreter output in non-streaming mode' );
1750 }
1751 }
1752 }
1753 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'image_generation_call' && isset( $output_item['result'] ) ) {
1754 // Handle image generation results
1755 $base64Image = $output_item['result'];
1756 $images[] = $base64Image;
1757
1758 Meow_MWAI_Logging::log( 'Responses API: Found generated image in non-streaming mode' );
1759 }
1760 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'mcp_approval_request' ) {
1761 // IMPORTANT: MCP approval requests are already handled via streaming events
1762 // We must skip them here to prevent duplicate function calls
1763 // MCP tools are executed remotely by OpenAI and don't need local execution
1764 Meow_MWAI_Logging::log( 'Responses API: Skipping MCP approval request for ' . $output_item['name'] . ' (already handled via events)' );
1765 }
1766 }
1767 }
1768
1769 // If we couldn't find content in output, try other locations
1770 if ( empty( $content ) ) {
1771 if ( isset( $data['text'] ) ) {
1772 if ( is_string( $data['text'] ) ) {
1773 $content = $data['text'];
1774 }
1775 elseif ( is_array( $data['text'] ) ) {
1776 // Only implode if it's an array of strings, not complex structures
1777 $textParts = array_filter( $data['text'], 'is_string' );
1778 if ( !empty( $textParts ) ) {
1779 $content = implode( '', $textParts );
1780 }
1781 }
1782 }
1783 elseif ( isset( $data['content'] ) ) {
1784 if ( is_array( $data['content'] ) && isset( $data['content'][0]['text'] ) ) {
1785 $content = $data['content'][0]['text'];
1786 }
1787 elseif ( is_string( $data['content'] ) ) {
1788 $content = $data['content'];
1789 }
1790 }
1791 }
1792
1793 // If still no content found, log for debugging
1794 if ( empty( $content ) ) {
1795 // Check if $data is actually an array before using array_keys
1796 if ( is_array( $data ) ) {
1797 Meow_MWAI_Logging::log( 'Responses API: No content found in response. Structure: ' . json_encode( array_keys( $data ) ) );
1798 if ( isset( $data['output'][0] ) ) {
1799 Meow_MWAI_Logging::log( 'Responses API: First output item: ' . json_encode( $data['output'][0] ) );
1800 }
1801 if ( isset( $data['text'] ) ) {
1802 Meow_MWAI_Logging::log( 'Responses API: Text field structure: ' . json_encode( $data['text'] ) );
1803 }
1804 } else {
1805 // If $data is not an array, it might be an error string
1806 Meow_MWAI_Logging::log( 'Responses API: Invalid response data type. Data: ' . ( is_string( $data ) ? $data : json_encode( $data ) ) );
1807 }
1808 // Log the entire response for debugging
1809 Meow_MWAI_Logging::log( 'Responses API: Full response data: ' . json_encode( $data ) );
1810 }
1811
1812
1813 // Handle code interpreter sandbox files if we have a container ID
1814 if ( !empty( $codeInterpreterContainerId ) ) {
1815 $content = $this->handle_code_interpreter_sandbox_files(
1816 $content,
1817 $codeInterpreterContainerId,
1818 $query
1819 );
1820 }
1821
1822 $message = [ 'role' => 'assistant', 'content' => $content ];
1823 if ( !empty( $tool_calls ) ) {
1824 $message['tool_calls'] = $tool_calls;
1825 Meow_MWAI_Logging::log( 'Responses API: Found ' . count( $tool_calls ) . ' tool calls' );
1826 }
1827
1828 $returned_choices = [[ 'message' => $message ]];
1829
1830 // Add images as additional choices
1831 if ( !empty( $images ) ) {
1832 foreach ( $images as $base64Image ) {
1833 $returned_choices[] = [ 'b64_json' => $base64Image ];
1834 }
1835 Meow_MWAI_Logging::log( 'Responses API: Added ' . count( $images ) . ' images to choices' );
1836 }
1837
1838
1839 // Extract usage information
1840 $usage = $data['usage'] ?? [];
1841 $returned_in_tokens = $usage['input_tokens'] ?? null;
1842 $returned_out_tokens = $usage['output_tokens'] ?? null;
1843 $returned_price = $usage['cost'] ?? null;
1844 }
1845
1846 // Store response ID for future stateful requests
1847 if ( !empty( $returned_id ) ) {
1848 $this->previousResponseId = $returned_id;
1849 $reply->set_id( $returned_id );
1850 }
1851 // Set the results
1852 $reply->set_choices( $returned_choices );
1853
1854 // Handle tokens usage
1855 $this->handle_tokens_usage(
1856 $reply,
1857 $query,
1858 $returned_model,
1859 $returned_in_tokens,
1860 $returned_out_tokens,
1861 $returned_price
1862 );
1863
1864 return $reply;
1865 }
1866 catch ( Exception $e ) {
1867 $service = $this->get_service_name();
1868 Meow_MWAI_Logging::error( "$service (Responses API): " . $e->getMessage() );
1869 $message = "$service (Responses API): " . $e->getMessage();
1870 throw new Exception( $message );
1871 }
1872 finally {
1873 if ( !is_null( $streamCallback ) ) {
1874 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1875 }
1876 }
1877 }
1878
1879 /**
1880 * Override handle_tokens_usage to set accuracy properly
1881 */
1882 public function handle_tokens_usage(
1883 $reply,
1884 $query,
1885 $returned_model,
1886 $returned_in_tokens,
1887 $returned_out_tokens,
1888 $returned_price = null
1889 ) {
1890 // Call parent to handle the actual usage recording
1891 parent::handle_tokens_usage(
1892 $reply,
1893 $query,
1894 $returned_model,
1895 $returned_in_tokens,
1896 $returned_out_tokens,
1897 $returned_price
1898 );
1899
1900 // Set accuracy based on data availability
1901 if ( !is_null( $returned_price ) && !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
1902 // Responses API with cost field or OpenRouter style = full accuracy
1903 $reply->set_usage_accuracy( 'full' );
1904 } elseif ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
1905 // Tokens from API but price calculated = tokens accuracy
1906 $reply->set_usage_accuracy( 'tokens' );
1907 } else {
1908 // Everything estimated
1909 $reply->set_usage_accuracy( 'estimated' );
1910 }
1911 }
1912
1913 /**
1914 * Override image query handling for gpt-image-1 model
1915 */
1916 public function run_image_query( $query ) {
1917 // IMPORTANT: We use the standard Images API for gpt-image-1 (not Responses API)
1918 // Even though Responses API supports image_generation tool, it would let the
1919 // orchestrator model choose which image model to use. By using the Images API
1920 // directly, we ensure gpt-image-1 is actually used as requested by the user.
1921
1922 // Use standard implementation for all image models including gpt-image-1
1923 return parent::run_image_query( $query );
1924 }
1925
1926 /**
1927 * Override transcription to support new models
1928 */
1929 public function run_transcribe_query( $query ) {
1930 // Check if using new transcription models
1931 $newTranscribeModels = ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe'];
1932 if ( in_array( $query->model, $newTranscribeModels ) ) {
1933 // These still use the /audio/transcriptions endpoint but with new models
1934 // Just need to make sure the model name is passed correctly
1935 }
1936
1937 // Use parent implementation (still uses audio endpoint)
1938 return parent::run_transcribe_query( $query );
1939 }
1940
1941 /**
1942 * Override embedding query to support new models
1943 */
1944 public function run_embedding_query( $query ) {
1945 // Check if using new embedding models
1946 $newEmbeddingModels = ['text-embedding-3-small', 'text-embedding-3-large'];
1947 if ( in_array( $query->model, $newEmbeddingModels ) ) {
1948 // These still use the /embeddings endpoint but with improved models
1949 // The parent implementation should handle this correctly
1950 }
1951
1952 // Use parent implementation
1953 return parent::run_embedding_query( $query );
1954 }
1955
1956 /**
1957 * Enhanced error handling for Responses API
1958 */
1959 protected function handle_responses_errors( $data ) {
1960 // Handle Responses API specific errors
1961 if ( isset( $data['error'] ) ) {
1962 $error = $data['error'];
1963 $message = $error['message'] ?? 'Unknown error';
1964 $type = $error['type'] ?? null;
1965 $code = $error['code'] ?? null;
1966
1967 // Special handling for "No tool output found" errors
1968 if ( strpos( $message, 'No tool output found' ) !== false ) {
1969 // Log this error with details when queries debug is enabled
1970 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1971 error_log( '[AI Engine Queries] Responses API Tool Output Error:' );
1972 error_log( '[AI Engine Queries] Error: ' . $message );
1973 error_log( '[AI Engine Queries] This typically means the function call outputs were not properly formatted or are missing.' );
1974
1975 // Log the last request body if available
1976 if ( property_exists( $this, 'lastRequestBody' ) && $this->lastRequestBody ) {
1977 error_log( '[AI Engine Queries] Last request body: ' . json_encode( $this->lastRequestBody, JSON_PRETTY_PRINT ) );
1978 }
1979 }
1980 }
1981
1982 $errorMessage = $message;
1983 if ( $type ) {
1984 $errorMessage .= " (Type: $type)";
1985 }
1986 if ( $code ) {
1987 $errorMessage .= " (Code: $code)";
1988 }
1989
1990 throw new Exception( $errorMessage );
1991 }
1992
1993 // Check for event-based errors
1994 if ( isset( $data['event'] ) && $data['event'] === 'response.error' ) {
1995 $error = $data['error'] ?? [];
1996 $message = $error['message'] ?? 'Response API error';
1997 throw new Exception( $message );
1998 }
1999
2000 // Fallback to parent error handling
2001 parent::handle_response_errors( $data );
2002 }
2003
2004 /**
2005 * Add method to reset conversation state
2006 */
2007 public function reset_conversation_state() {
2008 $this->previousResponseId = null;
2009 $this->conversationState = [];
2010 }
2011
2012
2013 /**
2014 * Check the connection to OpenAI by listing models.
2015 * This is a free metadata call that verifies API key validity.
2016 */
2017 public function connection_check() {
2018 try {
2019 $url = $this->get_models_endpoint();
2020 $response = $this->execute( 'GET', $url );
2021
2022 if ( !isset( $response['data'] ) || !is_array( $response['data'] ) ) {
2023 throw new Exception( 'Invalid response format from OpenAI' );
2024 }
2025
2026 $modelCount = count( $response['data'] );
2027 $availableModels = [];
2028
2029 // Get first 5 models for display
2030 $displayModels = array_slice( $response['data'], 0, 5 );
2031 foreach ( $displayModels as $model ) {
2032 if ( isset( $model['id'] ) ) {
2033 $availableModels[] = $model['id'];
2034 }
2035 }
2036
2037 return [
2038 'success' => true,
2039 'service' => 'OpenAI',
2040 'message' => "Connection successful. Found {$modelCount} models.",
2041 'details' => [
2042 'endpoint' => $url,
2043 'model_count' => $modelCount,
2044 'sample_models' => $availableModels,
2045 'organization' => $response['organization'] ?? null
2046 ]
2047 ];
2048 }
2049 catch ( Exception $e ) {
2050 return [
2051 'success' => false,
2052 'service' => 'OpenAI',
2053 'error' => $e->getMessage(),
2054 'details' => [
2055 'endpoint' => $this->get_models_endpoint()
2056 ]
2057 ];
2058 }
2059 }
2060
2061
2062 /**
2063 * Handle code interpreter sandbox files
2064 * Parses sandbox links from content, downloads files, and replaces links
2065 */
2066 protected function handle_code_interpreter_sandbox_files( $content, $containerId, $query, $fileCitations = [], $isStreaming = false ) {
2067 if ( empty( $containerId ) || empty( $content ) ) {
2068 return $content;
2069 }
2070
2071 // Use streamCodeInterpreterFiles if available (from annotations)
2072 if ( !empty( $this->streamCodeInterpreterFiles ) ) {
2073 $fileCitations = $this->streamCodeInterpreterFiles;
2074 }
2075
2076 // Parse sandbox links from content
2077 $sandboxLinks = $this->parse_sandbox_links( $content );
2078
2079 if ( empty( $sandboxLinks ) ) {
2080 return $content;
2081 }
2082
2083 Meow_MWAI_Logging::log( 'Code Interpreter: Processing ' . count( $sandboxLinks ) . ' sandbox files' );
2084
2085 $containerFiles = [];
2086
2087 // If we have file citations, use them directly (skip container list API)
2088 if ( !empty( $fileCitations ) ) {
2089 foreach ( $fileCitations as $citation ) {
2090 if ( isset( $citation['file_id'] ) ) {
2091 $containerFiles[] = [
2092 'id' => $citation['file_id'],
2093 'path' => $citation['path'] ?? ( '/mnt/data/' . $citation['filename'] ),
2094 'filename' => $citation['filename'] ?? null
2095 ];
2096 }
2097 }
2098 } else {
2099 // Only try container API if we don't have file citations
2100 error_log( '[AI Engine] No file citations, will try container list API' );
2101 $containerFiles = $this->list_container_files( $containerId, $query );
2102 }
2103
2104 if ( empty( $containerFiles ) ) {
2105 error_log( '[AI Engine] WARNING: No files found from citations or container API' );
2106 Meow_MWAI_Logging::warn( 'No files found in container ' . $containerId );
2107 return $content;
2108 }
2109
2110
2111
2112 // Process each sandbox link
2113 $replacements = 0;
2114 foreach ( $sandboxLinks as $sandboxPath ) {
2115 $filename = basename( $sandboxPath );
2116
2117 // Find the file in container
2118 $fileId = $this->find_container_file_id( $containerFiles, $filename );
2119
2120 if ( !$fileId ) {
2121 error_log( '[AI Engine] ERROR: File ID not found for: ' . $filename );
2122 Meow_MWAI_Logging::warn( 'Code Interpreter: File not found in container: ' . $filename );
2123 continue;
2124 }
2125
2126
2127 // Try to download the file with retries if streaming
2128 $publicUrl = null;
2129 $maxRetries = $isStreaming ? 3 : 1;
2130 $retryDelay = 2; // seconds
2131
2132 for ( $attempt = 1; $attempt <= $maxRetries; $attempt++ ) {
2133 if ( $attempt > 1 ) {
2134 error_log( '[AI Engine] Retry attempt ' . $attempt . ' after ' . $retryDelay . ' seconds...' );
2135 sleep( $retryDelay );
2136 $retryDelay *= 2; // exponential backoff
2137 }
2138
2139 $publicUrl = $this->download_container_file( $containerId, $fileId, $filename, $query );
2140
2141 if ( $publicUrl ) {
2142 break;
2143 }
2144 }
2145
2146 if ( $publicUrl ) {
2147 // Replace sandbox link with public URL
2148 $content = str_replace( $sandboxPath, $publicUrl, $content );
2149 $replacements++;
2150 Meow_MWAI_Logging::log( 'Replaced sandbox link: ' . $filename . ' -> ' . $publicUrl );
2151 } else {
2152
2153 // If download fails, create a message about it
2154 $errorMessage = sprintf(
2155 '[File: %s - Download temporarily unavailable, refresh page to retry]',
2156 $filename
2157 );
2158 $content = str_replace( $sandboxPath, $errorMessage, $content );
2159 $replacements++;
2160 }
2161 }
2162
2163
2164 return $content;
2165 }
2166
2167 /**
2168 * Parse sandbox links from content
2169 */
2170 protected function parse_sandbox_links( $content ) {
2171 $links = [];
2172
2173 // Match various sandbox link patterns
2174 $patterns = [
2175 '/sandbox:\/mnt\/data\/[^)\s]+/', // Basic pattern
2176 '/\(sandbox:\/mnt\/data\/[^)]+\)/', // In parentheses
2177 '/\[([^\]]*)\]\(sandbox:\/mnt\/data\/[^)]+\)/', // Markdown links
2178 ];
2179
2180 foreach ( $patterns as $pattern ) {
2181 if ( preg_match_all( $pattern, $content, $matches ) ) {
2182 foreach ( $matches[0] as $match ) {
2183 // Extract just the sandbox path
2184 if ( preg_match( '/sandbox:\/mnt\/data\/[^)\s\]]+/', $match, $pathMatch ) ) {
2185 $links[] = $pathMatch[0];
2186 }
2187 }
2188 }
2189 }
2190
2191 return array_unique( $links );
2192 }
2193
2194 /**
2195 * List files in a container
2196 */
2197 protected function list_container_files( $containerId, $query ) {
2198 try {
2199 // Use the execute function with the path format it expects
2200 $path = '/containers/' . $containerId . '/files';
2201
2202 // Try to call the API (remove streaming handler for JSON requests)
2203 $response = null;
2204 try {
2205 $response = $this->without_stream_handler( function() use ( $path ) {
2206 return $this->execute( 'GET', $path, null, null, true );
2207 });
2208 }
2209 catch ( Exception $api_exception ) {
2210 // If it's a 404, the container might not exist yet or might be expired
2211 if ( strpos( $api_exception->getMessage(), '404' ) !== false ) {
2212 // Wait a moment and retry once
2213 sleep( 2 );
2214
2215 try {
2216 $response = $this->without_stream_handler( function() use ( $path ) {
2217 return $this->execute( 'GET', $path, null, null, true );
2218 });
2219 }
2220 catch ( Exception $retry_exception ) {
2221 throw $retry_exception;
2222 }
2223 } else {
2224 throw $api_exception;
2225 }
2226 }
2227
2228 // Check if response is null or empty array
2229 if ( $response === null || ( is_array( $response ) && empty( $response ) ) ) {
2230 // Try waiting a bit for files to be ready
2231 sleep( 3 );
2232
2233 // Try one more time
2234 $response = $this->execute( 'GET', $path, null, null, true );
2235
2236 // If still empty, wait longer and try once more
2237 if ( $response === null || ( is_array( $response ) && empty( $response ) ) ) {
2238 sleep( 5 );
2239 $response = $this->execute( 'GET', $path, null, null, true );
2240 }
2241 }
2242
2243 if ( isset( $response['data'] ) && is_array( $response['data'] ) ) {
2244 return $response['data'];
2245 } else if ( is_array( $response ) && isset( $response[0] ) ) {
2246 // Maybe the response is directly an array of files
2247 return $response;
2248 }
2249 }
2250 catch ( Exception $e ) {
2251 Meow_MWAI_Logging::warn( 'Failed to list container files: ' . $e->getMessage() );
2252 }
2253
2254 return [];
2255 }
2256
2257 /**
2258 * Find file ID by filename in container files list
2259 */
2260 protected function find_container_file_id( $containerFiles, $filename ) {
2261 foreach ( $containerFiles as $file ) {
2262 // Check if filename matches the end of the path
2263 if ( isset( $file['path'] ) ) {
2264 // Handle cases where path might contain multiple filenames separated by spaces
2265 $paths = preg_split( '/\s+/', $file['path'] );
2266 foreach ( $paths as $path ) {
2267 if ( str_ends_with( $path, $filename ) ) {
2268 return $file['id'];
2269 }
2270 }
2271 }
2272
2273 // Also check direct filename match
2274 if ( isset( $file['filename'] ) && $file['filename'] === $filename ) {
2275 return $file['id'];
2276 }
2277 }
2278
2279 return null;
2280 }
2281
2282 /**
2283 * Execute HTTP request without streaming handler interference
2284 */
2285 private function without_stream_handler( callable $fn ) {
2286 $cb = [ $this, 'stream_handler' ];
2287 $had = has_action( 'http_api_curl', $cb );
2288 if ( $had ) {
2289 remove_action( 'http_api_curl', $cb );
2290 }
2291 try {
2292 return $fn();
2293 } finally {
2294 if ( $had ) {
2295 add_action( 'http_api_curl', $cb, 10, 3 );
2296 }
2297 }
2298 }
2299
2300 /**
2301 * Download a file from container and store it locally
2302 */
2303 protected function download_container_file( $containerId, $fileId, $filename, $query ) {
2304 try {
2305 $fileContent = null;
2306
2307 // For container files (cfile_*), we MUST use the Container API
2308 if ( strpos( $fileId, 'cfile_' ) === 0 ) {
2309 if ( empty( $containerId ) ) {
2310 throw new Exception( 'Container ID is required for downloading container files' );
2311 }
2312
2313 // Use the Container API endpoint
2314 $path = '/containers/' . $containerId . '/files/' . $fileId . '/content';
2315
2316 try {
2317 // Remove streaming handler and download binary content
2318 $headers = [ 'Accept' => '*/*' ];
2319 $fileContent = $this->without_stream_handler( function() use ( $path, $headers ) {
2320 // false = raw binary content, not JSON
2321 return $this->execute( 'GET', $path, null, $headers, false );
2322 });
2323
2324 if ( strlen( $fileContent ) > 0 ) {
2325 Meow_MWAI_Logging::log( 'Container API: Downloaded ' . strlen( $fileContent ) . ' bytes for ' . $filename );
2326 } else {
2327 throw new Exception( 'Container file returned empty content' );
2328 }
2329 } catch ( Exception $e ) {
2330 throw $e;
2331 }
2332 } else {
2333 // Regular file_* files use the standard Files API
2334 $filesPath = '/files/' . $fileId . '/content';
2335 $headers = [ 'Accept' => '*/*' ];
2336 $fileContent = $this->without_stream_handler( function() use ( $filesPath, $headers ) {
2337 return $this->execute( 'GET', $filesPath, null, $headers, false );
2338 });
2339 }
2340
2341 if ( empty( $fileContent ) ) {
2342 error_log( '[AI Engine] ERROR: Both APIs failed to return content' );
2343 throw new Exception( 'Empty file content received from both Files API and Container API' );
2344 }
2345
2346 // Save to temporary file
2347 $tmpFile = tempnam( sys_get_temp_dir(), 'mwai_code_' );
2348 file_put_contents( $tmpFile, $fileContent );
2349
2350 // Upload to our file system
2351 $purpose = 'assistant-out';
2352 $metadata = [
2353 'source' => 'code_interpreter',
2354 'container_id' => $containerId,
2355 'file_id' => $fileId
2356 ];
2357
2358 $refId = $this->core->files->upload_file( $tmpFile, $filename, $purpose, $metadata, $query->envId );
2359
2360 // Update the file's refId to match the OpenAI file ID
2361 $internalFileId = $this->core->files->get_id_from_refId( $refId );
2362 $this->core->files->update_refId( $internalFileId, $fileId );
2363
2364 // Get the public URL
2365 $publicUrl = $this->core->files->get_url( $fileId );
2366
2367 // Clean up temp file
2368 @unlink( $tmpFile );
2369
2370 return $publicUrl;
2371 }
2372 catch ( Exception $e ) {
2373 error_log( '[AI Engine] EXCEPTION in download_container_file: ' . $e->getMessage() );
2374 error_log( '[AI Engine] Stack trace: ' . $e->getTraceAsString() );
2375 Meow_MWAI_Logging::warn( 'Failed to download container file ' . $filename . ': ' . $e->getMessage() );
2376 return null;
2377 }
2378 }
2379
2380
2381 /**
2382 * Get the models endpoint URL
2383 */
2384 protected function get_models_endpoint() {
2385 $endpoint = null;
2386
2387 // Same logic as build_url to determine the endpoint
2388 if ( $this->envType === 'openai' ) {
2389 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
2390 }
2391 else if ( $this->envType === 'azure' ) {
2392 $endpoint = isset( $this->env['endpoint'] ) ? $this->env['endpoint'] : null;
2393 }
2394
2395 if ( empty( $endpoint ) ) {
2396 throw new Exception( 'Endpoint is not defined for envType: ' . $this->envType );
2397 }
2398
2399 // Remove any existing API paths to get base URL
2400 $endpoint = str_replace( '/chat/completions', '', $endpoint );
2401 $endpoint = str_replace( '/v1/responses', '', $endpoint );
2402 $endpoint = rtrim( $endpoint, '/' );
2403
2404 // For Azure, we don't need the /v1 prefix
2405 if ( $this->envType === 'azure' ) {
2406 // Use the same API version as defined in the parent class
2407 return $endpoint . '/openai/models?api-version=2024-12-01-preview';
2408 }
2409
2410 // For OpenAI, ensure we have the /v1 prefix
2411 if ( strpos( $endpoint, '/v1' ) === false ) {
2412 $endpoint .= '/v1';
2413 }
2414
2415 return $endpoint . '/models';
2416 }
2417
2418 }
2419