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