Files
OmniRoute/tests/unit/repro-6953.test.ts
Diego Rodrigues de Sa e Souza 52b26c88c9 fix(sse): sanitize empty-signature thinking blocks + hoist strict-provider system messages (#7583)
* fix(sse): sanitize empty-signature thinking blocks + hoist strict-provider system messages (#6953, #7293)

#6953: prepareClaudeRequest's "preserve latest-assistant thinking verbatim"
guard (claudeHelper.ts, anti-400 for legitimate Anthropic replay) did not
distinguish a genuine Claude signature from an empty one fabricated by a
non-Anthropic leg (e.g. codex reasoning_content). It forwarded signature:""
verbatim to Anthropic, which always 400s ("Invalid signature in thinking
block"), permanently locking combo routing onto the non-Anthropic leg. The
response-side half of this bug (openai-to-claude.ts synthesizing the empty
signature in the first place) was already fixed by #6982/PR#6982; this PR
closes the remaining request-side half. Fix: the verbatim-preserve guard now
requires every thinking-ish block on the latest assistant message to carry a
non-empty signature/data; otherwise it falls through to the existing
sanitization path (redacted_thinking + DEFAULT_THINKING_CLAUDE_SIGNATURE)
already applied to older turns.

#7293: translateRequest() is the single outbound choke point every chat
request passes through, including same-format (OpenAI→OpenAI) passthrough
where none of the format-specific translators run. systemMessageMustBeFirst()
/ PROVIDERS_SYSTEM_MUST_BE_FIRST (src/lib/memory/injection.ts, #6135/PR#6225)
was only consulted by the memory injector, so a client-injected system
message landing mid-array (OpenCode/Kilo Code style clients, Discussion
#6129) reached strict providers (xiaomi-mimo) untouched and 400'd. Fix: a new
helper (open-sse/translator/helpers/strictSystemHoist.ts) hoists every system
message onto index 0 for strict providers, reusing systemMessageMustBeFirst()
as the single source of truth, merging (never dropping) multiple offenders in
original order, and no-op'ing (same array reference) for non-strict providers
and already-compliant requests to preserve prompt-cache prefix stability.

Both defects live in the same file cluster (openai-to-claude request-path
translator + its helpers), hence one PR for both issues per triage guidance.

Regression tests:
- tests/unit/repro-6953.test.ts — RED (actual signature:'' forwarded) → GREEN
- tests/unit/probe-7293-strict-system-hoist.test.ts — RED (system message
  left at index 10 of 70) → GREEN, plus multi-offender merge, existing-leading
  merge, non-strict-provider no-op, and already-compliant no-op cases.

Gates run: file-size, complexity, cognitive-complexity (both at/under
baseline), typecheck:core (clean), eslint on changed files (clean),
test:vitest (254/254 green), plus all directly relevant existing suites
(translator-claude-helper-thinking, translator-xiaomi-mimo-reasoning-replay,
memory-system-first-6135, dashscope-cache-control-openai-2069,
xiaomi-mimo-provider, role-normalizer, translation.golden,
translators.property, translator-helper-branches, translator-claude-to-openai,
translator-same-format-null-flush — all green).

Closes #6953
Closes #7293

* chore(quality): prune the now-stale claudeHelper no-explicit-any suppression (#6953)

The #6953 fix removed the single `any` that config/quality/eslint-suppressions.json
still had frozen for open-sse/translator/helpers/claudeHelper.ts, so the entry became
stale and ESLint's stale-suppression enforcement failed the 'No new ESLint warnings'
gate — the gate went red because the code got better.

Pruned that one entry only (never a global --prune-suppressions: other entries are
other sessions' frozen debt).
2026-07-17 07:04:49 -03:00

28 lines
1.2 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
const { prepareClaudeRequest } = await import("../../open-sse/translator/helpers/claudeHelper.ts");
const { DEFAULT_THINKING_CLAUDE_SIGNATURE } = await import(
"../../open-sse/config/defaultThinkingSignature.ts"
);
test("#6953: latest-assistant thinking block with EMPTY signature must not be forwarded verbatim to an Anthropic-native leg", () => {
const body: Record<string, unknown> = {
thinking: { type: "enabled", budget_tokens: 4096 },
model: "claude-opus-4-8",
messages: [
{ role: "user", content: [{ type: "text", text: "review this diff" }] },
{
role: "assistant",
content: [
{ type: "thinking", thinking: "Reviewing Rust diff for compliance...", signature: "" },
{ type: "text", text: "Looks fine." },
],
},
{ role: "user", content: [{ type: "text", text: "go ahead and commit" }] },
],
};
prepareClaudeRequest(body, "claude");
const tb = body.messages[1].content[0];
assert.notEqual(tb.signature, "", "empty/foreign thinking signature must not be forwarded verbatim");
if (tb.type === "redacted_thinking") assert.equal(tb.data, DEFAULT_THINKING_CLAUDE_SIGNATURE);
});