Repository navigation
fix(translator): merge consecutive same-role contents for Gemini - #5659
diegosouzapw wants to merge 1 commit into
Conversation
…t from 9router#2191) The OpenAI->Gemini request translator built result.contents without a consecutive-same-role merge pass (unlike the Kiro and Claude request paths). Client history with adjacent user turns — or a tool-result turn (mapped to role:"user") immediately followed by a plain user turn — produced two adjacent role:"user" entries, which Gemini-family endpoints (incl. Antigravity / Vertex) reject with 400 INVALID_ARGUMENT "Request contains consecutive messages with the same role". The empty-parts half was already guarded (if parts.length > 0). Collapse adjacent same-role entries by concatenating their parts right after the message-conversion loop. Regression guard reproduces both the two-user and the tool-result-then-user cases and asserts a normal alternating conversation is unchanged. Reported-by: warelik (decolua/9router#2191)
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Code Review
This pull request introduces a helper function mergeConsecutiveSameRoleContents to merge adjacent messages with the same role when translating OpenAI requests to Gemini format, preventing 400 errors from Gemini APIs. It also adds unit tests to verify this behavior. The review feedback points out that the merge function mutates the input array's elements directly, which can cause unintended side-effects, and suggests a pure alternative using shallow copying.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| function mergeConsecutiveSameRoleContents(contents: GeminiContent[]): GeminiContent[] { | ||
| const merged: GeminiContent[] = []; | ||
| for (const entry of contents) { | ||
| const last = merged[merged.length - 1]; | ||
| if (last && last.role === entry.role) { | ||
| last.parts.push(...entry.parts); | ||
| } else { | ||
| merged.push(entry); | ||
| } | ||
| } | ||
| return merged; | ||
| } |
There was a problem hiding this comment.
The mergeConsecutiveSameRoleContents function mutates the elements of the input contents array directly via last.parts.push(...). This introduces side-effects if the input array or its elements are referenced or reused elsewhere (e.g., in other parts of the application, caches, or tests).
To ensure the function is pure and free of side-effects, consider returning new objects and shallow copying the parts array instead of mutating the existing ones.
function mergeConsecutiveSameRoleContents(contents: GeminiContent[]): GeminiContent[] {
const merged: GeminiContent[] = [];
for (const entry of contents) {
const last = merged[merged.length - 1];
if (last && last.role === entry.role) {
merged[merged.length - 1] = {
...last,
parts: [...last.parts, ...entry.parts],
};
} else {
merged.push(entry);
}
}
return merged;
}
Summary
contents[]entries before sending, so Gemini-family endpoints (incl. Antigravity / Vertex) stop rejecting valid requests.Root cause
open-sse/translator/request/openai-to-gemini.ts::openaiToGeminiBase()builtresult.contentswith no consecutive-same-role merge pass — unlike the Kiro (openai-to-kiro.ts) and Claude (claudeHelper.ts) request paths which already merge/alternate. Client history with two adjacentuserturns — or a tool-result turn (mapped torole:"user") immediately followed by a plain user turn — produced two adjacentrole:"user"entries. Gemini rejects that with400 INVALID_ARGUMENT "Request contains consecutive messages with the same role". (The empty-partshalf was already guarded byif (parts.length > 0).)Fix
Add a small
mergeConsecutiveSameRoleContents()pass that collapses adjacent same-role entries by concatenating theirparts, applied right after the message-conversion loop. Normal alternating conversations are unchanged.Attribution
Thanks to @warelik for the original report.
Test plan
tests/unit/translator-gemini-consecutive-role-2191.test.ts(red → green): two-user merge, tool-result-then-user alternation, alternating-unchanged.eslint(changed files) cleannpm run typecheck:corecleannpm run check:cyclescleantypecheck:noimplicit:core— only 2 pre-existing base-reds inopen-sse/services/combo.ts(untouched by this PR)