Skip to content

fix(translator): drop unsigned thinking blocks instead of fabricating a Claude signature - #12386

Merged
diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.51from
pacocartones:fix/claude-thinking-undefined-signature
Sep 2, 2026
Merged

diegosouzapw merged 3 commits into
diegosouzapw:release/v3.8.51from
pacocartones:fix/claude-thinking-undefined-signature

Conversation

@pacocartones

Copy link
Copy Markdown
Contributor

Summary

  • 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 at open-sse/translator/request/openai-to-claude.ts. prepareClaudeRequest treats 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".
  • The check now treats a missing signature the same as an empty one (!part.signature), aligned with the stricter check already used in claudeHelper.ts: the block is dropped instead of fabricated. Real signatures are still preserved verbatim; redacted_thinking handling is unchanged.

Related Issues

Validation

  • Change type: translator
  • Focused tests and category gates from the golden path: tests/unit/openai-to-claude-undefined-signature-12105.test.ts (new, RED 2/3 on the base, GREEN 3/3), all tests/unit/*openai-to-claude*.test.ts + *claude-helper*.test.ts (104/104), npm run typecheck:core 0, npm run check:open-sse-typecheck OK, npm run check:changelog-integrity OK
  • npm 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)
  • Reconciled with the current active release base release/v3.8.51; focused checks rerun afterward
  • Production-code changes include a new or updated automated test in this PR
  • SonarQube is temporarily opt-in while the private project has no quota; it is not a PR gate.

Mutation: 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-end openaiToClaudeRequest then prepareClaudeRequest("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 fallback now 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

Reviewer Notes

  • One existing test was deliberately flipped (see above); the product decision follows the maintainer's comment on the issue.
  • After this change the part.signature || DEFAULT_THINKING_CLAUDE_SIGNATURE fallback at the push is reachable only for redacted_thinking blocks; left in place to keep the diff surgical.

pacocartones and others added 2 commits September 1, 2026 23:42
… 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>
@diegosouzapw
diegosouzapw merged commit 01d97be into diegosouzapw:release/v3.8.51 Sep 2, 2026
16 checks passed
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(backend): cross-provider reasoning_content emitted as unsigned thinking block → invalid signature on replay to Claude

2 participants