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