-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Python: Fix OpenAI Responses API 400 error on multi-turn conversations with previous_response_id #3800
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?
Conversation
- Added logic to filter assistant messages when using previous_response_id - Updated _prepare_options to determine conversation type before message preparation - Modified _prepare_messages_for_openai to accept filter_for_continuation parameter - When using previous_response_id (resp_*), only NEW user messages after last assistant are sent - Added comprehensive tests for message filtering behavior - All existing tests pass Co-authored-by: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>
…nse_id - Updated filtering logic to preserve system/developer messages (API accepts these roles) - System messages are collected from before last assistant and combined with new user messages - Updated test to verify system message preservation - All 92 tests pass Co-authored-by: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>
Co-authored-by: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>
Python Test Coverage Report •
Python Unit Test Overview
|
||||||||||||||||||||||||||||||
|
@copilot run the pre-commit hooks and add any files that are modified |
Co-authored-by: markwallace-microsoft <127216156+markwallace-microsoft@users.noreply.github.com>
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.
Pull request overview
Fixes multi-turn continuation failures with the OpenAI Responses API by adjusting how message history is prepared when continuing via previous_response_id (resp_*), aligning client behavior with the API’s expected schema.
Changes:
- Detect
resp_*continuation early in_prepare_options()and enable message filtering accordingly. - Add continuation-aware filtering behavior to
_prepare_messages_for_openai(...). - Add unit tests validating filtering behavior for
resp_*vsconv_*vs no conversation id.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| python/packages/core/agent_framework/openai/_responses_client.py | Adds continuation-aware message preparation for resp_* IDs and wires it into option preparation. |
| python/packages/core/tests/openai/test_openai_responses_client.py | Adds tests verifying message filtering behavior across conversation id modes. |
| async def test_message_filtering_with_previous_response_id() -> None: | ||
| """Test that assistant messages are filtered when using previous_response_id.""" | ||
| client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") | ||
|
|
||
| # Create a multi-turn conversation with history | ||
| messages = [ | ||
| ChatMessage(role="system", text="You are a helpful assistant"), | ||
| ChatMessage(role="user", text="My name is Alice"), | ||
| ChatMessage(role="assistant", text="Nice to meet you, Alice!"), | ||
| ChatMessage(role="user", text="What's my name?"), | ||
| ] | ||
|
|
||
| # When using previous_response_id, assistant messages should be filtered but system messages preserved | ||
| options = await client._prepare_options( | ||
| messages, | ||
| {"conversation_id": "resp_12345"}, # Using resp_ prefix | ||
| ) # type: ignore | ||
|
|
||
| # Should include: system message + last user message | ||
| assert "input" in options | ||
| input_messages = options["input"] | ||
| assert len(input_messages) == 2, f"Expected 2 messages (system + user), got {len(input_messages)}" | ||
| assert input_messages[0]["role"] == "system" | ||
| assert input_messages[1]["role"] == "user" | ||
| assert "What's my name?" in str(input_messages[1]) | ||
|
|
||
| # Verify previous_response_id is set | ||
| assert options["previous_response_id"] == "resp_12345" | ||
|
|
||
|
|
||
| async def test_message_filtering_without_previous_response_id() -> None: | ||
| """Test that all messages are included when NOT using previous_response_id.""" | ||
| client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") | ||
|
|
||
| # Same conversation as above | ||
| messages = [ | ||
| ChatMessage(role="system", text="You are a helpful assistant"), | ||
| ChatMessage(role="user", text="My name is Alice"), | ||
| ChatMessage(role="assistant", text="Nice to meet you, Alice!"), | ||
| ChatMessage(role="user", text="What's my name?"), | ||
| ] | ||
|
|
||
| # Without conversation_id, all messages should be included | ||
| options = await client._prepare_options(messages, {}) # type: ignore | ||
|
|
||
| # Should include all messages | ||
| assert "input" in options | ||
| input_messages = options["input"] | ||
| # System (1) + User (1) + Assistant (1) + User (1) = 4 messages | ||
| assert len(input_messages) == 4 | ||
|
|
||
| # Verify previous_response_id is NOT set | ||
| assert "previous_response_id" not in options | ||
|
|
||
|
|
||
| async def test_message_filtering_with_conv_prefix() -> None: | ||
| """Test that messages are NOT filtered when using conv_ prefix (conversation ID).""" | ||
| client = OpenAIResponsesClient(model_id="test-model", api_key="test-key") | ||
|
|
||
| messages = [ | ||
| ChatMessage(role="system", text="You are a helpful assistant"), | ||
| ChatMessage(role="user", text="My name is Alice"), | ||
| ChatMessage(role="assistant", text="Nice to meet you, Alice!"), | ||
| ChatMessage(role="user", text="What's my name?"), | ||
| ] | ||
|
|
||
| # When using conv_ prefix, should use conversation parameter, not previous_response_id | ||
| options = await client._prepare_options( | ||
| messages, | ||
| {"conversation_id": "conv_abc123"}, # Using conv_ prefix | ||
| ) # type: ignore | ||
|
|
||
| # All messages should be included (no filtering for conversation IDs) | ||
| assert "input" in options | ||
| input_messages = options["input"] | ||
| assert len(input_messages) == 4 | ||
|
|
||
| # Verify conversation is set, not previous_response_id | ||
| assert options.get("conversation") == "conv_abc123" | ||
| assert "previous_response_id" not in options | ||
|
|
Copilot
AI
Feb 10, 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 tests cover filtering assistant messages for resp_* continuation, but don’t cover the case where a tool message appears after the last assistant turn (which the client intends to filter out for previous_response_id). Adding a test that includes a ChatMessage(role="tool", ...) after the last assistant message and asserting it is excluded from options["input"] would better lock in the intended fix and prevent regressions.
| # Get all messages after the last assistant (new user messages) | ||
| new_messages = chat_messages[last_assistant_idx + 1 :] | ||
| # Combine: system messages + new messages | ||
| chat_messages = system_messages + list(new_messages) |
Copilot
AI
Feb 10, 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.
In the continuation filtering path, new_messages = chat_messages[last_assistant_idx + 1:] is appended without restricting roles. If a tool message (e.g., function_result/tool output) exists after the last assistant turn (possible in tool-loop error/retry scenarios), it will be sent to the Responses API even though this block’s comment/docstring says assistant messages and function results should be filtered out. Consider explicitly filtering the retained messages to roles the Responses API accepts for previous_response_id continuation (e.g., keep system/developer plus only user messages after the last assistant; drop tool and any other roles).
| # Get all messages after the last assistant (new user messages) | |
| new_messages = chat_messages[last_assistant_idx + 1 :] | |
| # Combine: system messages + new messages | |
| chat_messages = system_messages + list(new_messages) | |
| # Get all messages after the last assistant, but keep only supported roles | |
| # (system/developer/user) for continuation. | |
| new_messages = [ | |
| msg | |
| for msg in chat_messages[last_assistant_idx + 1 :] | |
| if msg.role in ("system", "developer", "user") | |
| ] | |
| # Combine: system messages + filtered new messages | |
| chat_messages = system_messages + new_messages |
Motivation and Context
OpenAI Responses API rejects assistant messages when using
previous_response_idfor conversation continuation. The API expects only system/developer/user messages in theinputarray - assistant responses are already stored server-side.Description
Schema mismatch:
OpenAIResponsesClientwas sending all conversation history regardless of continuation method. The Responses API has different requirements:previous_response_id(resp_*): Expects only system/developer/user messagesconversation(conv_*): Expects full conversation historyChanges:
_prepare_options(): Determine conversation type before message preparation_prepare_messages_for_openai(): Addfilter_for_continuationparameterprevious_response_idis used:Example:
Azure clients inherit fix through
RawOpenAIResponsesClientbase class.Contribution Checklist
Original prompt
This section details on the original issue you should resolve
<issue_title>Python: [Bug]: OpenAIResponsesClient + Responses API 400 invalid_prompt when using messages array input (schema / validation mismatch with latest Responses API)</issue_title>
<issue_description>### Description
When using
agent_framework’sOpenAIResponsesClientwith the OpenAI Responses API, the request fails with a 400invalid_prompterror on a multi-turn conversation.The error payload suggests that the request body built by
OpenAIResponsesClient(and/or the underlyingopenaiSDK types) is no longer compatible with the current Responses API schema as documented here:https://developers.openai.com/api/reference/resources/responses/methods/create
Code Sample
import asyncio from typing import Any, MutableMapping, Sequence from agent_framework import ChatMessage, ChatMessageStore, ChatMessageStoreProtocol from agent_framework.openai import OpenAIChatClient, OpenAIResponsesClient async def multi_turn_example(): OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" OPENROUTER_API_KEY = "sk-...redacted..." # real key in my local file model_id = "openrouter/aurora-alpha" chat_client = OpenAIResponsesClient( api_key=OPENROUTER_API_KEY, base_url=OPENROUTER_BASE_URL, model_id=model_id, ) agent = chat_client.create_agent( name="ChatBot", instructions="You are a helpful assistant", store=False, chat_message_store_factory=lambda: ChatMessageStore(), ) # Create a thread for persistent conversation thread = agent.get_new_thread() # First interaction response1 = await agent.run("My name is Alice", thread=thread) print(f"Agent: {response1.text}") # Second interaction – the agent should remember the name response2 = await agent.run("What's my name?", thread=thread) print(f"Agent: {response2.text}") # Expected to mention "Alice" # Serialize thread for storage serialized = await thread.serialize() # Later, deserialize and continue conversation new_thread = await agent.deserialize_thread(serialized) response3 = await agent.run("What did we talk about?", thread=new_thread) print(f"Agent: {response3.text}") # Expected to remember previous context if __name__ == "__main__": asyncio.run(multi_turn_example())Error Messages / Stack Traces
Agent: Nice to meet you, Alice! How can I assist you today? request content: {"input":[{"role":"system","content":[{"type":"input_text","text":"You are a helpful assistant"}]},{"role":"user","content":[{"type":"input_text","text":"My name is Alice"}]},{"role":"assistant","content":[{"type":"output_text","text":"Nice to meet you, Alice! How can I assist you today?"}]},{"role":"user","content":[{"type":"input_text","text":"What's my name?"}]}],"model":"openrouter/aurora-alpha","store":false,"stream":false} Traceback (most recent call last): File "/root/workspace/ofnil-agentic-rag/.venv/lib/python3.12/site-packages/agent_framework/openai/_responses_client.py", line 100, in _inner_get_response response = await client.responses.create(stream=False, **run_options) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/root/workspace/ofnil-agentic-rag/.venv/lib/python3.12/site-packages/openai/resources/responses/responses.py", line 2259, in create return await self._post( ^^^^^^^^^^^^^^^^^ File "/root/workspace/ofnil-agentic-rag/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1795, in post return await self.request(cast_to, opts, stream=stream, stream_cls=stream_cls) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/root/workspace/ofnil-agentic-rag/.venv/lib/python3.12/site-packages/openai/_base_client.py", line 1595, in request raise self._make_status_error_from_response(err.response) from None openai.BadRequestError: Error code: 400 - {'error': {'code': 'invalid_prompt', 'message': 'Invalid Responses API request'}, 'metadata': {'raw': '[\n {\n "code": "invalid_union",\n "errors": [\n [\n {\n "expected": "string",\n "code": "invalid_type",\n "path": [],\n "message": "Invalid input: expected string, received array"\n }\n ],\n [\n {\n "code": "invalid_union",\n "errors": [\n [\n {\n "code": "invalid_value",\n "values": [\n "reasoning"\n ],\n "path": [\n "type"\n ],\n "message": "Invalid input: expected \\"reasoning\\""\n },\n {\n "expected": "string",\n "code": "invalid_type",\n "path": [\n "id"\n ],\n "message": "Invalid input: expected str... </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes microsoft/agent-framework#3795 <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.