PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.2.7
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.2.7
3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
anthropic.php 8 months ago chatml.php 7 months ago core.php 7 months ago factory.php 8 months ago google.php 7 months ago mistral.php 9 months ago open-router.php 7 months ago openai.php 7 months ago perplexity.php 10 months ago replicate.php 7 months ago
openai.php
2887 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 protected $codeInterpreterCompleted = false; // Track if code interpreter has completed
38 // IMPORTANT: OpenAI Responses API sends the same function call in both:
39 // 1. response.output_item.done - when individual function call completes
40 // 2. response.completed - with all function calls in the final response
41 // We must deduplicate to avoid processing the same function twice
42
43 public static function create( $core, $env ) {
44 self::$creating = true;
45 if ( class_exists( 'MeowPro_MWAI_OpenAI' ) ) {
46 $instance = new MeowPro_MWAI_OpenAI( $core, $env );
47 }
48 else {
49 $instance = new self( $core, $env );
50 }
51 self::$creating = false;
52 return $instance;
53 }
54
55 public function __construct( $core, $env ) {
56 $isOwnClass = get_class( $this ) === 'Meow_MWAI_Engines_OpenAI';
57 if ( $isOwnClass && !self::$creating ) {
58 throw new \Exception( 'Please use the create() method to instantiate the Meow_MWAI_Engines_OpenAI class.' );
59 }
60 parent::__construct( $core, $env );
61 $this->set_environment();
62 }
63
64 public function reset_stream() {
65 parent::reset_stream();
66 $this->mcpServerCount = 0;
67 $this->mcpTotalToolCount = 0;
68 $this->emittedFunctionResults = [];
69 $this->streamImages = [];
70 $this->seenCallIds = [];
71 }
72
73 /**
74 * Check if a model should use the new Responses API
75 */
76 protected function should_use_responses_api( $model ) {
77 // First check if Responses API is enabled in settings
78 $options = $this->core->get_all_options();
79 $responsesApiEnabled = $options['ai_responses_api'] ?? true;
80
81 if ( !$responsesApiEnabled ) {
82 return false;
83 }
84
85 // Check if this is a prompt query - prompts REQUIRE Responses API
86 if ( isset( $this->currentQuery ) ) {
87 $promptData = $this->currentQuery->getExtraParam( 'prompt' );
88 if ( !empty( $promptData ) && !empty( $promptData['id'] ) ) {
89 return true;
90 }
91 }
92
93 // Azure supports Responses API in preview
94 // Model tag check below will determine if the specific model supports it
95
96 // Check if the model has the 'responses' tag
97 $modelInfo = $this->retrieve_model_info( $model );
98 if ( $modelInfo && !empty( $modelInfo['tags'] ) ) {
99 return in_array( 'responses', $modelInfo['tags'] );
100 }
101
102 return false;
103 }
104
105 /**
106 * Set conversation state for stateful responses
107 */
108 public function set_previous_response_id( $responseId ) {
109 $this->previousResponseId = $responseId;
110 }
111
112 /**
113 * Get conversation state
114 */
115 public function get_conversation_state() {
116 return $this->conversationState;
117 }
118
119 /**
120 * Build body for Responses API
121 */
122 protected function build_responses_body( $query, $streamCallback = null ) {
123 // For Azure, we need to use the deployment name as the model
124 $model = $query->model;
125 if ( $this->envType === 'azure' ) {
126 // Find the deployment name for this model
127 if ( isset( $this->env['deployments'] ) && is_array( $this->env['deployments'] ) ) {
128 foreach ( $this->env['deployments'] as $deployment ) {
129 if ( isset( $deployment['model'] ) && $deployment['model'] === $query->model && isset( $deployment['name'] ) ) {
130 $model = $deployment['name'];
131 break;
132 }
133 }
134 }
135 }
136
137 $body = [
138 'model' => $model,
139 'stream' => !is_null( $streamCallback ),
140 ];
141
142 // Handle different query types for Responses API
143 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
144 // Check if using Prompt mode
145 $promptData = $query->getExtraParam( 'prompt' );
146 if ( !empty( $promptData ) && !empty( $promptData['id'] ) ) {
147 // Use prompt instead of instructions
148 $body['prompt'] = $promptData;
149 // Remove model since it's configured in the prompt
150 unset( $body['model'] );
151 }
152 else if ( !empty( $query->instructions ) ) {
153 // Use simplified instructions + input format for basic queries
154 $body['instructions'] = $query->instructions;
155 }
156
157 // Determine history strategy
158 $historyStrategy = $query->historyStrategy;
159
160 // Treat empty string as null for automatic mode
161 if ( empty( $historyStrategy ) ) {
162 $historyStrategy = null;
163 }
164
165 // If historyStrategy is null (automatic), use response_id when previousResponseId is available
166 if ( $historyStrategy === null && !empty( $query->previousResponseId ) ) {
167 $historyStrategy = 'response_id';
168 }
169
170 // Debug logging for all queries when using Responses API
171 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
172
173 if ( $queries_debug ) {
174 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
175 error_log( '[AI Engine] Feedback query blocks: ' . count( $query->blocks ?? [] ) );
176 }
177 }
178
179 // Handle based on history strategy
180 // For Responses API, feedback queries MUST use previous_response_id to maintain conversation state
181 if ( $historyStrategy === 'response_id' && !empty( $query->previousResponseId ) ) {
182 // Use ResponseIdManager to validate the response ID
183 if ( $this->core->responseIdManager->is_valid_for_responses_api( $query->previousResponseId ) ) {
184 // Use incremental mode with previous_response_id
185 $body['previous_response_id'] = $query->previousResponseId;
186
187 // Debug logging
188 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
189 if ( $queries_debug ) {
190 error_log( '[AI Engine Queries] Using previous_response_id: ' . $query->previousResponseId );
191 }
192 }
193 else {
194 // Log warning if queries debug is enabled
195 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
196 if ( $queries_debug ) {
197 error_log( '[AI Engine Queries] Warning: ' .
198 Meow_MWAI_FunctionCallException::invalid_response_id(
199 $query->previousResponseId,
200 'Responses API',
201 'resp'
202 )->getMessage() );
203 }
204 // Fall through to full history mode
205 $historyStrategy = 'full_history';
206 }
207
208 }
209
210 // If we're still in response_id mode after validation, use incremental input
211 if ( $historyStrategy === 'response_id' && !empty( $body['previous_response_id'] ) ) {
212 // Check if this is a feedback query (function call response)
213 if ( $query instanceof Meow_MWAI_Query_Feedback && !empty( $query->blocks ) ) {
214 // For feedback queries with previous_response_id, we need to include:
215 // 1. The function_call from the model
216 // 2. The function_call_output with the result
217 $body['input'] = $this->build_feedback_input_for_responses_api( $query );
218
219 // Debug: Log the feedback input structure
220 if ( $queries_debug ) {
221 error_log( '[AI Engine Queries] Feedback input structure: ' . json_encode( $body['input'], JSON_PRETTY_PRINT ) );
222 }
223 }
224 else {
225 // Regular user message
226 $content = [
227 [
228 'type' => 'input_text',
229 'text' => $query->get_message()
230 ]
231 ];
232
233 // Check for attached files (unified approach)
234 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
235 foreach ( $attachments as $file ) {
236 // Check if it's an image or a file (PDF, etc.) BEFORE trying to get data
237 $mimeType = $file->get_mimeType() ?? '';
238 $isImage = strpos( $mimeType, 'image/' ) === 0;
239
240 if ( $isImage ) {
241 $fileUrl = $query->image_remote_upload === 'url'
242 ? $file->get_url()
243 : $file->get_inline_base64_url();
244 $content[] = [
245 'type' => 'input_image',
246 'image_url' => $fileUrl
247 ];
248 }
249 else {
250 // For non-images (PDFs, documents), use file_id approach
251 // IMPORTANT: Only use files that have been uploaded to OpenAI (provider_file_id type)
252 if ( $file->get_type() === 'provider_file_id' ) {
253 $fileId = $file->get_refId();
254 $content[] = [
255 'type' => 'input_file',
256 'file_id' => $fileId
257 ];
258 }
259 else {
260 // File hasn't been uploaded to OpenAI yet - should have been done in prepare_query
261 Meow_MWAI_Logging::warn( 'Responses API: File not uploaded to OpenAI yet (type: ' . $file->get_type() . ')' );
262 }
263 }
264 }
265
266 $body['input'] = [
267 [
268 'role' => 'user',
269 'content' => $content
270 ]
271 ];
272
273 // Add context if present
274 if ( !empty( $query->context ) ) {
275 // Prepend context as a separate input_text in the same message
276 array_unshift( $body['input'][0]['content'], [
277 'type' => 'input_text',
278 'text' => $query->context . "\n\n"
279 ] );
280 }
281 }
282 }
283 else {
284 // Use full history mode (internal) or when no previous_response_id
285
286 // Build input - always use array format for Responses API
287 $hasAttachments = method_exists( $query, 'getAttachments' ) && !empty( $query->getAttachments() );
288 if ( !empty( $query->messages ) || $hasAttachments || $query instanceof Meow_MWAI_Query_Feedback ) {
289 $body['input'] = $this->build_responses_input_array( $query );
290 }
291 else {
292 // Even for simple text, Responses API expects message format
293 $body['input'] = [
294 [
295 'role' => 'user',
296 'content' => [
297 [
298 'type' => 'input_text',
299 'text' => $query->get_message()
300 ]
301 ]
302 ]
303 ];
304 }
305
306 // Add context if present
307 if ( !empty( $query->context ) ) {
308 if ( isset( $body['input'] ) && is_string( $body['input'] ) ) {
309 $body['input'] = $query->context . "\n\n" . $body['input'];
310 }
311 else {
312 // Add context as system message
313 array_unshift( $body['input'], [
314 'role' => 'system',
315 'content' => $query->context
316 ] );
317 }
318 }
319 }
320
321 // Parameters - skip these when using Prompt mode
322 $promptData = $query->getExtraParam( 'prompt' );
323 $isPromptMode = !empty( $promptData ) && !empty( $promptData['id'] );
324
325 if ( !$isPromptMode ) {
326 if ( !empty( $query->maxTokens ) ) {
327 $body['max_output_tokens'] = $query->maxTokens;
328 }
329
330 // Handle temperature parameter - GPT-5 models don't support it
331 if ( !empty( $query->temperature ) && $query->temperature !== 1 ) {
332 // Check if this is a GPT-5 model (gpt-5, gpt-5-mini, gpt-5-nano)
333 if ( strpos( $query->model, 'gpt-5' ) !== 0 ) {
334 $body['temperature'] = $query->temperature;
335 }
336 // For GPT-5 models, skip the temperature parameter entirely
337 }
338 }
339
340 // Handle reasoning parameter only for models that support it
341 if ( !$isPromptMode && !empty( $query->reasoning ) ) {
342 // Check if the model has the 'reasoning' tag
343 $modelInfo = $this->retrieve_model_info( $query->model );
344 if ( $modelInfo && !empty( $modelInfo['tags'] ) && in_array( 'reasoning', $modelInfo['tags'] ) ) {
345 // Add reasoning parameter as an object (Responses API expects object)
346 // { reasoning: { effort: 'none|minimal|low|medium|high' } }
347 $body['reasoning'] = [ 'effort' => $query->reasoning ];
348 }
349 }
350
351 // Handle verbosity parameter only for models that support it
352 if ( !$isPromptMode && !empty( $query->verbosity ) ) {
353 // Check if the model has the 'verbosity' tag
354 $modelInfo = $this->retrieve_model_info( $query->model );
355 if ( $modelInfo && !empty( $modelInfo['tags'] ) && in_array( 'verbosity', $modelInfo['tags'] ) ) {
356 // Add verbosity parameter if set (inside text object)
357 if ( !isset( $body['text'] ) || !is_array( $body['text'] ) ) {
358 $body['text'] = [];
359 }
360 $body['text']['verbosity'] = $query->verbosity;
361 }
362 }
363
364 // Note: The Responses API does not support the 'n' parameter for multiple results
365 // Unlike the Chat Completions API, Responses API generates one response at a time
366 // If multiple results are needed, separate requests must be made
367 // Reference: https://platform.openai.com/docs/api-reference/responses
368 if ( !empty( $query->maxResults ) && $query->maxResults > 1 ) {
369 Meow_MWAI_Logging::warn( 'Responses API does not support multiple results (n parameter). Only one result will be generated.' );
370 }
371
372 if ( !empty( $query->stop ) ) {
373 $body['stop'] = $query->stop;
374 }
375
376 if ( !empty( $query->responseFormat ) && $query->responseFormat === 'json' ) {
377 // Responses API uses 'text.format' instead of 'response_format'
378 if ( !isset( $body['text'] ) || !is_array( $body['text'] ) ) {
379 $body['text'] = [];
380 }
381 $body['text']['format'] = [ 'type' => 'json_object' ];
382 }
383
384 // Function calling - convert to tools
385 // IMPORTANT: Tools must be included in ALL requests, even when using previous_response_id
386 // The API needs to know which functions are available throughout the entire conversation
387 if ( !empty( $query->functions ) ) {
388 $body['tools'] = $this->build_responses_tools( $query->functions );
389 // IMPORTANT: Enable parallel tool calls to allow multiple function calls in one response
390 // TODO: OpenAI's Responses API has a bug where it only returns ONE function call even when
391 // parallel_tool_calls=true is set and multiple functions are clearly needed. This works correctly
392 // with the Chat Completions API. Monitor OpenAI's updates and test again in the future.
393 // Issue discovered: August 2025 - Only getDeskTemperature is called when both desk AND outdoor are requested.
394 $body['parallel_tool_calls'] = true;
395 }
396
397 // Add MCP servers if available
398 if ( isset( $query->mcpServers ) && is_array( $query->mcpServers ) && !empty( $query->mcpServers ) ) {
399 $mcp_envs = $this->core->get_option( 'mcp_envs' );
400 $this->mcpServerCount = count( $query->mcpServers );
401
402 foreach ( $query->mcpServers as $mcpServer ) {
403 if ( isset( $mcpServer['id'] ) ) {
404 // Find the full MCP server configuration by ID
405 foreach ( $mcp_envs as $env ) {
406 if ( $env['id'] === $mcpServer['id'] ) {
407 // Sanitize server label for OpenAI requirements
408 $server_label = $env['name'] . '_' . $env['id'];
409 // Remove spaces and special characters
410 $server_label = preg_replace( '/[^a-zA-Z0-9_]/', '', $server_label );
411 // Replace double or tripe underscores with single underscore
412 $server_label = preg_replace( '/_{2,}/', '_', $server_label );
413 // Ensure it starts with a letter
414 if ( !preg_match( '/^[a-zA-Z]/', $server_label ) ) {
415 $server_label = 'mcp_' . $server_label;
416 }
417
418 $mcp_tool = [
419 'type' => 'mcp',
420 'server_label' => $server_label,
421 'server_url' => $env['url'],
422 'require_approval' => 'never'
423 ];
424
425 // Add authorization header if available
426 if ( !empty( $env['token'] ) ) {
427 $mcp_tool['headers'] = [
428 'Authorization' => 'Bearer ' . $env['token']
429 ];
430 }
431
432 // Add to tools array
433 if ( !isset( $body['tools'] ) ) {
434 $body['tools'] = [];
435 }
436 $body['tools'][] = $mcp_tool;
437
438 break;
439 }
440 }
441 }
442 }
443 }
444
445 // Add tool_choice parameter if tools are present
446 if ( !empty( $body['tools'] ) ) {
447 // Default to 'auto' to let the model choose
448 $body['tool_choice'] = 'auto';
449 }
450
451 // Add tools (web_search, image_generation, code_interpreter) if specified
452 if ( !empty( $query->tools ) && is_array( $query->tools ) ) {
453
454 // Ensure tools array exists
455 if ( !isset( $body['tools'] ) ) {
456 $body['tools'] = [];
457 }
458
459 // Add each enabled tool
460 foreach ( $query->tools as $tool ) {
461 if ( in_array( $tool, ['web_search', 'image_generation', 'code_interpreter'] ) ) {
462 $toolConfig = [ 'type' => $tool ];
463
464 // Image generation requires partial_images when streaming
465 if ( $tool === 'image_generation' && !empty( $streamCallback ) ) {
466 $toolConfig['partial_images'] = 1;
467 }
468
469 // Code interpreter requires container configuration
470 if ( $tool === 'code_interpreter' ) {
471 $toolConfig['container'] = [ 'type' => 'auto' ];
472 // Add file_ids if available in the query
473 if ( !empty( $query->fileIds ) && is_array( $query->fileIds ) ) {
474 $toolConfig['container']['file_ids'] = $query->fileIds;
475 }
476 // Code interpreter tool configured
477 }
478
479 $body['tools'][] = $toolConfig;
480 Meow_MWAI_Logging::log( 'Responses API: Added tool ' . $tool . ' to request' );
481 }
482 }
483 }
484
485 // Add file_search tool if OpenAI Vector Store is configured
486 if ( !empty( $query->embeddingsEnvId ) ) {
487 Meow_MWAI_Logging::log( 'Responses API: Checking embeddings environment - embeddingsEnvId: ' . $query->embeddingsEnvId );
488
489 $embeddingsEnv = $this->core->get_embeddings_env( $query->embeddingsEnvId );
490
491 if ( $embeddingsEnv && $embeddingsEnv['type'] === 'openai-vector-store' ) {
492 Meow_MWAI_Logging::log( 'Responses API: Found OpenAI Vector Store environment' );
493
494 // Check if the OpenAI environment matches
495 $openai_env_id = $embeddingsEnv['openai_env_id'] ?? null;
496
497 Meow_MWAI_Logging::log( 'Responses API: Comparing environments - embeddings OpenAI env: ' . ( $openai_env_id ?? 'null' ) . ', current env: ' . $this->envId );
498
499 if ( $openai_env_id === $this->envId && !empty( $embeddingsEnv['store_id'] ) ) {
500 // Ensure tools array exists
501 if ( !isset( $body['tools'] ) ) {
502 $body['tools'] = [];
503 }
504
505 // Add file_search tool with vector store ID
506 $body['tools'][] = [
507 'type' => 'file_search',
508 'vector_store_ids' => [ $embeddingsEnv['store_id'] ]
509 ];
510
511 Meow_MWAI_Logging::log( 'Responses API: Added file_search tool with vector store: ' . $embeddingsEnv['store_id'] );
512 }
513 else {
514 if ( $openai_env_id !== $this->envId ) {
515 Meow_MWAI_Logging::log( 'Responses API: Environment mismatch - file_search tool not added' );
516 }
517 if ( empty( $embeddingsEnv['store_id'] ) ) {
518 Meow_MWAI_Logging::log( 'Responses API: No store_id configured - file_search tool not added' );
519 }
520 }
521 }
522 else {
523 Meow_MWAI_Logging::log( 'Responses API: Embeddings environment is not OpenAI Vector Store type (type: ' . ( $embeddingsEnv['type'] ?? 'null' ) . ')' );
524 }
525 }
526 else {
527 Meow_MWAI_Logging::log( 'Responses API: No embeddingsEnvId in query - file_search tool not added' );
528 }
529
530 // Note: Responses API doesn't support stream_options parameter
531 // Usage tracking is handled differently in the streaming response
532 }
533 else if ( $query instanceof Meow_MWAI_Query_Image ) {
534 // For image generation, we can use the integrated approach
535 if ( $query->model === 'gpt-image-1' ) {
536 $body['tools'] = [[
537 'type' => 'image_generation'
538 ]];
539 $body['input'] = $query->get_message();
540 }
541 else {
542 // Fallback to old API for DALL-E models
543 return $this->build_body( $query, $streamCallback );
544 }
545 }
546
547 // Debug logging for feedback queries
548 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
549 Meow_MWAI_Logging::log( 'Responses API: Feedback query body: ' . json_encode( $body ) );
550 }
551
552 // Ensure parallel_tool_calls is set when we have tools
553 if ( !empty( $body['tools'] ) && !isset( $body['parallel_tool_calls'] ) ) {
554 $body['parallel_tool_calls'] = true;
555 }
556
557 // Azure Responses API doesn't support web_search tool yet (preview limitation)
558 if ( $this->envType === 'azure' && !empty( $body['tools'] ) ) {
559 $body['tools'] = array_values( array_filter( $body['tools'], function ( $tool ) {
560 $toolType = $tool['type'] ?? null;
561 if ( $toolType === 'web_search' ) {
562 Meow_MWAI_Logging::log( 'Responses API: Removing web_search tool for Azure (not supported in preview)' );
563 return false;
564 }
565 return true;
566 } ) );
567 }
568
569 return $body;
570 }
571
572 /**
573 * Build tool messages for feedback when using previous_response_id
574 */
575 protected function build_tool_messages_for_feedback( $query ) {
576 $messages = [];
577
578 if ( $query instanceof Meow_MWAI_Query_Feedback && !empty( $query->blocks ) ) {
579 foreach ( $query->blocks as $block ) {
580 if ( isset( $block['feedbacks'] ) ) {
581 foreach ( $block['feedbacks'] as $feedback ) {
582 // Get the tool call ID from the original request
583 $toolId = $feedback['request']['toolId'] ?? null;
584
585 if ( $toolId ) {
586 // According to Responses API spec, tool results should use role:"tool"
587 $toolMessage = [
588 'role' => 'tool',
589 'tool_call_id' => $toolId,
590 'content' => [
591 [
592 'type' => 'tool_result',
593 'tool_result' => (string) ( $feedback['reply']['value'] ?? '' )
594 ]
595 ]
596 ];
597 $messages[] = $toolMessage;
598
599 Meow_MWAI_Logging::log( 'Responses API: Added tool result with tool_call_id ' . $toolId . ' - Message: ' . json_encode( $toolMessage ) );
600 }
601 }
602 }
603 }
604 }
605
606 return $messages;
607 }
608
609 /**
610 * Build input array for complex message structures
611 */
612 protected function build_responses_input_array( $query ) {
613 // Use the MessageBuilder service for streamlined message building
614 // Note: Files are uploaded via prepare_query() BEFORE streaming hooks are set
615 $messages = $this->core->messageBuilder->build_responses_api_messages( $query );
616
617 // Note: Function result events are now emitted centrally in core.php
618 // when the function is actually executed
619
620 // Debug logging
621 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
622 if ( $queries_debug && $query instanceof Meow_MWAI_Query_Feedback ) {
623 error_log( '[AI Engine Queries] Feedback query messages order:' );
624 foreach ( $messages as $idx => $msg ) {
625 if ( isset( $msg['type'] ) ) {
626 $log_msg = ' [' . $idx . '] ' . $msg['type'];
627 if ( $msg['type'] === 'function_call' ) {
628 $log_msg .= ' - ' . ( $msg['name'] ?? 'unknown' ) . ' (call_id: ' . ( $msg['call_id'] ?? 'none' ) . ')';
629 }
630 elseif ( $msg['type'] === 'function_call_output' ) {
631 $log_msg .= ' (call_id: ' . ( $msg['call_id'] ?? 'none' ) . ', output: ' . substr( $msg['output'] ?? '', 0, 50 ) . ')';
632 }
633 error_log( '[AI Engine Queries]' . $log_msg );
634 }
635 elseif ( isset( $msg['role'] ) ) {
636 $content_preview = '';
637 if ( isset( $msg['content'] ) ) {
638 if ( is_string( $msg['content'] ) ) {
639 $content_preview = ' - "' . substr( $msg['content'], 0, 50 ) . '"';
640 }
641 elseif ( is_array( $msg['content'] ) && isset( $msg['content'][0]['text'] ) ) {
642 $content_preview = ' - "' . substr( $msg['content'][0]['text'], 0, 50 ) . '"';
643 }
644 elseif ( is_array( $msg['content'] ) && isset( $msg['content'][0]['type'] ) && $msg['content'][0]['type'] === 'input_text' ) {
645 $content_preview = ' - "' . substr( $msg['content'][0]['text'] ?? '', 0, 50 ) . '"';
646 }
647 }
648 error_log( '[AI Engine Queries] [' . $idx . '] ' . $msg['role'] . $content_preview );
649 }
650 }
651 }
652
653 return $messages;
654 }
655
656 /**
657 * Convert functions to Responses API tools format
658 */
659 protected function build_responses_tools( $functions ) {
660 $tools = [];
661
662 foreach ( $functions as $function ) {
663 $functionData = $function->serializeForOpenAI();
664
665 // Ensure the function data has all required fields
666 if ( !isset( $functionData['name'] ) || empty( $functionData['name'] ) ) {
667 Meow_MWAI_Logging::warn( 'Function missing required name field' );
668 continue;
669 }
670
671 // Responses API expects a flatter structure
672 $parameters = $functionData['parameters'] ?? null;
673
674 // Ensure parameters has the correct structure
675 if ( !$parameters ) {
676 $parameters = [
677 'type' => 'object',
678 'properties' => new stdClass(),
679 'required' => []
680 ];
681 }
682 else {
683 // Ensure properties is an object, not an array when empty
684 if ( isset( $parameters['properties'] ) &&
685 is_array( $parameters['properties'] ) &&
686 empty( $parameters['properties'] ) ) {
687 $parameters['properties'] = new stdClass();
688 }
689 }
690
691 $tool = [
692 'type' => 'function',
693 'name' => $functionData['name'],
694 'description' => $functionData['description'] ?? '',
695 'parameters' => $parameters,
696 'strict' => false // Set to false for now, can be made configurable later
697 ];
698
699 $tools[] = $tool;
700 }
701
702 return $tools;
703 }
704
705 /**
706 * Build feedback input for Responses API when using previous_response_id.
707 *
708 * The Responses API requires a very specific format for function results:
709 * 1. Echo the exact function_call message from the model
710 * 2. Provide the function_call_output with matching call_id
711 *
712 * This method extracts these from the feedback blocks and formats them correctly.
713 *
714 * @param Meow_MWAI_Query_Feedback $query The feedback query containing function results
715 * @return array Array of messages in Responses API format
716 */
717 protected function build_feedback_input_for_responses_api( $query ) {
718 // Use the MessageBuilder service for streamlined message building
719 $messages = $this->core->messageBuilder->build_feedback_only_messages( $query );
720
721 // For Responses API, the input should be wrapped in a specific structure
722 // According to OpenAI docs, function results should be sent as an array of messages
723 return $messages;
724 }
725
726 /**
727 * Build URL for Responses API
728 */
729 protected function build_responses_url() {
730 if ( $this->envType === 'azure' ) {
731 // Azure v1 Responses API endpoint (preview)
732 $endpoint = isset( $this->env['endpoint'] ) ? rtrim( $this->env['endpoint'], '/' ) : null;
733
734 // Handle legacy full path endpoints for backward compatibility
735 if ( strpos( $endpoint, '/openai/responses' ) !== false || strpos( $endpoint, '/openai/v1/responses' ) !== false ) {
736 // Extract the base URL (remove the path and query params)
737 $baseUrl = str_replace( '/openai/responses', '', $endpoint );
738 $baseUrl = str_replace( '/openai/v1/responses', '', $baseUrl );
739 $baseUrl = preg_replace( '/\?.*$/', '', $baseUrl );
740
741 // For Azure v1 Responses API, we do NOT include deployment in the URL
742 // The deployment name goes in the request body as 'model'
743 $url = $baseUrl . '/openai/v1/responses';
744
745 // Preserve the API version if it was included
746 if ( strpos( $endpoint, 'api-version=' ) !== false ) {
747 preg_match( '/api-version=([^&]+)/', $endpoint, $matches );
748 $apiVersion = $matches[1] ?? 'preview';
749 $url .= '?api-version=' . $apiVersion;
750 }
751 else {
752 $url .= '?api-version=preview';
753 }
754 }
755 else {
756 // Standard format: just the resource domain
757 // Ensure the endpoint has the proper protocol
758 if ( strpos( $endpoint, 'http' ) !== 0 ) {
759 $endpoint = 'https://' . $endpoint;
760 }
761
762 // Build the v1 endpoint without deployment in path
763 // For Azure v1 Responses API, deployment goes in the body, not the URL
764 $url = rtrim( $endpoint, '/' ) . '/openai/v1/responses?api-version=preview';
765 }
766 }
767 else {
768 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
769 $url = trailingslashit( $endpoint ) . 'responses';
770 }
771
772 return $url;
773 }
774
775 /**
776 * Get Azure endpoint with protocol
777 */
778 private function get_azure_endpoint() {
779 $endpoint = isset( $this->env['endpoint'] ) ? rtrim( $this->env['endpoint'], '/' ) : null;
780
781 if ( empty( $endpoint ) ) {
782 throw new Exception( 'Azure endpoint not configured. Please set the endpoint URL in your Azure environment settings.' );
783 }
784
785 // Ensure endpoint has protocol
786 if ( strpos( $endpoint, 'http://' ) !== 0 && strpos( $endpoint, 'https://' ) !== 0 ) {
787 $endpoint = 'https://' . $endpoint;
788 }
789
790 return $endpoint;
791 }
792
793 /**
794 * Extract Azure region from endpoint URL
795 * Azure OpenAI endpoints can be in different formats:
796 * - https://my-resource.openai.azure.com (custom subdomain - region not in URL)
797 * - https://eastus2.api.cognitive.microsoft.com (region-based)
798 *
799 * For custom subdomains, we need to use the Azure REST API to get the region,
800 * but for simplicity we'll check if a region is explicitly set in the env,
801 * otherwise default to common regions based on the resource name pattern.
802 */
803 private function get_azure_region( $endpoint ) {
804 // Check if region is explicitly set in environment
805 if ( isset( $this->env['region'] ) && !empty( $this->env['region'] ) ) {
806 return $this->env['region'];
807 }
808
809 // Try to extract from region-based endpoint format
810 if ( preg_match( '/([a-z0-9]+)\.api\.cognitive\.microsoft\.com/', $endpoint, $matches ) ) {
811 return $matches[1];
812 }
813
814 // For custom subdomain endpoints, try to infer from common patterns
815 // or default to the most common realtime regions
816 if ( preg_match( '/\.openai\.azure\.com/', $endpoint ) ) {
817 // Default to eastus2 as it's one of the primary realtime regions
818 // User can override this by setting the region in their environment
819 return 'eastus2';
820 }
821
822 // Fallback default
823 return 'eastus2';
824 }
825
826 /**
827 * Build Azure realtime sessions URL
828 */
829 private function build_azure_realtime_url( $endpoint ) {
830 // Azure uses /openai/realtimeapi/sessions (not /openai/realtime/sessions)
831 // The deployment is sent in the POST body, not in the URL
832 return $endpoint . '/openai/realtimeapi/sessions?api-version=2025-04-01-preview';
833 }
834
835 /**
836 * Build Azure v1 URL for containers/files
837 */
838 private function build_azure_v1_url( $endpoint, $url ) {
839 $fullUrl = $endpoint . '/openai/v1' . $url;
840
841 // Add API version
842 $hasQuery = strpos( $fullUrl, '?' ) !== false;
843 return $fullUrl . ( $hasQuery ? '&' : '?' ) . 'api-version=preview';
844 }
845
846 /**
847 * Override execute to handle Azure v1 endpoints for containers, files, and realtime
848 */
849 public function execute(
850 $method,
851 $url,
852 $query = null,
853 $formFields = null,
854 $json = true,
855 $extraHeaders = null,
856 $streamCallback = null
857 ) {
858 // For Azure container/files/realtime operations, use v1 endpoint
859 if ( $this->envType === 'azure' &&
860 ( strpos( $url, '/containers/' ) !== false ||
861 strpos( $url, '/files/' ) !== false ||
862 strpos( $url, '/realtime/' ) !== false ) ) {
863
864 $endpoint = $this->get_azure_endpoint();
865
866 // Build the appropriate URL based on the operation type
867 if ( strpos( $url, '/realtime/sessions' ) !== false ) {
868 $fullUrl = $this->build_azure_realtime_url( $endpoint );
869 }
870 else {
871 $fullUrl = $this->build_azure_v1_url( $endpoint, $url );
872 }
873
874 // Prepare headers
875 $headers = [
876 'Content-Type' => 'application/json',
877 'api-key' => $this->apiKey
878 ];
879
880 if ( $extraHeaders ) {
881 $headers = array_merge( $headers, $extraHeaders );
882 }
883
884 // Prepare body
885 $body = null;
886 if ( $method !== 'GET' && !empty( $query ) ) {
887 if ( is_string( $query ) ) {
888 $body = $query;
889 }
890 else {
891 $body = $this->safe_json_encode( $query, 'Azure v1 query' );
892 }
893 }
894
895 $options = [
896 'headers' => $headers,
897 'method' => $method,
898 'timeout' => MWAI_TIMEOUT,
899 'body' => $body,
900 'sslverify' => false
901 ];
902
903 // Log if debug enabled
904 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
905 if ( $queries_debug ) {
906 error_log( '[AI Engine Queries] Azure v1 Request to: ' . $fullUrl );
907 error_log( '[AI Engine Queries] Method: ' . $method );
908 error_log( '[AI Engine Queries] Headers: ' . json_encode( array_keys( $headers ) ) );
909 if ( !empty( $body ) ) {
910 error_log( '[AI Engine Queries] Request Body: ' . $body );
911 }
912 }
913
914 // Make the request
915 $res = wp_remote_request( $fullUrl, $options );
916
917 if ( is_wp_error( $res ) ) {
918 throw new Exception( $res->get_error_message() );
919 }
920
921 $response_code = wp_remote_retrieve_response_code( $res );
922 $res = wp_remote_retrieve_body( $res );
923
924 // Log response
925 if ( $queries_debug ) {
926 error_log( '[AI Engine Queries] Azure v1 Response Code: ' . $response_code );
927 error_log( '[AI Engine Queries] Azure v1 Response: ' . $res );
928 }
929
930 // Handle response
931 if ( strpos( $url, '/content' ) !== false ) {
932 // Binary content download
933 return $res;
934 }
935
936 // JSON response
937 $data = json_decode( $res, true );
938
939 $this->handle_response_errors( $data );
940
941 return $data;
942 }
943
944 // For non-container operations, use parent implementation
945 return parent::execute( $method, $url, $query, $formFields, $json, $extraHeaders, $streamCallback );
946 }
947
948 /**
949 * Override build_options to add Azure-specific headers for Responses API
950 */
951 protected function build_options( $headers, $json = null, $forms = null, $method = 'POST' ) {
952 // Add Azure-specific headers if using Azure with Responses API
953 if ( $this->envType === 'azure' && !empty( $json ) ) {
954 // Check if image_generation tool is present
955 if ( isset( $json['tools'] ) && is_array( $json['tools'] ) ) {
956 foreach ( $json['tools'] as $tool ) {
957 if ( isset( $tool['type'] ) && $tool['type'] === 'image_generation' ) {
958 // For Azure, add the image generation deployment header
959 // Look for an image deployment in the Azure deployments
960 if ( isset( $this->env['deployments'] ) && is_array( $this->env['deployments'] ) ) {
961 foreach ( $this->env['deployments'] as $deployment ) {
962 // Check if this is an image model deployment (gpt-image-1)
963 if ( isset( $deployment['model'] ) && $deployment['model'] === 'gpt-image-1' && isset( $deployment['name'] ) ) {
964 $headers['x-ms-oai-image-generation-deployment'] = $deployment['name'];
965 Meow_MWAI_Logging::log( 'Responses API: Added Azure image generation deployment header: ' . $deployment['name'] );
966 break;
967 }
968 }
969 }
970 break;
971 }
972 }
973 }
974 }
975
976 // Call parent's build_options
977 return parent::build_options( $headers, $json, $forms, $method );
978 }
979
980 /**
981 * Handle Responses API streaming data
982 */
983 protected function responses_stream_data_handler( $json ) {
984 $content = null;
985 static $currentItemType = null; // Track the current output item type
986 // Load event helper
987 if ( !class_exists( 'Meow_MWAI_Event' ) ) {
988 require_once MWAI_PATH . '/classes/event.php';
989 }
990
991 // Get response metadata
992 if ( isset( $json['id'] ) ) {
993 $this->inId = $json['id'];
994 Meow_MWAI_Logging::log( 'Responses API Streaming: Found response ID in stream: ' . $this->inId );
995 }
996 if ( isset( $json['model'] ) ) {
997 $this->inModel = $json['model'];
998 }
999
1000 // Handle different event types for Responses API
1001 $eventType = $json['type'] ?? null;
1002
1003 // Debug streaming events
1004 if ( isset( $_GET['debug_mcp'] ) ) {
1005 error_log( 'AI_ENGINE_DEBUG: Streaming type: ' . ( $eventType ?? 'no_type' ) . ' - Data: ' . json_encode( $json ) );
1006 }
1007
1008 switch ( $eventType ) {
1009 // ===== LIFECYCLE EVENTS =====
1010
1011 case 'response.created':
1012 // Emitted when a response object is created - contains initial response metadata
1013 $response = $json['response'] ?? [];
1014 $this->inId = $response['id'] ?? null;
1015 $this->inModel = $response['model'] ?? null;
1016 if ( $this->inId ) {
1017 }
1018 break;
1019
1020 case 'response.queued':
1021 // Response is queued and waiting to start processing
1022 // We can log this for debugging purposes
1023 Meow_MWAI_Logging::log( 'Responses API: Response queued for processing' );
1024 break;
1025
1026 case 'response.in_progress':
1027 // Emitted repeatedly while the response is being generated
1028 // Contains partial response state but typically not used for streaming text
1029 break;
1030
1031 case 'response.completed':
1032 // Response is fully generated - extract any function calls from completed output
1033 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1034 error_log( '[AI Engine Queries] Current streamToolCalls count: ' . count( $this->streamToolCalls ) );
1035 }
1036
1037 $response = $json['response'] ?? [];
1038
1039 // Extract usage information from response.completed event
1040 if ( isset( $response['usage'] ) ) {
1041 $usage = $response['usage'];
1042
1043 // Set stream tokens from usage data
1044 // Responses API uses input_tokens/output_tokens
1045 $inputTokens = $usage['input_tokens'] ?? $usage['prompt_tokens'] ?? null;
1046 $outputTokens = $usage['output_tokens'] ?? $usage['completion_tokens'] ?? null;
1047
1048 if ( $inputTokens !== null ) {
1049 $this->streamInTokens = (int) $inputTokens;
1050 }
1051 if ( $outputTokens !== null ) {
1052 $this->streamOutTokens = (int) $outputTokens;
1053 }
1054 if ( isset( $usage['cost'] ) ) {
1055 $this->streamCost = (float) $usage['cost'];
1056 }
1057 }
1058
1059 $outputs = $response['output'] ?? [];
1060
1061 foreach ( $outputs as $idx => $output ) {
1062 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1063 error_log( '[AI Engine Queries] Output ' . $idx . ' type: ' . ( $output['type'] ?? 'unknown' ) . ', status: ' . ( $output['status'] ?? 'no-status' ) );
1064 }
1065
1066 if ( isset( $output['type'] ) && $output['type'] === 'function_call' &&
1067 isset( $output['status'] ) && $output['status'] === 'completed' ) {
1068 // Note: Responses API uses 'call_id' not 'id' for function calls
1069 $callId = $output['call_id'] ?? $output['id'] ?? null;
1070 $functionName = $output['name'] ?? '';
1071
1072 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1073 error_log( '[AI Engine Queries] Processing function_call: ' . $functionName . ' (id: ' . $callId . ')' );
1074 }
1075
1076 // IMPORTANT: Deduplicate function calls
1077 // OpenAI sends the same function call in both response.output_item.done
1078 // and response.completed events. We track call IDs to avoid duplicates.
1079 if ( in_array( $callId, $this->seenCallIds, true ) ) {
1080 // Skip duplicate - already processed in response.output_item.done
1081 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1082 error_log( '[AI Engine Queries] Skipping duplicate call ID: ' . $callId );
1083 }
1084 continue;
1085 }
1086
1087 // First time seeing this call ID - add it
1088 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1089 error_log( '[AI Engine Queries] response.completed adding tool call: ' . $functionName . ' (id: ' . $callId . ')' );
1090 }
1091 $this->seenCallIds[] = $callId;
1092 $this->streamToolCalls[] = [
1093 'id' => $callId,
1094 'type' => 'function',
1095 'function' => [
1096 'name' => $functionName,
1097 'arguments' => $output['arguments'] ?? '{}'
1098 ]
1099 ];
1100 }
1101 }
1102 break;
1103
1104 case 'response.incomplete':
1105 // Response stopped before completion (e.g., max_tokens reached)
1106 $details = $json['response']['incomplete_details'] ?? [];
1107 Meow_MWAI_Logging::warn( 'Responses API: Response incomplete - ' . json_encode( $details ) );
1108 break;
1109
1110 case 'response.failed':
1111 // Response generation failed
1112 $error = $json['response']['error'] ?? [];
1113 $message = $error['message'] ?? 'Response generation failed';
1114 throw new Exception( $message );
1115
1116 // ===== OUTPUT ITEM EVENTS =====
1117
1118 case 'response.output_item.added':
1119 // New output item added (e.g., message, function_call, etc.)
1120 // Track the type of the current output item
1121 if ( isset( $json['item'] ) && isset( $json['item']['type'] ) ) {
1122 $item = $json['item'];
1123 $itemType = $item['type'];
1124 $currentItemType = $itemType;
1125
1126 // Code interpreter items are handled in event processing
1127
1128 // Don't emit events here for web search or image generation - wait for more specific events
1129 // This prevents duplicate events
1130
1131 // If it's an MCP call, store the tool name
1132 if ( $itemType === 'mcp_call' && isset( $item['id'] ) && isset( $item['name'] ) ) {
1133 $this->mcpToolNames[$item['id']] = $item['name'];
1134 Meow_MWAI_Logging::log( 'Responses API: MCP tool call added - ' . $item['name'] . ' (id: ' . $item['id'] . ')' );
1135
1136 if ( $this->currentDebugMode ) {
1137 $event = Meow_MWAI_Event::mcp_calling( $item['name'], $item['id'] )
1138 ->set_metadata( 'name', $item['name'] )
1139 ->set_metadata( 'server_label', $item['server_label'] ?? null );
1140 call_user_func( $this->streamCallback, $event );
1141 }
1142 }
1143 }
1144 break;
1145
1146 case 'response.output_item.done':
1147 // Output item completed - check for MCP approval requests or tool lists
1148 if ( isset( $json['item'] ) && isset( $json['item']['type'] ) ) {
1149 $item = $json['item'];
1150 $itemType = $item['type'];
1151
1152 // Reset current item type when we complete a message item
1153 if ( $itemType === 'message' ) {
1154 $currentItemType = null;
1155 }
1156
1157 if ( $itemType === 'function_call' ) {
1158 // Regular function call completed - send event
1159 if ( $this->currentDebugMode && $this->streamCallback ) {
1160 $event = Meow_MWAI_Event::function_calling( $item['name'] ?? 'unknown', json_decode( $item['arguments'] ?? '{}', true ) )
1161 ->set_metadata( 'call_id', $item['call_id'] ?? null );
1162 call_user_func( $this->streamCallback, $event );
1163 }
1164
1165 // Add to streamToolCalls for execution
1166 // Note: Responses API uses 'call_id' not 'id' for function calls
1167 $callId = $item['call_id'] ?? $item['id'] ?? null;
1168 $functionName = $item['name'] ?? '';
1169
1170 // Add to our deduplication tracking
1171 // We process function calls here as they complete individually during streaming
1172 // The response.completed event will also try to add them, so we track IDs
1173 if ( !in_array( $callId, $this->seenCallIds, true ) ) {
1174 $this->seenCallIds[] = $callId;
1175
1176 $this->streamToolCalls[] = [
1177 'id' => $callId,
1178 'type' => 'function',
1179 'function' => [
1180 'name' => $functionName,
1181 'arguments' => $item['arguments'] ?? '{}'
1182 ]
1183 ];
1184 }
1185 }
1186 elseif ( $itemType === 'mcp_approval_request' ) {
1187 // IMPORTANT: MCP (Model Context Protocol) tools are executed remotely by OpenAI
1188 // Unlike regular function calls, MCP tools do NOT need local execution
1189 // Therefore, we should NOT add them to streamToolCalls array
1190 // This prevents creation of unnecessary feedback queries and second response cycles
1191 Meow_MWAI_Logging::log( 'Responses API: MCP approval request for ' . $item['name'] . ' from server ' . $item['server_label'] . ' (handled remotely)' );
1192 }
1193 elseif ( $item['type'] === 'mcp_call' ) {
1194 // IMPORTANT: MCP calls are already executed remotely by OpenAI's infrastructure
1195 // The result is included in the same response stream
1196 // We must NOT add these to streamToolCalls to avoid duplicate execution attempts
1197 Meow_MWAI_Logging::log( 'Responses API: MCP call completed - ' . $item['name'] . ' (already executed remotely)' );
1198
1199 // Send event for completed MCP call when debug is enabled
1200 if ( $this->currentDebugMode && isset( $item['name'] ) ) {
1201 $args = json_decode( $item['arguments'] ?? '{}', true );
1202 $output = $item['output'] ?? null;
1203
1204 // Skip the tool_call event for MCP calls since we already sent mcp_tool_call
1205 // This prevents duplicate events in the UI
1206
1207 // Then send a separate event for the tool result
1208 if ( $output ) {
1209 // Format the output preview
1210 $outputPreview = is_array( $output ) ? json_encode( $output ) : (string) $output;
1211 if ( strlen( $outputPreview ) > 100 ) {
1212 $outputPreview = substr( $outputPreview, 0, 100 ) . '...';
1213 }
1214
1215 $resultEvent = Meow_MWAI_Event::mcp_result( $item['name'] )
1216 ->set_metadata( 'output', $output );
1217 call_user_func( $this->streamCallback, $resultEvent );
1218 }
1219
1220 // Don't return content since we've already sent events
1221 $content = null;
1222 }
1223 }
1224 elseif ( $itemType === 'web_search_call' ) {
1225 // Web search completed - don't emit event here
1226 // The event will be emitted by the response.web_search_call.completed handler
1227 // This prevents duplicate events
1228 Meow_MWAI_Logging::log( 'Responses API: Web search output item completed (event handled by specific handler)' );
1229 }
1230 elseif ( $itemType === 'code_interpreter_call' ) {
1231 // Code interpreter completed
1232 Meow_MWAI_Logging::log( 'Responses API: Code interpreter output item completed' );
1233
1234 // Store container ID if available
1235 if ( isset( $item['container_id'] ) ) {
1236 $this->streamContainerId = $item['container_id'];
1237 Meow_MWAI_Logging::log( 'Responses API: Found container_id in streaming: ' . $this->streamContainerId );
1238 }
1239
1240 // Check for files in the result
1241 if ( isset( $item['result'] ) ) {
1242 $result = $item['result'];
1243
1244 // Look for files in the result
1245 if ( isset( $result['files'] ) ) {
1246 // Store these files
1247 if ( !isset( $this->streamCodeInterpreterFiles ) ) {
1248 $this->streamCodeInterpreterFiles = [];
1249 }
1250
1251 foreach ( $result['files'] as $file ) {
1252 $this->streamCodeInterpreterFiles[] = $file;
1253 Meow_MWAI_Logging::log( 'Responses API: Captured file from result: ' . ( $file['filename'] ?? $file['id'] ?? 'unknown' ) );
1254 }
1255 }
1256
1257 // Handle standard output
1258 if ( isset( $result['stdout'] ) && !empty( $result['stdout'] ) ) {
1259 // Add code output to the response content
1260 $content = "\n```\n" . $result['stdout'] . "\n```\n";
1261 Meow_MWAI_Logging::log( 'Responses API: Code interpreter stdout: ' . substr( $result['stdout'], 0, 100 ) );
1262 }
1263 }
1264 }
1265 elseif ( $itemType === 'image_generation_call' ) {
1266 // Image generation completed
1267 Meow_MWAI_Logging::log( 'Responses API: Image generation output item completed' );
1268
1269 // Extract the base64 image from the result
1270 if ( isset( $item['result'] ) ) {
1271 $base64Image = $item['result'];
1272
1273 // Store the image for later processing
1274 if ( !isset( $this->streamImages ) ) {
1275 $this->streamImages = [];
1276 }
1277
1278 $this->streamImages[] = $base64Image;
1279
1280 Meow_MWAI_Logging::log( 'Responses API: Stored generated image (base64 length: ' . strlen( $base64Image ) . ')' );
1281 }
1282 }
1283 elseif ( $item['type'] === 'mcp_list_tools' ) {
1284 // MCP tools list discovered
1285 $server_label = $item['server_label'] ?? 'unknown';
1286 $tools_count = isset( $item['tools'] ) ? count( $item['tools'] ) : 0;
1287 $this->mcpTotalToolCount += $tools_count;
1288 Meow_MWAI_Logging::log( 'Responses API: MCP tools list from server ' . $server_label . ' containing ' . $tools_count . ' tools' );
1289
1290 // Send event for tools discovery using the aggregated format
1291 if ( $this->currentDebugMode ) {
1292 $serverCount = $this->mcpServerCount > 0 ? $this->mcpServerCount : 1;
1293 $event = Meow_MWAI_Event::mcp_discovery( $serverCount, $this->mcpTotalToolCount );
1294 call_user_func( $this->streamCallback, $event );
1295 }
1296
1297 // Log first few tools for debugging
1298 if ( isset( $item['tools'] ) && is_array( $item['tools'] ) ) {
1299 $sample_tools = array_slice( $item['tools'], 0, 3 );
1300 foreach ( $sample_tools as $tool ) {
1301 Meow_MWAI_Logging::log( 'Responses API: MCP tool "' . ( $tool['name'] ?? 'unnamed' ) . '": ' . ( $tool['description'] ?? 'no description' ) );
1302 }
1303 if ( $tools_count > 3 ) {
1304 Meow_MWAI_Logging::log( 'Responses API: ... and ' . ( $tools_count - 3 ) . ' more tools' );
1305 }
1306 }
1307 }
1308 }
1309 break;
1310
1311 // ===== CONTENT PART EVENTS =====
1312
1313 case 'response.content_part.added':
1314 // New content part added to an output item
1315 // Indicates start of a new content section (text, image, etc.)
1316 // Check if this is MCP-related content that shouldn't be shown
1317 if ( isset( $json['part']['type'] ) ) {
1318 $partType = $json['part']['type'];
1319
1320 // Just log the part type for debugging
1321 // We can use this info later if needed
1322 }
1323 break;
1324
1325 case 'response.content_part.done':
1326 // Content part is finalized
1327 // No more deltas will be sent for this content part
1328 break;
1329
1330 // ===== TEXT STREAMING EVENTS =====
1331
1332 case 'response.output_text.delta':
1333 // Streaming text chunk for the current content part
1334 if ( isset( $json['delta'] ) ) {
1335 // Send a status event for the first content chunk
1336 if ( $this->currentDebugMode && !$this->contentStarted ) {
1337 $this->contentStarted = true;
1338 $statusEvent = Meow_MWAI_Event::generating_response();
1339 call_user_func( $this->streamCallback, $statusEvent );
1340 }
1341 $content = $json['delta'];
1342 }
1343 break;
1344
1345 case 'response.output_text.done':
1346 // Final text for the content part
1347 // Contains the complete accumulated text
1348 // Don't send response_completed here - ChatbotContext adds "Request completed"
1349 $this->contentStarted = false;
1350 break;
1351
1352 case 'response.refusal.delta':
1353 // Streaming refusal message chunk
1354 // Model is refusing to generate the requested content
1355 if ( isset( $json['delta'] ) ) {
1356 // We might want to stream refusals as regular content
1357 $content = $json['delta'];
1358 }
1359 break;
1360
1361 case 'response.refusal.done':
1362 // Final refusal message
1363 // Contains the complete refusal reason
1364 break;
1365
1366 case 'response.function_call_arguments.delta':
1367 // Streaming JSON arguments for a function call
1368 // We don't stream these to UI as they're not human-readable
1369 break;
1370
1371 case 'response.function_call_arguments.done':
1372 // Complete function call arguments
1373 // Already handled in response.output_item.done for function_call type
1374 break;
1375
1376 // ===== FILE & WEB SEARCH EVENTS =====
1377
1378 case 'response.file_search_call.in_progress':
1379 // File search started
1380 Meow_MWAI_Logging::log( 'Responses API: File search in progress' );
1381 break;
1382
1383 case 'response.file_search_call.searching':
1384 // Actively searching files
1385 break;
1386
1387 case 'response.file_search_call.completed':
1388 // File search finished
1389 break;
1390
1391 case 'response.web_search_call.in_progress':
1392 // Web search started - only emit one event at the start
1393 Meow_MWAI_Logging::log( 'Responses API: Web search in progress' );
1394 if ( $this->currentDebugMode && $this->streamCallback ) {
1395 $event = Meow_MWAI_Event::status( 'Searching the web...' );
1396 call_user_func( $this->streamCallback, $event );
1397 }
1398 break;
1399
1400 case 'response.web_search_call.searching':
1401 // Actively searching - don't emit duplicate events
1402 if ( isset( $json['query'] ) ) {
1403 Meow_MWAI_Logging::log( 'Responses API: Searching for: ' . $json['query'] );
1404 }
1405 break;
1406
1407 case 'response.web_search_call.completed':
1408 // Web search finished
1409 Meow_MWAI_Logging::log( 'Responses API: Web search completed' );
1410
1411 // The completed event doesn't contain results, just metadata
1412 // Results are likely embedded in the model's response text
1413 if ( $this->currentDebugMode && $this->streamCallback ) {
1414 $message = 'Web search completed';
1415 $event = Meow_MWAI_Event::status( $message );
1416 call_user_func( $this->streamCallback, $event );
1417 }
1418 break;
1419
1420 // ===== IMAGE GENERATION EVENTS =====
1421
1422 case 'response.image_generation_call.in_progress':
1423 // Image generation started
1424 Meow_MWAI_Logging::log( 'Responses API: Image generation in progress' );
1425 if ( $this->currentDebugMode && $this->streamCallback ) {
1426 $event = Meow_MWAI_Event::status( 'Generating image...' );
1427 call_user_func( $this->streamCallback, $event );
1428 }
1429 break;
1430
1431 case 'response.image_generation_call.generating':
1432 // Image is being generated
1433 break;
1434
1435 case 'response.image_generation_call.partial_image':
1436 // Partial image data (base64)
1437 // Could be used for progressive image display
1438 if ( isset( $json['partial_image_b64'] ) ) {
1439 Meow_MWAI_Logging::log( 'Responses API: Received partial image index ' . ( $json['partial_image_index'] ?? 'unknown' ) );
1440 // For now, we don't display partial images, but we could in the future
1441 }
1442 break;
1443
1444 case 'response.image_generation_call.completed':
1445 // Image generation finished
1446 Meow_MWAI_Logging::log( 'Responses API: Image generation completed' );
1447
1448 // Note: The actual image data comes in response.output_item.done event
1449 // This event just signals completion
1450
1451 if ( $this->currentDebugMode && $this->streamCallback ) {
1452 $event = Meow_MWAI_Event::status( 'Image generated.' );
1453 call_user_func( $this->streamCallback, $event );
1454 }
1455 break;
1456
1457 // ===== CODE INTERPRETER EVENTS =====
1458
1459 case 'response.code_interpreter_call.in_progress':
1460 // Code interpreter started
1461
1462 // Check for container_id in the event
1463 if ( isset( $json['container_id'] ) ) {
1464 $this->streamContainerId = $json['container_id'];
1465 error_log( '[AI Engine] Found container_id in code_interpreter_call.in_progress: ' . $this->streamContainerId );
1466 }
1467
1468 // Also check in item if present
1469 if ( isset( $json['item']['container_id'] ) ) {
1470 $this->streamContainerId = $json['item']['container_id'];
1471 error_log( '[AI Engine] Found container_id in item: ' . $this->streamContainerId );
1472 }
1473
1474 // Container ID captured if available
1475 break;
1476
1477 case 'response.code_interpreter_call.running':
1478 // Code is being executed
1479 // Check for container_id here too
1480 if ( isset( $json['container_id'] ) ) {
1481 $this->streamContainerId = $json['container_id'];
1482 Meow_MWAI_Logging::log( 'Responses API: Found container_id in running event: ' . $this->streamContainerId );
1483 }
1484 break;
1485
1486 case 'response.code_interpreter_call.stdout':
1487 // Standard output from code execution
1488 if ( isset( $json['stdout'] ) ) {
1489 Meow_MWAI_Logging::log( 'Responses API: Code output - ' . substr( $json['stdout'], 0, 100 ) );
1490 }
1491 break;
1492
1493 case 'response.code_interpreter_call.stderr':
1494 // Standard error from code execution
1495 if ( isset( $json['stderr'] ) ) {
1496 Meow_MWAI_Logging::log( 'Responses API: Code error - ' . $json['stderr'] );
1497 }
1498 break;
1499
1500 case 'response.code_interpreter_call.completed':
1501 // Code interpreter finished - files are now ready for download
1502 Meow_MWAI_Logging::log( 'Responses API: Code interpreter completed' );
1503
1504 // Check for container_id in completed event
1505 if ( isset( $json['container_id'] ) ) {
1506 $this->streamContainerId = $json['container_id'];
1507 Meow_MWAI_Logging::log( 'Responses API: Container ID: ' . $this->streamContainerId );
1508 }
1509
1510 // Mark that code interpreter has completed
1511 $this->codeInterpreterCompleted = true;
1512
1513 // Send CODE event to client
1514 if ( $this->currentDebugMode && $this->streamCallback ) {
1515 $codeEvent = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['CODE'] ) )
1516 ->set_content( 'Code execution completed.' );
1517 call_user_func( $this->streamCallback, $codeEvent );
1518 }
1519 break;
1520
1521 case 'response.code_interpreter_call_code.delta':
1522 // Streaming code being written/executed by the code interpreter
1523 // This should NOT be added to the main content
1524 if ( isset( $json['delta'] ) ) {
1525 // Send CODE event only for the first code delta
1526 if ( empty( $this->streamContentCode ) && $this->currentDebugMode && $this->streamCallback ) {
1527 $codeEvent = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['CODE'] ) )
1528 ->set_content( 'Writing code...' );
1529 call_user_func( $this->streamCallback, $codeEvent );
1530 }
1531
1532 // Accumulate code in streamContentCode instead of content
1533 $this->streamContentCode .= $json['delta'];
1534
1535 Meow_MWAI_Logging::log( 'Responses API: Code interpreter code delta - ' . substr( $json['delta'], 0, 100 ) );
1536 }
1537 // Important: Don't return any content here so it's not added to streamContent
1538 return null;
1539
1540 case 'response.code_interpreter_call_code.done':
1541 // Code interpreter code writing completed
1542 if ( !empty( $this->streamContentCode ) && $this->currentDebugMode && $this->streamCallback ) {
1543 $lines = substr_count( $this->streamContentCode, "\n" ) + 1;
1544
1545 // Send the complete code as a collapsed CODE event
1546 // Set summary as content (shown when collapsed) and full code as metadata (shown when expanded)
1547 $codeEvent = ( new Meow_MWAI_Event( 'live', MWAI_STREAM_TYPES['CODE'] ) )
1548 ->set_content( "Wrote Python code ($lines lines)" )
1549 ->set_visibility( MWAI_STREAM_VISIBILITY['COLLAPSED'] )
1550 ->set_metadata( 'full_code', $this->streamContentCode );
1551 call_user_func( $this->streamCallback, $codeEvent );
1552
1553 Meow_MWAI_Logging::log( 'Responses API: Code interpreter code completed - ' . strlen( $this->streamContentCode ) . ' bytes' );
1554 }
1555 break;
1556
1557 case 'response.code_interpreter_file_citation':
1558 case 'code_interpreter_file_citation':
1559 // Code interpreter has created or cited a file
1560 // This event contains the file_id for files generated during code execution
1561 if ( isset( $json['file_id'] ) ) {
1562 if ( !isset( $this->streamCodeInterpreterFiles ) ) {
1563 $this->streamCodeInterpreterFiles = [];
1564 }
1565 $file_info = [
1566 'file_id' => $json['file_id'],
1567 'filename' => $json['filename'] ?? null,
1568 'file_type' => $json['file_type'] ?? null,
1569 'path' => isset( $json['path'] ) ? $json['path'] : ( isset( $json['filename'] ) ? '/mnt/data/' . $json['filename'] : null )
1570 ];
1571 $this->streamCodeInterpreterFiles[] = $file_info;
1572 error_log( '[AI Engine] File citation captured: ' . json_encode( $file_info ) );
1573 Meow_MWAI_Logging::log( 'Responses API: Code interpreter file citation - file_id: ' . $json['file_id'] );
1574 }
1575 break;
1576
1577 // ===== MCP (Model Context Protocol) EVENTS =====
1578
1579 case 'response.mcp_call.in_progress':
1580 // MCP tool call is running
1581 $itemId = $json['item_id'] ?? null;
1582 $toolName = isset( $this->mcpToolNames[$itemId] ) ? $this->mcpToolNames[$itemId] : 'unknown';
1583
1584 Meow_MWAI_Logging::log( 'Responses API: MCP tool call in progress - ' . $toolName );
1585 break;
1586
1587 case 'response.mcp_call.arguments.delta':
1588 case 'response.mcp_call_arguments.delta':
1589 // Streaming arguments for MCP tool call
1590 // Don't stream these JSON arguments to the UI
1591 // These contain the function parameters like {"post_type":"post",...}
1592 break;
1593
1594 case 'response.mcp_call.arguments.done':
1595 case 'response.mcp_call_arguments.done':
1596 // Complete arguments for MCP tool call
1597 break;
1598
1599 case 'response.mcp_call.completed':
1600 // MCP tool call succeeded
1601 break;
1602
1603 case 'response.mcp_call.failed':
1604 // MCP tool call failed
1605 $error = $json['error'] ?? [];
1606 Meow_MWAI_Logging::error( 'Responses API: MCP tool call failed - ' . ( $error['message'] ?? 'Unknown error' ) );
1607 break;
1608
1609 case 'response.mcp_list_tools.in_progress':
1610 // Listing MCP tools has started
1611 Meow_MWAI_Logging::log( 'Responses API: MCP tools discovery in progress' );
1612 break;
1613
1614 case 'response.mcp_list_tools.completed':
1615 // MCP tools listing completed successfully
1616 break;
1617
1618 case 'response.mcp_list_tools.failed':
1619 // MCP tools listing failed
1620 $error = $json['error'] ?? [];
1621 $message = 'MCP tools listing failed: ' . ( $error['message'] ?? 'Unknown error' );
1622 Meow_MWAI_Logging::error( 'Responses API: ' . $message );
1623 throw new Exception( $message );
1624 break;
1625
1626 // ===== REASONING EVENTS (for o1/o3 models) =====
1627
1628 case 'response.reasoning.delta':
1629 // Streaming reasoning text chunk
1630 // Internal reasoning process of the model
1631 break;
1632
1633 case 'response.reasoning.done':
1634 // Complete reasoning text
1635 break;
1636
1637 case 'response.reasoning_summary_part.added':
1638 // New reasoning summary part added
1639 break;
1640
1641 case 'response.reasoning_summary_part.done':
1642 // Reasoning summary part completed
1643 break;
1644
1645 case 'response.reasoning_summary_text.delta':
1646 // Streaming reasoning summary text
1647 break;
1648
1649 case 'response.reasoning_summary_text.done':
1650 // Complete reasoning summary
1651 break;
1652
1653 // ===== ANNOTATION EVENTS =====
1654
1655 case 'response.output_text_annotation.added':
1656 case 'response.output_text.annotation.added':
1657 // Text annotation added - check for container file citations
1658 if ( isset( $json['annotation'] ) ) {
1659 $annotation = $json['annotation'];
1660
1661 // Check if this is a container file citation
1662 if ( isset( $annotation['type'] ) && $annotation['type'] === 'container_file_citation' ) {
1663 // Initialize files array if needed
1664 if ( !isset( $this->streamCodeInterpreterFiles ) ) {
1665 $this->streamCodeInterpreterFiles = [];
1666 }
1667
1668 // Extract file information
1669 $fileInfo = [
1670 'file_id' => $annotation['file_id'] ?? null,
1671 'filename' => $annotation['filename'] ?? null,
1672 'container_id' => $annotation['container_id'] ?? null
1673 ];
1674
1675 // Store the file info if we have a file_id
1676 if ( $fileInfo['file_id'] ) {
1677 $this->streamCodeInterpreterFiles[] = $fileInfo;
1678
1679 // Also store container ID if available
1680 if ( $fileInfo['container_id'] && !$this->streamContainerId ) {
1681 $this->streamContainerId = $fileInfo['container_id'];
1682 }
1683
1684 Meow_MWAI_Logging::log( 'Responses API: File citation - ' . $fileInfo['filename'] . ' (' . $fileInfo['file_id'] . ')' );
1685 }
1686 }
1687 }
1688 break;
1689
1690 case 'response.completed':
1691 // Response fully completed - function calls are already handled in response.output_item.done
1692 break;
1693
1694 // ===== ERROR EVENTS =====
1695
1696 case 'error':
1697 // Generic error event
1698 $error = $json['error'] ?? $json;
1699 $message = $error['message'] ?? 'Unknown error occurred';
1700 $code = $error['code'] ?? null;
1701 if ( $code ) {
1702 $message .= " (Code: $code)";
1703 }
1704 throw new Exception( $message );
1705
1706 default:
1707 // Unknown event type - log for debugging
1708 Meow_MWAI_Logging::log( 'Responses API: Unknown event type: ' . $eventType );
1709
1710 // Check if this might be a different streaming format
1711 if ( isset( $json['delta'] ) && is_string( $json['delta'] ) ) {
1712 $content = $json['delta'];
1713 }
1714 elseif ( isset( $json['content'] ) && is_string( $json['content'] ) ) {
1715 $content = $json['content'];
1716 }
1717 }
1718
1719 // Handle usage data (legacy - kept for Chat Completions API compatibility)
1720 // Note: Responses API sets usage in response.completed event instead
1721 $usage = $json['usage'] ?? [];
1722 $inputTokens = $usage['input_tokens'] ?? $usage['prompt_tokens'] ?? null;
1723 $outputTokens = $usage['output_tokens'] ?? $usage['completion_tokens'] ?? null;
1724
1725 if ( $inputTokens !== null && $outputTokens !== null ) {
1726 $this->streamInTokens = (int) $inputTokens;
1727 $this->streamOutTokens = (int) $outputTokens;
1728 if ( isset( $usage['cost'] ) ) {
1729 $this->streamCost = (float) $usage['cost'];
1730 }
1731 }
1732
1733 return $content;
1734 }
1735
1736 /**
1737 * Override stream data handler to support both APIs
1738 */
1739 protected function stream_data_handler( $json ) {
1740 // Check if this is a Responses API event (uses 'type' field)
1741 if ( isset( $json['type'] ) && strpos( $json['type'], 'response.' ) === 0 ) {
1742 return $this->responses_stream_data_handler( $json );
1743 }
1744
1745 // Fallback to ChatML handler
1746 return parent::stream_data_handler( $json );
1747 }
1748
1749 /**
1750 * Override reset to include OpenAI-specific state
1751 */
1752 protected function reset_request_state() {
1753 parent::reset_request_state();
1754
1755 // Reset OpenAI-specific state
1756 $this->streamImages = [];
1757 $this->streamContentCode = '';
1758 $this->streamContainerId = null;
1759 $this->streamCodeInterpreterFiles = [];
1760 $this->codeInterpreterCompleted = false;
1761 }
1762
1763 /**
1764 * Override run_completion_query to route to appropriate API
1765 */
1766 public function run_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
1767 // Reset request-specific state to prevent leakage between requests
1768 $this->reset_request_state();
1769
1770 // Store current query for should_use_responses_api check
1771 $this->currentQuery = $query;
1772
1773 // Check if this is a GPT-5 model
1774 $isGpt5Model = strpos( $query->model, 'gpt-5' ) === 0;
1775
1776 // Check which API to use
1777 $useResponsesApi = $this->should_use_responses_api( $query->model );
1778
1779 // GPT-5 models MUST use Responses API
1780 if ( $isGpt5Model && !$useResponsesApi ) {
1781 $options = $this->core->get_all_options();
1782 $responsesApiEnabled = $options['ai_responses_api'] ?? true;
1783
1784 if ( !$responsesApiEnabled ) {
1785 throw new Exception( 'GPT-5 models require the Responses API to be enabled. Please enable "Use Responses API" in AI Engine settings.' );
1786 }
1787
1788 // If Responses API is enabled but model doesn't have the tag, force it
1789 return $this->run_responses_completion_query( $query, $streamCallback );
1790 }
1791
1792 // Check if we should use Responses API
1793 if ( $useResponsesApi ) {
1794 return $this->run_responses_completion_query( $query, $streamCallback );
1795 }
1796
1797 // Fallback to ChatML implementation
1798 $reply = parent::run_completion_query( $query, $streamCallback );
1799
1800 // Check for empty output when reasoning is enabled (GPT-5 models)
1801 // This safety check is here in case GPT-5 somehow uses the regular API
1802 if ( strpos( $query->model, 'gpt-5' ) === 0 && !empty( $query->reasoning ) ) {
1803 if ( empty( $reply->result ) || trim( $reply->result ) === '' ) {
1804 if ( empty( $reply->needFeedbacks ) && empty( $reply->needClientActions ) ) {
1805 throw new Exception(
1806 'The model returned an empty response. This typically happens when reasoning consumes all available tokens. ' .
1807 'Please increase the Max Tokens setting to allow space for both reasoning and the actual response. ' .
1808 'Current Max Tokens: ' . ( $query->maxTokens ?? 'default' ) . '. ' .
1809 'Try setting it to at least ' . ( ( $query->maxTokens ?? 4096 ) + 2000 ) . ' tokens.'
1810 );
1811 }
1812 }
1813 }
1814
1815 return $reply;
1816 }
1817
1818 /**
1819 * Run completion query using Responses API
1820 */
1821 protected function run_responses_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
1822 // Store current query for URL building (needed for Azure deployment name)
1823 $this->currentQuery = $query;
1824
1825 // Check if we have functions that might require feedback
1826 $hasFunctions = !empty( $query->functions );
1827
1828 $isStreaming = !is_null( $streamCallback );
1829
1830 // Initialize debug mode
1831 $this->init_debug_mode( $query );
1832
1833 // IMPORTANT: Prepare query BEFORE setting up streaming hooks
1834 // The streaming hook intercepts ALL wp_remote_* calls, so preparation must happen first
1835 $this->prepare_query( $query );
1836
1837 if ( $isStreaming ) {
1838 $this->streamCallback = $streamCallback;
1839 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1840 }
1841
1842 $this->reset_stream();
1843 $body = $this->build_responses_body( $query, $streamCallback );
1844 $url = $this->build_responses_url();
1845 $headers = $this->build_headers( $query );
1846 $options = $this->build_options( $headers, $body );
1847
1848 // Store the request body for debugging
1849 $this->lastRequestBody = $body;
1850
1851 // Debug log for Responses API
1852 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
1853 if ( $queries_debug ) {
1854 error_log( '[AI Engine Queries] Using Responses API' );
1855 error_log( '[AI Engine Queries] Request URL: ' . $url );
1856 error_log( '[AI Engine Queries] Request Body: ' . json_encode( $body, JSON_PRETTY_PRINT ) );
1857
1858 // Log specific tool information
1859 if ( isset( $body['tools'] ) && is_array( $body['tools'] ) ) {
1860 error_log( '[AI Engine Queries] Tools included in request:' );
1861 foreach ( $body['tools'] as $index => $tool ) {
1862 $toolInfo = 'Tool ' . $index . ': type=' . ( $tool['type'] ?? 'unknown' );
1863 if ( $tool['type'] === 'file_search' && isset( $tool['vector_store_ids'] ) ) {
1864 $toolInfo .= ', vector_store_ids=' . json_encode( $tool['vector_store_ids'] );
1865 }
1866 error_log( '[AI Engine Queries] - ' . $toolInfo );
1867 }
1868 }
1869 else {
1870 error_log( '[AI Engine Queries] No tools included in request' );
1871 }
1872 }
1873
1874 // Emit "Request sent" event for feedback queries
1875 if ( $this->currentDebugMode && !empty( $streamCallback ) &&
1876 ( $query instanceof Meow_MWAI_Query_Feedback || $query instanceof Meow_MWAI_Query_AssistFeedback ) ) {
1877 $event = Meow_MWAI_Event::request_sent()
1878 ->set_metadata( 'is_feedback', true )
1879 ->set_metadata( 'feedback_count', count( $query->blocks ) );
1880 call_user_func( $streamCallback, $event );
1881 }
1882
1883 try {
1884 // Log the input being sent for feedback queries
1885 if ( $queries_debug && $query instanceof Meow_MWAI_Query_Feedback && isset( $body['input'] ) ) {
1886 error_log( '[AI Engine Queries] Sending feedback with ' . count( $body['input'] ) . ' messages to Responses API' );
1887 error_log( '[AI Engine Queries] Previous Response ID: ' . ( $body['previous_response_id'] ?? 'none' ) );
1888 foreach ( $body['input'] as $idx => $msg ) {
1889 $msgType = is_array( $msg ) && isset( $msg['type'] ) ? $msg['type'] : 'unknown';
1890 $callId = is_array( $msg ) && isset( $msg['call_id'] ) ? $msg['call_id'] : 'no-id';
1891 error_log( '[AI Engine Queries] Message ' . $idx . ': type=' . $msgType . ', call_id=' . $callId );
1892 if ( $msgType === 'function_call' && isset( $msg['name'] ) ) {
1893 error_log( '[AI Engine Queries] Function name: ' . $msg['name'] );
1894 }
1895 if ( $msgType === 'function_call_output' && isset( $msg['output'] ) ) {
1896 error_log( '[AI Engine Queries] Output: ' . substr( $msg['output'], 0, 50 ) . '...' );
1897 }
1898 }
1899 }
1900
1901 $res = $this->run_query( $url, $options, $streamCallback );
1902 $reply = new Meow_MWAI_Reply( $query );
1903
1904 $returned_id = null;
1905 $returned_model = $this->inModel;
1906 $returned_in_tokens = null;
1907 $returned_out_tokens = null;
1908 $returned_price = null;
1909 $returned_choices = [];
1910
1911 // Streaming Mode
1912 if ( $isStreaming ) {
1913 if ( empty( $this->streamContent ) ) {
1914 $error = $this->try_decode_error( $this->streamBuffer );
1915 if ( !is_null( $error ) ) {
1916 throw new Exception( $error );
1917 }
1918 }
1919
1920 $returned_id = $this->inId;
1921 $returned_model = $this->inModel ? $this->inModel : $query->model;
1922
1923 // Debug: Log model extraction for streaming
1924 if ( $queries_debug ) {
1925 error_log( '[AI Engine Queries] Model extraction (streaming):' );
1926 error_log( ' - Stream model: ' . ( $this->inModel ?? 'NOT SET' ) );
1927 error_log( ' - Query model: ' . $query->model );
1928 error_log( ' - Using model: ' . $returned_model );
1929 }
1930
1931 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
1932
1933 // Store code interpreter code if any
1934 if ( !empty( $this->streamContentCode ) ) {
1935 $reply->contentCode = $this->streamContentCode;
1936 Meow_MWAI_Logging::log( 'Responses API: Stored ' . strlen( $this->streamContentCode ) . ' bytes of code interpreter code' );
1937 }
1938
1939 // REMOVED - We'll handle files after streaming completes, not here
1940
1941 if ( !empty( $this->streamToolCalls ) ) {
1942 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1943 error_log( '[AI Engine Queries] Responses API: Found ' . count( $this->streamToolCalls ) . ' tool calls in streaming response' );
1944 foreach ( $this->streamToolCalls as $idx => $toolCall ) {
1945 error_log( '[AI Engine Queries] Tool call ' . $idx . ': ' . $toolCall['function']['name'] . ' (id: ' . $toolCall['id'] . ')' );
1946 }
1947 }
1948 $message['tool_calls'] = $this->streamToolCalls;
1949 }
1950
1951 if ( !is_null( $this->streamInTokens ) ) {
1952 $returned_in_tokens = $this->streamInTokens;
1953 }
1954 if ( !is_null( $this->streamOutTokens ) ) {
1955 $returned_out_tokens = $this->streamOutTokens;
1956 }
1957 if ( !is_null( $this->streamCost ) ) {
1958 $returned_price = $this->streamCost;
1959 }
1960
1961 // Handle code interpreter sandbox files ONLY if code interpreter has completed
1962 if ( !empty( $this->streamContainerId ) && !empty( $this->streamContent ) && $this->codeInterpreterCompleted ) {
1963 // Check for sandbox links before processing
1964 if ( strpos( $this->streamContent, 'sandbox:' ) !== false ) {
1965 // Pass file citations if available
1966 $fileCitations = isset( $this->streamCodeInterpreterFiles ) ? $this->streamCodeInterpreterFiles : [];
1967
1968 // Download files and replace sandbox links
1969 $this->streamContent = $this->handle_code_interpreter_sandbox_files(
1970 $this->streamContent,
1971 $this->streamContainerId,
1972 $query,
1973 $fileCitations,
1974 true // streaming mode
1975 );
1976 }
1977
1978 // Update the message content with replaced links
1979 $message['content'] = $this->streamContent;
1980 }
1981
1982 $returned_choices = [ [ 'message' => $message ] ];
1983
1984 // Add generated images to the content if any
1985 if ( !empty( $this->streamImages ) ) {
1986 // Add images as additional choices with b64_json format
1987 foreach ( $this->streamImages as $base64Image ) {
1988 $returned_choices[] = [ 'b64_json' => $base64Image ];
1989 }
1990 Meow_MWAI_Logging::log( 'Responses API: Added ' . count( $this->streamImages ) . ' images to choices (streaming)' );
1991 }
1992
1993 // Log streaming response data if queries debug is enabled
1994 if ( $queries_debug ) {
1995 error_log( '[AI Engine Queries] Streaming Response Collected:' );
1996 $streaming_data = [
1997 'id' => $returned_id,
1998 'model' => $returned_model,
1999 'content_length' => strlen( $this->streamContent ),
2000 'content_preview' => substr( $this->streamContent, 0, 200 ) . ( strlen( $this->streamContent ) > 200 ? '...' : '' ),
2001 'tool_calls' => !empty( $this->streamToolCalls ) ? count( $this->streamToolCalls ) . ' tool calls' : 'none',
2002 'usage' => [
2003 'input_tokens' => $returned_in_tokens,
2004 'output_tokens' => $returned_out_tokens,
2005 'cost' => $returned_price
2006 ]
2007 ];
2008
2009 // Log tool calls details if present
2010 if ( !empty( $this->streamToolCalls ) ) {
2011 $streaming_data['tool_calls_details'] = [];
2012 foreach ( $this->streamToolCalls as $tool_call ) {
2013 $streaming_data['tool_calls_details'][] = [
2014 'id' => $tool_call['id'] ?? 'unknown',
2015 'name' => $tool_call['function']['name'] ?? 'unknown',
2016 'arguments' => substr( $tool_call['function']['arguments'] ?? '{}', 0, 100 ) . '...'
2017 ];
2018 }
2019 }
2020
2021 error_log( json_encode( $streaming_data, JSON_PRETTY_PRINT ) );
2022 }
2023 }
2024 // Standard Mode
2025 else {
2026 $data = $res['data'];
2027 if ( empty( $data ) ) {
2028 throw new Exception( 'No content received (res is null).' );
2029 }
2030
2031 // Debug logging for non-streaming mode
2032 if ( $queries_debug ) {
2033 error_log( '[AI Engine Queries] Full response structure (non-streaming):' );
2034 error_log( json_encode( $data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
2035
2036 // Look for container_id in the response
2037 $this->search_for_container_id_recursive( $data, '' );
2038 }
2039
2040 // Ensure $data is an array
2041 if ( !is_array( $data ) ) {
2042 $error_message = is_string( $data ) ? $data : 'Invalid response format';
2043 throw new Exception( 'Responses API error: ' . $error_message );
2044 }
2045
2046 // Handle Responses API response format
2047 $returned_id = $data['id'] ?? null;
2048 $returned_model = $data['model'] ?? $query->model;
2049
2050 // Debug: Log model extraction
2051 if ( $queries_debug ) {
2052 error_log( '[AI Engine Queries] Model extraction:' );
2053 error_log( ' - Response model: ' . ( $data['model'] ?? 'NOT SET' ) );
2054 error_log( ' - Query model: ' . $query->model );
2055 error_log( ' - Using model: ' . $returned_model );
2056 }
2057
2058 // Extract content from Responses API format
2059 $content = '';
2060 $tool_calls = [];
2061 $images = [];
2062
2063 if ( isset( $data['output'] ) && is_array( $data['output'] ) ) {
2064
2065 foreach ( $data['output'] as $idx => $output_item ) {
2066 if ( isset( $output_item['type'] ) && $output_item['type'] === 'message' && isset( $output_item['content'] ) ) {
2067 // Handle message content array - this is the actual text content
2068 if ( is_array( $output_item['content'] ) ) {
2069 foreach ( $output_item['content'] as $content_item ) {
2070 // The actual text is in content_item['text'] for type 'output_text'
2071 if ( isset( $content_item['type'] ) && $content_item['type'] === 'output_text' && isset( $content_item['text'] ) ) {
2072 $content .= $content_item['text'];
2073 }
2074 // Fallback checks for other possible structures
2075 elseif ( isset( $content_item['content'] ) && is_string( $content_item['content'] ) ) {
2076 $content .= $content_item['content'];
2077 }
2078 elseif ( is_string( $content_item ) ) {
2079 $content .= $content_item;
2080 }
2081 }
2082 }
2083 }
2084 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'function_call' ) {
2085 // Responses API returns function_call type with call_id
2086 $callId = $output_item['call_id'] ?? $output_item['id'] ?? null;
2087 $functionName = $output_item['name'] ?? '';
2088 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
2089 error_log( '[AI Engine Queries] Found function_call: ' . $functionName . ' (call_id: ' . $callId . ')' );
2090 }
2091
2092 $tool_calls[] = [
2093 'id' => $callId,
2094 'type' => 'function',
2095 'function' => [
2096 'name' => $functionName,
2097 'arguments' => $output_item['arguments'] ?? '{}'
2098 ]
2099 ];
2100 }
2101 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'code_interpreter_call' ) {
2102 // Handle code interpreter calls - both with and without results
2103
2104 // Store container ID if available (this is the primary location)
2105 if ( isset( $output_item['container_id'] ) ) {
2106 $codeInterpreterContainerId = $output_item['container_id'];
2107 Meow_MWAI_Logging::log( 'Responses API: Found container_id for code interpreter: ' . $codeInterpreterContainerId );
2108 }
2109
2110 // Log the entire output_item structure for debugging
2111 if ( $queries_debug ) {
2112 error_log( '[AI Engine Queries] Code interpreter output_item structure:' );
2113 error_log( json_encode( $output_item, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ) );
2114 }
2115
2116 // Handle results if they exist
2117 if ( isset( $output_item['result'] ) ) {
2118 $result = $output_item['result'];
2119
2120 // Also check for container_id in the result itself (backup location)
2121 if ( isset( $result['container_id'] ) && !isset( $codeInterpreterContainerId ) ) {
2122 $codeInterpreterContainerId = $result['container_id'];
2123 Meow_MWAI_Logging::log( 'Responses API: Found container_id in result: ' . $codeInterpreterContainerId );
2124 }
2125
2126 // Append stdout to content if available
2127 if ( isset( $result['stdout'] ) && !empty( $result['stdout'] ) ) {
2128 $content .= "\n```\n" . $result['stdout'] . "\n```\n";
2129 Meow_MWAI_Logging::log( 'Responses API: Found code interpreter output in non-streaming mode' );
2130 }
2131 }
2132 }
2133 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'image_generation_call' && isset( $output_item['result'] ) ) {
2134 // Handle image generation results
2135 $base64Image = $output_item['result'];
2136 $images[] = $base64Image;
2137
2138 Meow_MWAI_Logging::log( 'Responses API: Found generated image in non-streaming mode' );
2139 }
2140 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'mcp_approval_request' ) {
2141 // IMPORTANT: MCP approval requests are already handled via streaming events
2142 // We must skip them here to prevent duplicate function calls
2143 // MCP tools are executed remotely by OpenAI and don't need local execution
2144 Meow_MWAI_Logging::log( 'Responses API: Skipping MCP approval request for ' . $output_item['name'] . ' (already handled via events)' );
2145 }
2146 }
2147 }
2148
2149 // If we couldn't find content in output, try other locations
2150 if ( empty( $content ) ) {
2151 if ( isset( $data['text'] ) ) {
2152 if ( is_string( $data['text'] ) ) {
2153 $content = $data['text'];
2154 }
2155 elseif ( is_array( $data['text'] ) ) {
2156 // Only implode if it's an array of strings, not complex structures
2157 $textParts = array_filter( $data['text'], 'is_string' );
2158 if ( !empty( $textParts ) ) {
2159 $content = implode( '', $textParts );
2160 }
2161 }
2162 }
2163 elseif ( isset( $data['content'] ) ) {
2164 if ( is_array( $data['content'] ) && isset( $data['content'][0]['text'] ) ) {
2165 $content = $data['content'][0]['text'];
2166 }
2167 elseif ( is_string( $data['content'] ) ) {
2168 $content = $data['content'];
2169 }
2170 }
2171 }
2172
2173 // If still no content found, log for debugging
2174 if ( empty( $content ) ) {
2175 // Check if $data is actually an array before using array_keys
2176 if ( is_array( $data ) ) {
2177 Meow_MWAI_Logging::log( 'Responses API: No content found in response. Structure: ' . json_encode( array_keys( $data ) ) );
2178 if ( isset( $data['output'][0] ) ) {
2179 Meow_MWAI_Logging::log( 'Responses API: First output item: ' . json_encode( $data['output'][0] ) );
2180 }
2181 if ( isset( $data['text'] ) ) {
2182 Meow_MWAI_Logging::log( 'Responses API: Text field structure: ' . json_encode( $data['text'] ) );
2183 }
2184 }
2185 else {
2186 // If $data is not an array, it might be an error string
2187 Meow_MWAI_Logging::log( 'Responses API: Invalid response data type. Data: ' . ( is_string( $data ) ? $data : json_encode( $data ) ) );
2188 }
2189 // Log the entire response for debugging
2190 Meow_MWAI_Logging::log( 'Responses API: Full response data: ' . json_encode( $data ) );
2191 }
2192
2193 // Handle code interpreter sandbox files if we have a container ID
2194 if ( !empty( $codeInterpreterContainerId ) ) {
2195 $content = $this->handle_code_interpreter_sandbox_files(
2196 $content,
2197 $codeInterpreterContainerId,
2198 $query
2199 );
2200 }
2201
2202 $message = [ 'role' => 'assistant', 'content' => $content ];
2203 if ( !empty( $tool_calls ) ) {
2204 $message['tool_calls'] = $tool_calls;
2205 Meow_MWAI_Logging::log( 'Responses API: Found ' . count( $tool_calls ) . ' tool calls' );
2206 }
2207
2208 $returned_choices = [[ 'message' => $message ]];
2209
2210 // Add images as additional choices
2211 if ( !empty( $images ) ) {
2212 foreach ( $images as $base64Image ) {
2213 $returned_choices[] = [ 'b64_json' => $base64Image ];
2214 }
2215 Meow_MWAI_Logging::log( 'Responses API: Added ' . count( $images ) . ' images to choices' );
2216 }
2217
2218 // Extract usage information
2219 // Responses API uses input_tokens/output_tokens
2220 $usage = $data['usage'] ?? [];
2221 $returned_in_tokens = $usage['input_tokens'] ?? $usage['prompt_tokens'] ?? null;
2222 $returned_out_tokens = $usage['output_tokens'] ?? $usage['completion_tokens'] ?? null;
2223 $returned_price = $usage['cost'] ?? null;
2224 }
2225
2226 // Store response ID for future stateful requests
2227 if ( !empty( $returned_id ) ) {
2228 $this->previousResponseId = $returned_id;
2229 $reply->set_id( $returned_id );
2230 }
2231 // Set the results
2232 $reply->set_choices( $returned_choices );
2233
2234 // Check for empty output when reasoning is enabled (GPT-5 models)
2235 // This can happen when reasoning consumes all available tokens
2236 if ( strpos( $query->model, 'gpt-5' ) === 0 && !empty( $query->reasoning ) ) {
2237 // Check if the reply has no content
2238 if ( empty( $reply->result ) || trim( $reply->result ) === '' ) {
2239 // Check if we have function calls - those are valid even without text content
2240 if ( empty( $reply->needFeedbacks ) && empty( $reply->needClientActions ) ) {
2241 throw new Exception(
2242 'The model returned an empty response. This typically happens when reasoning consumes all available tokens. ' .
2243 'Please increase the Max Tokens setting to allow space for both reasoning and the actual response. ' .
2244 'Current Max Tokens: ' . ( $query->maxTokens ?? 'default' ) . '. ' .
2245 'Try setting it to at least ' . ( ( $query->maxTokens ?? 4096 ) + 2000 ) . ' tokens.'
2246 );
2247 }
2248 }
2249 }
2250
2251 // Handle tokens usage
2252 $this->handle_tokens_usage(
2253 $reply,
2254 $query,
2255 $returned_model,
2256 $returned_in_tokens,
2257 $returned_out_tokens,
2258 $returned_price
2259 );
2260
2261 return $reply;
2262 }
2263 catch ( Exception $e ) {
2264 $service = $this->get_service_name();
2265 Meow_MWAI_Logging::error( "$service (Responses API): " . $e->getMessage() );
2266 $message = "$service (Responses API): " . $e->getMessage();
2267 throw new Exception( $message );
2268 }
2269 finally {
2270 if ( !is_null( $streamCallback ) ) {
2271 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
2272 }
2273 }
2274 }
2275
2276 /**
2277 * Override handle_tokens_usage to set accuracy properly
2278 */
2279 public function handle_tokens_usage(
2280 $reply,
2281 $query,
2282 $returned_model,
2283 $returned_in_tokens,
2284 $returned_out_tokens,
2285 $returned_price = null
2286 ) {
2287 // Call parent to handle the actual usage recording
2288 parent::handle_tokens_usage(
2289 $reply,
2290 $query,
2291 $returned_model,
2292 $returned_in_tokens,
2293 $returned_out_tokens,
2294 $returned_price
2295 );
2296
2297 // Set accuracy based on data availability
2298 if ( !is_null( $returned_price ) && !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
2299 // Responses API with cost field or OpenRouter style = full accuracy
2300 $reply->set_usage_accuracy( 'full' );
2301 }
2302 elseif ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
2303 // Tokens from API but price calculated = tokens accuracy
2304 $reply->set_usage_accuracy( 'tokens' );
2305 }
2306 else {
2307 // Everything estimated
2308 $reply->set_usage_accuracy( 'estimated' );
2309 }
2310 }
2311
2312 /**
2313 * Override image query handling for gpt-image-1 model
2314 */
2315 public function run_image_query( $query, $streamCallback = null ) {
2316 // IMPORTANT: We use the standard Images API for gpt-image-1 (not Responses API)
2317 // Even though Responses API supports image_generation tool, it would let the
2318 // orchestrator model choose which image model to use. By using the Images API
2319 // directly, we ensure gpt-image-1 is actually used as requested by the user.
2320
2321 // Use standard implementation for all image models including gpt-image-1
2322 return parent::run_image_query( $query, $streamCallback );
2323 }
2324
2325 /**
2326 * Override transcription to support new models
2327 */
2328 public function run_transcribe_query( $query ) {
2329 // Check if using new transcription models
2330 $newTranscribeModels = ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe'];
2331 if ( in_array( $query->model, $newTranscribeModels ) ) {
2332 // These still use the /audio/transcriptions endpoint but with new models
2333 // Just need to make sure the model name is passed correctly
2334 }
2335
2336 // Use parent implementation (still uses audio endpoint)
2337 return parent::run_transcribe_query( $query );
2338 }
2339
2340 /**
2341 * Override embedding query to support new models
2342 */
2343 public function run_embedding_query( $query ) {
2344 // Check if using new embedding models
2345 $newEmbeddingModels = ['text-embedding-3-small', 'text-embedding-3-large'];
2346 if ( in_array( $query->model, $newEmbeddingModels ) ) {
2347 // These still use the /embeddings endpoint but with improved models
2348 // The parent implementation should handle this correctly
2349 }
2350
2351 // Use parent implementation
2352 return parent::run_embedding_query( $query );
2353 }
2354
2355 /**
2356 * Enhanced error handling for Responses API
2357 */
2358 protected function handle_responses_errors( $data ) {
2359 // Handle Responses API specific errors
2360 if ( isset( $data['error'] ) ) {
2361 $error = $data['error'];
2362 $message = $error['message'] ?? 'Unknown error';
2363 $type = $error['type'] ?? null;
2364 $code = $error['code'] ?? null;
2365
2366 // Special handling for "No tool output found" errors
2367 if ( strpos( $message, 'No tool output found' ) !== false ) {
2368 // Log this error with details when queries debug is enabled
2369 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
2370 error_log( '[AI Engine Queries] Responses API Tool Output Error:' );
2371 error_log( '[AI Engine Queries] Error: ' . $message );
2372 error_log( '[AI Engine Queries] This typically means the function call outputs were not properly formatted or are missing.' );
2373
2374 // Log the last request body if available
2375 if ( property_exists( $this, 'lastRequestBody' ) && $this->lastRequestBody ) {
2376 error_log( '[AI Engine Queries] Last request body: ' . json_encode( $this->lastRequestBody, JSON_PRETTY_PRINT ) );
2377 }
2378 }
2379 }
2380
2381 $errorMessage = $message;
2382 if ( $type ) {
2383 $errorMessage .= " (Type: $type)";
2384 }
2385 if ( $code ) {
2386 $errorMessage .= " (Code: $code)";
2387 }
2388
2389 throw new Exception( $errorMessage );
2390 }
2391
2392 // Check for event-based errors
2393 if ( isset( $data['event'] ) && $data['event'] === 'response.error' ) {
2394 $error = $data['error'] ?? [];
2395 $message = $error['message'] ?? 'Response API error';
2396 throw new Exception( $message );
2397 }
2398
2399 // Fallback to parent error handling
2400 parent::handle_response_errors( $data );
2401 }
2402
2403 /**
2404 * Add method to reset conversation state
2405 */
2406 public function reset_conversation_state() {
2407 $this->previousResponseId = null;
2408 $this->conversationState = [];
2409 }
2410
2411 /**
2412 * Check the connection to OpenAI by listing models.
2413 * This is a free metadata call that verifies API key validity.
2414 */
2415 public function connection_check() {
2416 try {
2417 $url = $this->get_models_endpoint();
2418 $response = $this->execute( 'GET', $url );
2419
2420 if ( !isset( $response['data'] ) || !is_array( $response['data'] ) ) {
2421 throw new Exception( 'Invalid response format from OpenAI' );
2422 }
2423
2424 $modelCount = count( $response['data'] );
2425 $availableModels = [];
2426
2427 // Get first 5 models for display
2428 $displayModels = array_slice( $response['data'], 0, 5 );
2429 foreach ( $displayModels as $model ) {
2430 if ( isset( $model['id'] ) ) {
2431 $availableModels[] = $model['id'];
2432 }
2433 }
2434
2435 return [
2436 'success' => true,
2437 'service' => 'OpenAI',
2438 'message' => "Connection successful. Found {$modelCount} models.",
2439 'details' => [
2440 'endpoint' => $url,
2441 'model_count' => $modelCount,
2442 'sample_models' => $availableModels,
2443 'organization' => $response['organization'] ?? null
2444 ]
2445 ];
2446 }
2447 catch ( Exception $e ) {
2448 return [
2449 'success' => false,
2450 'service' => 'OpenAI',
2451 'error' => $e->getMessage(),
2452 'details' => [
2453 'endpoint' => $this->get_models_endpoint()
2454 ]
2455 ];
2456 }
2457 }
2458
2459 /**
2460 * Handle code interpreter sandbox files
2461 * Parses sandbox links from content, downloads files, and replaces links
2462 */
2463 protected function handle_code_interpreter_sandbox_files( $content, $containerId, $query, $fileCitations = [], $isStreaming = false ) {
2464 if ( empty( $containerId ) || empty( $content ) ) {
2465 return $content;
2466 }
2467
2468 // Use streamCodeInterpreterFiles if available (from annotations)
2469 if ( !empty( $this->streamCodeInterpreterFiles ) ) {
2470 $fileCitations = $this->streamCodeInterpreterFiles;
2471 }
2472
2473 // Parse sandbox links from content
2474 $sandboxLinks = $this->parse_sandbox_links( $content );
2475
2476 if ( empty( $sandboxLinks ) ) {
2477 return $content;
2478 }
2479
2480 Meow_MWAI_Logging::log( 'Code Interpreter: Processing ' . count( $sandboxLinks ) . ' sandbox files' );
2481
2482 $containerFiles = [];
2483
2484 // If we have file citations, use them directly (skip container list API)
2485 if ( !empty( $fileCitations ) ) {
2486 foreach ( $fileCitations as $citation ) {
2487 if ( isset( $citation['file_id'] ) ) {
2488 $containerFiles[] = [
2489 'id' => $citation['file_id'],
2490 'path' => $citation['path'] ?? ( '/mnt/data/' . $citation['filename'] ),
2491 'filename' => $citation['filename'] ?? null
2492 ];
2493 }
2494 }
2495 }
2496 else {
2497 // Only try container API if we don't have file citations
2498 error_log( '[AI Engine] No file citations, will try container list API' );
2499 $containerFiles = $this->list_container_files( $containerId, $query );
2500 }
2501
2502 if ( empty( $containerFiles ) ) {
2503 error_log( '[AI Engine] WARNING: No files found from citations or container API' );
2504 Meow_MWAI_Logging::warn( 'No files found in container ' . $containerId );
2505 return $content;
2506 }
2507
2508 // Process each sandbox link
2509 $replacements = 0;
2510 foreach ( $sandboxLinks as $sandboxPath ) {
2511 $filename = basename( $sandboxPath );
2512
2513 // Find the file in container
2514 $fileId = $this->find_container_file_id( $containerFiles, $filename );
2515
2516 if ( !$fileId ) {
2517 error_log( '[AI Engine] ERROR: File ID not found for: ' . $filename );
2518 Meow_MWAI_Logging::warn( 'Code Interpreter: File not found in container: ' . $filename );
2519 continue;
2520 }
2521
2522 // Try to download the file with retries if streaming
2523 $publicUrl = null;
2524 $maxRetries = $isStreaming ? 3 : 1;
2525 $retryDelay = 2; // seconds
2526
2527 for ( $attempt = 1; $attempt <= $maxRetries; $attempt++ ) {
2528 if ( $attempt > 1 ) {
2529 error_log( '[AI Engine] Retry attempt ' . $attempt . ' after ' . $retryDelay . ' seconds...' );
2530 sleep( $retryDelay );
2531 $retryDelay *= 2; // exponential backoff
2532 }
2533
2534 $publicUrl = $this->download_container_file( $containerId, $fileId, $filename, $query );
2535
2536 if ( $publicUrl ) {
2537 break;
2538 }
2539 }
2540
2541 if ( $publicUrl ) {
2542 // Replace sandbox link with public URL
2543 $content = str_replace( $sandboxPath, $publicUrl, $content );
2544 $replacements++;
2545 Meow_MWAI_Logging::log( 'Replaced sandbox link: ' . $filename . ' -> ' . $publicUrl );
2546 }
2547 else {
2548
2549 // If download fails, create a message about it
2550 $errorMessage = sprintf(
2551 '[File: %s - Download temporarily unavailable, refresh page to retry]',
2552 $filename
2553 );
2554 $content = str_replace( $sandboxPath, $errorMessage, $content );
2555 $replacements++;
2556 }
2557 }
2558
2559 return $content;
2560 }
2561
2562 /**
2563 * Parse sandbox links from content
2564 */
2565 protected function parse_sandbox_links( $content ) {
2566 $links = [];
2567
2568 // Match various sandbox link patterns
2569 $patterns = [
2570 '/sandbox:\/mnt\/data\/[^)\s]+/', // Basic pattern
2571 '/\(sandbox:\/mnt\/data\/[^)]+\)/', // In parentheses
2572 '/\[([^\]]*)\]\(sandbox:\/mnt\/data\/[^)]+\)/', // Markdown links
2573 ];
2574
2575 foreach ( $patterns as $pattern ) {
2576 if ( preg_match_all( $pattern, $content, $matches ) ) {
2577 foreach ( $matches[0] as $match ) {
2578 // Extract just the sandbox path
2579 if ( preg_match( '/sandbox:\/mnt\/data\/[^)\s\]]+/', $match, $pathMatch ) ) {
2580 $links[] = $pathMatch[0];
2581 }
2582 }
2583 }
2584 }
2585
2586 return array_unique( $links );
2587 }
2588
2589 /**
2590 * List files in a container
2591 */
2592 protected function list_container_files( $containerId, $query ) {
2593 try {
2594 // Use the execute function with the path format it expects
2595 $path = '/containers/' . $containerId . '/files';
2596
2597 // Try to call the API (remove streaming handler for JSON requests)
2598 $response = null;
2599 try {
2600 $response = $this->without_stream_handler( function () use ( $path ) {
2601 return $this->execute( 'GET', $path, null, null, true );
2602 } );
2603 }
2604 catch ( Exception $api_exception ) {
2605 // If it's a 404, the container might not exist yet or might be expired
2606 if ( strpos( $api_exception->getMessage(), '404' ) !== false ) {
2607 // Wait a moment and retry once
2608 sleep( 2 );
2609
2610 try {
2611 $response = $this->without_stream_handler( function () use ( $path ) {
2612 return $this->execute( 'GET', $path, null, null, true );
2613 } );
2614 }
2615 catch ( Exception $retry_exception ) {
2616 throw $retry_exception;
2617 }
2618 }
2619 else {
2620 throw $api_exception;
2621 }
2622 }
2623
2624 // Check if response is null or empty array
2625 if ( $response === null || ( is_array( $response ) && empty( $response ) ) ) {
2626 // Try waiting a bit for files to be ready
2627 sleep( 3 );
2628
2629 // Try one more time
2630 $response = $this->execute( 'GET', $path, null, null, true );
2631
2632 // If still empty, wait longer and try once more
2633 if ( $response === null || ( is_array( $response ) && empty( $response ) ) ) {
2634 sleep( 5 );
2635 $response = $this->execute( 'GET', $path, null, null, true );
2636 }
2637 }
2638
2639 if ( isset( $response['data'] ) && is_array( $response['data'] ) ) {
2640 return $response['data'];
2641 }
2642 else if ( is_array( $response ) && isset( $response[0] ) ) {
2643 // Maybe the response is directly an array of files
2644 return $response;
2645 }
2646 }
2647 catch ( Exception $e ) {
2648 Meow_MWAI_Logging::warn( 'Failed to list container files: ' . $e->getMessage() );
2649 }
2650
2651 return [];
2652 }
2653
2654 /**
2655 * Find file ID by filename in container files list
2656 */
2657 protected function find_container_file_id( $containerFiles, $filename ) {
2658 foreach ( $containerFiles as $file ) {
2659 // Check if filename matches the end of the path
2660 if ( isset( $file['path'] ) ) {
2661 // Handle cases where path might contain multiple filenames separated by spaces
2662 $paths = preg_split( '/\s+/', $file['path'] );
2663 foreach ( $paths as $path ) {
2664 if ( str_ends_with( $path, $filename ) ) {
2665 return $file['id'];
2666 }
2667 }
2668 }
2669
2670 // Also check direct filename match
2671 if ( isset( $file['filename'] ) && $file['filename'] === $filename ) {
2672 return $file['id'];
2673 }
2674 }
2675
2676 return null;
2677 }
2678
2679 /**
2680 * Execute HTTP request without streaming handler interference
2681 */
2682 private function without_stream_handler( callable $fn ) {
2683 $cb = [ $this, 'stream_handler' ];
2684 $had = has_action( 'http_api_curl', $cb );
2685 if ( $had ) {
2686 remove_action( 'http_api_curl', $cb );
2687 }
2688 try {
2689 return $fn();
2690 }
2691 finally {
2692 if ( $had ) {
2693 add_action( 'http_api_curl', $cb, 10, 3 );
2694 }
2695 }
2696 }
2697
2698 /**
2699 * Download a file from container and store it locally
2700 */
2701 protected function download_container_file( $containerId, $fileId, $filename, $query ) {
2702 try {
2703 $fileContent = null;
2704
2705 // For container files (cfile_*), we MUST use the Container API
2706 if ( strpos( $fileId, 'cfile_' ) === 0 ) {
2707 if ( empty( $containerId ) ) {
2708 throw new Exception( 'Container ID is required for downloading container files' );
2709 }
2710
2711 // Use the Container API endpoint
2712 $path = '/containers/' . $containerId . '/files/' . $fileId . '/content';
2713
2714 try {
2715 // Remove streaming handler and download binary content
2716 $headers = [ 'Accept' => '*/*' ];
2717 $fileContent = $this->without_stream_handler( function () use ( $path, $headers ) {
2718 // false = raw binary content, not JSON
2719 return $this->execute( 'GET', $path, null, $headers, false );
2720 } );
2721
2722 if ( strlen( $fileContent ) > 0 ) {
2723 Meow_MWAI_Logging::log( 'Container API: Downloaded ' . strlen( $fileContent ) . ' bytes for ' . $filename );
2724 }
2725 else {
2726 throw new Exception( 'Container file returned empty content' );
2727 }
2728 }
2729 catch ( Exception $e ) {
2730 throw $e;
2731 }
2732 }
2733 else {
2734 // Regular file_* files use the standard Files API
2735 $filesPath = '/files/' . $fileId . '/content';
2736 $headers = [ 'Accept' => '*/*' ];
2737 $fileContent = $this->without_stream_handler( function () use ( $filesPath, $headers ) {
2738 return $this->execute( 'GET', $filesPath, null, $headers, false );
2739 } );
2740 }
2741
2742 if ( empty( $fileContent ) ) {
2743 error_log( '[AI Engine] ERROR: Both APIs failed to return content' );
2744 throw new Exception( 'Empty file content received from both Files API and Container API' );
2745 }
2746
2747 // Save to temporary file
2748 $tmpFile = tempnam( sys_get_temp_dir(), 'mwai_code_' );
2749 file_put_contents( $tmpFile, $fileContent );
2750
2751 // Upload to our file system
2752 $purpose = 'assistant-out';
2753 $metadata = [
2754 'source' => 'code_interpreter',
2755 'container_id' => $containerId,
2756 'file_id' => $fileId
2757 ];
2758
2759 $refId = $this->core->files->upload_file( $tmpFile, $filename, $purpose, $metadata, $query->envId );
2760
2761 // Update the file's refId to match the OpenAI file ID
2762 $internalFileId = $this->core->files->get_id_from_refId( $refId );
2763 $this->core->files->update_refId( $internalFileId, $fileId );
2764
2765 // Get the public URL
2766 $publicUrl = $this->core->files->get_url( $fileId );
2767
2768 // Clean up temp file
2769 @unlink( $tmpFile );
2770
2771 return $publicUrl;
2772 }
2773 catch ( Exception $e ) {
2774 error_log( '[AI Engine] EXCEPTION in download_container_file: ' . $e->getMessage() );
2775 error_log( '[AI Engine] Stack trace: ' . $e->getTraceAsString() );
2776 Meow_MWAI_Logging::warn( 'Failed to download container file ' . $filename . ': ' . $e->getMessage() );
2777 return null;
2778 }
2779 }
2780
2781 /**
2782 * Get the models endpoint URL
2783 */
2784 protected function get_models_endpoint() {
2785 $endpoint = null;
2786
2787 // Same logic as build_url to determine the endpoint
2788 if ( $this->envType === 'openai' ) {
2789 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
2790 }
2791 else if ( $this->envType === 'azure' ) {
2792 $endpoint = isset( $this->env['endpoint'] ) ? $this->env['endpoint'] : null;
2793 }
2794
2795 if ( empty( $endpoint ) ) {
2796 throw new Exception( 'Endpoint is not defined for envType: ' . $this->envType );
2797 }
2798
2799 // Remove any existing API paths to get base URL
2800 $endpoint = str_replace( '/chat/completions', '', $endpoint );
2801 $endpoint = str_replace( '/v1/responses', '', $endpoint );
2802 $endpoint = rtrim( $endpoint, '/' );
2803
2804 // For Azure, use the v1 endpoint for consistency with Responses API
2805 if ( $this->envType === 'azure' ) {
2806 // Use v1 models endpoint with preview API version
2807 return $endpoint . '/openai/v1/models?api-version=preview';
2808 }
2809
2810 // For OpenAI, ensure we have the /v1 prefix
2811 if ( strpos( $endpoint, '/v1' ) === false ) {
2812 $endpoint .= '/v1';
2813 }
2814
2815 return $endpoint . '/models';
2816 }
2817
2818 /**
2819 * Prepare query by uploading files to OpenAI Files API.
2820 *
2821 * This method overrides the base prepare_query() to handle OpenAI-specific file uploads.
2822 * Files are uploaded to OpenAI's Files API before the query is executed, ensuring they
2823 * have provider_file_id references that can be used in messages.
2824 *
2825 * @param Meow_MWAI_Query_Text $query The query with potential file attachments
2826 */
2827 protected function prepare_query( $query ) {
2828 // Get all attachments using the unified method
2829 $attachments = method_exists( $query, 'getAttachments' ) ? $query->getAttachments() : [];
2830
2831 if ( empty( $attachments ) ) {
2832 return;
2833 }
2834
2835 // Process each attachment - upload non-images to OpenAI Files API
2836 foreach ( $attachments as $index => $file ) {
2837 $mimeType = $file->get_mimeType() ?? '';
2838 $isImage = strpos( $mimeType, 'image/' ) === 0;
2839
2840 // Skip images - they're sent as base64/URL, not uploaded to Files API
2841 if ( $isImage ) {
2842 continue;
2843 }
2844
2845 if ( $file->get_type() !== 'provider_file_id' ) {
2846 // File hasn't been uploaded to OpenAI yet - upload it now
2847 try {
2848 // Get data directly from file system (not via URL download)
2849 $refId = $file->get_refId();
2850 $data = $this->core->files->get_data( $refId );
2851 $filename = $file->get_filename();
2852
2853 // WORKAROUND: Create a fresh engine instance for upload (matches chatbot.php approach)
2854 $uploadEngine = Meow_MWAI_Engines_Factory::get_openai( $this->core, $query->envId );
2855 $uploadedFile = $uploadEngine->upload_file( $filename, $data, 'user_data' );
2856
2857 $fileId = $uploadedFile['id'] ?? null;
2858
2859 if ( $fileId ) {
2860 // Replace with provider_file_id reference in both arrays
2861 if ( !empty( $query->attachedFiles ) && isset( $query->attachedFiles[$index] ) ) {
2862 $query->attachedFiles[$index] = Meow_MWAI_Query_DroppedFile::from_provider_file_id(
2863 $fileId,
2864 $file->get_purpose(),
2865 $file->get_mimeType()
2866 );
2867 }
2868 // Also update legacy attachedFile if this is the first file
2869 if ( $index === 0 && !empty( $query->attachedFile ) ) {
2870 $query->attachedFile = Meow_MWAI_Query_DroppedFile::from_provider_file_id(
2871 $fileId,
2872 $file->get_purpose(),
2873 $file->get_mimeType()
2874 );
2875 }
2876 }
2877 }
2878 catch ( Exception $e ) {
2879 error_log( '[AI Engine] Failed to upload file to OpenAI Files API: ' . $e->getMessage() );
2880 // Keep the original file - MessageBuilder will skip it
2881 }
2882 }
2883 }
2884 }
2885
2886 }
2887