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