diff --git a/open-sse/translator/index.ts b/open-sse/translator/index.ts index 501d2ffdbc..0e4dcc23ff 100644 --- a/open-sse/translator/index.ts +++ b/open-sse/translator/index.ts @@ -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; } diff --git a/tests/unit/probe-7293-strict-system-hoist.test.ts b/tests/unit/probe-7293-strict-system-hoist.test.ts index da1bce3480..bb66a38774 100644 --- a/tests/unit/probe-7293-strict-system-hoist.test.ts +++ b/tests/unit/probe-7293-strict-system-hoist.test.ts @@ -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/); +});