Skip to content

fix(drift): classify the new Gemini families and resolve Live AUDIO modality from the provider - #392

Merged
contextablemark merged 2 commits into
mainfrom
fix/gemini-model-families
Aug 30, 2026
Merged

fix(drift): classify the new Gemini families and resolve Live AUDIO modality from the provider#392
contextablemark merged 2 commits into
mainfrom
fix/gemini-model-families

Conversation

@jpr5

@jpr5 jpr5 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes the two Gemini legs of the Drift Tests workflow, red on every run since 2026-08-27 (latest: run 33296393200). Supersedes drift-bot PRs #388 and #389 — see below.

What was wrong

Google shipped three model families the registry had never seen, and one of them also broke the Gemini Live probe.

Family GA Declared capability Classification
gemini-3.5-transcribe 2026-08-26 input Audio (up to 1 hour) → output Text, Word annotations EXCLUDE
gemini-3.5-transcribe-live 2026-08-26 declares bidiGenerateContent; emits TEXT, refuses AUDIO EXCLUDE
gemini-omni-1.1-flash 2026-08-27 input Video with audio → output Video with audio EXCLUDE

None of the three emits a chat completion, so none can be text-generation drift.

Classified on capability, never on the id string. For gemini-omni-1.1-flash the substring argues the opposite: "omni" names OpenAI's text-capable line, so a name rule would have INCLUDED a video-generation model. The deciding evidence is the provider's declared input/output signature — the same instrument used for gemini-3.7-flash-video-understanding-eap, and the one whose absence caused the historical native-audio misclassification.

gemini-3.5-transcribe-live carries two independently observed capability facts:

  1. It declares bidiGenerateContent — the Live leg's fetchLiveCapableModels filters on that method alone with no name heuristic, and it selected this id.
  2. It cannot emit AUDIO — Google refused the resulting session out of band with code=1007 reason="The requested combination of response modalities (AUDIO) is not supported by the model. models/gemini-3.5-transcribe-live".

So it is a bidi Live surface that emits TEXT: a streaming transcriber, not a native-audio model.

The Live leg's bug (cause DETERMINED, not assumed)

ws-gemini-live.drift.ts documented the premise "every model exposing bidiGenerateContent is a native-audio model that supports only AUDIO". Fact 2 above falsifies it. The listing offered the transcriber first, the probe asked for AUDIO, and the provider refused — so the leg reported provider-refusal noise instead of drift on every run.

The fix is neither a name filter nor a hardcoded pin — both are mistakes this leg has already made. Response modality is a per-model capability /models does not express, so the only authority is the provider, and now the leg asks: resolveLiveModelCandidates keeps the whole ordered candidate list and driveGeminiLiveAudio walks it, treating a 1007 modality refusal as disqualify this candidate, advance to the next. A listing whose Live models all refuse AUDIO is an honest skip — there is no AUDIO turn to grade — not drift. Every other failure propagates unchanged (negative-control test included).

Red-green proof

Leg 1 — classification, through the real enumerate→normalize→subtract pipeline

npx vitest run --config vitest.config.drift.ts src/__tests__/drift/models.drift.ts -t "Gemini: every live family is classified"

RED (EXIT=1) — byte-matches the CI critical list:

 × full live /models wave is fully classified (2026-07-16 drift) > Gemini: every live family is classified (zero unclassified)
AssertionError: expected [ 'gemini-3.5-transcribe', …(2) ] to deeply equal []
+   "gemini-3.5-transcribe",
+   "gemini-3.5-transcribe-live",
+   "gemini-omni-1.1-flash",
      Tests  1 failed | 24 skipped (25)

GREEN (EXIT=0):

 ✓ src/__tests__/drift/models.drift.ts (25 tests | 24 skipped)
      Tests  1 passed | 24 skipped (25)

Same pair on the behavioural specs (text-drift.test.ts): RED Tests 3 failed | 6 passed (9) (expected false to be true on isClassifiedFamily for each family) → GREEN Tests 56 passed (56) across text-drift + logic-pin + model-registry.

Leg 2 — Live modality, through the REAL probe, transport and framing

npx vitest run src/__tests__/drift/ws-gemini-live-modality.test.ts -t "a bidiGenerateContent model that refuses AUDIO"

Driven against this repo's existing local TLS fake provider, now enforcing the real per-model rule (the rule lives in the "provider", not in the expectations, so the test cannot satisfy itself). RED produced by reverting the advance to the pre-fix behaviour:

RED (EXIT=1) — reproduces the CI failure verbatim:

 ✓ RED: driving the first candidate directly is refused with the observed 1007
 × GREEN: driveGeminiLiveAudio advances past the refusal and completes an AUDIO turn
   → WebSocket closed by server during waitUntil: code=1007 reason="The requested combination of
     response modalities (AUDIO) is not supported by the model. models/gemini-3.5-transcribe-live".
     Collected 0 messages: [] step=setupComplete model=models/gemini-3.5-transcribe-live
 × returns null — an honest skip — when EVERY candidate refuses AUDIO
   → WebSocket closed by server during waitUntil: code=1007 reason="The requested combination of
     response modalities (AUDIO) is not supported by the model. models/gemini-3.5-transcribe-live".
      Tests  2 failed | 3 passed | 18 skipped (23)

GREEN (EXIT=0):

      Tests  5 passed | 18 skipped (23)

The green case asserts the advance happened on the wire (the fake provider received both setups, in listing order), that a real audio turn followed (audioPartCount > 0, textPartCount === 0, one turnComplete), and that the refusal was surfaced rather than swallowed.

⚠️ What is NOT proven here

No provider credentials exist in the environment this was developed inGEMINI_API_KEY, GOOGLE_API_KEY and GOOGLE_GENERATIVE_AI_API_KEY are all absent (presence checked only; no value was read). So neither the live Gemini /models leg nor the live Gemini Live WebSocket leg was exercised against Google.

  • The classification proof is complete regardless: it drives the production unclassifiedFamilies pipeline over the exact ids the live canary reported, and that pipeline is the whole of the failing check.
  • The Live modality proof is fixture-driven, against a local fake provider enforcing Google's rule with refusal bytes copied verbatim from the drift-report artifact. This is the same standard the previous, mirror-image refusal was landed under (see the head of ws-gemini-live-modality.test.ts). That the live endpoint accepts the advance can only be confirmed by a scheduled run with a real GOOGLE_API_KEY.

GOOGLE_API_KEY in CI is what closes that gap; the nightly Drift Tests run is the confirmation.

Relationship to #388 / #389

Both are drift-bot needs-human-decision PRs holding Decision: pending note stubs — #389 is a strict superset of #388 (it adds gemini-omni-1.1-flash). Neither carries a code change, and as their own bodies explain, merging them applies nothing: includeFamilies/excludeFamilies are checksum-pinned and drift-sync's gate-1 allowlist cannot touch logic-pin.test.ts, so a follow-up run reports gate-failed, not ok-applied.

This PR is the by-hand procedure those bodies prescribe, in one reviewed change: the classification, both re-pins, the resolved notes (same filenames, now Status: RESOLVED with the capability rationale — an EXCLUDE has no automated path and must be recorded in prose), and the offline /models wave. #388 and #389 should be closed when this merges.

Test status

Gate Result
vitest run EXIT=0 — 178 files, 5347 passed
vitest run --config vitest.config.drift.ts EXIT=0 — 134 passed, 57 skipped (live legs skip without keys)
eslint . EXIT=0
prettier --check . EXIT=0
tsdown build EXIT=0

Note for reviewers: the repo's tsconfig.json excludes src/__tests__, so tsc --noEmit -p tsconfig.json typechecks none of the files this PR touches. The changed test files were typechecked explicitly under the same compiler options; that pass caught a real error (WSResult used across a module boundary without being exported) that the project typecheck reported clean.

Out of scope

The AG-UI leg of the same failing run is a separate concern owned elsewhere and is untouched here.

CI on this PR — read the gating, not the job names

Two things this PR's own checks CANNOT tell you:

  1. The drift job is skipping on pull_request. It is gated to the scheduled run, so the Gemini /models and Gemini Live legs this PR fixes do not execute here. A green PR runs a subset. Confirmation is the next scheduled Drift Tests run on main (or a manual workflow_dispatch), with GOOGLE_API_KEY present.
  2. agui-schema-drift fails, and it is PRE-EXISTING. The same job is failure on the nightly main run 33296393200 (26 AGUIEventType criticals — SUBAGENT_STARTED and friends). This PR touches no AG-UI file; that concern is owned separately.

Every other check is green: static quality (eslint, prettier, commitlint, exports), the full Node test matrix (20/22/24), the Python matrix, Sentinel, and preview.

jpr5 added 2 commits August 30, 2026 08:43
…eclared capability

The Gemini /models canary has been red since 2026-08-27 on three unclassified
families. Classified all three EXCLUDE from the provider's declared
capabilities, never from the id string:

  gemini-3.5-transcribe       input Audio -> output "Text, Word annotations"
                              (GA 2026-08-26, non-streaming speech-to-text)
  gemini-3.5-transcribe-live  the Live-API half of the same model. Declares
                              bidiGenerateContent — proven by the Live leg's
                              discovery, which filters on that method ALONE and
                              selected it — and cannot emit AUDIO, proven by
                              Google refusing the session with code=1007 "The
                              requested combination of response modalities
                              (AUDIO) is not supported by the model".
  gemini-omni-1.1-flash       input "Video with audio" -> output "Video with
                              audio" (GA 2026-08-27, conversational video
                              generation and editing)

None emits a chat completion, so none can be text-generation drift. The
substring would have argued the OPPOSITE for the last one: "omni" names
OpenAI's text-capable line, so a name rule would have INCLUDED a video model.

Includes the deliberate excludeFamilies.gemini re-pin, the resolved
drift-proposals notes (superseding bot PRs #388/#389), the three ids in the
offline live /models wave, and behavioural coverage through the real
enumerate->normalize->subtract pipeline — so dropping an entry reddens on what
the classification MEANS, not only on a moved checksum.
… not the listing

The Gemini Live leg rested on a premise Google falsified: "every model exposing
bidiGenerateContent is a native-audio model that supports only AUDIO". On
2026-08-26 Google shipped gemini-3.5-transcribe-live, a streaming
speech-to-text model that declares bidiGenerateContent and emits only TEXT.
Discovery filters on that declared method alone — correctly, since the model
NAME carries no capability signal — so the listing handed it to the probe, the
probe asked for AUDIO, and the provider refused:

  code=1007 reason="The requested combination of response modalities (AUDIO) is
  not supported by the model. models/gemini-3.5-transcribe-live"

reddening the whole leg on every run from 2026-08-27 (run 33296393200).

The fix is neither a name filter nor a hardcoded pin — both are mistakes this
leg has already made. Response modality is a per-model capability the /models
listing does not express, so the only authority on it is the provider, and now
the leg ASKS: resolveLiveModelCandidates keeps the whole ordered candidate list
and driveGeminiLiveAudio walks it, treating a 1007 modality refusal as
"disqualify this candidate, advance to the next". A listing whose Live models
ALL refuse AUDIO is an honest skip — there is no AUDIO turn to grade — not
drift. Every other failure (handshake infra status, mute provider, timeout)
propagates unchanged, with a negative-control test pinning that.

Proven against the existing local TLS fake provider, which now enforces the
real PER-MODEL rule, driving the REAL probe through the REAL transport and
framing: red on the verbatim 1007 above, green on the advance to a model that
serves AUDIO. NOT proven here: the live endpoint's behaviour, which needs a
real GOOGLE_API_KEY.
@pkg-pr-new

pkg-pr-new Bot commented Aug 30, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@copilotkit/aimock@392

commit: 0b691a0

@contextablemark contextablemark left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the model-family classifications, candidate-resolution path, failure propagation, regression coverage, and the live PR drift artifacts. No blocking findings.

@contextablemark
contextablemark merged commit f28b6f2 into main Aug 30, 2026
27 of 28 checks passed
@contextablemark
contextablemark deleted the fix/gemini-model-families branch August 30, 2026 16:20
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