mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 22:52:19 +03:00
fix(sse): re-run strict system hoist after format translation (#10803)
Re-runs hoistLeadingSystemMessage on the final outbound array at translateRequest's single return, instead of only pre-translation. claudeToOpenAI (and the Responses source path, which never ran the pre-translation hoist at all since `messages` doesn't exist yet there) re-introduces/normalizes a leading system message after the hoist already ran, so a strict provider (e.g. vLLM/Qwen3, xiaomi-mimo) could still receive a non-compliant array and 400 with "System message must be at the beginning." Validated live against a vLLM/Qwen3 endpoint (documented in the PR) plus in an isolated worktree boarded onto origin/release/v3.8.50 (0 conflicts, 2 files): - 39/39 focused tests pass (probe-7293-strict-system-hoist including the new Claude-source regression case, memory-system-first-6135, claude-system-role-cache-boundary, memory-cache-safe-injection). - check-file-size, check-changelog-integrity: OK. - typecheck:core: clean. - check-complexity / check-cognitive-complexity: OK, both under baseline. Co-authored-by: Kizuno18 <Kizuno18@users.noreply.github.com>
This commit is contained in:
@@ -756,6 +756,19 @@ export function translateRequest(
|
||||
delete result[RESPONSES_STORE_MARKER];
|
||||
}
|
||||
|
||||
// #7293 follow-up: the pre-translation hoist above normalizes the *source*
|
||||
// message array, which a target translator can then undo. `claudeToOpenAI`
|
||||
// pushes `body.system` as a fresh leading system message before appending the
|
||||
// converted messages, so an already-hoisted system lands at index 1 again;
|
||||
// a Responses-source request has no `messages` at all until translation, so
|
||||
// the earlier call is a no-op for it. Re-run on the final outbound array —
|
||||
// it is the only shape the upstream actually sees. Idempotent: same array
|
||||
// reference for non-strict providers and already-compliant requests, so
|
||||
// prompt-cache prefixes stay stable.
|
||||
if (targetFormat === FORMATS.OPENAI && result.messages && Array.isArray(result.messages)) {
|
||||
result.messages = hoistLeadingSystemMessage(result.messages, provider);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -142,3 +142,39 @@ test("#7293: already-compliant strict-provider request is a no-op (prompt-cache
|
||||
|
||||
assert.deepEqual(result.messages, messages);
|
||||
});
|
||||
|
||||
test("#7293: Claude-source request keeps a single leading system message after claudeToOpenAI re-adds body.system", () => {
|
||||
// Claude Code's real shape: a top-level `system` field AND a system-role
|
||||
// message inside `messages`. claudeToOpenAI pushes body.system as the leading
|
||||
// system message and then appends the converted messages, so hoisting before
|
||||
// translation is not enough — the offender reappears at index 1.
|
||||
const body = {
|
||||
model: "mimo-v2.5",
|
||||
system: [{ type: "text", text: "You are a coding assistant." }],
|
||||
messages: [
|
||||
{ role: "user", content: "hi" },
|
||||
{ role: "system", content: "deferred tools list" },
|
||||
{ role: "user", content: "go" },
|
||||
],
|
||||
};
|
||||
|
||||
const result = translateRequest(
|
||||
FORMATS.CLAUDE,
|
||||
FORMATS.OPENAI,
|
||||
"mimo-v2.5",
|
||||
body,
|
||||
false,
|
||||
null,
|
||||
"xiaomi-mimo"
|
||||
);
|
||||
|
||||
const outMessages = result.messages as Array<{ role: string; content: string }>;
|
||||
const systemIndices = outMessages
|
||||
.map((m, i) => (m.role === "system" ? i : -1))
|
||||
.filter((i) => i >= 0);
|
||||
|
||||
assert.deepEqual(systemIndices, [0]);
|
||||
// Merge, never drop: both the top-level system and the offender survive.
|
||||
assert.match(outMessages[0].content, /You are a coding assistant\./);
|
||||
assert.match(outMessages[0].content, /deferred tools list/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user