Skip to content

fix(gemini): never send a functionCall-opening turn with no preceding user turn - #13573

Merged
diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
hartmark:fix/gemini-leading-functioncall-turn
Sep 16, 2026
Merged

diegosouzapw merged 2 commits into
diegosouzapw:release/v3.8.51from
hartmark:fix/gemini-leading-functioncall-turn

Conversation

@hartmark

Copy link
Copy Markdown
Contributor

Problem

Gemini rejects any contents[] that opens with a functionCall-bearing "model" turn:

[400]: Please ensure that function call turn comes immediately after a user turn or after a function response turn.

Observed live on the default combo: a request retried this identical error against every one of 3-5 Gemini variants tried (each one rejects the same corrupted history identically), before the new identical-error circuit breaker (#13338) correctly stopped it fast instead of exhausting all 11 fallback targets over ~13 minutes.

Root cause

mergeConsecutiveSameRoleContents (9router#2191) already guarantees strict user/model alternation for every contents[] index >= 1 (Gemini only ever uses those two roles in contents, and the merge collapses any adjacent same-role pair). That leaves exactly one way to violate Gemini's rule: history that opens with a functionCall-bearing turn, with nothing before it at all.

This is reachable whenever the true leading user turn goes missing from a request's history while a mid-conversation assistant tool-call turn survives — e.g. continuation reconstruction, context compression, or a truncated client history. I was not able to pin down the exact upstream trigger with certainty from static analysis alone (flagged as a known gap in #13338); this fix closes the resulting Gemini-side failure regardless of which upstream path produces it.

Fix

ensureHistoryDoesNotOpenWithFunctionCall (openai-to-gemini/helpers.ts): if contents[0] is a "model" turn containing a functionCall part, prepend a minimal synthetic "user" turn. Applied right after the existing same-role merge in both the OpenAI→Gemini and Claude→Gemini request paths (siblings that share the same helper).

Testing

  • New unit tests in translator-gemini-consecutive-role-2191.test.ts: the helper directly (prepends when needed, leaves valid/empty input alone), plus a full-translator test reproducing the exact reported failure shape (history opening on an assistant tool-call turn) and asserting contents[0].role is always "user".
  • Confirmed the new tests fail without the fix (TypeError: ... is not a function / missing leading-role assertion).
  • Two existing translator-claude-to-gemini.test.ts fixtures intentionally started their messages[] on an assistant tool_use with no leading user turn (unit-testing field mapping in isolation, never exercised against real Gemini) — updated their content-index assertions to account for the now-correct synthetic leading turn; no behavior change to what they're actually testing.
  • Full sibling suite (207 tests across every openai-to-gemini/claude-to-gemini test file) passes.
  • tsc -p tsconfig.typecheck-core.json clean.

🤖 Generated with Claude Code

… user turn

Gemini rejects contents[] that opens with a functionCall-bearing "model"
turn:

  400 INVALID_ARGUMENT "Please ensure that function call turn comes
  immediately after a user turn or after a function response turn."

mergeConsecutiveSameRoleContents (9router#2191) already guarantees strict
user/model alternation for every index >= 1, so the only remaining way to
violate this rule is history that OPENS with such a turn -- observed live
when the true leading user turn is missing from reconstructed/compressed
history while a mid-conversation assistant tool-call turn survives.

ensureHistoryDoesNotOpenWithFunctionCall prepends a minimal synthetic user
turn in that case, applied to both the OpenAI->Gemini and Claude->Gemini
request paths right after the existing same-role merge.

Regression test reproduces the exact reported live failure shape (history
starting on an assistant tool-call turn) through the full translator and
asserts contents[0].role is always "user".
hartmark added a commit to hartmark/OmniRoute that referenced this pull request Sep 13, 2026
…pening turn with no preceding user turn) into dev/omniroute-dev-combined
hartmark added a commit to hartmark/OmniRoute that referenced this pull request Sep 13, 2026
…pening turn with no preceding user turn) into dev/omniroute-dev-combined
hartmark added a commit to hartmark/OmniRoute that referenced this pull request Sep 14, 2026
…pening turn with no preceding user turn) into dev/omniroute-dev-combined
hartmark added a commit to hartmark/OmniRoute that referenced this pull request Sep 14, 2026
…pening turn with no preceding user turn) into dev/omniroute-dev-combined
@diegosouzapw

Copy link
Copy Markdown
Owner

Ran your new tests locally at the PR head — 27/27 pass, including the full-translator
reproduction of the reported failure shape. Clean, well-scoped fix. Only thing missing is a
changelog.d/ fragment — could you add one? Once that's in this looks ready to merge.

@hartmark

Copy link
Copy Markdown
Contributor Author

Added the changelog fragment — changelog.d/fixes/13573-gemini-leading-functioncall-turn.md, verified with check:changelog-integrity. Ready to merge from my side.

hartmark added a commit to hartmark/OmniRoute that referenced this pull request Sep 15, 2026
…pening turn with no preceding user turn) into dev/omniroute-dev-combined
hartmark added a commit to hartmark/OmniRoute that referenced this pull request Sep 15, 2026
…pening turn with no preceding user turn) into dev/omniroute-dev-combined
@diegosouzapw
diegosouzapw merged commit 3df5d50 into diegosouzapw:release/v3.8.51 Sep 16, 2026
11 of 16 checks passed
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
… user turn (diegosouzapw#13573)

Merged. Gemini rejecting a turn that opens with a `functionCall` and no preceding user turn is a contract we have to honour on our side; building the turn correctly is cheaper than reading the upstream 400 that comes back.

Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thank you.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants