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