Skip to content

fix: keep ids ascending across clock steps and never end Gemini requests on a model turn - #55515

Open
piercypixel wants to merge 1 commit into
anomalyco:v1from
piercypixel:fix/monotonic-ids-gemini-model-turn
Open

piercypixel wants to merge 1 commit into
anomalyco:v1from
piercypixel:fix/monotonic-ids-gemini-model-turn

Conversation

@piercypixel

Copy link
Copy Markdown

Issue for this PR

Closes #55514 (same user-visible error as #45359 / #47034; complements #55382)

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

Gemini rejects any request whose contents end on a model turn. #55514 shows how v1 produces that shape without any prefill: Identifier.create mints IDs from Date.now() and only guards against two IDs in the same millisecond, parts are replayed in ID order, so when the wall clock steps backward inside a step (NTP correction, VM resume, WSL2) the tool part sorts before its own step-start. convertToModelMessages then opens a new block at that step-start and the step's reasoning ends up as a trailing assistant message.

Two changes:

  1. Identifier.create keeps a last floor for wall-clock IDs and bumps any value that would not sort after it. Same-millisecond IDs come out exactly as before, explicit timestamps are minted as given and never touch the floor (truncation.test.ts depends on that), and descending IDs stay descending because the bump happens before the bit inversion. This stops new steps from being stored out of order.
  2. ProviderTransform.message, for @ai-sdk/google and @ai-sdk/google-vertex only, drops a trailing reasoning-only assistant message (thought text is not something the model can continue from; the thought signature lives on the tool call, which keeps its result) and then primes a user "Continue." turn behind any other assistant tail, the same move as fix(provider): keep Gemini requests from ending with a model turn #55382 and the existing synthetic "Done." turn in normalizeMessages. Outbound only, so sessions that already contain a misordered step, and the compaction-continuation case from Gemini 400 "Requests ending with a model turn are not supported" when a step is stored out of order: Identifier.create is not monotonic across backward clock steps #55514, recover without rewriting stored history.

Not touched: message order still uses time_created, so a backward step can still swap two whole messages; each still ends on a tool result, which Gemini accepts.

How did you verify your code works?

From packages/opencode/:

  • bun test ./test/id/id.test.ts: on pristine v1 the two backward-step cases fail; with the fix all 4 pass.
  • bun test ./test/id/id.test.ts ./test/provider/transform.test.ts ./test/tool/truncation.test.ts: 614 pass, 0 fail.
  • bun run typecheck: 0 errors before and after. Root bun turbo typecheck ran in the pre-push hook and passed. Prettier clean on the four files.
  • Live replay against gemini-3.8-flash through @ai-sdk/google with the exact shape from the failing session: [user, assistant(tool-call), tool(result)] 200; plus trailing assistant(reasoning) 400 "Requests ending with a model turn are not supported."; same input through ProviderTransform.message 200. A text-only assistant tail: 400 unguarded, 200 with the primed turn.

Screenshots / recordings

Not a UI change.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

…sts on a model turn

Identifier.create derived IDs from Date.now() with no guard against the wall
clock stepping backward (NTP corrections, VM or WSL2 resume). Parts are
replayed in ID order, so a tool part minted after a backward step sorted
before its own step-start; convertToModelMessages then split that step's
reasoning into a trailing assistant block and Gemini rejected the request
with 400 "Requests ending with a model turn are not supported."

- Identifier.create: keep wall-clock IDs strictly ascending through a
  backward step. Explicit timestamps are minted as given.
- ProviderTransform.message: for @ai-sdk/google and @ai-sdk/google-vertex,
  drop a trailing reasoning-only assistant message and prime a user turn
  behind any other assistant tail. Outbound only, so sessions that already
  hold a misordered step and continued turns recover without touching the
  stored transcript.
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

This branch has not been deployed

No deployments
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.

1 participant