fix: stop emitting thinking blocks with null signatures from chat-completions responses - #12
Open
Michael J. Jabbour (michaeljabbour) wants to merge 1 commit into
Open
Conversation
…pletions responses OpenAI-style chat-completions APIs have no signed extended thinking, so the two persisted-append sites in this module fabricated ThinkingBlock content with signature=None. Anthropic strict-validates every thinking block's signature as a non-empty string on replay; a single null-signature block in history 400s the entire next request and bricks the session after a provider switch. Deletes the two persisted ThinkingBlock appends (non-streaming _build_response, streaming _complete_streaming) while keeping the ephemeral live-thinking UI stream (ThinkingContent event blocks / llm:stream_block_* hook events) intact -- those are render-only and never replayed to a provider. Fixes microsoft-amplifier/amplifier-support#206
Author
Cross-provider resume hardening set — complete PR indexThis PR is the producer-side fix in the set tracked on microsoft-amplifier/amplifier-support#208. Full set for reviewers:
Belt-and-suspenders note: even with this producer fix merged, provider-anthropic#72's consumer-side filter still protects against other producers (e.g. provider-openai Responses artifacts) and legacy transcripts written before this fix. Design rationale and deferred items: support#208 comment. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
provider-chat-completionsfabricatedthinkingcontent blocks from OpenAI-compatiblereasoning_contentfields, then persisted them into session history withsignature: null. Anthropic's API strict-validates everythinkingblock'ssignatureas a non-empty string on replay; a single null-signature block anywhere in history 400s the entire next request (messages.N.content.0.thinking.signature.str: Input should be a valid string) and bricks the session the moment a user switches providers mid-conversation.Root cause
OpenAI-style chat-completions APIs have no signed extended thinking — the concept doesn't exist on that side of the wire. This module never set
signatureanywhere, so theThinkingBlocks it fabricated always fell back to the field'sNonedefault. That's fine as long as the session stays onchat-completions, but the moment history is replayed toprovider-anthropic(e.g. after a provider swap), those null-signature blocks get rejected outright.What this PR does
Implements the issue's preferred fix (option 1): stop emitting persisted
thinkingcontent blocks from this module._build_response()(previouslyamplifier_module_provider_chat_completions/__init__.py:650)_complete_streaming()(previously:992)ThinkingContentevent blocks and thellm:stream_block_*hook events are unchanged, since they're render-only and never replayed to a provider.contentis now[TextBlock](+ToolCallBlocks when tools are called) for chat-completions responses;text/content_blocksare unaffected.This is consistent with the module's own outbound wire path, which already silently drops
ThinkingBlockwhen re-serializing history (_convert_messages_to_wire,:554-555) — a persisted content list without thinking blocks is already a fully-supported round-trip shape for this module.Explicitly out of scope:
provider-openaiis untouched. Its Responses-API reasoning artifacts are functional for OpenAI-side replay; cross-provider safety for those is handled by the sibling consumer-side PR inprovider-anthropic.Tests
TDD: both new tests were written first and confirmed failing against the old behavior before the fix landed.
tests/test_provider.py::test_reasoning_content_becomes_thinking_block→test_reasoning_content_does_not_become_thinking_block: asserts_build_response()never returns aThinkingBlockwhile theTextBlockstill carries the response text.tests/test_streaming.py::test_reasoning_content_does_not_become_persisted_thinking_block: assertsawait provider.complete(...)never returns aThinkingBlockinresp.content, while confirming the ephemeralllm:stream_block_delta(block_type="thinking") events still fire unchanged.test_streaming.pyreasoning/thinking section) pass unchanged, proving the live-thinking UI stream was not touched.TestConfigParsing::test_default_base_url, confirmed failing identically onorigin/mainprior to this diff — a stale test/code mismatch around a hardcoded defaultbase_url, out of scope for this surgical fix).Offline (no live API) verification: constructed faked OpenAI SDK objects carrying
reasoning_content, called_build_response(...)andawait provider.complete(...)directly, and confirmed the actual persisted response objects (result.content,resp.content) contain zeroThinkingBlocks whileTextBlocks survive intact.Related
Part of the cross-provider resume hardening set tracked on microsoft-amplifier/amplifier-support#208. Sibling PRs in
provider-anthropic: consumer-side thinking-block sanitization for #207, and an effort-clamp fix for #289.Fixes microsoft-amplifier/amplifier-support#206