Repository navigation
fix(translator): drop unsigned thinking blocks instead of fabricating a Claude signature - #12386
Merged
diegosouzapw merged 3 commits intoSep 2, 2026
Conversation
… a Claude signature The response translator builds a `thinking` block from cross-provider `reasoning_content` and never attaches a `signature`. The client stores that block verbatim and replays it on the next turn. When that turn is served by an Anthropic-native rung, openaiToClaudeRequest only treated `signature: ""` as synthesized (diegosouzapw#6953); a block whose signature field was absent fell through to the DEFAULT_THINKING_CLAUDE_SIGNATURE fallback. prepareClaudeRequest then classified that non-empty string as a genuine signature on the latest assistant turn and forwarded the block verbatim, so Anthropic rejected the request with `400 Invalid signature in thinking block` and the combo stayed pinned to the non-Anthropic rung. Align the check with the stricter one already used in claudeHelper.ts: a missing signature is dropped exactly like an empty one. Older turns and tool_use precursors are unaffected: prepareClaudeRequest already rewrites them to a signature-less redacted_thinking placeholder, which Anthropic accepts without validation. The diegosouzapw#6953 test that pinned the fallback for an absent signature is updated to the new expectation; its own file header already described missing signatures as strippable. Closes diegosouzapw#12105 Co-authored-by: Leon Marcos <leonaniagomez@gmail.com>
…njection test "Unit Tests fast-path (2/4)" failed on `translateRequest does NOT inject duplicate thinking for Claude-format messages with existing thinking block` (tests/unit/translator-helper-branches.test.ts). The test sent an assistant turn with an UNSIGNED thinking block plus a tool_use, then asserted that the client's thinking text survived and that the reasoning cache was not replayed. Since 03c7d4e the request translator drops a thinking block that carries no signature instead of stamping DEFAULT_THINKING_CLAUDE_SIGNATURE on it (diegosouzapw#12105, mirroring claudeHelper's non-empty-signature check). With the block gone, the Kimi Coding replay step in translateRequest correctly sees a tool_use turn without a thinking precursor and re-hydrates the cached reasoning, so the assertion "original thinking should be preserved" read the cached text. The intent of the test — a valid client thinking block must not be replaced by, or duplicated with, cached reasoning — is unchanged. Give the block a signature so it is the valid block the test wants to protect; the assertions stay as they were. Add a sibling test pinning the new unsigned case for Kimi Coding: the unsigned block is dropped, the cached reasoning is replayed exactly once before tool_use, and the replayed block carries no fabricated signature. Co-authored-by: Leon Marcos <leonaniagomez@gmail.com>
muhamadgalihsaputra
pushed a commit
to niyatna/NiyatnaRoute
that referenced
this pull request
Sep 27, 2026
… a Claude signature (diegosouzapw#12386) A thinking content part arriving with no signature — typical after a cross-provider hop where reasoning_content was converted into a thinking block — was stamped with DEFAULT_THINKING_CLAUDE_SIGNATURE. prepareClaudeRequest treats any non-empty signature on the latest assistant turn as genuine and preserves it verbatim, so the fabricated one reached Anthropic and the replay failed with "Invalid signature". A missing signature is now treated the same as an empty one, aligned with the stricter check claudeHelper.ts already used: the block is dropped rather than fabricated. Real signatures are still preserved verbatim and redacted_thinking is unchanged. Validated in a combined worktree with all 25 PRs of this batch boarded together: typecheck:core clean, 443/443 node-runner tests plus 14/14 vitest across every test file the batch touches, and check-changelog-integrity, check:cycles (418 files), check:provider-consistency (272 REGISTRY entries, 355 canonical providers), check:docs-counts, check:docs-sync (42 locales) and check-file-size all green. Thanks @pacocartones.
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
thinkingcontent part arriving with nosignature(typical after a cross-provider hop wherereasoning_contentwas converted into athinkingblock) was stamped withDEFAULT_THINKING_CLAUDE_SIGNATUREatopen-sse/translator/request/openai-to-claude.ts.prepareClaudeRequesttreats any non-empty signature on the latest assistant turn as genuine and preserves it verbatim, so the fabricated signature reached Anthropic and the replay failed with "Invalid signature".!part.signature), aligned with the stricter check already used inclaudeHelper.ts: the block is dropped instead of fabricated. Real signatures are still preserved verbatim;redacted_thinkinghandling is unchanged.Related Issues
Validation
tests/unit/openai-to-claude-undefined-signature-12105.test.ts(new, RED 2/3 on the base, GREEN 3/3), alltests/unit/*openai-to-claude*.test.ts+*claude-helper*.test.ts(104/104),npm run typecheck:core0,npm run check:open-sse-typecheckOK,npm run check:changelog-integrityOKnpm run lint— repository-wide eslint exit 0 (run with--pass-on-unpruned-suppressions; the literal command reports only pre-existing unused global suppressions on this base)release/v3.8.51; focused checks rerun afterwardMutation: reverting the condition to
=== ""fails 3 of 8 tests; restored, the file hash is identical and 8/8 pass.Tests Added Or Updated
tests/unit/openai-to-claude-undefined-signature-12105.test.ts(new): an undefined signature is dropped rather than stamped; end-to-endopenaiToClaudeRequestthenprepareClaudeRequest("claude")with tool_use and thinking enabled keeps no block with the default signature and preserves tool_use; a real signature is still passed through verbatim.tests/unit/openai-to-claude-strip-empty-signature-6953.test.ts: the case#6953: thinking block with undefined signature (Claude-format) is preserved with fallbacknow asserts the drop and is renamed#6953/#12105 .... It pinned the behaviour this issue reverses; the file header ("strip thinking blocks with empty/missing signatures entirely") already described the new behaviour.Coverage Notes
open-sse/translator/request/openai-to-claude.ts: the changed branch is covered by the new file and the flipped fix(backend): Combo models: synthetic thinking blocks with empty signature permanently poison the Anthropic leg — one codex turn captures the whole session (silent 400 fallback) #6953 case; the otherDEFAULT_THINKING_CLAUDE_SIGNATUREassertions in the suite concernredacted_thinking.dataandprepareClaudeRequestand still pass.Reviewer Notes
part.signature || DEFAULT_THINKING_CLAUDE_SIGNATUREfallback at the push is reachable only forredacted_thinkingblocks; left in place to keep the diff surgical.