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