mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 21:02:12 +03:00
* 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).
28 lines
1.2 KiB
TypeScript
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);
|
|
});
|