-
Notifications
You must be signed in to change notification settings - Fork 3.9k
feat: Add support for OpenAI Responses API #4791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
4fc3f3c
d563f98
9b4cb97
4ee8c9b
0a4044d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -28,6 +28,146 @@ | |||||||||||||||||||||||||||||||||||||||||||
| request_timeout = 600 | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| DEFAULT_MODEL_NAME = "gpt-4o" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| class ResponsesAPIWrapper: | ||||||||||||||||||||||||||||||||||||||||||||
| """Wrapper to convert Responses API format to Chat Completions format""" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, responses_obj): | ||||||||||||||||||||||||||||||||||||||||||||
| self._responses = responses_obj | ||||||||||||||||||||||||||||||||||||||||||||
| self._convert_to_chat_format() | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def _convert_to_chat_format(self): | ||||||||||||||||||||||||||||||||||||||||||||
| """Convert responses API output to chat completions choices format""" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Create a mock choices structure | ||||||||||||||||||||||||||||||||||||||||||||
| class MockChoice: | ||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||
| self.message = type( | ||||||||||||||||||||||||||||||||||||||||||||
| "obj", | ||||||||||||||||||||||||||||||||||||||||||||
| (object,), | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| "content": "", | ||||||||||||||||||||||||||||||||||||||||||||
| "tool_calls": None, | ||||||||||||||||||||||||||||||||||||||||||||
| "reasoning_content": None, | ||||||||||||||||||||||||||||||||||||||||||||
| "reasoning": None, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| )() | ||||||||||||||||||||||||||||||||||||||||||||
| self.finish_reason = None | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| choice = MockChoice() | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Extract content from output items if available | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(self._responses, "output") and self._responses.output: | ||||||||||||||||||||||||||||||||||||||||||||
| for item in self._responses.output: | ||||||||||||||||||||||||||||||||||||||||||||
| # Handle ResponseOutputMessage type | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(item, "type") and item.type == "message": | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(item, "content") and item.content: | ||||||||||||||||||||||||||||||||||||||||||||
| # Concatenate all text items to avoid losing content | ||||||||||||||||||||||||||||||||||||||||||||
| text_parts = [] | ||||||||||||||||||||||||||||||||||||||||||||
| for content_item in item.content: | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(content_item, "text") and content_item.text: | ||||||||||||||||||||||||||||||||||||||||||||
| text_parts.append(content_item.text) | ||||||||||||||||||||||||||||||||||||||||||||
| if text_parts: | ||||||||||||||||||||||||||||||||||||||||||||
| choice.message.content = "\n".join(text_parts) | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
| # Fallback: direct text attribute | ||||||||||||||||||||||||||||||||||||||||||||
| elif hasattr(item, "text") and item.text: | ||||||||||||||||||||||||||||||||||||||||||||
| choice.message.content = item.text | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
| # Fallback: dict format | ||||||||||||||||||||||||||||||||||||||||||||
| elif isinstance(item, dict): | ||||||||||||||||||||||||||||||||||||||||||||
| if "text" in item: | ||||||||||||||||||||||||||||||||||||||||||||
| choice.message.content = item["text"] | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Always initialize self.choices for consistent interface | ||||||||||||||||||||||||||||||||||||||||||||
| self.choices = [choice] | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Copy other attributes | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(self._responses, "id"): | ||||||||||||||||||||||||||||||||||||||||||||
| self.id = self._responses.id | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(self._responses, "usage"): | ||||||||||||||||||||||||||||||||||||||||||||
| self.usage = self._responses.usage | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def __getattr__(self, name): | ||||||||||||||||||||||||||||||||||||||||||||
| """Fallback to original responses object for other attributes""" | ||||||||||||||||||||||||||||||||||||||||||||
| return getattr(self._responses, name) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| class StreamingResponsesAPIWrapper: | ||||||||||||||||||||||||||||||||||||||||||||
| """Wrapper for streaming responses API to mimic chat completions stream""" | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self, responses_stream): | ||||||||||||||||||||||||||||||||||||||||||||
| self._stream = responses_stream | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def __iter__(self): | ||||||||||||||||||||||||||||||||||||||||||||
| return self | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| def __next__(self): | ||||||||||||||||||||||||||||||||||||||||||||
| # This will properly propagate StopIteration when the stream ends | ||||||||||||||||||||||||||||||||||||||||||||
| event = next(self._stream) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Wrap each event to look like chat completions format | ||||||||||||||||||||||||||||||||||||||||||||
| class MockChoice: | ||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||
| self.delta = type( | ||||||||||||||||||||||||||||||||||||||||||||
| "obj", | ||||||||||||||||||||||||||||||||||||||||||||
| (object,), | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| "content": None, | ||||||||||||||||||||||||||||||||||||||||||||
| "function_call": None, | ||||||||||||||||||||||||||||||||||||||||||||
| "reasoning_content": None, | ||||||||||||||||||||||||||||||||||||||||||||
| "reasoning": None, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| )() | ||||||||||||||||||||||||||||||||||||||||||||
| self.finish_reason = None | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| class MockChunk: | ||||||||||||||||||||||||||||||||||||||||||||
| def __init__(self): | ||||||||||||||||||||||||||||||||||||||||||||
| self.choices = [MockChoice()] | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| mock_chunk = MockChunk() | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Extract finish_reason if available | ||||||||||||||||||||||||||||||||||||||||||||
| finish_reason = None | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(event, "finish_reason"): | ||||||||||||||||||||||||||||||||||||||||||||
| finish_reason = event.finish_reason | ||||||||||||||||||||||||||||||||||||||||||||
| elif hasattr(event, "output"): | ||||||||||||||||||||||||||||||||||||||||||||
| for item in event.output: | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(item, "finish_reason"): | ||||||||||||||||||||||||||||||||||||||||||||
| finish_reason = item.finish_reason | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
| if isinstance(item, dict) and "finish_reason" in item: | ||||||||||||||||||||||||||||||||||||||||||||
| finish_reason = item["finish_reason"] | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| mock_chunk.choices[0].finish_reason = finish_reason | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| # Handle Responses API event stream format | ||||||||||||||||||||||||||||||||||||||||||||
| # Check for OUTPUT_TEXT_DELTA events (have delta attribute with text) | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(event, "delta") and event.delta: | ||||||||||||||||||||||||||||||||||||||||||||
| mock_chunk.choices[0].delta.content = event.delta | ||||||||||||||||||||||||||||||||||||||||||||
| # Fallback for other formats | ||||||||||||||||||||||||||||||||||||||||||||
| elif hasattr(event, "output"): | ||||||||||||||||||||||||||||||||||||||||||||
| for item in event.output: | ||||||||||||||||||||||||||||||||||||||||||||
| # Handle ResponseOutputMessage type | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(item, "type") and item.type == "message": | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(item, "content") and item.content: | ||||||||||||||||||||||||||||||||||||||||||||
| for content_item in item.content: | ||||||||||||||||||||||||||||||||||||||||||||
| if hasattr(content_item, "text") and content_item.text: | ||||||||||||||||||||||||||||||||||||||||||||
| mock_chunk.choices[0].delta.content = content_item.text | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
| if mock_chunk.choices[0].delta.content: | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
| # Fallback: direct text attribute | ||||||||||||||||||||||||||||||||||||||||||||
| elif hasattr(item, "text"): | ||||||||||||||||||||||||||||||||||||||||||||
| mock_chunk.choices[0].delta.content = item.text | ||||||||||||||||||||||||||||||||||||||||||||
| break | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
| # Propagate finish_reason from the underlying chunk if available | |
| finish_reason = None | |
| # Check for finish_reason on delta first (most similar to chat completions) | |
| if hasattr(chunk, "delta") and hasattr(chunk.delta, "finish_reason"): | |
| finish_reason = chunk.delta.finish_reason | |
| # Fallback: finish_reason directly on the chunk | |
| elif hasattr(chunk, "finish_reason"): | |
| finish_reason = chunk.finish_reason | |
| # Fallback: look for finish_reason on output items | |
| elif hasattr(chunk, "output"): | |
| for item in chunk.output: | |
| if hasattr(item, "finish_reason"): | |
| finish_reason = item.finish_reason | |
| break | |
| if isinstance(item, dict) and "finish_reason" in item: | |
| finish_reason = item["finish_reason"] | |
| break | |
| mock_chunk.choices[0].finish_reason = finish_reason |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new AIDER_WIRE_API environment variable and wire_api model setting are not documented. Users need to know: (1) the purpose of this setting, (2) valid values ("chat" or "responses"), (3) when to use each option, and (4) that the environment variable overrides the model setting. Consider adding documentation in the appropriate user-facing documentation files or adding docstring documentation to the ModelSettings class explaining the wire_api field.
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code doesn't validate the wire_api value. If AIDER_WIRE_API or the model's wire_api setting contains an invalid value (e.g., "foo"), the code will silently default to using the chat completions API because the if condition on line 1141 only checks for "responses". This could lead to confusion if users mistype the value. Consider adding validation to raise a clear error for invalid wire_api values, or at least log a warning when an unrecognized value is encountered.
| # Use responses API or chat completions API based on wire_api setting | |
| # Use responses API or chat completions API based on wire_api setting | |
| # Validate wire_api to catch misconfigurations (e.g., typos) | |
| valid_wire_api_values = {None, "", "responses"} | |
| if wire_api not in valid_wire_api_values: | |
| sys.stderr.write( | |
| f"Warning: Unrecognized wire_api value '{wire_api}'. " | |
| "Falling back to chat completions API.\n" | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The wrapper creates mock objects with content initialized to None, but doesn't validate that content was successfully extracted before creating the choices list. If no valid content is found in any output item (all branches fail or items are empty), choice.message.content and choice.delta.content will remain None. While this might be acceptable for some edge cases, it could lead to unexpected None values propagating through the codebase. Consider initializing content to an empty string instead of None for better consistency with the chat completions API behavior.