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