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