Skip to content

Python: fix(python/openai): use full-history mode by default to fix tool call streaming with OpenRouter#5111

Open
Bash892 wants to merge 4 commits into
microsoft:mainfrom
Bash892:fix/openrouter-tool-result-5105
Open

Python: fix(python/openai): use full-history mode by default to fix tool call streaming with OpenRouter#5111
Bash892 wants to merge 4 commits into
microsoft:mainfrom
Bash892:fix/openrouter-tool-result-5105

Conversation

@Bash892
Copy link
Copy Markdown

@Bash892 Bash892 commented Apr 5, 2026

Summary

Fixes #5105 — streaming crashes after tool execution when using OpenRouter or any provider that doesn't support previous_response_id.

Root cause: _get_conversation_id returned response.id by default (when store was not explicitly False). This caused the executor to enter server-managed continuation mode — clearing the full conversation history and sending only the tool result in the next request. Providers like OpenRouter ignore previous_response_id and received a bare function_call_output at position 0 → HTTP 400.

Fix: _get_conversation_id now returns None unless store=True is explicitly set. Server-managed conversation mode is opt-in; the default is full-history mode, which is universally compatible.

Users who want the server-managed optimization (OpenAI native only) should set store=True in their chat options.

Changes

  • python/packages/openai/agent_framework_openai/_chat_client.py: changed if store is Falseif store is not True in _get_conversation_id
  • python/packages/openai/tests/openai/test_openai_chat_client.py: updated streaming event tests to cover both store=True and store=None; added test_parse_response_with_store_none_returns_none as a regression test

Test plan

  • uv run pytest packages/openai/tests/ -m "not integration" — 270 passed, 0 failures
  • Verify streaming with OpenRouter no longer raises HTTP 400 after tool execution
  • Verify store=True still enables server-managed conversation mode for OpenAI native

…aming with OpenRouter

_get_conversation_id now returns None unless store=True is explicitly set,
making server-managed conversation mode opt-in. This ensures compatibility
with providers that don't support previous_response_id (e.g. OpenRouter),
where sending only a tool result without full conversation context caused
an HTTP 400 error.

Fixes microsoft#5105
@github-actions github-actions Bot changed the title fix(python/openai): use full-history mode by default to fix tool call streaming with OpenRouter Python: fix(python/openai): use full-history mode by default to fix tool call streaming with OpenRouter Apr 5, 2026
@Bash892
Copy link
Copy Markdown
Author

Bash892 commented Apr 5, 2026

@microsoft-github-policy-service agree

Copy link
Copy Markdown
Contributor

@chetantoshniwal chetantoshniwal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 90%

✓ Correctness

This PR fixes a correctness issue where _get_conversation_id would return a conversation ID when store=None (the default), causing providers that don't support previous_response_id (e.g. OpenRouter) to fail. The fix changes the guard from if store is False to if store is not True, so only an explicit store=True opt-in enables server-managed conversation mode. The logic change is correct and the tests are well-structured. One note: the same _get_conversation_id method in _responses_client.py (line 448) still uses the old if store is False guard, but this may be intentional since the Responses API is OpenAI-native and always supports previous_response_id.

✓ Security Reliability

This PR fixes a reliability issue (GitHub #5105) where _get_conversation_id would return a conversation ID when store=None (the default), causing failures with OpenAI-compatible providers (e.g., OpenRouter) that don't support previous_response_id. The fix correctly changes the guard from if store is False to if store is not True, ensuring only explicit opt-in via store=True enables server-managed conversation mode. The code change is minimal and well-tested. No security issues are introduced. Note that the sibling _responses_client.py still uses the old if store is False check, but this is likely intentional since the Responses API is OpenAI-native and always supports conversation IDs.

✓ Test Coverage

The test coverage for this behavioral change is solid. The new test test_parse_response_with_store_none_returns_none directly validates the regression fix (issue #5105) by asserting that both store=None and store=False return None from _get_conversation_id. The streaming tests (test_streaming_response_created_type and test_streaming_response_in_progress_type) were properly updated to verify both store=True (returns conversation_id) and store=None (returns None) paths. One minor gap: the non-streaming _get_conversation_id path lacks a direct store=True with-conversation positive test in this diff, though this is likely covered by a pre-existing test. A store=False case for streaming is untested but is functionally identical to store=None under the new is not True guard. Overall the tests are meaningful — they assert specific return values rather than just exercising code paths.

✓ Design Approach

The change correctly inverts the semantics of _get_conversation_id from opt-out (return None only when store=False) to opt-in (return non-None only when store=True). Previously, store=None (the default when a caller omits the field) would fall through the guard and return a previous_response_id-based conversation ID, which breaks providers like OpenRouter that don't support that parameter. The fix properly treats store=None the same as store=False, making server-managed conversation mode strictly opt-in. The change is minimal, addresses the root cause rather than masking it, and the updated tests adequately cover all three store states. No blocking design issues found.


Automated review by chetantoshniwal's agents

assert client._get_conversation_id(mock_response, store=False) is None


def test_parse_response_uses_response_id_when_no_conversation() -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test covers the store=None and store=False negative cases well, but doesn't verify the positive case (store=True returning conv_456). Adding assert client._get_conversation_id(mock_response, store=True) == 'conv_456' would make this a complete tri-state test and guard against future regressions that might accidentally break the store=True path.

Suggested change
def test_parse_response_uses_response_id_when_no_conversation() -> None:
assert client._get_conversation_id(mock_response, store=None) is None
assert client._get_conversation_id(mock_response, store=False) is None
assert client._get_conversation_id(mock_response, store=True) == "conv_456"

mode, which is compatible with providers that do not support ``previous_response_id``
(e.g. OpenRouter).
"""
if store is not True:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so this is not the way, this client is designed to connect to the Responses API and that API (at least in both OpenAI and Foundry) uses service side storage by default, so only when store is explicitly set to False do we not store anything, the assumption for these clients is that when store is not set, the value is True, hence the check for store=False above. In order to use this as expected with other providers, either create a issue on their end that they adhere to the responses API which means they would have to support the store=True mechanism, or that they raise when store=None, or in your own code, set the options to store=False

@Bash892 Bash892 requested a review from eavanvalkenburg May 20, 2026 20:11
Copilot AI review requested due to automatic review settings May 20, 2026 20:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR changes the OpenAI Python chat client to default to full-history mode (no server-managed continuation) to avoid streaming failures after tool calls on providers that don’t support previous_response_id (e.g., OpenRouter), while keeping server-managed mode available via explicit store=True.

Changes:

  • Updated _get_conversation_id to return None unless store=True is explicitly set, making server-managed continuation opt-in.
  • Added/updated unit tests to cover store=None (default) vs store=True behavior for both response parsing and streaming events.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
python/packages/openai/agent_framework_openai/_chat_client.py Makes conversation-id/continuation behavior opt-in by requiring store=True to emit a conversation identifier.
python/packages/openai/tests/openai/test_openai_chat_client.py Adds regression coverage ensuring default (store=None) uses full-history mode and streaming only includes conversation_id when store=True.

Comment thread python/packages/openai/agent_framework_openai/_chat_client.py Outdated
@Bash892
Copy link
Copy Markdown
Author

Bash892 commented May 20, 2026

Hey @eavanvalkenburg — appreciate the thorough review and totally understand the concern about the Responses API design intent.

That said, I want to push back a bit on the framing. The agent-framework positions itself as a multi-provider framework, not an OpenAI/Foundry-exclusive client. Defaulting to server-managed conversation mode (store=True assumption) means any provider that doesn't support previous_response_id — OpenRouter, Anthropic, Mistral, etc. — fails silently with an HTTP 400. That's a pretty rough default for a framework that advertises broad provider support.

The fix doesn't remove server-managed mode — it just makes it opt-in with store=True, which is one line for OpenAI native users. Meanwhile, everyone else gets a working default out of the box.

On your suggestion to have users set store=False explicitly — that works as a workaround, but it puts the burden on every non-OpenAI user to know about an internal implementation detail just to avoid a crash. That feels like the wrong abstraction layer for the fix.

Would you be open to merging with the opt-in approach, with updated docs making clear that store=True is required for OpenAI native server-managed mode? Happy to add that if it helps get this across the line.

Bash892 and others added 2 commits May 20, 2026 22:07
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Streaming fails with OpenRouter 400: tool role without preceding tool_calls

5 participants