…Anthropic API
Fixes microsoft-amplifier/amplifier-support#207.
Sessions that switch providers mid-conversation (e.g. some turns on
provider-chat-completions or provider-openai, interleaved with anthropic
turns) can persist assistant thinking blocks whose signature is null,
absent, or otherwise not something Anthropic minted. Anthropic strict-
validates thinking.signature as a non-empty string on every replay
request, so a single bad block anywhere in history 400s the ENTIRE
request and bricks the session on every future resume attempt.
Adds AnthropicProvider._sanitize_thinking_blocks(messages, model), a
defensive chokepoint pass run in _complete_chat_request on the fully
assembled message list, before cache control is applied. Both the
streaming transport (client.messages.stream) and the non-streaming
transport (client.messages.with_raw_response.create) consume the same
params dict built there -- as does the refusal-fallback retry path --
so one sanitize pass covers all three call sites.
The sanitizer:
- Strips thinking/redacted_thinking blocks whose signature is not a
valid non-empty string, covering both malformed shapes seen in the
wild: {"signature": null} (provider-chat-completions) and a missing
signature key entirely, with the reasoning payload carried under
content instead (provider-openai's Responses API shape).
- Tolerates non-dict content-array entries instead of crashing.
- Inserts a minimal placeholder text block if stripping would leave an
assistant message with an empty content array (Anthropic rejects
those too).
- Logs one aggregate logger.warning (count + model) and emits a
provider:thinking_signature_stripped hook event per request when
anything was stripped, matching the existing
provider:tool_sequence_repaired observability pattern. No log/event
and no behavior change at all for clean histories.
Part of the cross-provider resume hardening set tracked on
microsoft-amplifier/amplifier-support#208, alongside the
provider-chat-completions producer-side fix for #206 and an
effort-clamp fix for #289.
Adds tests/test_thinking_sanitization.py (12 tests) driving the fix
through the real complete() pipeline and asserting on the captured
params["messages"] payload (the same dict the SDK call receives) --
covering both malformed shapes, valid-signature preservation, mixed
thinking+tool_call+text ordering, empty-content placeholder insertion,
non-dict block tolerance, and the warning/event observability surface.
Full suite: 538 passed (526 pre-existing + 12 new), same 3 pre-existing
unrelated failures in test_tool_repair.py's streaming fixtures (a
MockStreamManager/async-iterator mismatch predating this change,
confirmed present on main before this patch).
Fixes microsoft-amplifier/amplifier-support#207
Root cause
Sessions that switch providers mid-conversation (e.g. some turns handled by
provider-chat-completionsorprovider-openai, interleaved withanthropicturns) can persist assistant
thinkingcontent blocks whosesignatureisnull, absent entirely, or otherwise not something Anthropic minted — thesigning/verification scheme is provider-specific and cannot be retrofitted
onto a block another provider produced.
Anthropic strict-validates
thinking.signatureas a non-empty string onevery replay request. A single malformed block anywhere in history is
enough for the entire request to 400, e.g.:
...which bricks the session on every subsequent resume attempt, even though
only one message out of many is at fault.
What this PR does
Adds
AnthropicProvider._sanitize_thinking_blocks(messages, model), adefensive chokepoint pass run once in
_complete_chat_requeston the fullyassembled
all_messageslist, immediately after message conversion/combinationand before cache-control is applied. Both the streaming transport
(
client.messages.stream) and the non-streaming transport(
client.messages.with_raw_response.create) consume the sameparamsdictbuilt there — as does the refusal-fallback retry path — so one sanitize
pass covers all three call sites.
The sanitizer:
thinking/redacted_thinkingblocks whosesignatureis not avalid non-empty string, covering both malformed shapes seen in the wild:
{"type": "thinking", "thinking": "...", "signature": null}—round-tripped through
provider-chat-completions' history format.{"type": "thinking", "content": ["<encrypted>", "rs_..."]}with nosignaturekey at all —provider-openai's Responses API persistsencrypted reasoning content + a reasoning-item id instead of a signature.
dict.get(a missingkey and an explicit
Noneare indistinguishable to.get()).corrupted/partial transcript) instead of crashing.
assistant message with an empty content array — Anthropic rejects empty
content arrays just as strictly as it rejects unsigned thinking blocks.
logger.warning(stripped count + model) and emits aprovider:thinking_signature_strippedhook event per request whenanything was actually stripped — matching the existing
provider:tool_sequence_repairedobservability pattern already usedelsewhere in this module. No log, no event, and no behavior change at
all for clean histories.
thinkingblocks, and all other block types, untouched.Tests
tests/test_thinking_sanitization.py(12 tests, TDD: written and confirmedfailing before the fix landed) drives the fix through the real
complete()pipeline and asserts on the captured
params["messages"]— the same dicthanded to the (mocked) Anthropic SDK call, i.e. the actual outgoing payload:
signature: nullthinking block → stripped from the payload.signaturekey, cross-providercontentpayload → stripped (both via the full pipeline and via a directunit test of
_sanitize_thinking_blockswith a hand-built dict thatgenuinely omits the
signaturekey, proving.get()-based access ratherthan indexing).
placeholder inserted, content never empty.
removed; other blocks intact and in original order.
redacted_thinkingwithout asignaturekey → left untouched (Anthropicdoesn't require one there).
actually stripped (and not emitted on clean/valid histories).
Full suite after the fix: 538 passed (526 pre-existing + 12 new), with
the same 3 pre-existing, unrelated failures in
test_tool_repair.py'sstreaming fixtures (a
MockStreamManager/async-iterator mismatch, confirmedpresent on
mainbefore this change — not touched by this PR).Part of a set
This PR is part of the cross-provider resume hardening set tracked on
microsoft-amplifier/amplifier-support#208. Sibling PRs: the
provider-chat-completionsproducer-side fix formicrosoft-amplifier/amplifier-support#206, and an effort-clamp fix for
microsoft-amplifier/amplifier-support#289.
Follow-up to verify
The separate
amplifier-module-provider-anthropic-fablepackage is expectedto inherit this fix automatically via subclassing
AnthropicProvider— butthat package should be checked to confirm it does not override
_complete_chat_requestor_convert_messagesin a way that bypasses thenew sanitize call site.