import test from "node:test"; import assert from "node:assert/strict"; // Regression for 9router#2191: the OpenAI->Gemini request translator must not // emit two adjacent `contents[]` entries with the same role. Gemini-family APIs // (incl. Antigravity / Vertex) reject those with // 400 INVALID_ARGUMENT "Request contains consecutive messages with the same role". // The translator had no consecutive-same-role merge pass (unlike the Kiro and // Claude paths), so consecutive `user` turns — or a tool-result turn (role:user) // immediately followed by a plain user turn — produced an invalid alternation. const { openaiToGeminiRequest, mergeConsecutiveSameRoleContents } = await import( "../../open-sse/translator/request/openai-to-gemini.ts" ); type GeminiContent = { role: string; parts: Array> }; type GeminiReq = { contents: GeminiContent[] }; function assertNoConsecutiveSameRole(contents: GeminiContent[], label: string) { for (let i = 1; i < contents.length; i++) { assert.notStrictEqual( contents[i].role, contents[i - 1].role, `${label}: contents[${i - 1}] and contents[${i}] both have role "${contents[i].role}" ` + `(Gemini rejects consecutive same-role messages)` ); } } test("OpenAI -> Gemini merges two consecutive user messages into one content block", () => { const body = { messages: [ { role: "user", content: "Hello" }, { role: "user", content: "Additional context" }, ], }; const result = openaiToGeminiRequest("gemini-2.5-pro", body, false) as GeminiReq; assertNoConsecutiveSameRole(result.contents, "two-user"); // The two user turns collapse into a single user content carrying both parts. assert.equal(result.contents.length, 1, "expected the two user turns to merge into one"); assert.equal(result.contents[0].role, "user"); const texts = result.contents[0].parts.map((p) => p.text); assert.deepEqual(texts, ["Hello", "Additional context"]); }); test("OpenAI -> Gemini does not emit a tool-result(user) turn adjacent to a user turn", () => { // Agentic history: user -> assistant(tool_call) -> tool(result) -> user. // The assistant block pushes model + user(toolResponse); the trailing plain // user turn would otherwise produce two adjacent role:"user" contents. const body = { messages: [ { role: "user", content: "List files" }, { role: "assistant", content: null, tool_calls: [ { id: "call_1", type: "function", function: { name: "ls", arguments: '{"path":"."}' }, }, ], }, { role: "tool", tool_call_id: "call_1", content: "a.ts\nb.ts" }, { role: "user", content: "Now read a.ts" }, ], }; const result = openaiToGeminiRequest("gemini-2.5-pro", body, false) as GeminiReq; assertNoConsecutiveSameRole(result.contents, "tool-result-then-user"); // Roles must strictly alternate: user, model, user (toolResp + "Now read a.ts" merged). assert.deepEqual( result.contents.map((c) => c.role), ["user", "model", "user"] ); }); test("OpenAI -> Gemini keeps a normally alternating conversation unchanged", () => { const body = { messages: [ { role: "user", content: "Hi" }, { role: "assistant", content: "Hello there" }, { role: "user", content: "How are you?" }, ], }; const result = openaiToGeminiRequest("gemini-2.5-pro", body, false) as GeminiReq; assertNoConsecutiveSameRole(result.contents, "alternating"); assert.deepEqual( result.contents.map((c) => c.role), ["user", "model", "user"] ); }); test("mergeConsecutiveSameRoleContents merges adjacent same-role entries without mutating the input", () => { const userPartsA = [{ text: "Hello" }]; const userPartsB = [{ text: "Additional context" }]; const input: GeminiContent[] = [ { role: "user", parts: userPartsA }, { role: "user", parts: userPartsB }, { role: "model", parts: [{ text: "Hi" }] }, ]; const merged = mergeConsecutiveSameRoleContents(input) as GeminiContent[]; // Merged output: one user block carrying both parts, then the model block. assert.deepEqual( merged.map((c) => c.role), ["user", "model"] ); assert.deepEqual( merged[0].parts.map((p) => p.text), ["Hello", "Additional context"] ); // The caller's input objects and their parts arrays must be untouched. assert.equal(input.length, 3, "input array must not be mutated"); assert.equal(userPartsA.length, 1, "first input parts array must not be mutated"); assert.equal(userPartsB.length, 1, "second input parts array must not be mutated"); assert.notStrictEqual(merged[0].parts, userPartsA, "merged parts must be a fresh array"); });