PluginProbe ʕ •ᴥ•ʔ
AI Engine – The Chatbot, AI Framework & MCP for WordPress / 3.0.0
AI Engine – The Chatbot, AI Framework & MCP for WordPress v3.0.0
3.5.9 3.5.8 3.5.7 3.5.6 3.5.5 3.5.4 3.5.3 3.5.2 3.5.1 3.5.0 3.4.9 3.4.8 3.4.7 0.2.1 1.6.91 0.2.2 1.6.92 0.2.3 1.6.93 0.2.4 1.6.94 0.2.5 1.6.95 0.2.6 1.6.96 0.2.7 1.6.97 0.2.8 1.6.98 0.2.9 1.6.99 0.3.0 1.7.0 0.3.1 1.7.1 0.3.2 1.7.2 0.3.3 1.7.3 0.3.4 1.7.4 0.3.5 1.7.5 0.3.6 1.7.6 0.4.0 1.7.7 0.4.1 1.7.8 0.4.2 1.7.9 0.4.3 1.8.0 0.4.4 1.8.1 0.4.5 1.8.2 0.4.6 1.8.3 0.4.7 1.8.4 0.4.8 1.8.5 0.4.9 1.8.6 0.5.0 1.8.7 0.5.1 1.8.8 0.5.2 1.8.9 0.5.3 1.9.0 0.5.4 1.9.1 0.5.5 1.9.2 0.5.6 1.9.3 0.5.7 1.9.4 0.5.8 1.9.5 0.5.9 1.9.6 0.6.0 1.9.7 0.6.1 1.9.8 0.6.2 1.9.81 0.6.3 1.9.82 0.6.4 1.9.83 0.6.5 1.9.84 0.6.6 1.9.85 0.6.7 1.9.86 0.6.8 1.9.87 0.6.9 1.9.88 0.7.0 1.9.89 0.7.1 1.9.90 0.7.2 1.9.91 0.7.3 1.9.92 0.7.4 1.9.93 0.7.5 1.9.94 0.7.6 1.9.95 0.7.7 1.9.96 0.7.8 1.9.97 0.7.9 1.9.98 0.8.0 1.9.99 0.8.1 2.0.0 0.8.2 2.0.1 0.8.3 2.0.2 0.8.4 2.0.3 0.8.5 2.0.4 0.8.6 2.0.5 0.8.7 2.0.6 0.8.8 2.0.7 0.8.9 2.0.8 0.9.0 2.0.9 0.9.2 2.1.0 0.9.3 2.1.1 0.9.4 2.1.2 0.9.5 2.1.3 0.9.6 2.1.4 0.9.7 2.1.5 0.9.8 2.1.6 0.9.81 2.1.7 0.9.82 2.1.8 0.9.83 2.1.9 0.9.84 2.2.0 0.9.85 2.2.1 0.9.86 2.2.2 0.9.87 2.2.3 0.9.88 2.2.4 0.9.89 2.2.5 0.9.9 2.2.51 0.9.91 2.2.52 0.9.92 2.2.53 0.9.93 2.2.54 0.9.94 2.2.56 0.9.95 2.2.57 0.9.96 2.2.6 0.9.97 2.2.60 0.9.98 2.2.61 0.9.99 2.2.62 1.0.0 2.2.63 1.0.01 2.2.70 1.0.1 2.2.80 1.0.2 2.2.81 1.0.3 2.2.90 1.0.4 2.2.91 1.0.5 2.2.92 1.0.6 2.2.93 1.0.7 2.2.94 1.0.8 2.2.95 1.0.9 2.3.0 1.1.0 2.3.1 1.1.1 2.3.2 1.1.2 2.3.3 1.1.3 2.3.4 1.1.4 2.3.5 1.1.5 2.3.6 1.1.6 2.3.7 1.1.7 2.3.8 1.1.8 2.3.9 1.1.9 2.4.0 1.2.0 2.4.1 1.2.1 2.4.2 1.2.2 2.4.3 1.2.21 2.4.4 1.2.3 2.4.5 1.2.30 2.4.6 1.3.0 2.4.7 1.3.1 2.4.8 1.3.2 2.4.9 1.3.3 2.5.0 1.3.31 2.5.1 1.3.32 2.5.2 1.3.33 2.5.3 1.3.34 2.5.4 1.3.35 2.5.5 1.3.36 2.5.6 1.3.37 2.5.7 1.3.38 2.5.8 1.3.39 2.5.9 1.3.40 2.6.0 1.3.41 2.6.1 1.3.42 2.6.2 1.3.43 2.6.3 1.3.44 2.6.5 1.3.45 2.6.6 1.3.46 2.6.7 1.3.47 2.6.8 1.3.48 2.6.9 1.3.49 2.7.0 1.3.50 2.7.1 1.3.51 2.7.2 1.3.52 2.7.3 1.3.53 2.7.4 1.3.54 2.7.5 1.3.56 2.7.6 1.3.57 2.7.7 1.3.58 2.7.8 1.3.59 2.7.9 1.3.60 2.8.0 1.3.61 2.8.1 1.3.62 2.8.2 1.3.63 2.8.3 1.3.64 2.8.4 1.3.65 2.8.5 1.3.66 2.8.6 1.3.67 2.8.7 1.3.68 2.8.8 1.3.69 2.8.9 1.3.70 2.9.0 1.3.71 2.9.1 1.3.72 2.9.2 1.3.73 2.9.3 1.3.74 2.9.4 1.3.75 2.9.5 1.3.76 2.9.6 1.3.77 2.9.7 1.3.78 2.9.8 1.3.79 2.9.9 1.3.80 3.0.0 1.3.81 3.0.1 1.3.82 3.0.2 1.3.83 3.0.3 1.3.84 3.0.4 1.3.85 3.0.5 1.3.86 3.0.6 1.3.87 3.0.7 1.3.88 3.0.8 1.3.89 3.0.9 1.3.90 3.1.0 1.3.91 3.1.1 1.3.92 3.1.2 1.3.93 3.1.3 1.3.94 3.1.4 1.3.95 3.1.5 1.3.96 3.1.6 1.3.97 3.1.7 1.3.98 3.1.8 1.3.99 3.1.9 1.4.0 3.2.0 1.4.1 3.2.1 1.4.2 3.2.2 1.4.3 3.2.3 1.4.4 3.2.4 1.4.5 3.2.5 1.4.6 3.2.6 1.4.7 3.2.7 1.4.8 3.2.8 1.4.9 3.2.9 1.5.0 3.3.0 1.5.1 3.3.1 1.5.2 3.3.2 1.5.3 3.3.3 1.5.4 3.3.4 1.5.5 3.3.5 1.5.6 3.3.6 1.5.7 3.3.7 1.5.8 3.3.8 1.5.9 3.3.9 1.6.0 3.4.0 1.6.1 3.4.1 1.6.2 3.4.2 1.6.3 3.4.3 1.6.5 3.4.4 1.6.51 3.4.5 1.6.52 3.4.6 1.6.53 1.6.54 1.6.55 1.6.56 1.6.57 1.6.58 1.6.59 1.6.60 1.6.61 1.6.62 1.6.63 1.6.64 1.6.65 1.6.66 1.6.67 1.6.68 trunk 1.6.69 0.0.1 1.6.70 0.0.2 1.6.71 0.0.3 1.6.72 0.0.4 1.6.73 0.0.5 1.6.74 0.0.6 1.6.75 0.0.7 1.6.76 0.0.8 1.6.77 0.0.9 1.6.78 0.1.0 1.6.79 0.1.1 1.6.81 0.1.2 1.6.82 0.1.3 1.6.83 0.1.4 1.6.84 0.1.5 1.6.85 0.1.6 1.6.86 0.1.7 1.6.87 0.1.8 1.6.88 0.1.9 1.6.89 0.2.0 1.6.90
ai-engine / classes / engines / openai.php
ai-engine / classes / engines Last commit date
traits 1 year ago anthropic.php 10 months ago chatml.php 10 months ago core.php 11 months ago factory.php 1 year ago google.php 10 months ago hugging-face.php 1 year ago open-router.php 1 year ago openai.php 10 months ago perplexity.php 1 year ago replicate.php 1 year ago
openai.php
1812 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 protected $currentQuery = null;
28 protected $streamImages = [];
29 protected $seenCallIds = []; // Track seen call IDs to prevent duplicates
30 protected $lastRequestBody = null; // For debugging
31 protected $contentStarted = false; // Track if content streaming has started
32 // IMPORTANT: OpenAI Responses API sends the same function call in both:
33 // 1. response.output_item.done - when individual function call completes
34 // 2. response.completed - with all function calls in the final response
35 // We must deduplicate to avoid processing the same function twice
36
37 public static function create( $core, $env ) {
38 self::$creating = true;
39 if ( class_exists( 'MeowPro_MWAI_OpenAI' ) ) {
40 $instance = new MeowPro_MWAI_OpenAI( $core, $env );
41 }
42 else {
43 $instance = new self( $core, $env );
44 }
45 self::$creating = false;
46 return $instance;
47 }
48
49 public function __construct( $core, $env ) {
50 $isOwnClass = get_class( $this ) === 'Meow_MWAI_Engines_OpenAI';
51 if ( $isOwnClass && !self::$creating ) {
52 throw new \Exception( 'Please use the create() method to instantiate the Meow_MWAI_Engines_OpenAI class.' );
53 }
54 parent::__construct( $core, $env );
55 $this->set_environment();
56 }
57
58 public function reset_stream() {
59 parent::reset_stream();
60 $this->mcpServerCount = 0;
61 $this->mcpTotalToolCount = 0;
62 $this->emittedFunctionResults = [];
63 $this->streamImages = [];
64 $this->seenCallIds = [];
65 }
66
67 /**
68 * Check if a model should use the new Responses API
69 */
70 protected function should_use_responses_api( $model ) {
71 // First check if Responses API is enabled in settings
72 $options = $this->core->get_all_options();
73 $responsesApiEnabled = $options['ai_responses_api'] ?? true;
74
75 if ( !$responsesApiEnabled ) {
76 return false;
77 }
78
79 // Azure doesn't support Responses API yet
80 if ( $this->envType === 'azure' ) {
81 return false;
82 }
83
84 // Check if the model has the 'responses' tag
85 $modelInfo = $this->retrieve_model_info( $model );
86 if ( $modelInfo && !empty( $modelInfo['tags'] ) ) {
87 return in_array( 'responses', $modelInfo['tags'] );
88 }
89
90 return false;
91 }
92
93 /**
94 * Set conversation state for stateful responses
95 */
96 public function set_previous_response_id( $responseId ) {
97 $this->previousResponseId = $responseId;
98 }
99
100 /**
101 * Get conversation state
102 */
103 public function get_conversation_state() {
104 return $this->conversationState;
105 }
106
107 /**
108 * Build body for Responses API
109 */
110 protected function build_responses_body( $query, $streamCallback = null ) {
111 $body = [
112 'model' => $query->model,
113 'stream' => !is_null( $streamCallback ),
114 ];
115
116 // Handle different query types for Responses API
117 if ( $query instanceof Meow_MWAI_Query_Text || $query instanceof Meow_MWAI_Query_Feedback ) {
118 // Use simplified instructions + input format for basic queries
119 if ( !empty( $query->instructions ) ) {
120 $body['instructions'] = $query->instructions;
121 }
122
123 // Determine history strategy
124 $historyStrategy = $query->historyStrategy;
125
126 // Treat empty string as null for automatic mode
127 if ( empty( $historyStrategy ) ) {
128 $historyStrategy = null;
129 }
130
131 // If historyStrategy is null (automatic), use response_id when previousResponseId is available
132 if ( $historyStrategy === null && !empty( $query->previousResponseId ) ) {
133 $historyStrategy = 'response_id';
134 }
135
136 // Debug logging for all queries when using Responses API
137 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
138
139 if ( $queries_debug ) {
140 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
141 error_log( '[AI Engine] Feedback query blocks: ' . count( $query->blocks ?? [] ) );
142 }
143 }
144
145 // Handle based on history strategy
146 // For Responses API, feedback queries MUST use previous_response_id to maintain conversation state
147 if ( $historyStrategy === 'response_id' && !empty( $query->previousResponseId ) ) {
148 // Use ResponseIdManager to validate the response ID
149 if ( $this->core->responseIdManager->is_valid_for_responses_api( $query->previousResponseId ) ) {
150 // Use incremental mode with previous_response_id
151 $body['previous_response_id'] = $query->previousResponseId;
152
153 // Debug logging
154 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
155 if ( $queries_debug ) {
156 error_log( '[AI Engine Queries] Using previous_response_id: ' . $query->previousResponseId );
157 }
158 }
159 else {
160 // Log warning if queries debug is enabled
161 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
162 if ( $queries_debug ) {
163 error_log( '[AI Engine Queries] Warning: ' .
164 Meow_MWAI_FunctionCallException::invalid_response_id(
165 $query->previousResponseId,
166 'Responses API',
167 'resp'
168 )->getMessage() );
169 }
170 // Fall through to full history mode
171 $historyStrategy = 'full_history';
172 }
173
174 }
175
176 // If we're still in response_id mode after validation, use incremental input
177 if ( $historyStrategy === 'response_id' && !empty( $body['previous_response_id'] ) ) {
178 // Check if this is a feedback query (function call response)
179 if ( $query instanceof Meow_MWAI_Query_Feedback && !empty( $query->blocks ) ) {
180 // For feedback queries with previous_response_id, we need to include:
181 // 1. The function_call from the model
182 // 2. The function_call_output with the result
183 $body['input'] = $this->build_feedback_input_for_responses_api( $query );
184
185 // Debug: Log the feedback input structure
186 if ( $queries_debug ) {
187 error_log( '[AI Engine Queries] Feedback input structure: ' . json_encode( $body['input'], JSON_PRETTY_PRINT ) );
188 }
189 }
190 else {
191 // Regular user message
192 $content = [
193 [
194 'type' => 'input_text',
195 'text' => $query->get_message()
196 ]
197 ];
198
199 // Check for attached file/image
200 if ( $query->attachedFile ) {
201 $imageUrl = $query->image_remote_upload === 'url'
202 ? $query->attachedFile->get_url()
203 : $query->attachedFile->get_inline_base64_url();
204
205 $content[] = [
206 'type' => 'input_image',
207 'image_url' => $imageUrl
208 ];
209 }
210
211 $body['input'] = [
212 [
213 'role' => 'user',
214 'content' => $content
215 ]
216 ];
217
218 // Add context if present
219 if ( !empty( $query->context ) ) {
220 // Prepend context as a separate input_text in the same message
221 array_unshift( $body['input'][0]['content'], [
222 'type' => 'input_text',
223 'text' => $query->context . "\n\n"
224 ] );
225 }
226 }
227 }
228 else {
229 // Use full history mode (internal) or when no previous_response_id
230
231 // Build input - always use array format for Responses API
232 if ( !empty( $query->messages ) || $query->attachedFile || $query instanceof Meow_MWAI_Query_Feedback ) {
233 $body['input'] = $this->build_responses_input_array( $query );
234 }
235 else {
236 // Even for simple text, Responses API expects message format
237 $body['input'] = [
238 [
239 'role' => 'user',
240 'content' => [
241 [
242 'type' => 'input_text',
243 'text' => $query->get_message()
244 ]
245 ]
246 ]
247 ];
248 }
249
250 // Add context if present
251 if ( !empty( $query->context ) ) {
252 if ( isset( $body['input'] ) && is_string( $body['input'] ) ) {
253 $body['input'] = $query->context . "\n\n" . $body['input'];
254 }
255 else {
256 // Add context as system message
257 array_unshift( $body['input'], [
258 'role' => 'system',
259 'content' => $query->context
260 ] );
261 }
262 }
263 }
264
265 // Parameters
266 if ( !empty( $query->maxTokens ) ) {
267 $body['max_output_tokens'] = $query->maxTokens;
268 }
269
270 // Handle temperature parameter - GPT-5 models don't support it
271 if ( !empty( $query->temperature ) && $query->temperature !== 1 ) {
272 // Check if this is a GPT-5 model (gpt-5, gpt-5-mini, gpt-5-nano)
273 if ( strpos( $query->model, 'gpt-5' ) !== 0 ) {
274 $body['temperature'] = $query->temperature;
275 }
276 // For GPT-5 models, skip the temperature parameter entirely
277 }
278
279 // Handle GPT-5 specific parameters: reasoning and verbosity
280 if ( strpos( $query->model, 'gpt-5' ) === 0 ) {
281 // Add reasoning parameter as an object (Responses API expects object)
282 // { reasoning: { effort: 'minimal|low|medium|high' } }
283 if ( !empty( $query->reasoning ) ) {
284 $body['reasoning'] = [ 'effort' => $query->reasoning ];
285 }
286 // Add verbosity parameter if set (inside text object)
287 if ( !empty( $query->verbosity ) ) {
288 if ( !isset( $body['text'] ) || !is_array( $body['text'] ) ) {
289 $body['text'] = [];
290 }
291 $body['text']['verbosity'] = $query->verbosity;
292 }
293 }
294
295 // Note: The Responses API does not support the 'n' parameter for multiple results
296 // Unlike the Chat Completions API, Responses API generates one response at a time
297 // If multiple results are needed, separate requests must be made
298 // Reference: https://platform.openai.com/docs/api-reference/responses
299 if ( !empty( $query->maxResults ) && $query->maxResults > 1 ) {
300 Meow_MWAI_Logging::warn( 'Responses API does not support multiple results (n parameter). Only one result will be generated.' );
301 }
302
303 if ( !empty( $query->stop ) ) {
304 $body['stop'] = $query->stop;
305 }
306
307 if ( !empty( $query->responseFormat ) && $query->responseFormat === 'json' ) {
308 // Responses API uses 'text.format' instead of 'response_format'
309 if ( !isset( $body['text'] ) || !is_array( $body['text'] ) ) {
310 $body['text'] = [];
311 }
312 $body['text']['format'] = [ 'type' => 'json_object' ];
313 }
314
315 // Function calling - convert to tools
316 // IMPORTANT: Tools must be included in ALL requests, even when using previous_response_id
317 // The API needs to know which functions are available throughout the entire conversation
318 if ( !empty( $query->functions ) ) {
319 $body['tools'] = $this->build_responses_tools( $query->functions );
320 // IMPORTANT: Enable parallel tool calls to allow multiple function calls in one response
321 // TODO: OpenAI's Responses API has a bug where it only returns ONE function call even when
322 // parallel_tool_calls=true is set and multiple functions are clearly needed. This works correctly
323 // with the Chat Completions API. Monitor OpenAI's updates and test again in the future.
324 // Issue discovered: August 2025 - Only getDeskTemperature is called when both desk AND outdoor are requested.
325 $body['parallel_tool_calls'] = true;
326 }
327
328
329 // Add MCP servers if available
330 if ( isset( $query->mcpServers ) && is_array( $query->mcpServers ) && !empty( $query->mcpServers ) ) {
331 $mcp_envs = $this->core->get_option( 'mcp_envs' );
332 $this->mcpServerCount = count( $query->mcpServers );
333
334 foreach ( $query->mcpServers as $mcpServer ) {
335 if ( isset( $mcpServer['id'] ) ) {
336 // Find the full MCP server configuration by ID
337 foreach ( $mcp_envs as $env ) {
338 if ( $env['id'] === $mcpServer['id'] ) {
339 // Sanitize server label for OpenAI requirements
340 $server_label = $env['name'] . '_' . $env['id'];
341 // Remove spaces and special characters
342 $server_label = preg_replace( '/[^a-zA-Z0-9_]/', '', $server_label );
343 // Replace double or tripe underscores with single underscore
344 $server_label = preg_replace( '/_{2,}/', '_', $server_label );
345 // Ensure it starts with a letter
346 if ( !preg_match( '/^[a-zA-Z]/', $server_label ) ) {
347 $server_label = 'mcp_' . $server_label;
348 }
349
350 $mcp_tool = [
351 'type' => 'mcp',
352 'server_label' => $server_label,
353 'server_url' => $env['url'],
354 'require_approval' => 'never'
355 ];
356
357 // Add authorization header if available
358 if ( !empty( $env['token'] ) ) {
359 $mcp_tool['headers'] = [
360 'Authorization' => 'Bearer ' . $env['token']
361 ];
362 }
363
364 // Add to tools array
365 if ( !isset( $body['tools'] ) ) {
366 $body['tools'] = [];
367 }
368 $body['tools'][] = $mcp_tool;
369
370 break;
371 }
372 }
373 }
374 }
375 }
376
377 // Add tool_choice parameter if tools are present
378 if ( !empty( $body['tools'] ) ) {
379 // Default to 'auto' to let the model choose
380 $body['tool_choice'] = 'auto';
381 }
382
383 // Add tools (web_search, image_generation) if specified
384 if ( !empty( $query->tools ) && is_array( $query->tools ) ) {
385 // Ensure tools array exists
386 if ( !isset( $body['tools'] ) ) {
387 $body['tools'] = [];
388 }
389
390 // Add each enabled tool
391 foreach ( $query->tools as $tool ) {
392 if ( in_array( $tool, ['web_search', 'image_generation'] ) ) {
393 $toolConfig = [ 'type' => $tool ];
394
395 // Image generation requires partial_images when streaming
396 if ( $tool === 'image_generation' && !empty( $streamCallback ) ) {
397 $toolConfig['partial_images'] = 1;
398 }
399
400 $body['tools'][] = $toolConfig;
401 Meow_MWAI_Logging::log( 'Responses API: Added tool ' . $tool . ' to request' );
402 }
403 }
404 }
405
406 // Add file_search tool if OpenAI Vector Store is configured
407 if ( !empty( $query->embeddingsEnvId ) ) {
408 Meow_MWAI_Logging::log( 'Responses API: Checking embeddings environment - embeddingsEnvId: ' . $query->embeddingsEnvId );
409
410 $embeddingsEnv = $this->core->get_embeddings_env( $query->embeddingsEnvId );
411
412 if ( $embeddingsEnv && $embeddingsEnv['type'] === 'openai-vector-store' ) {
413 Meow_MWAI_Logging::log( 'Responses API: Found OpenAI Vector Store environment' );
414
415 // Check if the OpenAI environment matches
416 $openai_env_id = $embeddingsEnv['openai_env_id'] ?? null;
417
418 Meow_MWAI_Logging::log( 'Responses API: Comparing environments - embeddings OpenAI env: ' . ( $openai_env_id ?? 'null' ) . ', current env: ' . $this->envId );
419
420 if ( $openai_env_id === $this->envId && !empty( $embeddingsEnv['store_id'] ) ) {
421 // Ensure tools array exists
422 if ( !isset( $body['tools'] ) ) {
423 $body['tools'] = [];
424 }
425
426 // Add file_search tool with vector store ID
427 $body['tools'][] = [
428 'type' => 'file_search',
429 'vector_store_ids' => [ $embeddingsEnv['store_id'] ]
430 ];
431
432 Meow_MWAI_Logging::log( 'Responses API: Added file_search tool with vector store: ' . $embeddingsEnv['store_id'] );
433 } else {
434 if ( $openai_env_id !== $this->envId ) {
435 Meow_MWAI_Logging::log( 'Responses API: Environment mismatch - file_search tool not added' );
436 }
437 if ( empty( $embeddingsEnv['store_id'] ) ) {
438 Meow_MWAI_Logging::log( 'Responses API: No store_id configured - file_search tool not added' );
439 }
440 }
441 } else {
442 Meow_MWAI_Logging::log( 'Responses API: Embeddings environment is not OpenAI Vector Store type (type: ' . ( $embeddingsEnv['type'] ?? 'null' ) . ')' );
443 }
444 } else {
445 Meow_MWAI_Logging::log( 'Responses API: No embeddingsEnvId in query - file_search tool not added' );
446 }
447
448 // Note: Responses API doesn't support stream_options parameter
449 // Usage tracking is handled differently in the streaming response
450 }
451 else if ( $query instanceof Meow_MWAI_Query_Image ) {
452 // For image generation, we can use the integrated approach
453 if ( $query->model === 'gpt-image-1' ) {
454 $body['tools'] = [[
455 'type' => 'image_generation'
456 ]];
457 $body['input'] = $query->get_message();
458 }
459 else {
460 // Fallback to old API for DALL-E models
461 return $this->build_body( $query, $streamCallback );
462 }
463 }
464
465 // Debug logging for feedback queries
466 if ( $query instanceof Meow_MWAI_Query_Feedback ) {
467 Meow_MWAI_Logging::log( 'Responses API: Feedback query body: ' . json_encode( $body ) );
468 }
469
470 // Ensure parallel_tool_calls is set when we have tools
471 if ( !empty( $body['tools'] ) && !isset( $body['parallel_tool_calls'] ) ) {
472 $body['parallel_tool_calls'] = true;
473 }
474
475 return $body;
476 }
477
478 /**
479 * Build tool messages for feedback when using previous_response_id
480 */
481 protected function build_tool_messages_for_feedback( $query ) {
482 $messages = [];
483
484 if ( $query instanceof Meow_MWAI_Query_Feedback && !empty( $query->blocks ) ) {
485 foreach ( $query->blocks as $block ) {
486 if ( isset( $block['feedbacks'] ) ) {
487 foreach ( $block['feedbacks'] as $feedback ) {
488 // Get the tool call ID from the original request
489 $toolId = $feedback['request']['toolId'] ?? null;
490
491 if ( $toolId ) {
492 // According to Responses API spec, tool results should use role:"tool"
493 $toolMessage = [
494 'role' => 'tool',
495 'tool_call_id' => $toolId,
496 'content' => [
497 [
498 'type' => 'tool_result',
499 'tool_result' => (string) ( $feedback['reply']['value'] ?? '' )
500 ]
501 ]
502 ];
503 $messages[] = $toolMessage;
504
505 Meow_MWAI_Logging::log( 'Responses API: Added tool result with tool_call_id ' . $toolId . ' - Message: ' . json_encode( $toolMessage ) );
506 }
507 }
508 }
509 }
510 }
511
512 return $messages;
513 }
514
515 /**
516 * Build input array for complex message structures
517 */
518 protected function build_responses_input_array( $query ) {
519 // Use the MessageBuilder service for streamlined message building
520 $messages = $this->core->messageBuilder->build_responses_api_messages( $query );
521
522 // Note: Function result events are now emitted centrally in core.php
523 // when the function is actually executed
524
525 // Debug logging
526 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
527 if ( $queries_debug && $query instanceof Meow_MWAI_Query_Feedback ) {
528 error_log( '[AI Engine Queries] Feedback query messages order:' );
529 foreach ( $messages as $idx => $msg ) {
530 if ( isset( $msg['type'] ) ) {
531 $log_msg = ' [' . $idx . '] ' . $msg['type'];
532 if ( $msg['type'] === 'function_call' ) {
533 $log_msg .= ' - ' . ( $msg['name'] ?? 'unknown' ) . ' (call_id: ' . ( $msg['call_id'] ?? 'none' ) . ')';
534 }
535 elseif ( $msg['type'] === 'function_call_output' ) {
536 $log_msg .= ' (call_id: ' . ( $msg['call_id'] ?? 'none' ) . ', output: ' . substr( $msg['output'] ?? '', 0, 50 ) . ')';
537 }
538 error_log( '[AI Engine Queries]' . $log_msg );
539 }
540 elseif ( isset( $msg['role'] ) ) {
541 $content_preview = '';
542 if ( isset( $msg['content'] ) ) {
543 if ( is_string( $msg['content'] ) ) {
544 $content_preview = ' - "' . substr( $msg['content'], 0, 50 ) . '"';
545 }
546 elseif ( is_array( $msg['content'] ) && isset( $msg['content'][0]['text'] ) ) {
547 $content_preview = ' - "' . substr( $msg['content'][0]['text'], 0, 50 ) . '"';
548 }
549 elseif ( is_array( $msg['content'] ) && isset( $msg['content'][0]['type'] ) && $msg['content'][0]['type'] === 'input_text' ) {
550 $content_preview = ' - "' . substr( $msg['content'][0]['text'] ?? '', 0, 50 ) . '"';
551 }
552 }
553 error_log( '[AI Engine Queries] [' . $idx . '] ' . $msg['role'] . $content_preview );
554 }
555 }
556 }
557
558 return $messages;
559 }
560
561 /**
562 * Convert functions to Responses API tools format
563 */
564 protected function build_responses_tools( $functions ) {
565 $tools = [];
566
567 foreach ( $functions as $function ) {
568 $functionData = $function->serializeForOpenAI();
569
570 // Ensure the function data has all required fields
571 if ( !isset( $functionData['name'] ) || empty( $functionData['name'] ) ) {
572 Meow_MWAI_Logging::warn( 'Function missing required name field' );
573 continue;
574 }
575
576 // Responses API expects a flatter structure
577 $parameters = $functionData['parameters'] ?? null;
578
579 // Ensure parameters has the correct structure
580 if ( !$parameters ) {
581 $parameters = [
582 'type' => 'object',
583 'properties' => new stdClass(),
584 'required' => []
585 ];
586 }
587 else {
588 // Ensure properties is an object, not an array when empty
589 if ( isset( $parameters['properties'] ) &&
590 is_array( $parameters['properties'] ) &&
591 empty( $parameters['properties'] ) ) {
592 $parameters['properties'] = new stdClass();
593 }
594 }
595
596 $tool = [
597 'type' => 'function',
598 'name' => $functionData['name'],
599 'description' => $functionData['description'] ?? '',
600 'parameters' => $parameters,
601 'strict' => false // Set to false for now, can be made configurable later
602 ];
603
604 $tools[] = $tool;
605 }
606
607 return $tools;
608 }
609
610 /**
611 * Build feedback input for Responses API when using previous_response_id.
612 *
613 * The Responses API requires a very specific format for function results:
614 * 1. Echo the exact function_call message from the model
615 * 2. Provide the function_call_output with matching call_id
616 *
617 * This method extracts these from the feedback blocks and formats them correctly.
618 *
619 * @param Meow_MWAI_Query_Feedback $query The feedback query containing function results
620 * @return array Array of messages in Responses API format
621 */
622 protected function build_feedback_input_for_responses_api( $query ) {
623 // Use the MessageBuilder service for streamlined message building
624 $messages = $this->core->messageBuilder->build_feedback_only_messages( $query );
625
626 // For Responses API, the input should be wrapped in a specific structure
627 // According to OpenAI docs, function results should be sent as an array of messages
628 return $messages;
629 }
630
631 /**
632 * Build URL for Responses API
633 */
634 protected function build_responses_url() {
635 if ( $this->envType === 'azure' ) {
636 // Azure uses a different URL structure
637 $endpoint = isset( $this->env['endpoint'] ) ? $this->env['endpoint'] : null;
638 $url = trailingslashit( $endpoint ) . 'openai/responses?' . $this->azureApiVersion;
639 }
640 else {
641 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
642 $url = trailingslashit( $endpoint ) . 'responses';
643 }
644
645 return $url;
646 }
647
648 /**
649 * Handle Responses API streaming data
650 */
651 protected function responses_stream_data_handler( $json ) {
652 $content = null;
653 static $currentItemType = null; // Track the current output item type
654 // Load event helper
655 if ( !class_exists( 'Meow_MWAI_Event' ) ) {
656 require_once MWAI_PATH . '/classes/event.php';
657 }
658
659 // Get response metadata
660 if ( isset( $json['id'] ) ) {
661 $this->inId = $json['id'];
662 Meow_MWAI_Logging::log( 'Responses API Streaming: Found response ID in stream: ' . $this->inId );
663 }
664 if ( isset( $json['model'] ) ) {
665 $this->inModel = $json['model'];
666 }
667
668 // Handle different event types for Responses API
669 $eventType = $json['type'] ?? null;
670
671 // Debug streaming events
672 if ( isset( $_GET['debug_mcp'] ) ) {
673 error_log( 'AI_ENGINE_DEBUG: Streaming type: ' . ( $eventType ?? 'no_type' ) . ' - Data: ' . json_encode( $json ) );
674 }
675
676 switch ( $eventType ) {
677 // ===== LIFECYCLE EVENTS =====
678
679 case 'response.created':
680 // Emitted when a response object is created - contains initial response metadata
681 $response = $json['response'] ?? [];
682 $this->inId = $response['id'] ?? null;
683 $this->inModel = $response['model'] ?? null;
684 if ( $this->inId ) {
685 }
686 break;
687
688 case 'response.queued':
689 // Response is queued and waiting to start processing
690 // We can log this for debugging purposes
691 Meow_MWAI_Logging::log( 'Responses API: Response queued for processing' );
692 break;
693
694 case 'response.in_progress':
695 // Emitted repeatedly while the response is being generated
696 // Contains partial response state but typically not used for streaming text
697 break;
698
699 case 'response.completed':
700 // Response is fully generated - extract any function calls from completed output
701 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
702 error_log( '[AI Engine Queries] Current streamToolCalls count: ' . count( $this->streamToolCalls ) );
703 }
704
705 $response = $json['response'] ?? [];
706 $outputs = $response['output'] ?? [];
707
708 foreach ( $outputs as $idx => $output ) {
709 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
710 error_log( '[AI Engine Queries] Output ' . $idx . ' type: ' . ( $output['type'] ?? 'unknown' ) . ', status: ' . ( $output['status'] ?? 'no-status' ) );
711 }
712
713 if ( isset( $output['type'] ) && $output['type'] === 'function_call' &&
714 isset( $output['status'] ) && $output['status'] === 'completed' ) {
715 // Note: Responses API uses 'call_id' not 'id' for function calls
716 $callId = $output['call_id'] ?? $output['id'] ?? null;
717 $functionName = $output['name'] ?? '';
718
719 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
720 error_log( '[AI Engine Queries] Processing function_call: ' . $functionName . ' (id: ' . $callId . ')' );
721 }
722
723 // IMPORTANT: Deduplicate function calls
724 // OpenAI sends the same function call in both response.output_item.done
725 // and response.completed events. We track call IDs to avoid duplicates.
726 if ( in_array( $callId, $this->seenCallIds, true ) ) {
727 // Skip duplicate - already processed in response.output_item.done
728 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
729 error_log( '[AI Engine Queries] Skipping duplicate call ID: ' . $callId );
730 }
731 continue;
732 }
733
734 // First time seeing this call ID - add it
735 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
736 error_log( '[AI Engine Queries] response.completed adding tool call: ' . $functionName . ' (id: ' . $callId . ')' );
737 }
738 $this->seenCallIds[] = $callId;
739 $this->streamToolCalls[] = [
740 'id' => $callId,
741 'type' => 'function',
742 'function' => [
743 'name' => $functionName,
744 'arguments' => $output['arguments'] ?? '{}'
745 ]
746 ];
747 }
748 }
749 break;
750
751 case 'response.incomplete':
752 // Response stopped before completion (e.g., max_tokens reached)
753 $details = $json['response']['incomplete_details'] ?? [];
754 Meow_MWAI_Logging::warn( 'Responses API: Response incomplete - ' . json_encode( $details ) );
755 break;
756
757 case 'response.failed':
758 // Response generation failed
759 $error = $json['response']['error'] ?? [];
760 $message = $error['message'] ?? 'Response generation failed';
761 throw new Exception( $message );
762
763 // ===== OUTPUT ITEM EVENTS =====
764
765 case 'response.output_item.added':
766 // New output item added (e.g., message, function_call, etc.)
767 // Track the type of the current output item
768 if ( isset( $json['item'] ) && isset( $json['item']['type'] ) ) {
769 $item = $json['item'];
770 $itemType = $item['type'];
771 $currentItemType = $itemType;
772
773 // Don't emit events here for web search or image generation - wait for more specific events
774 // This prevents duplicate events
775
776 // If it's an MCP call, store the tool name
777 if ( $itemType === 'mcp_call' && isset( $item['id'] ) && isset( $item['name'] ) ) {
778 $this->mcpToolNames[$item['id']] = $item['name'];
779 Meow_MWAI_Logging::log( 'Responses API: MCP tool call added - ' . $item['name'] . ' (id: ' . $item['id'] . ')' );
780
781 if ( $this->currentDebugMode ) {
782 $event = Meow_MWAI_Event::mcp_calling( $item['name'], $item['id'] )
783 ->set_metadata( 'name', $item['name'] )
784 ->set_metadata( 'server_label', $item['server_label'] ?? null );
785 call_user_func( $this->streamCallback, $event );
786 }
787 }
788 }
789 break;
790
791 case 'response.output_item.done':
792 // Output item completed - check for MCP approval requests or tool lists
793 if ( isset( $json['item'] ) && isset( $json['item']['type'] ) ) {
794 $item = $json['item'];
795 $itemType = $item['type'];
796
797 // Reset current item type when we complete a message item
798 if ( $itemType === 'message' ) {
799 $currentItemType = null;
800 }
801
802 if ( $itemType === 'function_call' ) {
803 // Regular function call completed - send event
804 if ( $this->currentDebugMode && $this->streamCallback ) {
805 $event = Meow_MWAI_Event::function_calling( $item['name'] ?? 'unknown', json_decode( $item['arguments'] ?? '{}', true ) )
806 ->set_metadata( 'call_id', $item['call_id'] ?? null );
807 call_user_func( $this->streamCallback, $event );
808 }
809
810 // Add to streamToolCalls for execution
811 // Note: Responses API uses 'call_id' not 'id' for function calls
812 $callId = $item['call_id'] ?? $item['id'] ?? null;
813 $functionName = $item['name'] ?? '';
814
815 // Add to our deduplication tracking
816 // We process function calls here as they complete individually during streaming
817 // The response.completed event will also try to add them, so we track IDs
818 if ( !in_array( $callId, $this->seenCallIds, true ) ) {
819 $this->seenCallIds[] = $callId;
820
821 $this->streamToolCalls[] = [
822 'id' => $callId,
823 'type' => 'function',
824 'function' => [
825 'name' => $functionName,
826 'arguments' => $item['arguments'] ?? '{}'
827 ]
828 ];
829 }
830 }
831 elseif ( $itemType === 'mcp_approval_request' ) {
832 // IMPORTANT: MCP (Model Context Protocol) tools are executed remotely by OpenAI
833 // Unlike regular function calls, MCP tools do NOT need local execution
834 // Therefore, we should NOT add them to streamToolCalls array
835 // This prevents creation of unnecessary feedback queries and second response cycles
836 Meow_MWAI_Logging::log( 'Responses API: MCP approval request for ' . $item['name'] . ' from server ' . $item['server_label'] . ' (handled remotely)' );
837 }
838 elseif ( $item['type'] === 'mcp_call' ) {
839 // IMPORTANT: MCP calls are already executed remotely by OpenAI's infrastructure
840 // The result is included in the same response stream
841 // We must NOT add these to streamToolCalls to avoid duplicate execution attempts
842 Meow_MWAI_Logging::log( 'Responses API: MCP call completed - ' . $item['name'] . ' (already executed remotely)' );
843
844 // Send event for completed MCP call when debug is enabled
845 if ( $this->currentDebugMode && isset( $item['name'] ) ) {
846 $args = json_decode( $item['arguments'] ?? '{}', true );
847 $output = $item['output'] ?? null;
848
849 // Skip the tool_call event for MCP calls since we already sent mcp_tool_call
850 // This prevents duplicate events in the UI
851
852 // Then send a separate event for the tool result
853 if ( $output ) {
854 // Format the output preview
855 $outputPreview = is_array( $output ) ? json_encode( $output ) : (string) $output;
856 if ( strlen( $outputPreview ) > 100 ) {
857 $outputPreview = substr( $outputPreview, 0, 100 ) . '...';
858 }
859
860 $resultEvent = Meow_MWAI_Event::mcp_result( $item['name'] )
861 ->set_metadata( 'output', $output );
862 call_user_func( $this->streamCallback, $resultEvent );
863 }
864
865 // Don't return content since we've already sent events
866 $content = null;
867 }
868 }
869 elseif ( $itemType === 'web_search_call' ) {
870 // Web search completed - don't emit event here
871 // The event will be emitted by the response.web_search_call.completed handler
872 // This prevents duplicate events
873 Meow_MWAI_Logging::log( 'Responses API: Web search output item completed (event handled by specific handler)' );
874 }
875 elseif ( $itemType === 'image_generation_call' ) {
876 // Image generation completed
877 Meow_MWAI_Logging::log( 'Responses API: Image generation output item completed' );
878
879 // Extract the base64 image from the result
880 if ( isset( $item['result'] ) ) {
881 $base64Image = $item['result'];
882
883 // Store the image for later processing
884 if ( !isset( $this->streamImages ) ) {
885 $this->streamImages = [];
886 }
887
888 $this->streamImages[] = $base64Image;
889
890 Meow_MWAI_Logging::log( 'Responses API: Stored generated image (base64 length: ' . strlen( $base64Image ) . ')' );
891 }
892 }
893 elseif ( $item['type'] === 'mcp_list_tools' ) {
894 // MCP tools list discovered
895 $server_label = $item['server_label'] ?? 'unknown';
896 $tools_count = isset( $item['tools'] ) ? count( $item['tools'] ) : 0;
897 $this->mcpTotalToolCount += $tools_count;
898 Meow_MWAI_Logging::log( 'Responses API: MCP tools list from server ' . $server_label . ' containing ' . $tools_count . ' tools' );
899
900 // Send event for tools discovery using the aggregated format
901 if ( $this->currentDebugMode ) {
902 $serverCount = $this->mcpServerCount > 0 ? $this->mcpServerCount : 1;
903 $event = Meow_MWAI_Event::mcp_discovery( $serverCount, $this->mcpTotalToolCount );
904 call_user_func( $this->streamCallback, $event );
905 }
906
907 // Log first few tools for debugging
908 if ( isset( $item['tools'] ) && is_array( $item['tools'] ) ) {
909 $sample_tools = array_slice( $item['tools'], 0, 3 );
910 foreach ( $sample_tools as $tool ) {
911 Meow_MWAI_Logging::log( 'Responses API: MCP tool "' . ( $tool['name'] ?? 'unnamed' ) . '": ' . ( $tool['description'] ?? 'no description' ) );
912 }
913 if ( $tools_count > 3 ) {
914 Meow_MWAI_Logging::log( 'Responses API: ... and ' . ( $tools_count - 3 ) . ' more tools' );
915 }
916 }
917 }
918 }
919 break;
920
921 // ===== CONTENT PART EVENTS =====
922
923 case 'response.content_part.added':
924 // New content part added to an output item
925 // Indicates start of a new content section (text, image, etc.)
926 // Check if this is MCP-related content that shouldn't be shown
927 if ( isset( $json['part']['type'] ) ) {
928 $partType = $json['part']['type'];
929
930 // Just log the part type for debugging
931 // We can use this info later if needed
932 }
933 break;
934
935 case 'response.content_part.done':
936 // Content part is finalized
937 // No more deltas will be sent for this content part
938 break;
939
940 // ===== TEXT STREAMING EVENTS =====
941
942 case 'response.output_text.delta':
943 // Streaming text chunk for the current content part
944 if ( isset( $json['delta'] ) ) {
945 // Send a status event for the first content chunk
946 if ( $this->currentDebugMode && !$this->contentStarted ) {
947 $this->contentStarted = true;
948 $statusEvent = Meow_MWAI_Event::generating_response();
949 call_user_func( $this->streamCallback, $statusEvent );
950 }
951 $content = $json['delta'];
952 }
953 break;
954
955 case 'response.output_text.done':
956 // Final text for the content part
957 // Contains the complete accumulated text
958 // Don't send response_completed here - ChatbotContext adds "Request completed"
959 $this->contentStarted = false;
960 break;
961
962 case 'response.refusal.delta':
963 // Streaming refusal message chunk
964 // Model is refusing to generate the requested content
965 if ( isset( $json['delta'] ) ) {
966 // We might want to stream refusals as regular content
967 $content = $json['delta'];
968 }
969 break;
970
971 case 'response.refusal.done':
972 // Final refusal message
973 // Contains the complete refusal reason
974 break;
975
976 case 'response.function_call_arguments.delta':
977 // Streaming JSON arguments for a function call
978 // We don't stream these to UI as they're not human-readable
979 break;
980
981 case 'response.function_call_arguments.done':
982 // Complete function call arguments
983 // Already handled in response.output_item.done for function_call type
984 break;
985
986 // ===== FILE & WEB SEARCH EVENTS =====
987
988 case 'response.file_search_call.in_progress':
989 // File search started
990 Meow_MWAI_Logging::log( 'Responses API: File search in progress' );
991 break;
992
993 case 'response.file_search_call.searching':
994 // Actively searching files
995 break;
996
997 case 'response.file_search_call.completed':
998 // File search finished
999 break;
1000
1001 case 'response.web_search_call.in_progress':
1002 // Web search started - only emit one event at the start
1003 Meow_MWAI_Logging::log( 'Responses API: Web search in progress' );
1004 if ( $this->currentDebugMode && $this->streamCallback ) {
1005 $event = Meow_MWAI_Event::status( 'Searching the web...' );
1006 call_user_func( $this->streamCallback, $event );
1007 }
1008 break;
1009
1010 case 'response.web_search_call.searching':
1011 // Actively searching - don't emit duplicate events
1012 if ( isset( $json['query'] ) ) {
1013 Meow_MWAI_Logging::log( 'Responses API: Searching for: ' . $json['query'] );
1014 }
1015 break;
1016
1017 case 'response.web_search_call.completed':
1018 // Web search finished
1019 Meow_MWAI_Logging::log( 'Responses API: Web search completed' );
1020
1021 // The completed event doesn't contain results, just metadata
1022 // Results are likely embedded in the model's response text
1023 if ( $this->currentDebugMode && $this->streamCallback ) {
1024 $message = 'Web search completed';
1025 $event = Meow_MWAI_Event::status( $message );
1026 call_user_func( $this->streamCallback, $event );
1027 }
1028 break;
1029
1030 // ===== IMAGE GENERATION EVENTS =====
1031
1032 case 'response.image_generation_call.in_progress':
1033 // Image generation started
1034 Meow_MWAI_Logging::log( 'Responses API: Image generation in progress' );
1035 if ( $this->currentDebugMode && $this->streamCallback ) {
1036 $event = Meow_MWAI_Event::status( 'Generating image...' );
1037 call_user_func( $this->streamCallback, $event );
1038 }
1039 break;
1040
1041 case 'response.image_generation_call.generating':
1042 // Image is being generated
1043 break;
1044
1045 case 'response.image_generation_call.partial_image':
1046 // Partial image data (base64)
1047 // Could be used for progressive image display
1048 if ( isset( $json['partial_image_b64'] ) ) {
1049 Meow_MWAI_Logging::log( 'Responses API: Received partial image index ' . ( $json['partial_image_index'] ?? 'unknown' ) );
1050 // For now, we don't display partial images, but we could in the future
1051 }
1052 break;
1053
1054 case 'response.image_generation_call.completed':
1055 // Image generation finished
1056 Meow_MWAI_Logging::log( 'Responses API: Image generation completed' );
1057
1058 // Note: The actual image data comes in response.output_item.done event
1059 // This event just signals completion
1060
1061 if ( $this->currentDebugMode && $this->streamCallback ) {
1062 $event = Meow_MWAI_Event::status( 'Image generated.' );
1063 call_user_func( $this->streamCallback, $event );
1064 }
1065 break;
1066
1067 // ===== MCP (Model Context Protocol) EVENTS =====
1068
1069 case 'response.mcp_call.in_progress':
1070 // MCP tool call is running
1071 $itemId = $json['item_id'] ?? null;
1072 $toolName = isset( $this->mcpToolNames[$itemId] ) ? $this->mcpToolNames[$itemId] : 'unknown';
1073
1074 Meow_MWAI_Logging::log( 'Responses API: MCP tool call in progress - ' . $toolName );
1075 break;
1076
1077 case 'response.mcp_call.arguments.delta':
1078 case 'response.mcp_call_arguments.delta':
1079 // Streaming arguments for MCP tool call
1080 // Don't stream these JSON arguments to the UI
1081 // These contain the function parameters like {"post_type":"post",...}
1082 break;
1083
1084 case 'response.mcp_call.arguments.done':
1085 case 'response.mcp_call_arguments.done':
1086 // Complete arguments for MCP tool call
1087 break;
1088
1089 case 'response.mcp_call.completed':
1090 // MCP tool call succeeded
1091 break;
1092
1093 case 'response.mcp_call.failed':
1094 // MCP tool call failed
1095 $error = $json['error'] ?? [];
1096 Meow_MWAI_Logging::error( 'Responses API: MCP tool call failed - ' . ( $error['message'] ?? 'Unknown error' ) );
1097 break;
1098
1099 case 'response.mcp_list_tools.in_progress':
1100 // Listing MCP tools has started
1101 Meow_MWAI_Logging::log( 'Responses API: MCP tools discovery in progress' );
1102 break;
1103
1104 case 'response.mcp_list_tools.completed':
1105 // MCP tools listing completed successfully
1106 break;
1107
1108 case 'response.mcp_list_tools.failed':
1109 // MCP tools listing failed
1110 $error = $json['error'] ?? [];
1111 $message = 'MCP tools listing failed: ' . ( $error['message'] ?? 'Unknown error' );
1112 Meow_MWAI_Logging::error( 'Responses API: ' . $message );
1113 throw new Exception( $message );
1114 break;
1115
1116 // ===== REASONING EVENTS (for o1/o3 models) =====
1117
1118 case 'response.reasoning.delta':
1119 // Streaming reasoning text chunk
1120 // Internal reasoning process of the model
1121 break;
1122
1123 case 'response.reasoning.done':
1124 // Complete reasoning text
1125 break;
1126
1127 case 'response.reasoning_summary_part.added':
1128 // New reasoning summary part added
1129 break;
1130
1131 case 'response.reasoning_summary_part.done':
1132 // Reasoning summary part completed
1133 break;
1134
1135 case 'response.reasoning_summary_text.delta':
1136 // Streaming reasoning summary text
1137 break;
1138
1139 case 'response.reasoning_summary_text.done':
1140 // Complete reasoning summary
1141 break;
1142
1143 // ===== ANNOTATION EVENTS =====
1144
1145 case 'response.output_text_annotation.added':
1146 case 'response.output_text.annotation.added':
1147 // Text annotation added (e.g., citations, references)
1148 // Can be used to add metadata to generated text
1149 break;
1150
1151 case 'response.completed':
1152 // Response fully completed - function calls are already handled in response.output_item.done
1153 break;
1154
1155 // ===== ERROR EVENTS =====
1156
1157 case 'error':
1158 // Generic error event
1159 $error = $json['error'] ?? $json;
1160 $message = $error['message'] ?? 'Unknown error occurred';
1161 $code = $error['code'] ?? null;
1162 if ( $code ) {
1163 $message .= " (Code: $code)";
1164 }
1165 throw new Exception( $message );
1166
1167 default:
1168 // Unknown event type - log for debugging
1169 Meow_MWAI_Logging::error( 'Responses API: Unknown event type: ' . $eventType );
1170
1171 // Check if this might be a different streaming format
1172 if ( isset( $json['delta'] ) && is_string( $json['delta'] ) ) {
1173 $content = $json['delta'];
1174 }
1175 elseif ( isset( $json['content'] ) && is_string( $json['content'] ) ) {
1176 $content = $json['content'];
1177 }
1178 }
1179
1180 // Handle usage data
1181 $usage = $json['usage'] ?? [];
1182 if ( isset( $usage['input_tokens'], $usage['output_tokens'] ) ) {
1183 $this->streamInTokens = (int) $usage['input_tokens'];
1184 $this->streamOutTokens = (int) $usage['output_tokens'];
1185 if ( isset( $usage['cost'] ) ) {
1186 $this->streamCost = (float) $usage['cost'];
1187 }
1188 }
1189
1190 return $content;
1191 }
1192
1193 /**
1194 * Override stream data handler to support both APIs
1195 */
1196 protected function stream_data_handler( $json ) {
1197 // Check if this is a Responses API event (uses 'type' field)
1198 if ( isset( $json['type'] ) && strpos( $json['type'], 'response.' ) === 0 ) {
1199 return $this->responses_stream_data_handler( $json );
1200 }
1201
1202 // Fallback to ChatML handler
1203 return parent::stream_data_handler( $json );
1204 }
1205
1206 /**
1207 * Override reset to include OpenAI-specific state
1208 */
1209 protected function reset_request_state() {
1210 parent::reset_request_state();
1211
1212 // Reset OpenAI-specific state
1213 $this->streamImages = [];
1214 }
1215
1216 /**
1217 * Override run_completion_query to route to appropriate API
1218 */
1219 public function run_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
1220 // Reset request-specific state to prevent leakage between requests
1221 $this->reset_request_state();
1222
1223 // Store current query for should_use_responses_api check
1224 $this->currentQuery = $query;
1225
1226 // Check if this is a GPT-5 model
1227 $isGpt5Model = strpos( $query->model, 'gpt-5' ) === 0;
1228
1229 // Debug: Always log which API we're using
1230 $useResponsesApi = $this->should_use_responses_api( $query->model );
1231
1232 // GPT-5 models MUST use Responses API
1233 if ( $isGpt5Model && !$useResponsesApi ) {
1234 $options = $this->core->get_all_options();
1235 $responsesApiEnabled = $options['ai_responses_api'] ?? true;
1236
1237 if ( !$responsesApiEnabled ) {
1238 throw new Exception( 'GPT-5 models require the Responses API to be enabled. Please enable "Use Responses API" in AI Engine settings.' );
1239 }
1240
1241 // If Responses API is enabled but model doesn't have the tag, force it
1242 return $this->run_responses_completion_query( $query, $streamCallback );
1243 }
1244
1245 // Check if we should use Responses API
1246 if ( $useResponsesApi ) {
1247 return $this->run_responses_completion_query( $query, $streamCallback );
1248 }
1249
1250 // Fallback to ChatML implementation
1251 return parent::run_completion_query( $query, $streamCallback );
1252 }
1253
1254 /**
1255 * Run completion query using Responses API
1256 */
1257 protected function run_responses_completion_query( $query, $streamCallback = null ): Meow_MWAI_Reply {
1258 // Check if we have functions that might require feedback
1259 $hasFunctions = !empty( $query->functions );
1260
1261
1262 $isStreaming = !is_null( $streamCallback );
1263
1264 // Initialize debug mode
1265 $this->init_debug_mode( $query );
1266
1267 if ( $isStreaming ) {
1268 $this->streamCallback = $streamCallback;
1269 add_action( 'http_api_curl', [ $this, 'stream_handler' ], 10, 3 );
1270 }
1271
1272 $this->reset_stream();
1273 $body = $this->build_responses_body( $query, $streamCallback );
1274 $url = $this->build_responses_url();
1275 $headers = $this->build_headers( $query );
1276 $options = $this->build_options( $headers, $body );
1277
1278 // Store the request body for debugging
1279 $this->lastRequestBody = $body;
1280
1281 // Debug log for Responses API
1282 $queries_debug = $this->core->get_option( 'queries_debug_mode' );
1283 if ( $queries_debug ) {
1284 error_log( '[AI Engine Queries] Using Responses API' );
1285 error_log( '[AI Engine Queries] Request URL: ' . $url );
1286 error_log( '[AI Engine Queries] Request Body: ' . json_encode( $body, JSON_PRETTY_PRINT ) );
1287
1288 // Log specific tool information
1289 if ( isset( $body['tools'] ) && is_array( $body['tools'] ) ) {
1290 error_log( '[AI Engine Queries] Tools included in request:' );
1291 foreach ( $body['tools'] as $index => $tool ) {
1292 $toolInfo = 'Tool ' . $index . ': type=' . ( $tool['type'] ?? 'unknown' );
1293 if ( $tool['type'] === 'file_search' && isset( $tool['vector_store_ids'] ) ) {
1294 $toolInfo .= ', vector_store_ids=' . json_encode( $tool['vector_store_ids'] );
1295 }
1296 error_log( '[AI Engine Queries] - ' . $toolInfo );
1297 }
1298 } else {
1299 error_log( '[AI Engine Queries] No tools included in request' );
1300 }
1301 }
1302
1303 // Emit "Request sent" event for feedback queries
1304 if ( $this->currentDebugMode && !empty( $streamCallback ) &&
1305 ( $query instanceof Meow_MWAI_Query_Feedback || $query instanceof Meow_MWAI_Query_AssistFeedback ) ) {
1306 $event = Meow_MWAI_Event::request_sent()
1307 ->set_metadata( 'is_feedback', true )
1308 ->set_metadata( 'feedback_count', count( $query->blocks ) );
1309 call_user_func( $streamCallback, $event );
1310 }
1311
1312 try {
1313 // Log the input being sent for feedback queries
1314 if ( $queries_debug && $query instanceof Meow_MWAI_Query_Feedback && isset( $body['input'] ) ) {
1315 error_log( '[AI Engine Queries] Sending feedback with ' . count( $body['input'] ) . ' messages to Responses API' );
1316 error_log( '[AI Engine Queries] Previous Response ID: ' . ( $body['previous_response_id'] ?? 'none' ) );
1317 foreach ( $body['input'] as $idx => $msg ) {
1318 $msgType = is_array( $msg ) && isset( $msg['type'] ) ? $msg['type'] : 'unknown';
1319 $callId = is_array( $msg ) && isset( $msg['call_id'] ) ? $msg['call_id'] : 'no-id';
1320 error_log( '[AI Engine Queries] Message ' . $idx . ': type=' . $msgType . ', call_id=' . $callId );
1321 if ( $msgType === 'function_call' && isset( $msg['name'] ) ) {
1322 error_log( '[AI Engine Queries] Function name: ' . $msg['name'] );
1323 }
1324 if ( $msgType === 'function_call_output' && isset( $msg['output'] ) ) {
1325 error_log( '[AI Engine Queries] Output: ' . substr( $msg['output'], 0, 50 ) . '...' );
1326 }
1327 }
1328 }
1329
1330 $res = $this->run_query( $url, $options, $streamCallback );
1331 $reply = new Meow_MWAI_Reply( $query );
1332
1333 $returned_id = null;
1334 $returned_model = $this->inModel;
1335 $returned_in_tokens = null;
1336 $returned_out_tokens = null;
1337 $returned_price = null;
1338 $returned_choices = [];
1339
1340 // Streaming Mode
1341 if ( $isStreaming ) {
1342 if ( empty( $this->streamContent ) ) {
1343 $error = $this->try_decode_error( $this->streamBuffer );
1344 if ( !is_null( $error ) ) {
1345 throw new Exception( $error );
1346 }
1347 }
1348
1349 $returned_id = $this->inId;
1350 $returned_model = $this->inModel ? $this->inModel : $query->model;
1351 $message = [ 'role' => 'assistant', 'content' => $this->streamContent ];
1352
1353 if ( !empty( $this->streamToolCalls ) ) {
1354 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1355 error_log( '[AI Engine Queries] Responses API: Found ' . count( $this->streamToolCalls ) . ' tool calls in streaming response' );
1356 foreach ( $this->streamToolCalls as $idx => $toolCall ) {
1357 error_log( '[AI Engine Queries] Tool call ' . $idx . ': ' . $toolCall['function']['name'] . ' (id: ' . $toolCall['id'] . ')' );
1358 }
1359 }
1360 $message['tool_calls'] = $this->streamToolCalls;
1361 }
1362
1363 if ( !is_null( $this->streamInTokens ) ) {
1364 $returned_in_tokens = $this->streamInTokens;
1365 }
1366 if ( !is_null( $this->streamOutTokens ) ) {
1367 $returned_out_tokens = $this->streamOutTokens;
1368 }
1369 if ( !is_null( $this->streamCost ) ) {
1370 $returned_price = $this->streamCost;
1371 }
1372
1373 $returned_choices = [ [ 'message' => $message ] ];
1374
1375 // Add generated images to the content if any
1376 if ( !empty( $this->streamImages ) ) {
1377 // Add images as additional choices with b64_json format
1378 foreach ( $this->streamImages as $base64Image ) {
1379 $returned_choices[] = [ 'b64_json' => $base64Image ];
1380 }
1381 Meow_MWAI_Logging::log( 'Responses API: Added ' . count( $this->streamImages ) . ' images to choices (streaming)' );
1382 }
1383
1384 // Log streaming response data if queries debug is enabled
1385 if ( $queries_debug ) {
1386 error_log( '[AI Engine Queries] Streaming Response Collected:' );
1387 $streaming_data = [
1388 'id' => $returned_id,
1389 'model' => $returned_model,
1390 'content_length' => strlen( $this->streamContent ),
1391 'content_preview' => substr( $this->streamContent, 0, 200 ) . ( strlen( $this->streamContent ) > 200 ? '...' : '' ),
1392 'tool_calls' => !empty( $this->streamToolCalls ) ? count( $this->streamToolCalls ) . ' tool calls' : 'none',
1393 'usage' => [
1394 'input_tokens' => $returned_in_tokens,
1395 'output_tokens' => $returned_out_tokens,
1396 'cost' => $returned_price
1397 ]
1398 ];
1399
1400 // Log tool calls details if present
1401 if ( !empty( $this->streamToolCalls ) ) {
1402 $streaming_data['tool_calls_details'] = [];
1403 foreach ( $this->streamToolCalls as $tool_call ) {
1404 $streaming_data['tool_calls_details'][] = [
1405 'id' => $tool_call['id'] ?? 'unknown',
1406 'name' => $tool_call['function']['name'] ?? 'unknown',
1407 'arguments' => substr( $tool_call['function']['arguments'] ?? '{}', 0, 100 ) . '...'
1408 ];
1409 }
1410 }
1411
1412 error_log( json_encode( $streaming_data, JSON_PRETTY_PRINT ) );
1413 }
1414 }
1415 // Standard Mode
1416 else {
1417 $data = $res['data'];
1418 if ( empty( $data ) ) {
1419 throw new Exception( 'No content received (res is null).' );
1420 }
1421
1422 // Ensure $data is an array
1423 if ( !is_array( $data ) ) {
1424 $error_message = is_string( $data ) ? $data : 'Invalid response format';
1425 throw new Exception( 'Responses API error: ' . $error_message );
1426 }
1427
1428 // Handle Responses API response format
1429 $returned_id = $data['id'] ?? null;
1430 $returned_model = $data['model'] ?? $query->model;
1431
1432 // Extract content from Responses API format
1433 $content = '';
1434 $tool_calls = [];
1435 $images = [];
1436
1437
1438 if ( isset( $data['output'] ) && is_array( $data['output'] ) ) {
1439
1440 foreach ( $data['output'] as $idx => $output_item ) {
1441 if ( isset( $output_item['type'] ) && $output_item['type'] === 'message' && isset( $output_item['content'] ) ) {
1442 // Handle message content array - this is the actual text content
1443 if ( is_array( $output_item['content'] ) ) {
1444 foreach ( $output_item['content'] as $content_item ) {
1445 // The actual text is in content_item['text'] for type 'output_text'
1446 if ( isset( $content_item['type'] ) && $content_item['type'] === 'output_text' && isset( $content_item['text'] ) ) {
1447 $content .= $content_item['text'];
1448 }
1449 // Fallback checks for other possible structures
1450 elseif ( isset( $content_item['content'] ) && is_string( $content_item['content'] ) ) {
1451 $content .= $content_item['content'];
1452 }
1453 elseif ( is_string( $content_item ) ) {
1454 $content .= $content_item;
1455 }
1456 }
1457 }
1458 }
1459 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'function_call' ) {
1460 // Responses API returns function_call type with call_id
1461 $callId = $output_item['call_id'] ?? $output_item['id'] ?? null;
1462 $functionName = $output_item['name'] ?? '';
1463 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1464 error_log( '[AI Engine Queries] Found function_call: ' . $functionName . ' (call_id: ' . $callId . ')' );
1465 }
1466
1467 $tool_calls[] = [
1468 'id' => $callId,
1469 'type' => 'function',
1470 'function' => [
1471 'name' => $functionName,
1472 'arguments' => $output_item['arguments'] ?? '{}'
1473 ]
1474 ];
1475 }
1476 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'image_generation_call' && isset( $output_item['result'] ) ) {
1477 // Handle image generation results
1478 $base64Image = $output_item['result'];
1479 $images[] = $base64Image;
1480
1481 Meow_MWAI_Logging::log( 'Responses API: Found generated image in non-streaming mode' );
1482 }
1483 elseif ( isset( $output_item['type'] ) && $output_item['type'] === 'mcp_approval_request' ) {
1484 // IMPORTANT: MCP approval requests are already handled via streaming events
1485 // We must skip them here to prevent duplicate function calls
1486 // MCP tools are executed remotely by OpenAI and don't need local execution
1487 Meow_MWAI_Logging::log( 'Responses API: Skipping MCP approval request for ' . $output_item['name'] . ' (already handled via events)' );
1488 }
1489 }
1490 }
1491
1492 // If we couldn't find content in output, try other locations
1493 if ( empty( $content ) ) {
1494 if ( isset( $data['text'] ) ) {
1495 if ( is_string( $data['text'] ) ) {
1496 $content = $data['text'];
1497 }
1498 elseif ( is_array( $data['text'] ) ) {
1499 // Only implode if it's an array of strings, not complex structures
1500 $textParts = array_filter( $data['text'], 'is_string' );
1501 if ( !empty( $textParts ) ) {
1502 $content = implode( '', $textParts );
1503 }
1504 }
1505 }
1506 elseif ( isset( $data['content'] ) ) {
1507 if ( is_array( $data['content'] ) && isset( $data['content'][0]['text'] ) ) {
1508 $content = $data['content'][0]['text'];
1509 }
1510 elseif ( is_string( $data['content'] ) ) {
1511 $content = $data['content'];
1512 }
1513 }
1514 }
1515
1516 // If still no content found, log for debugging
1517 if ( empty( $content ) ) {
1518 // Check if $data is actually an array before using array_keys
1519 if ( is_array( $data ) ) {
1520 Meow_MWAI_Logging::log( 'Responses API: No content found in response. Structure: ' . json_encode( array_keys( $data ) ) );
1521 if ( isset( $data['output'][0] ) ) {
1522 Meow_MWAI_Logging::log( 'Responses API: First output item: ' . json_encode( $data['output'][0] ) );
1523 }
1524 if ( isset( $data['text'] ) ) {
1525 Meow_MWAI_Logging::log( 'Responses API: Text field structure: ' . json_encode( $data['text'] ) );
1526 }
1527 } else {
1528 // If $data is not an array, it might be an error string
1529 Meow_MWAI_Logging::log( 'Responses API: Invalid response data type. Data: ' . ( is_string( $data ) ? $data : json_encode( $data ) ) );
1530 }
1531 // Log the entire response for debugging
1532 Meow_MWAI_Logging::log( 'Responses API: Full response data: ' . json_encode( $data ) );
1533 }
1534
1535
1536 $message = [ 'role' => 'assistant', 'content' => $content ];
1537 if ( !empty( $tool_calls ) ) {
1538 $message['tool_calls'] = $tool_calls;
1539 Meow_MWAI_Logging::log( 'Responses API: Found ' . count( $tool_calls ) . ' tool calls' );
1540 }
1541
1542 $returned_choices = [[ 'message' => $message ]];
1543
1544 // Add images as additional choices
1545 if ( !empty( $images ) ) {
1546 foreach ( $images as $base64Image ) {
1547 $returned_choices[] = [ 'b64_json' => $base64Image ];
1548 }
1549 Meow_MWAI_Logging::log( 'Responses API: Added ' . count( $images ) . ' images to choices' );
1550 }
1551
1552
1553 // Extract usage information
1554 $usage = $data['usage'] ?? [];
1555 $returned_in_tokens = $usage['input_tokens'] ?? null;
1556 $returned_out_tokens = $usage['output_tokens'] ?? null;
1557 $returned_price = $usage['cost'] ?? null;
1558 }
1559
1560 // Store response ID for future stateful requests
1561 if ( !empty( $returned_id ) ) {
1562 $this->previousResponseId = $returned_id;
1563 $reply->set_id( $returned_id );
1564 }
1565 // Set the results
1566 $reply->set_choices( $returned_choices );
1567
1568 // Handle tokens usage
1569 $this->handle_tokens_usage(
1570 $reply,
1571 $query,
1572 $returned_model,
1573 $returned_in_tokens,
1574 $returned_out_tokens,
1575 $returned_price
1576 );
1577
1578 return $reply;
1579 }
1580 catch ( Exception $e ) {
1581 $service = $this->get_service_name();
1582 Meow_MWAI_Logging::error( "$service (Responses API): " . $e->getMessage() );
1583 $message = "$service (Responses API): " . $e->getMessage();
1584 throw new Exception( $message );
1585 }
1586 finally {
1587 if ( !is_null( $streamCallback ) ) {
1588 remove_action( 'http_api_curl', [ $this, 'stream_handler' ] );
1589 }
1590 }
1591 }
1592
1593 /**
1594 * Override handle_tokens_usage to set accuracy properly
1595 */
1596 public function handle_tokens_usage(
1597 $reply,
1598 $query,
1599 $returned_model,
1600 $returned_in_tokens,
1601 $returned_out_tokens,
1602 $returned_price = null
1603 ) {
1604 // Call parent to handle the actual usage recording
1605 parent::handle_tokens_usage(
1606 $reply,
1607 $query,
1608 $returned_model,
1609 $returned_in_tokens,
1610 $returned_out_tokens,
1611 $returned_price
1612 );
1613
1614 // Set accuracy based on data availability
1615 if ( !is_null( $returned_price ) && !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
1616 // Responses API with cost field or OpenRouter style = full accuracy
1617 $reply->set_usage_accuracy( 'full' );
1618 } elseif ( !is_null( $returned_in_tokens ) && !is_null( $returned_out_tokens ) ) {
1619 // Tokens from API but price calculated = tokens accuracy
1620 $reply->set_usage_accuracy( 'tokens' );
1621 } else {
1622 // Everything estimated
1623 $reply->set_usage_accuracy( 'estimated' );
1624 }
1625 }
1626
1627 /**
1628 * Override image query handling for gpt-image-1 model
1629 */
1630 public function run_image_query( $query ) {
1631 // IMPORTANT: We use the standard Images API for gpt-image-1 (not Responses API)
1632 // Even though Responses API supports image_generation tool, it would let the
1633 // orchestrator model choose which image model to use. By using the Images API
1634 // directly, we ensure gpt-image-1 is actually used as requested by the user.
1635
1636 // Use standard implementation for all image models including gpt-image-1
1637 return parent::run_image_query( $query );
1638 }
1639
1640 /**
1641 * Override transcription to support new models
1642 */
1643 public function run_transcribe_query( $query ) {
1644 // Check if using new transcription models
1645 $newTranscribeModels = ['gpt-4o-transcribe', 'gpt-4o-mini-transcribe'];
1646 if ( in_array( $query->model, $newTranscribeModels ) ) {
1647 // These still use the /audio/transcriptions endpoint but with new models
1648 // Just need to make sure the model name is passed correctly
1649 }
1650
1651 // Use parent implementation (still uses audio endpoint)
1652 return parent::run_transcribe_query( $query );
1653 }
1654
1655 /**
1656 * Override embedding query to support new models
1657 */
1658 public function run_embedding_query( $query ) {
1659 // Check if using new embedding models
1660 $newEmbeddingModels = ['text-embedding-3-small', 'text-embedding-3-large'];
1661 if ( in_array( $query->model, $newEmbeddingModels ) ) {
1662 // These still use the /embeddings endpoint but with improved models
1663 // The parent implementation should handle this correctly
1664 }
1665
1666 // Use parent implementation
1667 return parent::run_embedding_query( $query );
1668 }
1669
1670 /**
1671 * Enhanced error handling for Responses API
1672 */
1673 protected function handle_responses_errors( $data ) {
1674 // Handle Responses API specific errors
1675 if ( isset( $data['error'] ) ) {
1676 $error = $data['error'];
1677 $message = $error['message'] ?? 'Unknown error';
1678 $type = $error['type'] ?? null;
1679 $code = $error['code'] ?? null;
1680
1681 // Special handling for "No tool output found" errors
1682 if ( strpos( $message, 'No tool output found' ) !== false ) {
1683 // Log this error with details when queries debug is enabled
1684 if ( $this->core->get_option( 'queries_debug_mode' ) ) {
1685 error_log( '[AI Engine Queries] Responses API Tool Output Error:' );
1686 error_log( '[AI Engine Queries] Error: ' . $message );
1687 error_log( '[AI Engine Queries] This typically means the function call outputs were not properly formatted or are missing.' );
1688
1689 // Log the last request body if available
1690 if ( property_exists( $this, 'lastRequestBody' ) && $this->lastRequestBody ) {
1691 error_log( '[AI Engine Queries] Last request body: ' . json_encode( $this->lastRequestBody, JSON_PRETTY_PRINT ) );
1692 }
1693 }
1694 }
1695
1696 $errorMessage = $message;
1697 if ( $type ) {
1698 $errorMessage .= " (Type: $type)";
1699 }
1700 if ( $code ) {
1701 $errorMessage .= " (Code: $code)";
1702 }
1703
1704 throw new Exception( $errorMessage );
1705 }
1706
1707 // Check for event-based errors
1708 if ( isset( $data['event'] ) && $data['event'] === 'response.error' ) {
1709 $error = $data['error'] ?? [];
1710 $message = $error['message'] ?? 'Response API error';
1711 throw new Exception( $message );
1712 }
1713
1714 // Fallback to parent error handling
1715 parent::handle_response_errors( $data );
1716 }
1717
1718 /**
1719 * Add method to reset conversation state
1720 */
1721 public function reset_conversation_state() {
1722 $this->previousResponseId = null;
1723 $this->conversationState = [];
1724 }
1725
1726 /**
1727 * Check the connection to OpenAI by listing models.
1728 * This is a free metadata call that verifies API key validity.
1729 */
1730 public function connection_check() {
1731 try {
1732 $url = $this->get_models_endpoint();
1733 $response = $this->execute( 'GET', $url );
1734
1735 if ( !isset( $response['data'] ) || !is_array( $response['data'] ) ) {
1736 throw new Exception( 'Invalid response format from OpenAI' );
1737 }
1738
1739 $modelCount = count( $response['data'] );
1740 $availableModels = [];
1741
1742 // Get first 5 models for display
1743 $displayModels = array_slice( $response['data'], 0, 5 );
1744 foreach ( $displayModels as $model ) {
1745 if ( isset( $model['id'] ) ) {
1746 $availableModels[] = $model['id'];
1747 }
1748 }
1749
1750 return [
1751 'success' => true,
1752 'service' => 'OpenAI',
1753 'message' => "Connection successful. Found {$modelCount} models.",
1754 'details' => [
1755 'endpoint' => $url,
1756 'model_count' => $modelCount,
1757 'sample_models' => $availableModels,
1758 'organization' => $response['organization'] ?? null
1759 ]
1760 ];
1761 }
1762 catch ( Exception $e ) {
1763 return [
1764 'success' => false,
1765 'service' => 'OpenAI',
1766 'error' => $e->getMessage(),
1767 'details' => [
1768 'endpoint' => $this->get_models_endpoint()
1769 ]
1770 ];
1771 }
1772 }
1773
1774 /**
1775 * Get the models endpoint URL
1776 */
1777 protected function get_models_endpoint() {
1778 $endpoint = null;
1779
1780 // Same logic as build_url to determine the endpoint
1781 if ( $this->envType === 'openai' ) {
1782 $endpoint = apply_filters( 'mwai_openai_endpoint', 'https://api.openai.com/v1', $this->env );
1783 }
1784 else if ( $this->envType === 'azure' ) {
1785 $endpoint = isset( $this->env['endpoint'] ) ? $this->env['endpoint'] : null;
1786 }
1787
1788 if ( empty( $endpoint ) ) {
1789 throw new Exception( 'Endpoint is not defined for envType: ' . $this->envType );
1790 }
1791
1792 // Remove any existing API paths to get base URL
1793 $endpoint = str_replace( '/chat/completions', '', $endpoint );
1794 $endpoint = str_replace( '/v1/responses', '', $endpoint );
1795 $endpoint = rtrim( $endpoint, '/' );
1796
1797 // For Azure, we don't need the /v1 prefix
1798 if ( $this->envType === 'azure' ) {
1799 // Use the same API version as defined in the parent class
1800 return $endpoint . '/openai/models?api-version=2024-12-01-preview';
1801 }
1802
1803 // For OpenAI, ensure we have the /v1 prefix
1804 if ( strpos( $endpoint, '/v1' ) === false ) {
1805 $endpoint .= '/v1';
1806 }
1807
1808 return $endpoint . '/models';
1809 }
1810
1811 }
1812