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