diff --git a/devlog/_plan/260903_gemini_38_rollout/000_plan.md b/devlog/_plan/260903_gemini_38_rollout/000_plan.md new file mode 100644 index 0000000000..939854b3a7 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/000_plan.md @@ -0,0 +1,72 @@ +# Gemini 3.8 Flash rollout plan + +- Date: 2026-09-03 +- Session: `01a062e6-43d4-7ad2-8236-c75a8fb66a12` +- Work class: C3 — provider catalog, CCA wire routing, persisted config surface, usage pricing, generated metadata, docs and tests move together. +- Status: P (wp0, docs-only roadmap cycle). + +## Loop spec + +- Archetype: satisfy-spec integration. +- Trigger: Google shipped Gemini 3.8 Flash on 2026-09-02, and authenticated Antigravity discovery already returns three 3.8 wire ids ranked FIRST in the Recommended sort. +- Goal: make Gemini 3.8 Flash the selectable, correctly tiered Antigravity Flash model, and carry the same spec to every other surface that already names 3.6/3.7 — without inventing anything the vendor has not published. +- Non-goals: Vertex routing, OrcaRouter/OpenRouter seeding, widening request transport beyond `text`+`image`, hand-editing generated metadata, deleting historical price rows or usage attribution, any release or publish. +- Verifier: focused `bun test ` runs on the touched subsystems plus `bun run typecheck`. **The repository-wide local suite is forbidden by the user** ("로컬스위트는 절대 돌리지 말고"); exact-head GitHub CI is the authoritative full gate. +- Stop condition: 3.8 is picker-visible with a working low/medium/high ladder, every inventoried 3.6/3.7 surface is updated or carries a recorded reason not to be, focused tests and typecheck pass, CI is green on the exact head SHA, and the PR is merged into `dev` with ancestry proof. +- Memory artifact: this unit folder. +- Expected terminal outcomes: `DONE`; `BLOCKED` if CI or branch protection refuses for a reason outside this change; `NEEDS_HUMAN` if a pricing claim turns out unprovable. +- Escalation: each A gate dispatches one independent read-only reviewer on `gpt-5.6-sol` at high reasoning effort. After two failed reviewer correction loops on the same packet, the main session stops and reports. + +## The decision this plan turns on + +The 3.6 to 3.7 rollout (`devlog/_fin/260814_overnight_triage_release/020_gemini_37_flash.md`) was a **replacement**: the maintainer's operational fact was that Google pulls the previous Antigravity Flash model almost immediately, so 3.6 had to be deprecated in the same commit that introduced 3.7. + +**That premise does not hold for this launch, and both halves of the disproof are first-hand:** + +1. Google's own `latest-model` guide says Gemini 3.7 Flash "remains fully supported" and still lists it as Stable (see `001`). +2. A live CCA `:fetchAvailableModels` call on 2026-09-03 returns 3.5, 3.6, 3.7 **and** 3.8 wire ids simultaneously (see `002`). + +So 3.8 lands **additively**: it becomes the default and the recommended Flash row, while 3.7 stays picker-visible and every existing retirement mapping is left exactly where it is. Copying the 3.7 unit's deprecation section would delete a model the backend is still serving. + +## The second decision: wire shape + +3.7 expresses its tiers as `thinkingLevel` against ONE wire id (`gemini-3.7-flash-tiered`). 3.8 does not: CCA publishes three suffixed wire ids and no `-tiered` row. That makes 3.8 structurally a **3.6-shaped** model, and it must be registered through `ANTIGRAVITY_EFFORT_WIRE_MAP` (rule 2/3), never through `ANTIGRAVITY_THINKING_LEVEL_MODELS` (rule 1b). Registering it the 3.7 way would send `thinkingLevel` against a nonexistent `gemini-3.8-flash-tiered` wire id. + +## Work-phase map (dependency-ordered, PHASE-SPLIT-01) + +| Phase | Doc | Consumes | Delivers | +|---|---|---|---| +| wp0 | this folder | — | research + diff-level roadmap | +| wp1 | `010_wp1_antigravity_core.md` | wp0 | `antigravity-models.ts` catalog/ladder/routing + registry default | +| wp2 | `020_wp2_metadata_pricing.md` | wp1 | expected-prices rows, metadata source + regen | +| wp3 | `030_wp3_peripheral_surfaces.md` | wp2 | direct Google seed, free-directory, Cursor seed, sidecar default, docs | +| wp4 | `040_wp4_delivery.md` | wp3 | branch, `--no-verify` push, PR, exact-head CI, merge | + +wp1 is first because every later surface keys off the picker id and ladder it establishes. wp2 depends on wp1 because the price overlay is keyed by the picker id and the suffix wire ids wp1 introduces. wp3 is last among the code phases because it is the set of surfaces that merely *reference* the model rather than define it. + +## Scope + +### IN + +- `src/providers/antigravity-models.ts`, `src/providers/registry.ts` +- `src/usage/expected-prices.ts`, `scripts/model-metadata.source.json` (plus `bun run generate:model-metadata`) +- `src/providers/free-directory.ts`, `src/adapters/cursor/effort-map.ts`, `src/adapters/cursor/catalog.ts`, `src/web-search/index.ts` +- `docs-site/` provider and sidecar tables +- focused tests beside the existing Antigravity/catalog/price tests + +### OUT + +- `src/adapters/google.ts` `GEMINI_DIRECT_WIRE_RENAMES`: no `gemini-3.8-flash-tiered` id is proven on any surface, so adding a rename would invent a wire id. Recorded in `030`. +- `src/providers/model-rename-migration.ts`: nothing is retired by this change, so no new rename entry. The existing 3.6/3.5 to 3.7 entries stay, because 3.7 is still live. +- `RETIRED_FLASH_TIERS` and `ANTIGRAVITY_USAGE_BASE_BY_ID`: unchanged for the same reason. +- Vertex (`google-vertex` `defaultModel` stays frozen), OrcaRouter, OpenRouter, GitHub Copilot. + +## Accept criteria (goalplan c-1 through c-7) + +1. `gemini-3.8-flash` is one collapsed picker row, not three suffix rows. +2. Each of `low`/`medium`/`high` resolves to its own `gemini-3.8-flash-{tier}` wire id. +3. `gemini-3.7-flash` remains picker-visible and its `-tiered` routing is untouched. +4. Retired 3.6/3.5 ids still route to 3.7 with their recorded tier and stay picker-invisible. +5. Historical usage rows carrying 3.6/3.7 ids still aggregate under their own base. +6. `bun run typecheck` exits 0; only focused test files are run locally. +7. CI green on the exact head SHA and the PR merged into `dev`. diff --git a/devlog/_plan/260903_gemini_38_rollout/001_vendor_claim_ledger.md b/devlog/_plan/260903_gemini_38_rollout/001_vendor_claim_ledger.md new file mode 100644 index 0000000000..04c0983de4 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/001_vendor_claim_ledger.md @@ -0,0 +1,54 @@ +# 001 — Gemini 3.8 Flash: vendor claim ledger + +Research snapshot 2026-09-03 (KST), collected by an independent read-only research lane on +`gpt-5.6-sol` at high reasoning effort. Every row was verified by opening the linked official +page. Unprovable fields say `NOT PROVEN` rather than borrowing 3.7's value. + +| Claim | Value | Source | Page date | +|-------|-------|--------|-----------| +| Canonical Developer API id | `gemini-3.8-flash` | ai.google.dev/gemini-api/docs/models/gemini-3.8-flash | 2026-09-02 | +| Published aliases | stable id only; `-preview`/dated/`-latest` NOT PROVEN | same + docs/models | 2026-09-02 | +| Release date | 2026-09-02 | deepmind.google model card; docs.cloud.google.com | 2026-09-02 | +| Availability | GA, production-ready (not Preview) | latest-model guide | 2026-09-02 | +| Context window | 1,048,576 input tokens | model page | 2026-09-02 | +| Max output | 65,536 tokens | model page | 2026-09-02 | +| Input price | $0.75 / 1M through 2026-12-31, $1.50 / 1M from 2027-01-01 | Developer API pricing | 2026-09-02 | +| Output price (incl. thinking) | $3.75 / 1M through 2026-12-31, $7.50 / 1M from 2027-01-01 | Developer API pricing | 2026-09-02 | +| Separate thinking price | none — thinking billed as output | pricing | 2026-09-02 | +| Cache read | $0.075 / 1M through 2026-12-31, then $0.15 | pricing | 2026-09-02 | +| Cache storage | $0.50 / 1M tokens/hour through 2026-12-31, then $1.00 | pricing | 2026-09-02 | +| Batch / Flex | half of standard input and output | pricing | 2026-09-02 | +| Priority | $1.35 in / $6.75 out per 1M through 2026-12-31 | pricing | 2026-09-02 | +| Thinking parameter | `generation_config.thinking_level` (replaces `thinking_budget`) | latest-model | 2026-09-02 | +| Thinking values | `low` / `medium` / `high`, default `medium` | latest-model; Cloud guide | 2026-09-02 | +| `minimal` | unsupported — setting it returns a validation error | model page; Cloud guide | 2026-09-02 | +| Inputs | text, image, video, audio, PDF | model page | 2026-09-02 | +| Outputs | text only (no image/audio generation, no Live API) | model page | 2026-09-02 | +| Knowledge cutoff | March 2026 (some domains still January 2025) | DeepMind model card | 2026-09-02 | +| Antigravity default | proven for the Managed Agents agent and the Antigravity SDK; the desktop/CCA backend default is NOT PROVEN | latest-model | 2026-09-02 | +| Vertex / Agent Platform id | `gemini-3.8-flash`, `publishers/google/models/gemini-3.8-flash:generateContent` | Cloud developer guide | 2026-09-02 | +| **3.7 Flash deprecated?** | **No — Google says 3.7 Flash "remains fully supported" and still lists it Stable** | latest-model; models catalog | 2026-09-02 | +| CCA billing equivalence | NOT PROVEN — the listed prices are Developer API prices | pricing | 2026-09-02 | + +## Other providers OpenCodex integrates + +| Provider | 3.8 model id published? | Source | +|---|---|---| +| OpenRouter | YES — `google/gemini-3.8-flash` | openrouter.ai model page | +| Cursor | NO — models page and changelog still stop at 3.7 Flash | cursor.com/docs/models-and-pricing; /changelog | +| GitHub Copilot | NO — supported-model table lists 3.5/3.6/3.7 only | docs.github.com Copilot supported models | + +## Unprovable fields + +- 3.8-specific preview, dated, or `-latest` aliases. +- A standalone `blog.google` launch post (the date rests on the DeepMind card and the Cloud record). +- Cloud Code Assist billing equivalence to Developer API list prices. +- Cursor and GitHub Copilot 3.8 model ids. + +## Why the pricing row cannot be `verified` for Antigravity + +OpenCodex routes this model through CCA, and the pricing page distinguishes Developer API, +Enterprise Agent Platform, and managed Antigravity-agent pricing without proving equivalence +for the Cloud Code Assist backend. This is exactly the provenance caveat the 3.7 unit already +recorded, and `src/usage/expected-prices.ts` already has the right enum member for it: +`verified-derived`. Only a `google`-provider row may claim `verified`. diff --git a/devlog/_plan/260903_gemini_38_rollout/002_live_cca_probe.md b/devlog/_plan/260903_gemini_38_rollout/002_live_cca_probe.md new file mode 100644 index 0000000000..4a74fa3d98 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/002_live_cca_probe.md @@ -0,0 +1,62 @@ +# 002 — Live Cloud Code Assist evidence + +Probes run 2026-09-03 from this worktree against `daily-cloudcode-pa.googleapis.com` using the +already-stored local Antigravity OAuth credential and the repository's own +`ANTIGRAVITY_REQUEST_UA`. No token, refresh token, or project id was printed or persisted; the +probe scripts were removed after the run. + +## `v1internal:fetchAvailableModels` — the 3.8 rows + +| Wire id | displayName | maxTokens | maxOutputTokens | supportsThinking | thinkingBudget | minThinkingBudget | supportsImages | supportsVideo | +|---|---|---:|---:|---|---:|---:|---|---| +| `gemini-3.8-flash-low` | Gemini 3.8 Flash (Low) | 1048576 | 65536 | true | 1000 | 32 | true | true | +| `gemini-3.8-flash-medium` | Gemini 3.8 Flash (Medium) | 1048576 | 65536 | true | 4000 | 32 | true | true | +| `gemini-3.8-flash-high` | Gemini 3.8 Flash (High) | 1048576 | 65536 | true | -1 | 32 | true | true | + +**There is no `gemini-3.8-flash-tiered` row.** The payload does contain +`gemini-3.7-flash-tiered` and `gemini-3.6-flash-tiered`, so its absence for 3.8 is a fact about +this generation, not a gap in the probe. + +## `agentModelSorts` Recommended order (verbatim) + +``` +gemini-3.8-flash-high, gemini-3.8-flash-medium, gemini-3.8-flash-low, +gemini-3.7-flash-high, gemini-3.7-flash-medium, gemini-3.7-flash-low, +gemini-3.6-flash-high, gemini-3.6-flash-medium, gemini-3.6-flash-low, +gemini-pro-agent, gemini-3.1-pro-low, claude-sonnet-4-6, +claude-opus-4-6-thinking, gpt-oss-120b-medium +``` + +Two things follow. 3.8 outranks every other Flash generation, so it is the natural default. And +**3.7 and 3.6 are both still being served** — the "previous Flash is pulled immediately" +premise behind the 3.6 deprecation does not apply here. + +## `v1internal:generateContent` — all three tiers accept inference + +Minimal one-line prompts with `generationConfig.thinkingConfig.thinkingLevel` set to the +matching tier: + +| Wire model | HTTP | Output marker | +|---|---:|---| +| `gemini-3.8-flash-low` | 200 | `OK-LOW` | +| `gemini-3.8-flash-medium` | 200 | `OK-MEDIUM` | +| `gemini-3.8-flash-high` | 200 | `OK-HIGH` | + +This is the same pre-exposure proof the 3.6 rollout recorded: all three ids accept inference +before any catalog change ships, so the ladder in `010` cannot advertise a rung the backend +would reject. + +## What the running proxy does with them today + +`ocx models live --provider google-antigravity` currently publishes the three 3.8 ids as +**separate uncollapsed rows with `reasoningEfforts: []`** — the same broken shape #1897 +described. Discovery finds them, and no static rule knows they are one model, so they arrive as +three effortless picker entries. That is the defect wp1 closes. + +## Security boundary for these probes + +- Assets: local Antigravity OAuth access token and discovered project id. +- Trust boundary: local read of the existing credential store, then HTTPS to the fixed + registry-owned base URL. Model text cannot choose the destination, headers, or credential. +- Controls: nothing credential-bearing printed or written; probe files deleted after the run. +- Blast radius: three minimal quota-consuming inference calls. No configuration mutated. diff --git a/devlog/_plan/260903_gemini_38_rollout/003_audit_round1_synthesis.md b/devlog/_plan/260903_gemini_38_rollout/003_audit_round1_synthesis.md new file mode 100644 index 0000000000..ff1003789e --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/003_audit_round1_synthesis.md @@ -0,0 +1,136 @@ +# 003 — A-gate round 1: reviewer verdict and synthesis + +Reviewer: independent read-only lane on `gpt-5.6-sol`, high reasoning effort, anchored at +`529639a57`. Verdict: **FAIL**, blockers 1, 2, 3. + +Two of the three blockers were settled by running probes rather than by argument. Both +reviewer claims survived contact with the backend. + +## Blocker 1 (High) — static and discovered resolution return different shapes + +**Accepted, with a narrower fix than proposed.** + +The reviewer is right that `discoveredAntigravityEffortWireModelId` returns before +`hasOwnEffortLadder` is consulted, so once a live ladder is registered the resolver returns +`{ wireModelId }` with **no** `thinkingLevel`, while static rule 2/3 returns +`{ wireModelId, thinkingLevel }`. Same model, two request bodies. + +Probe (2026-09-03, CCA `:generateContent`) settles which is canonical: + +| Case | HTTP | Result | +|---|---:|---| +| `gemini-3.8-flash-medium`, no `thinkingConfig` | 200 | `OK` | +| `gemini-3.8-flash-low` + `thinkingLevel: HIGH` | 200 | `OK` | + +A suffixed wire id needs no `thinkingLevel`, and CCA silently accepts a **contradictory** +pairing rather than rejecting it — which is worse than an error, because the tier that +actually ran is unknowable from the response. So the suffix must be the sole carrier. + +**Fix (amends `010`):** rule 2/3 omits `thinkingLevel` when the resolved wire id already +encodes the tier. Scope it with an explicit set rather than a regex over all models: + +```ts +/** + * Base models whose every effort maps to a wire id that ALREADY encodes the tier. + * + * For these, sending thinkingLevel alongside the suffix states the effort twice, and CCA + * accepts a contradictory pair (probe: `-low` wire + HIGH level returns 200), so a mismatch + * would run at an unknown tier instead of failing loudly. It also makes static resolution + * byte-identical to the discovery path, which never emits thinkingLevel. + * + * gemini-3.1-pro is deliberately absent: its `high` rung is `gemini-pro-agent`, which carries + * no tier suffix, so there the level is the only thing naming the effort. + */ +const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set(["gemini-3.8-flash"]); +``` + +and in rule 2/3: + +```ts +if (effort && effort in effortMap) { + const wireModelId = effortMap[effort]!; + return ANTIGRAVITY_SUFFIX_TIER_MODELS.has(modelId) + ? { wireModelId } + : { wireModelId, thinkingLevel: effort }; +} +``` + +`gemini-3.1-pro` behavior is unchanged — deliberately, since altering it is outside this unit. + +Required test (activation scenario): `parse -> register -> resolve` and `resolve` without +discovery must return the SAME object for explicit `low`/`medium`/`high`, for unset effort, and +for clamped `max`/`xhigh`/`ultra`. That equality assertion is the regression guard; asserting +each path separately is what let the divergence exist. + +The reviewer's sidecar note is covered by the same fix: `src/web-search/gemini-executor.ts:51` +destructures `thinkingLevel` and only sends `thinkingConfig` when present, so once both paths +omit it the sidecar body stops depending on whether discovery has run. + +## Blocker 2 (High) — the Claude SDK identity paragraph guard is 3.7-only + +**Accepted. Reproduced, and it is not theoretical.** + +`src/adapters/google.ts:750` strips `ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH` only when +`parsed.modelId === "gemini-3.7-flash"`. Probes with that exact paragraph in +`systemInstruction`: + +| Case | HTTP | Result | +|---|---:|---| +| `gemini-3.8-flash-medium` + paragraph | 429 | `RESOURCE_EXHAUSTED` | +| `gemini-3.8-flash-high` + paragraph | 429 | `RESOURCE_EXHAUSTED` | +| `gemini-3.7-flash-tiered` + paragraph | 429 | `RESOURCE_EXHAUSTED` (control: known behavior) | +| `gemini-3.8-flash-medium`, paragraph stripped | 200 | `OK` | +| `gemini-3.8-flash-medium` + paragraph again | 429 | `RESOURCE_EXHAUSTED` | + +The strip/restore pair rules out incidental quota exhaustion: the same account, seconds apart, +succeeds without the paragraph and fails with it. A policy rejection surfacing as a quota 429 is +exactly the failure mode the original 3.7 fix documented. + +**This is the highest-value finding of the audit.** Shipping 3.8 as the default without it +would 429 every Claude-Agent-shaped request the moment the default moved, and the error text +would send users hunting a quota problem that does not exist. + +**Fix (amends `010`):** widen the guard from an equality check to the set of CCA Flash models +that reject the paragraph: + +```ts +const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist" + && ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS.has(parsed.modelId); +``` + +with the set holding `gemini-3.7-flash` and `gemini-3.8-flash`, and a comment recording that +membership is probe-established per generation, not assumed. A regression test beside +`tests/google-adapter.test.ts:250` asserts the paragraph is absent from the serialized +`systemInstruction` for both models, and still present for a non-CCA Google request. + +## Blocker 3 (Medium) — stale exact assertions and thin focused-test commands + +**Accepted in full.** These tests assert exact arrays and lengths, so they fail the moment the +catalogs grow: + +| Test | Line | What breaks | +|---|---:|---| +| `tests/google-hardening.test.ts` | 777 | exact `google.models` array | +| `tests/google-models-listing.test.ts` | 360 | exact discovered-id array | +| `tests/provider-registry-parity.test.ts` | 771 | `toHaveLength(6)` on Antigravity models | +| `tests/oauth-provider-reconcile.test.ts` | 142 | `toHaveLength(6)` after reconcile | + +`010`/`020`/`030` are amended to name these edits, and the focused commands now include +`google-hardening`, `google-models-listing`, `google-adapter`, and `usage-cost`. + +## Blockers 4-9 + +| # | Severity | Disposition | +|---|---|---| +| 4 | Medium | Accepted — `020` gains a dedicated test asserting an explicit `gemini-3.7-flash` default SURVIVES reconciliation, separate from the stale-default healing case. | +| 5 | Medium | Accepted — `010` gains the missing consumers: discovery-map completion (L164-166), discovery suppression (L597-601), discovery default selection (L405-410), and the context-window spread/alias derivation (L272-277). | +| 6 | Medium | Accepted — the Gemini free-directory row gets a row-specific `lastVerified: "2026-09-03"`; the shared `LAST_VERIFIED` constant is untouched so unrelated providers keep their real dates. | +| 7 | Low | Accepted — see `004_no_change_inventory.md`. | +| 8 | Low | Accepted — `GEMINI_FLASH_WIRE_ID` is renamed `GEMINI_RETIRED_FLASH_TARGET_WIRE_ID` and the rule-0 comment is corrected to say retired ids route to 3.7, not to "the current generation". | +| 9 | Low | Accepted with a correction to the reviewer's framing. `ANTIGRAVITY_WIRE_MODELS` is indeed consumed nowhere, so the plan's step 2 is cosmetic. Rather than edit dead data or delete a constant unrelated to this unit, `010` drops the step and records the observation as a follow-up. Deleting it is a separate cleanup with its own blast radius. | + +## Round outcome + +Every blocker is folded into the plan as a concrete amendment; none was rebutted on judgment +alone, and the two High findings were confirmed against the live backend. Round 2 re-audits the +amended plan with the same reviewer. diff --git a/devlog/_plan/260903_gemini_38_rollout/004_no_change_inventory.md b/devlog/_plan/260903_gemini_38_rollout/004_no_change_inventory.md new file mode 100644 index 0000000000..9b94d511b2 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/004_no_change_inventory.md @@ -0,0 +1,58 @@ +# 004 — no-change inventory + +Answers audit blocker 7: every remaining `gemini-3.6-flash` / `gemini-3.7-flash` occurrence +that this unit does NOT touch, with the reason. Criterion c-5 requires a recorded reason for +each, not silence. + +## Runtime and metadata + +| Location | Reason | +|---|---| +| `scripts/model-metadata.source.json` Kilo rows (~16356) | Third-party gateway roster captured from Kilo. Adding a 3.8 row would assert Kilo serves it; nothing proves that. | +| same, OpenCode Zen rows (~61353) | Same reason, different gateway. | +| same, Vercel AI Gateway rows (~77014) | Same reason. | +| `src/types/provider.ts:300` | Doc comment illustrating `directGeminiWireRenames` with the 3.7 `-tiered` rename. 3.8 has no `-tiered` id, so replacing the example would document a rename that does not exist. | +| `src/adapters/client-fingerprint.ts:56` | Explanatory prose about UA-gated 404s, not a model list. Reviewer independently confirmed. | +| `src/providers/command-code-efforts.ts:47` | Keyed by Command Code's own live roster, which has no 3.8 row. | +| `src/adapters/google.ts` `GEMINI_DIRECT_WIRE_RENAMES` | Would invent `gemini-3.8-flash-tiered`; the reviewer confirmed no such string exists anywhere in the tree, and CCA does not publish one. | +| `docs-site/.../providers.md:683` | `--retain-models` usage example. Any valid id works; churn without benefit. | +| `tests/google-output-clamp.test.ts` | `maxOutputTokensForGoogleModel` (`src/adapters/google.ts:83-89`) is FAMILY-based: any `gemini` id not matching the `pro` pattern returns 65536. `gemini-3.8-flash` already gets the right ceiling with no table entry, and `001` confirms 65,536 is the documented value. Adding a case would assert the family rule twice. (Round-2 blocker 5.) | + +## Tests using 3.6/3.7 as opaque fixtures + +These assert transport, quota, signature, vision, or listing behavior and merely need *a* +valid Gemini id. Rewriting them to 3.8 would enlarge the diff without testing anything new, +and would weaken coverage of the ids real users still have saved. + +`tests/antigravity-baseurl-override.test.ts:20`, `claude-agent-startup-sync.test.ts:46`, +`cli-headless-parity.test.ts:350`, `command-code-provider.test.ts:515`, +`commandcode-provider.test.ts:74`, `cursor-fast-listing.test.ts:44`, +`cursor-fast-tier.test.ts:43`, `cursor-integration-status.test.ts:89`, +`google-claude-prefill-guard.test.ts:85`, `google-errors.test.ts:13`, +`google-signature-history-roundtrip.test.ts:34`, `google-vertex-thought-signature.test.ts:15`, +`images/gemini-inline.test.ts:252`, `management-provider-validation.test.ts:338`, +`model-visibility-management-api.test.ts:31`, `provider-account-quota.test.ts:435`, +`provider-quota.test.ts:252`, `thought-signature-credential-scope.test.ts:32`, +`vision-backend-union.test.ts:60`. + +## Tests that DO change (behavioral assertions) + +| Test | Why it must change | +|---|---| +| `tests/google-antigravity-wire.test.ts` | Owns the ladder and collapse behavior 3.8 introduces. | +| `tests/gemini-37-flash-migration.test.ts` | Owns retirement semantics; must prove 3.7 is NOT retired by this change. | +| `tests/google-hardening.test.ts:777` | Exact `google.models` array. | +| `tests/google-models-listing.test.ts:360` | Exact discovered-id array. | +| `tests/provider-registry-parity.test.ts:771` | `toHaveLength(6)` on the Antigravity model list. | +| `tests/oauth-provider-reconcile.test.ts:82,142` | Default model and post-reconcile length. | +| `tests/google-adapter.test.ts:250` | Claude SDK paragraph strip guard (audit blocker 2). | +| `tests/gemini-web-search.test.ts:80,146` | Sidecar default model and resolved wire id. | +| `tests/cursor-effort-table.test.ts`, `cursor-catalog.test.ts` | Only if the preemptive Cursor seed is kept. | +| `tests/sidecar-settings-web-search-gate.test.ts:222` | Uses 3.7 as an available management row; changes only if the sidecar default assertion moves. | + +## Sidecar test note + +`tests/gemini-web-search.test.ts:146` currently expects `gemini-3.7-flash-tiered` for a `low` +effort call. After the default moves, the 3.8 equivalent expects `gemini-3.8-flash-low` and +**no** `thinkingConfig` (per `003` blocker 1). That difference is itself the proof the +suffix-tier decision reached the sidecar path. diff --git a/devlog/_plan/260903_gemini_38_rollout/005_audit_round2_synthesis.md b/devlog/_plan/260903_gemini_38_rollout/005_audit_round2_synthesis.md new file mode 100644 index 0000000000..de5079b6d9 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/005_audit_round2_synthesis.md @@ -0,0 +1,124 @@ +# 005 — A-gate round 2: verdict and synthesis + +Same reviewer, re-audit of the amended plan. Verdict **FAIL**, blockers 1 and 2 blocking. +Round 1's nine findings were all confirmed adequately fixed except where noted below; these +two are NEW defects introduced by the round-1 amendments themselves, which is exactly what a +second round is for. + +## Blocker 1 (High) — the suffix-tier fix does not cover clamped efforts + +**Accepted. Verified in code.** + +`ANTIGRAVITY_SUFFIX_TIER_MODELS` equalizes the two paths for `unset`/`low`/`medium`/`high` and +leaves `max`/`xhigh`/`ultra` diverging: + +| Path | `effort = "max"` | Why | +|---|---|---| +| discovered | `gemini-3.8-flash-high` | `resolveAntigravityThinkingLevel` clamps to `high` first (L400-408) | +| static rule 2/3 | `gemini-3.8-flash-medium` | `"max" in effortMap` is false, so it falls to `ANTIGRAVITY_DEFAULT_EFFORT` (L644-645) | + +A user asking for `max` gets `high` or `medium` depending on whether discovery has run. The +reviewer also correctly notes this is reachable in production, not just theoretically: +`src/web-search/gemini-executor.ts:51` passes the raw effort straight through without going +via `mapReasoningEffort`. + +Worse for the plan's own credibility: `010` test items 6 and 11 as written would FAIL against +the code `010` proposed. The plan contradicted itself. + +**Fix (amends `010` section 8a):** clamp before the map lookup for suffix-tier models, so both +paths perform the same normalization in the same order: + +```ts +const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId]; +if (effortMap) { + const suffixTiered = ANTIGRAVITY_SUFFIX_TIER_MODELS.has(modelId); + // Normalize FIRST for suffix-tiered models: the discovery path clamps max/xhigh/ultra to + // `high` before its lookup, so a static path that skips the clamp answers `medium` for the + // same request. Same input, two tiers, decided by whether discovery happened to run. + const requested = suffixTiered && effort + ? resolveAntigravityThinkingLevel(effort) ?? effort + : effort; + if (requested && requested in effortMap) { + const wireModelId = effortMap[requested]!; + return suffixTiered ? { wireModelId } : { wireModelId, thinkingLevel: requested }; + } + const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]!; + return { wireModelId: effortMap[defaultEffort]! }; +} +``` + +The `suffixTiered &&` guard keeps `gemini-3.1-pro` byte-identical to today: it has no `medium` +rung, and clamping there would change which wire id a `medium` request reaches — a behavior +change outside this unit. + +Test item 11 is extended to `max`, `xhigh`, `ultra`, and the stale lines at `010:13-14` and +`010:252-253` that still promise a `thinkingLevel` are corrected. + +## Blocker 2 (High) — partial-ladder suffix rows bypass the paragraph guard + +**Accepted. This is a genuinely subtle interaction and the reviewer found it by composing two +separate parts of the plan.** + +The guard set holds picker ids (`gemini-3.8-flash`) and compares against `parsed.modelId`, +which is correct for the collapsed row. But `010` section 3 deliberately keeps raw suffix ids +visible when CCA returns a PARTIAL ladder — that is the documented degradation path. In that +state a user selects `gemini-3.8-flash-high` directly, so `parsed.modelId` IS the suffix id, it +misses the base-only set, and the paragraph survives. + +The 429 probes in `003` were run against exactly those suffix wire ids, so this is not a +hypothetical gap: the ids proven to reject the paragraph are precisely the ones that would slip +past the guard. + +**Fix (amends `010` section 8b):** canonicalize before the membership test rather than +enumerating every spelling: + +```ts +/** + * Whether CCA rejects the Claude-Agent identity paragraph for this selector. + * + * Canonicalize first: when discovery returns a partial ladder the picker publishes RAW suffix + * ids (see parseAntigravityAvailableModels), so `parsed.modelId` can be `gemini-3.8-flash-high` + * rather than the collapsed base. Those are the exact ids the 429 probe used, so a base-only + * membership test would miss the degraded path — the one users hit when CCA is flaky, i.e. the + * worst possible time to also lose the guard. + */ +function rejectsClaudeSdkParagraph(modelId: string): boolean { + return ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS.has(canonicalAntigravityUsageModel(modelId)); +} +``` + +`canonicalAntigravityUsageModel` already collapses `gemini-3.8-flash-high` onto +`gemini-3.8-flash` via the `ANTIGRAVITY_EFFORT_WIRE_MAP` derivation, and leaves unknown ids as +identity, so it adds no new mapping surface. It must be exported from +`src/providers/antigravity-models.ts` (it already is) and imported by `src/adapters/google.ts`. + +Required test: a partial-discovery payload publishing `gemini-3.8-flash-high` as its own row, +then a serialized request asserting the paragraph is absent. + +## Blocker 3 (Medium) — direct Google 3.8 activation untested + +Accepted. `030` gains a direct AI Studio test: bare `gemini-3.8-flash` reaches the wire with no +synthetic `-tiered` rename, and the configured-ladder branch at `google.ts:782-790` emits +`thinkingConfig`. Adding the model to `modelReasoningEfforts` is what newly activates that +branch for 3.8, so it needs its own activation scenario. + +## Blocker 4 (Medium) — the rename plan contradicted itself + +Accepted, and embarrassing: `010` line 47 said the constant keeps its name while section 8c +renamed it. Section 8c now enumerates all three call sites — `antigravity-models.ts:201`, +`:233`, `:618` — and the contradictory sentence is removed. A declaration-only rename would not +even typecheck. + +## Blocker 5 (Low) — `tests/google-output-clamp.test.ts` + +**Accepted as a documentation gap, resolved as no-change with evidence.** + +`maxOutputTokensForGoogleModel` (`src/adapters/google.ts:83-89`) is family-based: any id +starting `gemini` and not matching the `pro` pattern returns 65536. `gemini-3.8-flash` therefore +already receives the correct documented ceiling with no table entry, which `001` confirms is +65,536. Recorded in `004` rather than changed. + +## Round outcome + +Both High blockers folded as concrete code amendments; three lesser findings folded or resolved +with evidence. Round 3 re-audits with the same reviewer. diff --git a/devlog/_plan/260903_gemini_38_rollout/006_maintainer_review_fold.md b/devlog/_plan/260903_gemini_38_rollout/006_maintainer_review_fold.md new file mode 100644 index 0000000000..eb4e39561e --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/006_maintainer_review_fold.md @@ -0,0 +1,56 @@ +# 006 — maintainer review fold (PR #3286) + +The maintainer review bot found a defect three audit rounds missed, and it was reproduced +against the live backend before being fixed. + +## The finding: retired ids reach the rejecting generation unguarded + +`rejectsClaudeSdkParagraph` keyed on the SELECTOR via `canonicalAntigravityUsageModel`. That +covers the collapsed base and the raw suffix rows, but not the third path into the same +generation: + +``` +gemini-3.6-flash --rule 0--> gemini-3.7-flash-tiered (a rejecting generation) +``` + +Retired ids deliberately keep their OWN identity in `ANTIGRAVITY_USAGE_BASE_BY_ID` — that is +the rule protecting historical spend from being relabelled — so they can never canonicalize +into the generation they actually call. The two mechanisms were each correct and combined into +a hole. + +Probe, 2026-09-03, live CCA: + +``` +resolveAntigravityEffortWireModel("gemini-3.6-flash") + -> { wireModelId: "gemini-3.7-flash-tiered", thinkingLevel: "medium" } +saved 3.6 selection + Claude SDK paragraph -> 429 RESOURCE_EXHAUSTED +``` + +So every saved 3.6/3.5 config would have kept 429ing after this PR — the exact class of +silent breakage the retirement machinery exists to prevent. + +**Fix:** judge on the ROUTED WIRE id, with the selector kept as a fallback. Naming a wire +spelling once now covers every selector that can reach that generation, instead of requiring +the set to enumerate selectors that redirect into it. + +The test that asserted the old behavior (`preserves the paragraph for another Cloud Code +Assist model`, using 3.6) was asserting the bug. It is replaced by one proving the retired id +IS stripped, plus a real control on `claude-sonnet-4-6` — a model with no recorded rejection, +where the paragraph is literally true. + +## Second finding: direct Google 3.7 advertises `minimal` + +Recorded in `050` as a follow-up; the maintainer asked whether to fold it in. Folded, because +the evidence is identical to 3.8's (Google documents `minimal` as a validation error for that +generation) and the line was already being edited in this PR. Leaving it would ship a catalog +that offers a rung the API rejects, in the same file where the neighbouring row was just +corrected for the same reason. 3.5 and 3.6 keep theirs — their pages still list it, and this +unit has no evidence about them. + +## Not folded + +| Item | Disposition | +|---|---| +| `ANTIGRAVITY_WIRE_MODELS` dead list | Stays in `050`. Deleting an unrelated dead constant mid-rollout widens the diff for no behavioral gain. | +| `gemini-3.5-flash` empty `modelInputModalities` | Pre-existing, unrelated to this diff, and changing the DEFAULT model's advertised modalities deserves its own evidence. Added to `050`. | +| Cursor preemptive seed | Kept. The static catalog is intersected with the live roster, so the row stays invisible until Cursor lists it, and the `glm-5.3` precedent is explicit. | diff --git a/devlog/_plan/260903_gemini_38_rollout/007_closeout.md b/devlog/_plan/260903_gemini_38_rollout/007_closeout.md new file mode 100644 index 0000000000..be6144f576 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/007_closeout.md @@ -0,0 +1,55 @@ +# 007 — closeout + +Terminal outcome: **DONE**. + +## What landed + +PR [#3286](https://github.com/lidge-jun/opencodex/pull/3286), squash-merged as `3d3c4fe26` +into `dev`. Ancestry proven: + +``` +git merge-base --is-ancestor 3d3c4fe26 FETCH_HEAD -> LANDED-ON-DEV +``` + +Five commits, one per work-phase plus the review fold: + +| Commit | Phase | Content | +|---|---|---| +| `be0cda383` | wp0 | 11-doc roadmap unit | +| `b460299dc` | wp1 | Antigravity catalog, suffix ladder, suffix-tier rule, paragraph guard, constant rename | +| `bd2b03089` | wp2 | metadata source + regen, 5 price rows, reconcile preservation test | +| `a8c2314f3` | wp3 | direct Google, free-directory, sidecar default, Cursor seed, docs | +| `ea79ec132` | wp3 | maintainer-review fold: routed-generation guard, 3.7 `minimal` removal | + +## Verification actually performed + +- `bun run typecheck` — exit 0 at every phase boundary. +- Focused `bun test` only, never the repository-wide suite (maintainer instruction). Final + focused set: 681 pass, 0 fail across 12 files. +- Full GitHub CI on the exact merged head `ea79ec132`: **25 success, 1 skipped, 0 failures**, + including all four Linux test shards, macOS, Windows keyring, npm-global on three OSes, + gates, storage policy, and `enforce-target`. +- Live CCA probes at three points: discovery shape, per-tier inference, and two adversarial + probes that each disproved a plan assumption. + +## What the process actually caught + +Worth recording, because the interesting failures were all invisible from the diff: + +| Round | Finding | How it was settled | +|---|---|---| +| A round 1 | 9 blockers, 2 High | folded; the two High ones were probe-confirmed | +| A round 2 | 5 more, 2 High — introduced BY the round-1 fixes | folded | +| A round 3 | PASS | — | +| Maintainer review on the pushed PR | retired ids reach the rejecting generation unguarded | reproduced at 429, fixed in `ea79ec132` | + +The last one is the lesson. Three adversarial rounds against the plan missed it because it +lives in the interaction between two mechanisms that are each individually correct: retired +ids keep their own usage identity (protecting historical spend), and the paragraph guard keyed +on the selector. Neither is wrong. Their composition left every saved 3.6/3.5 config 429ing. + +## Follow-ups + +Recorded in `050`: the dead `ANTIGRAVITY_WIRE_MODELS` list, `gemini-3.5-flash`'s empty +modalities entry, OpenRouter's published `google/gemini-3.8-flash`, Vertex's frozen default, +and the `gemini-3.1-pro` suffix-tier asymmetry. diff --git a/devlog/_plan/260903_gemini_38_rollout/010_wp1_antigravity_core.md b/devlog/_plan/260903_gemini_38_rollout/010_wp1_antigravity_core.md new file mode 100644 index 0000000000..716fc45854 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/010_wp1_antigravity_core.md @@ -0,0 +1,326 @@ +# 010 — wp1: Antigravity core surface + +One file does almost all of the work: `src/providers/antigravity-models.ts`. Plus one line in +`src/providers/registry.ts`. Everything here is diff-level and copy-paste executable. + +## Design decision restated (do not skip) + +3.8 uses the **suffix-wire** shape (`ANTIGRAVITY_EFFORT_WIRE_MAP`), like 3.6 did, NOT the +single-wire `thinkingLevel` shape 3.7 uses (`ANTIGRAVITY_THINKING_LEVEL_MODELS`). Evidence: +`002` — CCA serves `gemini-3.8-flash-{low,medium,high}` and no `-tiered` row. + +Trace through `resolveAntigravityEffortWireModel` to see why the map is mandatory rather than +cosmetic. With an `ANTIGRAVITY_EFFORT_WIRE_MAP` entry, rule 2/3 returns +`{ wireModelId: "gemini-3.8-flash-high" }` — the suffix alone, no `thinkingLevel`; see section +8a and `005` for why the level must NOT accompany it. Without the map, `gemini-3.8-flash` +is not a suffix id (rule 1 skips), has no thinking-level entry (rule 1b skips), has no effort map +(rule 2/3 skips), is not `claude-` (rule 4 skips), and falls to **rule 5**, which returns the bare +id with no tier at all — a picker row whose effort selector does nothing. + +## MODIFY `src/providers/antigravity-models.ts` + +### 1. Current-generation constants (near L16) + +Before: + +```ts +/** Current Antigravity Flash generation. */ +const GEMINI_FLASH_CURRENT = "gemini-3.7-flash"; +``` + +After: + +```ts +/** Current Antigravity Flash generation. */ +const GEMINI_FLASH_CURRENT = "gemini-3.8-flash"; + +/** + * Previous Flash generation, still served by CCA. + * + * 3.6 was pulled the moment 3.7 shipped, which is why RETIRED_FLASH_TIERS exists. 3.8 did not + * do that: Google documents 3.7 Flash as "remains fully supported", and a 2026-09-03 + * :fetchAvailableModels call returns 3.8, 3.7 AND 3.6 wire ids together. So 3.7 stays a + * first-class picker row instead of joining the retired map. + */ +const GEMINI_FLASH_PREVIOUS = "gemini-3.7-flash"; +``` + +`GEMINI_FLASH_WIRE_ID` keeps its VALUE (`gemini-3.7-flash-tiered`) — it is the retired-tier +redirect target, which is still 3.7 — but is RENAMED per section 8c. + +### 2. Wire model list (L52) — DROPPED after audit + +`ANTIGRAVITY_WIRE_MODELS` has no consumer outside its own declaration; discovery does not read +it (audit blocker 9). Editing it would change dead data and imply a behavioral effect that does +not exist. Left alone; whether the dead mirror should be deleted is a separate cleanup with its +own blast radius, recorded as a follow-up in `050`. + +### 3. Picker collapse map (`ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID`, L63) + +```ts +const ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID: Record = { + "gemini-3.8-flash-low": "gemini-3.8-flash", + "gemini-3.8-flash-medium": "gemini-3.8-flash", + "gemini-3.8-flash-high": "gemini-3.8-flash", + "gemini-3.1-pro-low": "gemini-3.1-pro", + "gemini-pro-agent": "gemini-3.1-pro", +}; +``` + +This is what makes `ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL` require all three rungs before the +collapsed row appears, so a partial CCA payload degrades to visible wire ids rather than a +ladder with missing rungs. The generic `-(low|medium|high)$` branch in +`pickerModelIdForDiscoveredWireId` would also collapse these, but only once +`gemini-3.8-flash` is in `ANTIGRAVITY_MODELS`; the explicit map is the belt to that suspenders +and mirrors how 3.1 Pro is handled. + +### 4. Effort ladder (`ANTIGRAVITY_MODEL_EFFORTS`, L145) + +```ts +export const ANTIGRAVITY_MODEL_EFFORTS: Record = { + "gemini-3.8-flash": ["low", "medium", "high"], + "gemini-3.7-flash": ["low", "medium", "high"], + ... +}; +``` + +No `minimal`: Google documents it as an error for this generation (`001`), and CCA exposes only +the three tiers (`002`). + +### 5. Effort-to-wire map (`ANTIGRAVITY_EFFORT_WIRE_MAP`, L153) + +```ts +const ANTIGRAVITY_EFFORT_WIRE_MAP: Record> = { + "gemini-3.8-flash": { + low: "gemini-3.8-flash-low", + medium: "gemini-3.8-flash-medium", + high: "gemini-3.8-flash-high", + }, + "gemini-3.1-pro": { low: "gemini-3.1-pro-low", high: "gemini-pro-agent" }, +}; +``` + +### 6. Default effort (`ANTIGRAVITY_DEFAULT_EFFORT`, L180) + +```ts +const ANTIGRAVITY_DEFAULT_EFFORT: Record = { + "gemini-3.8-flash": "medium", + "gemini-3.1-pro": "high", +}; +``` + +`medium` matches Google's documented `thinking_level` default (`001`) and the tier CCA marks +`recommended` with a finite 4000 thinking budget (`002`). Rule 2/3 requires this key: with an +effort map present and no default, `effortMap[defaultEffort]!` dereferences `undefined`. + +This constant has a SECOND consumer the first draft missed (audit blocker 5): +`discoveredAntigravityEffortWireModelId` (L405-410) reads it to pick the default rung from a +DISCOVERED ladder. So the value governs both the static and the live path, and an omission +would make live discovery fall back to `Object.values(effortMap)[0]` — an arbitrary rung +determined by CCA's key order. + +### 7. Picker list (`ANTIGRAVITY_MODELS`, L243) + +```ts +export const ANTIGRAVITY_MODELS = [ + GEMINI_FLASH_CURRENT, // gemini-3.8-flash + GEMINI_FLASH_PREVIOUS, // gemini-3.7-flash — still served, see 002 + "gemini-3.1-pro", + "gemini-3.1-flash-image", + "claude-sonnet-4-6", + "claude-opus-4-6-thinking", + "gpt-oss-120b-medium", +]; +``` + +### 8. Context windows + +`ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS` (L257) gains the three wire ids at `1_048_576`; +`ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` (L267) gains the collapsed `"gemini-3.8-flash": 1_048_576`. +Both are needed: the map has no fallback, and the collapsed id is not derivable from an alias +because 3.8 has no alias entry. + +### 8a. Suffix-tier carrier set (NEW — audit blocker 1) + +Static rule 2/3 returns `{ wireModelId, thinkingLevel }` while the discovery path returns +`{ wireModelId }` only. Same model, two different request bodies depending on whether discovery +has run. A probe (`003`) shows CCA accepts `gemini-3.8-flash-low` paired with +`thinkingLevel: HIGH` and returns 200 — a contradiction it will not reject, so the effective +tier becomes unknowable. The suffix must be the sole carrier: + +```ts +/** + * Base models whose every effort maps to a wire id that ALREADY encodes the tier. + * + * Sending thinkingLevel alongside such a suffix states the effort twice, and CCA accepts a + * contradictory pair rather than failing, so a mismatch would silently run at an unknown tier. + * Membership also makes static resolution byte-identical to the discovery path, which never + * emits thinkingLevel. + * + * gemini-3.1-pro is deliberately absent: its `high` rung is `gemini-pro-agent`, which carries + * no tier suffix, so there the level is the only thing naming the effort. + */ +const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set(["gemini-3.8-flash"]); +``` + +Rule 2/3 becomes (round-2 form — the round-1 draft left `max`/`xhigh`/`ultra` diverging, +see `005` blocker 1): + +```ts +const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId]; +if (effortMap) { + const suffixTiered = ANTIGRAVITY_SUFFIX_TIER_MODELS.has(modelId); + // Normalize FIRST for suffix-tiered models: the discovery path clamps max/xhigh/ultra to + // `high` before its lookup (L400-408), so a static path that skips the clamp answers + // `medium` for the same request. Same input, two tiers, decided by whether discovery ran. + const requested = suffixTiered && effort + ? resolveAntigravityThinkingLevel(effort) ?? effort + : effort; + if (requested && requested in effortMap) { + const wireModelId = effortMap[requested]!; + return suffixTiered ? { wireModelId } : { wireModelId, thinkingLevel: requested }; + } + const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]!; + return { wireModelId: effortMap[defaultEffort]! }; +} +``` + +The `suffixTiered &&` guard keeps `gemini-3.1-pro` byte-identical: it has no `medium` rung, so +clamping there would change which wire id a request reaches — outside this unit's scope (`050`). + +### 8b. Claude SDK paragraph guard (NEW — audit blocker 2) + +`src/adapters/google.ts:750` strips the rejected Claude-Agent identity paragraph only for +`gemini-3.7-flash`. Probes in `003` prove 3.8 rejects the same paragraph with a 429 that reads +as quota exhaustion, and succeeds the moment it is stripped. Making 3.8 the default without +this change would 429 every Claude-Agent-shaped request. + +```ts +// Membership is probe-established per generation, never assumed: 3.7 and 3.8 both answer 429 +// RESOURCE_EXHAUSTED when this paragraph survives into systemInstruction, and 200 without it. +const ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS = new Set([ + "gemini-3.7-flash", + "gemini-3.8-flash", +]); + +/** + * Canonicalize before the membership test: when discovery returns a PARTIAL ladder the picker + * publishes raw suffix ids, so `parsed.modelId` can be `gemini-3.8-flash-high` rather than the + * collapsed base. Those are the exact ids the 429 probe used, so a base-only test would miss + * the degraded path — the moment CCA is flaky is the worst time to also lose the guard. + * `canonicalAntigravityUsageModel` already collapses suffix ids via ANTIGRAVITY_EFFORT_WIRE_MAP + * and returns unknown ids unchanged, so this adds no new mapping surface. + */ +function rejectsClaudeSdkParagraph(modelId: string): boolean { + return ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS.has(canonicalAntigravityUsageModel(modelId)); +} + +const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist" + && rejectsClaudeSdkParagraph(parsed.modelId); +``` + +`canonicalAntigravityUsageModel` is already exported from `src/providers/antigravity-models.ts`; +`src/adapters/google.ts` gains the import. + +### 8c. Constant rename (audit blocker 8) + +`GEMINI_FLASH_WIRE_ID` becomes `GEMINI_RETIRED_FLASH_TARGET_WIRE_ID`. After 3.8 becomes +current, a constant named "the Flash wire id" holding `gemini-3.7-flash-tiered` reads as a bug. +Its rule-0 comment is corrected too: retired ids route to **3.7**, not to "the current +generation". + +**All four sites move together or typecheck fails** (audit round 2, blocker 4): the declaration +at `src/providers/antigravity-models.ts:23`, plus references at `:201` +(`ANTIGRAVITY_PICKER_TO_WIRE`), `:233` (the retired-alias `Object.fromEntries`), and `:618` +(rule 0's return). + +### 9. Input modalities (`ANTIGRAVITY_MODEL_INPUT_MODALITIES`, L281) + +```ts + "gemini-3.8-flash": ["text", "image"], +``` + +Google lists video, audio and PDF (`001`) and CCA reports `supportsVideo: true` (`002`), but +this proxy transports only `OcxTextContent` and `OcxImageContent`, and the Codex catalog +normalizes `input_modalities` against a closed enum where one out-of-enum value rejects the +ENTIRE catalog. The vendor capability is recorded in `001` as a fact about Google, not a claim +about this proxy. Same reasoning, same values as every other Gemini row. + +## What is deliberately NOT touched + +| Symbol | Why untouched | +|---|---| +| `RETIRED_FLASH_TIERS` | 3.7 is not retired (`001`, `002`). Adding it would strand a live model. | +| `ANTIGRAVITY_THINKING_LEVEL_MODELS` | 3.7 keeps its single-wire tiering; 3.8 must not join it. | +| `ANTIGRAVITY_PICKER_TO_WIRE` | Only for the `-tiered` rename; 3.8 has no `-tiered` id. | +| `ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES` | No saved config can name a 3.8 id yet. | +| `ANTIGRAVITY_USAGE_BASE_BY_ID` | Derives 3.8 automatically from `ANTIGRAVITY_EFFORT_WIRE_MAP`. | + +## Complete consumer chain (PLAN-FIELD-CHAIN-01, completed after audit) + +| Symbol | Consumers | +|---|---| +| `ANTIGRAVITY_EFFORT_WIRE_MAP` | static rule 2/3 (L639-645); discovery-map completion `completeDiscoveredEffortWireModelIds` (L164-166); discovery suppression via `hasOwnEffortLadder` (L597-601); `ANTIGRAVITY_USAGE_BASE_BY_ID` derivation | +| `ANTIGRAVITY_DEFAULT_EFFORT` | static rule 2/3 (L644); discovered-ladder default selection (L405-410) | +| `ANTIGRAVITY_MODEL_EFFORTS` | registry `modelReasoningEfforts` (`registry.ts:1753`) | +| `ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS` | exported-map spread and alias derivation (L272-277) | +| `ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` | registry `modelContextWindows` | +| `ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID` | reverse derivation `ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL` (L67-95); `pickerModelIdForDiscoveredWireId` | + +That last row is worth verifying rather than assuming: the IIFE walks +`ANTIGRAVITY_EFFORT_WIRE_MAP` and maps every wire value back to its base, so +`gemini-3.8-flash-high` collapses onto `gemini-3.8-flash` for usage aggregation with no new code. + +## MODIFY `src/providers/registry.ts` (L1753) + +`defaultModel: "gemini-3.7-flash"` becomes `defaultModel: "gemini-3.8-flash"`. The `models`, +`modelContextWindows`, `modelInputModalities` and `modelReasoningEfforts` fields already +reference the exported maps, so they follow automatically. + +## Tests — MODIFY `tests/google-antigravity-wire.test.ts` + +Add a `Gemini 3.8 Flash` describe block asserting: + +1. `ANTIGRAVITY_MODELS` contains `gemini-3.8-flash` **and** still contains `gemini-3.7-flash`. +2. Registry `google-antigravity` `defaultModel === "gemini-3.8-flash"`. +3. `ANTIGRAVITY_MODEL_EFFORTS["gemini-3.8-flash"]` equals `["low","medium","high"]`. +4. Each effort resolves to its own wire id, table-driven over the three tiers, each returning + NO `thinkingLevel` (the suffix is the sole tier carrier — section 8a). +5. No effort resolves to `gemini-3.8-flash-medium` by default — i.e. an unset effort returns the + `medium` wire id (activation scenario for the `ANTIGRAVITY_DEFAULT_EFFORT` branch). +6. `xhigh`/`max`/`ultra` clamp to the `gemini-3.8-flash-high` wire id on BOTH the static and + the discovered path (round-2 blocker 1: the round-1 draft returned the `medium` wire id + statically and the `high` one after discovery). +7. A discovery payload containing all three 3.8 wire ids collapses to exactly one + `gemini-3.8-flash` row carrying the full `effortWireModelIds` triple. +8. A payload containing only two of the three rungs does NOT collapse (partial-ladder guard). +9. Regression: `resolveAntigravityEffortWireModel("gemini-3.6-flash-high")` still returns + `gemini-3.7-flash-tiered` with `thinkingLevel: "high"`. +10. `canonicalAntigravityUsageModel("gemini-3.8-flash-high") === "gemini-3.8-flash"`, and + `canonicalAntigravityUsageModel("gemini-3.6-flash-high") === "gemini-3.6-flash-high"`. +11. **Path-equality (audit blocker 1):** for each of unset, `low`, `medium`, `high`, `max`, + `xhigh`, `ultra`, resolving WITH a registered discovery ladder returns an object deep-equal + to resolving WITHOUT one. Asserting the two paths separately is what allowed them to + diverge; the clamped efforts are the cases the round-1 fix missed. +12. **Paragraph guard (audit blocker 2):** the serialized CCA `systemInstruction` omits the + Claude SDK identity paragraph for both `gemini-3.7-flash` and `gemini-3.8-flash`, and a + non-CCA Google request still contains it. Add beside `tests/google-adapter.test.ts:250`. +13. **Partial-ladder guard (round-2 blocker 2):** a discovery payload publishing only + `gemini-3.8-flash-high` as its own row, then a serialized request selecting that suffix id, + still omits the paragraph. This is the case a base-only membership test would miss. + +## Stale exact assertions this phase must update (audit blocker 3) + +- `tests/provider-registry-parity.test.ts:771` — `toHaveLength(6)` becomes 7, plus 3.8 ladder + and context-window assertions mirroring the 3.7 ones. + +Item 8 is the activation scenario for the `requiredWireIds.every(...)` guard; item 5 for the +default-effort branch; item 6 for `resolveAntigravityThinkingLevel`'s clamp. + +## Focused verification for this phase + +```bash +bun test tests/google-antigravity-wire.test.ts tests/gemini-37-flash-migration.test.ts \ + tests/google-adapter.test.ts tests/provider-registry-parity.test.ts +bun run typecheck +``` diff --git a/devlog/_plan/260903_gemini_38_rollout/020_wp2_metadata_pricing.md b/devlog/_plan/260903_gemini_38_rollout/020_wp2_metadata_pricing.md new file mode 100644 index 0000000000..8a0d98b622 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/020_wp2_metadata_pricing.md @@ -0,0 +1,138 @@ +# 020 — wp2: metadata, pricing, and config migration + +Depends on wp1: every key below is the picker id or the wire ids wp1 introduces. + +## The trap this phase exists to avoid + +`resolveMatchedPriceExact()` (`src/usage/cost.ts:247-258`) returns bundled generated metadata +with `status: "verified"` **before** it consults the expected-price overlay. So if the new +`scripts/model-metadata.source.json` row copies its 3.6 neighbour and includes a `cost` block, +the `google-antigravity` `verified-derived` row below becomes unreachable and CCA cost is +reported as `verified` — asserting exactly the billing equivalence `001` says is NOT PROVEN. + +**The generated `google/gemini-3.8-flash` record must omit `cost`.** The 3.7 row at +`scripts/model-metadata.source.json:12046` already does this; copy that one, not the 3.6 one +at L12021 which carries a `cost` block. + +## MODIFY `scripts/model-metadata.source.json` + +Insert next to the existing `gemini-3.7-flash` record (L12046), under the `google` provider: + +```json +"gemini-3.8-flash": { + "id": "gemini-3.8-flash", + "name": "Gemini 3.8 Flash", + "api": "google-generative-ai", + "provider": "google", + "baseUrl": "https://generativelanguage.googleapis.com/v1beta", + "reasoning": true, + "input": ["text", "image"], + "contextWindow": 1048576, + "maxTokens": 65536, + "thinking": { "mode": "google-level", "minLevel": "low", "maxLevel": "high" } +} +``` + +`minLevel: "low"` (not `minimal`) because `001` proves `minimal` errors on this generation — +the same value the 3.7 record uses and the 3.6 record does not. + +`input` is `["text","image"]` for the transport reason in `010` section 9, even though the +vendor also accepts video/audio/PDF. + +Antigravity resolves generated metadata through the `google` bundle +(`src/generated/model-metadata.ts:27` maps `google-antigravity` to `google`), so this single +`google` record serves both surfaces. + +## Regenerate, never hand-edit + +```bash +bun run generate:model-metadata +``` + +`src/generated/model-metadata.ts` is byte-compared by `tests/model-metadata-sync.test.ts`, so +the regen must land in the same commit as the source edit. + +## MODIFY `src/usage/expected-prices.ts` + +### New price constant (beside `GEMINI_37_FLASH`, L60) + +```ts +// Gemini 3.8 Flash carries the same published promotional rate as 3.7 through 2026-12-31, +// rising to 1.50/7.50 on 2027-01-01 (ai.google.dev/gemini-api/docs/pricing, read 2026-09-03). +const GEMINI_38_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }; +``` + +Equal values to 3.7 today, but a SEPARATE constant: aliasing them would silently move 3.8 if +3.7's promotional rate is ever re-verified to a different number. + +### New source string (beside `GEMINI_37_PRICING`, L83) + +```ts +const GEMINI_38_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-09-03); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token"; +``` + +### New rows + +```ts +// CCA billing equivalence is unproven (see devlog 001), so the Antigravity rows are +// verified-derived: the NUMBER is proven, the claim that Antigravity charges it is inferred. +{ provider: "google-antigravity", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: `derived: Gemini 3.8 Flash promotional rate through 2026-12-31 ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, +{ provider: "google-antigravity", modelId: "gemini-3.8-flash-low", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, +{ provider: "google-antigravity", modelId: "gemini-3.8-flash-medium", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, +{ provider: "google-antigravity", modelId: "gemini-3.8-flash-high", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, +// Developer API row: the price IS published for this surface, so `verified`. +{ provider: "google", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: GEMINI_38_PRICING, verifiedAt: "2026-09-03", status: "verified" }, +``` + +The three suffix rows matter because usage rows can carry a wire id directly; the 3.6 block +(L151-153) is the precedent. + +### What must NOT be removed + +Every existing 3.5/3.6/3.7 row stays. Historical `usage.jsonl` rows still carry those ids, and +deleting a row silently zeroes the cost of requests the user already made. This rollout adds a +model; it retires nothing. + +## `src/providers/model-rename-migration.ts` — NO CHANGE, and why + +The migration exists for ids the vendor **took offline**. `001` proves 3.7 remains fully +supported and `002` proves CCA still serves it, so a `gemini-3.7-flash -> gemini-3.8-flash` +entry would rewrite a working saved selection out from under the user. The existing +3.6/3.5 to 3.7 entries stay untouched and keep working. + +`selectedModels` needs no migration for the same reason: a user who allowlisted +`gemini-3.7-flash` still gets a live model. + +## `src/oauth/index.ts` — NO CHANGE + +`OAUTH_RECONCILE_FIELDS` already refreshes `models`, `modelContextWindows`, +`modelInputModalities` and `modelReasoningEfforts` from the registry preset, so existing configs +pick up 3.8 on the next start. The `defaultModel` heal branch only fires when the stored default +is absent from the refreshed list; since 3.7 remains listed, an existing user's explicit 3.7 +default is preserved — which is the correct outcome. + +`isLegacyAntigravityStaticCatalog` (L1209) is a FROZEN v1 fingerprint that must keep naming +`gemini-3.6-flash`. Updating it would break the migration it exists to perform. + +## Tests + +- `tests/oauth-provider-reconcile.test.ts:82`: default becomes `gemini-3.8-flash`; L142's + `toHaveLength(6)` becomes 7 (audit blocker 3). +- **New case (audit blocker 4):** a config whose `defaultModel` is explicitly + `gemini-3.7-flash` must come OUT of `reconcileOAuthProviders` still holding that default, + while its capability maps refresh. The existing case starts from a retired 3.5 id and + therefore only exercises the stale-default HEALING branch; asserting 3.7 is still in `models` + does not prove the default survived. This is the activation scenario for the additive claim + in this doc — without it, "an existing 3.7 user keeps 3.7" is an untested assertion. +- New assertions near the existing price tests: an Antigravity 3.8 request resolves to the + `verified-derived` overlay rather than a `verified` bundled price (the activation scenario + for the omitted `cost` block). +- `tests/model-metadata-sync.test.ts` proves the regen is byte-synced. + +```bash +bun test tests/oauth-provider-reconcile.test.ts tests/model-metadata-sync.test.ts \ + tests/usage-summary.test.ts tests/usage-cost.test.ts +``` + +`tests/usage-cost.test.ts` is the owner of price resolution and was missing from the first +draft (audit blocker 3); it is where the `verified-derived`-wins assertion belongs. diff --git a/devlog/_plan/260903_gemini_38_rollout/030_wp3_peripheral_surfaces.md b/devlog/_plan/260903_gemini_38_rollout/030_wp3_peripheral_surfaces.md new file mode 100644 index 0000000000..f38909086d --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/030_wp3_peripheral_surfaces.md @@ -0,0 +1,122 @@ +# 030 — wp3: peripheral surfaces and docs + +The surfaces that *reference* the model rather than define it. Each one below is either +changed with its evidence, or explicitly not changed with its reason — no blanks +(c-5 requires exactly this). + +## CHANGE — `src/providers/registry.ts`, direct `google` provider (L1739) + +Google publishes `gemini-3.8-flash` on the Developer API (`001`), so the API-key surface gets it: + +```ts +models: ["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"], +modelContextWindows: { ..., "gemini-3.8-flash": 1_048_576 }, +modelInputModalities: { ..., "gemini-3.8-flash": ["text", "image"] }, +modelReasoningEfforts: { ..., "gemini-3.8-flash": ["low", "medium", "high"] }, +``` + +Note the ladder here is `["low","medium","high"]` with NO `minimal`, unlike the neighbouring +3.5/3.6/3.7 rows which all list `minimal`. `001` proves `minimal` returns a validation error on +3.8. (The 3.7 row listing `minimal` is a pre-existing inconsistency with its own model page; +correcting it is out of scope for this unit and is recorded here as a follow-up observation.) + +**`defaultModel` stays `gemini-3.5-flash`.** Adding a model elsewhere must not silently change +an existing API-key user's default — the same rule the 3.6 rollout fixed as decision 5. + +**Activation test required (round-2 blocker 3).** Adding 3.8 to `modelReasoningEfforts` newly +arms the configured-ladder branch at `src/adapters/google.ts:782-790` for this model, and +`resolveDirectGeminiWireModelId` newly sees an id absent from `GEMINI_DIRECT_WIRE_RENAMES`. +Neither is covered by a registry-metadata assertion. Add a direct AI Studio request test: +the wire id is bare `gemini-3.8-flash` with no synthetic `-tiered` rename, and the selected +effort arrives as `generationConfig.thinkingConfig.thinkingLevel`. + +## CHANGE — `src/providers/free-directory.ts` (L85) + +Prepend `gemini-3.8-flash` to the `gemini` entry's `models` array. It is a directory listing of +what the provider serves; `001` proves 3.8 is served. + +**Also give that row its own `lastVerified: "2026-09-03"`** (audit blocker 6). The shared +`LAST_VERIFIED = "2026-07-23"` constant at L56 documents when each endpoint was checked; adding +2026-09-03 evidence under a July date makes the field lie. Do NOT bump the shared constant — +that would stamp a verification date on unrelated providers nobody re-checked. + +## CHANGE — `src/web-search/index.ts` (L26) + +```ts +const DEFAULT_GEMINI_SIDECAR_MODEL = "gemini-3.8-flash"; +``` + +The sidecar runs `google_search` grounding over the Antigravity transport, so its default should +track the Antigravity default. Verified safe by `002`: all three 3.8 tiers accept inference, and +wp1 gives the id a real effort ladder, so `reasoning` still maps to a tier. + +`tests/gemini-web-search.test.ts` asserts the resolved wire id. For 3.7 that was +`gemini-3.7-flash-tiered`; for 3.8 the low-effort call must resolve to `gemini-3.8-flash-low`. +That assertion difference is itself the proof the suffix-wire shape reached the sidecar path. + +## CHANGE — `src/adapters/cursor/effort-map.ts` and `catalog.ts` + +Cursor has NOT announced 3.8 (`001`). The repository has a documented precedent for exactly +this: `glm-5.3` at `effort-map.ts:60` is commented `260814 preemptive: glm-5.3 seeded ahead of +Cursor's lineup update`. Follow it exactly, including the comment style: + +```ts +// 260903 preemptive: gemini-3.8-flash seeded ahead of Cursor's lineup update. Google documents +// low/medium/high with no `minimal` for this generation, unlike 3.6. +"gemini-3.8-flash": ["low", "medium", "high"], +``` + +And in `catalog.ts` beside the 3.7 entry (L202): + +```ts +"gemini-3.8-flash": { + displayName: "Gemini 3.8 Flash", + window: CONTEXT_GEMINI, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high"] } }, +}, +``` + +This is a static seed, not a claim that Cursor serves it: the Cursor catalog is intersected with +the live `GetUsableModels` roster, so an unseeded model stays invisible until Cursor lists it. +If the reviewer judges the seed speculative, dropping it is an acceptable amendment — the +precedent makes it defensible, not mandatory. + +## CHANGE — `docs-site/` + +- `src/content/docs/guides/sidecars.md:30` — default model becomes `gemini-3.8-flash`. +- `src/content/docs/reference/configuration/providers.md` — the `directGeminiWireRenames` + description at L139 keeps its 3.7 example verbatim, because that IS the model with the + `-tiered` rename. Do not rewrite the example to 3.8; it would document a rename that does + not exist. +- Check translated locales for the same two strings and keep them from contradicting English. + +## NO CHANGE — with reasons + +| Surface | Reason | +|---|---| +| `src/adapters/google.ts` `GEMINI_DIRECT_WIRE_RENAMES` | Adding `gemini-3.8-flash -> gemini-3.8-flash-tiered` would invent a wire id no source proves. `002` shows CCA has no `-tiered` row for 3.8, and no AI Studio deployment is known to. | +| `src/adapters/client-fingerprint.ts` | Its 3.7 mention is a comment about UA-gated 404s, not a model list. | +| `src/providers/command-code-efforts.ts` | Keyed by what Command Code's live roster returns; no 3.8 row observed. | +| `src/providers/model-rename-migration.ts` | Nothing retired — see `020`. | +| `google-vertex` `defaultModel` | Frozen pending Vertex-specific evidence. `001` does prove the Agent Platform id, but this provider's default was deliberately frozen and moving it is a separate decision. | +| OrcaRouter / OpenRouter seeds | OpenRouter DOES publish `google/gemini-3.8-flash` (`001`), but seeding router catalogs is out of this unit's scope; recorded as a follow-up. | +| `tests/fixtures/commandcode-models.json` | A recorded upstream fixture; editing it would falsify a capture. | + +## Focused verification for this phase + +Stale exact assertions this phase must update (audit blocker 3): + +- `tests/google-hardening.test.ts:777` — exact `google?.models` array gains `gemini-3.8-flash`, + plus context-window/modality/effort assertions mirroring the 3.7 rows. Note its ladder + assertion must be `["low","medium","high"]` with no `minimal`. +- `tests/google-models-listing.test.ts:360` — exact discovered-id array. + +```bash +bun test tests/gemini-web-search.test.ts tests/cursor-effort-table.test.ts \ + tests/cursor-effort-suffix.test.ts tests/cursor-catalog.test.ts \ + tests/codex-catalog.test.ts tests/provider-registry-parity.test.ts \ + tests/google-hardening.test.ts tests/google-models-listing.test.ts \ + tests/sidecar-settings-web-search-gate.test.ts +bun run typecheck +``` diff --git a/devlog/_plan/260903_gemini_38_rollout/040_wp4_delivery.md b/devlog/_plan/260903_gemini_38_rollout/040_wp4_delivery.md new file mode 100644 index 0000000000..32c00b04f6 --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/040_wp4_delivery.md @@ -0,0 +1,70 @@ +# 040 — wp4: delivery + +## Branch and commits + +This worktree starts detached at `529639a57`. Adopt in place (WORKTREE-GUARD-01): + +```bash +git switch -c codex/gemini-3.8-flash-rollout +``` + +One commit per work-phase (DEV-GIT-COMMIT-01): the docs unit, then wp1, wp2, wp3. + +## The push constraint, stated exactly + +The user said `로컬스위트는 절대 돌리지 말고 no verify로 푸시하고`. The repository's pre-push +hook runs the full suite, which is precisely what is forbidden, so: + +```bash +git push --no-verify -u origin codex/gemini-3.8-flash-rollout +``` + +`--no-verify` bypasses the LOCAL hook only. It does not and cannot bypass branch protection: +`dev`, `main` and `preview` carry rulesets requiring a reviewed PR, so a direct push to `dev` +is rejected regardless. This is a feature branch push, which is allowed. + +## Pull request + +Target `dev` (never `main`). Fill all three template sections from +`.github/PULL_REQUEST_TEMPLATE.md`: Summary, Verification, Checklist. No GUI change, so no +screenshot is required — but the description must not mention `gui`, or `enforce-target` will +demand one. + +The Verification section lists the focused commands actually run and states plainly that the +repository-wide suite was not run locally by the maintainer's instruction, with CI as the gate. + +## CI evidence standard + +`gh pr checks --required` returning empty is NOT green evidence. Read the full current rollup +for the exact head SHA: + +```bash +HEAD_SHA=$(git rev-parse HEAD) +gh pr checks --watch +gh api repos/:owner/:repo/commits/$HEAD_SHA/check-runs --jq '.check_runs[] | "\(.name) \(.status) \(.conclusion)"' +``` + +A rollup for a stale SHA proves nothing about the head being merged. + +## Merge and landing proof + +The user pre-authorized the merge (`ci 보고 바로 머지해놔`), scoped to this PR after CI is read. +Squash-merge, then prove the merge actually landed rather than trusting the API response: + +```bash +git fetch origin dev +git merge-base --is-ancestor FETCH_HEAD && echo LANDED +``` + +## Post-merge runtime check (optional but cheap) + +The user's proxy runs from a source checkout on port 10100. After the merge, that checkout can +be refreshed and `ocx models live --provider google-antigravity --json` should show one +`gemini-3.8-flash` row with `reasoningEfforts: ["low","medium","high"]` instead of today's three +effortless rows. Do NOT restart the user's service without asking; report the command instead. + +## Terminal outcomes for this phase + +- `DONE` — merged with ancestry proof. +- `BLOCKED` — CI red for a cause outside this change, or protection refuses the merge. +- `NEEDS_HUMAN` — a reviewer raises a scope question only the maintainer can settle. diff --git a/devlog/_plan/260903_gemini_38_rollout/050_followups.md b/devlog/_plan/260903_gemini_38_rollout/050_followups.md new file mode 100644 index 0000000000..63bf53f03d --- /dev/null +++ b/devlog/_plan/260903_gemini_38_rollout/050_followups.md @@ -0,0 +1,27 @@ +# 050 — follow-ups deliberately out of this unit + +Recorded rather than silently dropped, so a later unit can pick them up with the evidence +already attached. + +1. **`ANTIGRAVITY_WIRE_MODELS` is dead data.** The audit confirmed no consumer outside its own + declaration; discovery never reads it. It reads like a source of truth and is not one. + Deleting it is a cleanup with its own review surface, not a line in a model rollout. + +2. ~~**The direct `google` 3.7 row advertises `minimal`.**~~ FOLDED into this PR after the + maintainer review asked (see `006`): the evidence is the same one 3.8 relies on, and the + line was already being edited here. 3.5 and 3.6 keep theirs. + +2b. **`gemini-3.5-flash` has no `modelInputModalities` entry** on the direct `google` provider, + even though it is that provider's `defaultModel`. Pre-existing and unrelated to this diff, + but a default model with no advertised modalities is worth its own evidence pass. + +3. **OpenRouter publishes `google/gemini-3.8-flash`** (`001`). Seeding router catalogs is out of + scope here, but the id is proven whenever that unit happens. + +4. **Vertex.** `001` proves the Agent Platform publisher id + `publishers/google/models/gemini-3.8-flash`. `google-vertex.defaultModel` was deliberately + frozen pending Vertex-specific evidence; unfreezing it is a separate decision. + +5. **`ANTIGRAVITY_SUFFIX_TIER_MODELS` and `gemini-3.1-pro`.** 3.1 Pro keeps emitting + `thinkingLevel` beside a suffix wire id for `low`. Its `high` rung (`gemini-pro-agent`) has + no suffix, so the set cannot simply include it; sorting out that asymmetry is its own task. diff --git a/docs-site/src/content/docs/guides/sidecars.md b/docs-site/src/content/docs/guides/sidecars.md index 0606129f39..d0c79d272e 100644 --- a/docs-site/src/content/docs/guides/sidecars.md +++ b/docs-site/src/content/docs/guides/sidecars.md @@ -27,7 +27,7 @@ a missing credential produces no sidecar plan and the request takes the normal r | Backend | Runs | Credential | Notes | | --- | --- | --- | --- | | `xai` | Grok hosted `web_search` (+ opt-in `x_search`) on `api.x.ai` Responses | Stored Grok OAuth (`ocx login xai`) | `webSearchSidecar.xSearch` enables X search with `allowedXHandles`/`excludedXHandles` (max 20, mutually exclusive) and ISO `fromDate`/`toDate`. Default model `grok-4.6`. | -| `gemini` | `google_search` grounding on the Antigravity transport | Stored Antigravity OAuth with a discovered project (`ocx login google-antigravity`) | Default model `gemini-3.7-flash`; reasoning maps to the tiered thinking level. | +| `gemini` | `google_search` grounding on the Antigravity transport | Stored Antigravity OAuth with a discovered project (`ocx login google-antigravity`) | Default model `gemini-3.8-flash`; reasoning selects the matching tier. | | `exa` | Exa Search API (non-LLM result digest) | `webSearchSidecar.exaApiKey` | The key is write-only through the management API (never echoed, redacted from logs). No sidecar model applies. | ## Web-search sidecar diff --git a/scripts/model-metadata.source.json b/scripts/model-metadata.source.json index 654e324285..8cc73ca1c3 100644 --- a/scripts/model-metadata.source.json +++ b/scripts/model-metadata.source.json @@ -12062,6 +12062,25 @@ "maxLevel": "high" } }, + "gemini-3.8-flash": { + "id": "gemini-3.8-flash", + "name": "Gemini 3.8 Flash", + "api": "google-generative-ai", + "provider": "google", + "baseUrl": "https://generativelanguage.googleapis.com/v1beta", + "reasoning": true, + "input": [ + "text", + "image" + ], + "contextWindow": 1048576, + "maxTokens": 65536, + "thinking": { + "mode": "google-level", + "minLevel": "low", + "maxLevel": "high" + } + }, "gemini-flash-latest": { "id": "gemini-flash-latest", "name": "Gemini Flash Latest", diff --git a/src/adapters/cursor/catalog.ts b/src/adapters/cursor/catalog.ts index 5e8f82c794..f32249f051 100644 --- a/src/adapters/cursor/catalog.ts +++ b/src/adapters/cursor/catalog.ts @@ -205,6 +205,12 @@ export const CURSOR_CAPABILITIES: Record = { defaultVariant: "regular", variants: { regular: { levels: ["low", "medium", "high"] } }, }, + "gemini-3.8-flash": { + displayName: "Gemini 3.8 Flash", + window: CONTEXT_GEMINI, + defaultVariant: "regular", + variants: { regular: { levels: ["low", "medium", "high"] } }, + }, "kimi-k3": { displayName: "Kimi K3", window: CONTEXT_1M, diff --git a/src/adapters/cursor/effort-map.ts b/src/adapters/cursor/effort-map.ts index 8855892f06..2f1bcb8fc9 100644 --- a/src/adapters/cursor/effort-map.ts +++ b/src/adapters/cursor/effort-map.ts @@ -42,6 +42,10 @@ const CURSOR_MODEL_EFFORT_TIERS: Record = { // listing it here is also what admits the suffix into CANONICAL_EFFORT_SUFFIXES below. "gemini-3.6-flash": ["minimal", "low", "medium", "high"], "gemini-3.7-flash": ["low", "medium", "high"], + // 260903 preemptive: gemini-3.8-flash seeded ahead of Cursor's lineup update, the same way + // glm-5.3 was. Google documents low/medium/high with no `minimal` for this generation, + // unlike 3.6. The seed is inert until Cursor's live roster lists the id. + "gemini-3.8-flash": ["low", "medium", "high"], // Explicit-thinking variants (260825 live roster). Tiers are the rungs the wire actually // lists for each family, which is not always the same set the non-thinking id carries: // 4.6-opus thinks only at high/max, 4.5-opus only at high, 4.6-sonnet only at medium. diff --git a/src/adapters/google.ts b/src/adapters/google.ts index 01189a5938..7af5bbdbf1 100644 --- a/src/adapters/google.ts +++ b/src/adapters/google.ts @@ -30,7 +30,7 @@ import { clearAntigravityReplay, observeAntigravityReplay, } from "./google-antigravity-replay"; -import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models"; +import { canonicalAntigravityUsageModel, resolveAntigravityEffortWireModel } from "../providers/antigravity-models"; import { googleVertexLocationConfigError } from "../providers/google-vertex-location"; import { forgetThoughtSignatureForReplay, lookupReplayThoughtSignature } from "../responses/thought-signature-replay"; import { @@ -57,6 +57,43 @@ const GOOGLE_BREVITY_INSTRUCTION = [ const ANTIGRAVITY_REJECTED_CLAUDE_SDK_PARAGRAPH = "You are a Claude agent, built on Anthropic's Claude Agent SDK."; +/** + * CCA Flash generations that reject the Claude-Agent identity paragraph. + * + * Membership is probe-established per generation, never assumed: 3.7 and 3.8 both answer + * 429 RESOURCE_EXHAUSTED when this paragraph survives into `systemInstruction`, and 200 with + * it stripped — same account, seconds apart. A policy rejection wearing a quota error's + * clothing sends users hunting a quota problem that does not exist, so a new generation is + * added here only after the probe, and never dropped on the assumption that Google fixed it. + */ +const ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS = new Set([ + "gemini-3.7-flash", + "gemini-3.8-flash", +]); + +/** + * Whether CCA rejects the Claude-Agent identity paragraph for this request. + * + * Judged on the ROUTED WIRE id, not the selector, because three different selectors reach the + * same rejecting generation: + * + * - the collapsed base (`gemini-3.8-flash`); + * - a raw suffix id (`gemini-3.8-flash-high`), which the picker publishes whenever discovery + * returns a PARTIAL ladder; + * - a RETIRED id (`gemini-3.6-flash`), which rule 0 redirects onto `gemini-3.7-flash-tiered`. + * + * That last one is why a selector-keyed test is not enough: retired ids deliberately keep their + * own identity for usage accounting, so they never canonicalize into the generation they + * actually call. A saved 3.6 selection was probed at 429 with the paragraph intact for exactly + * this reason. Matching on the wire id also means a future generation is covered by naming its + * wire spelling once, rather than every selector that can reach it. + */ +function rejectsClaudeSdkParagraph(modelId: string, wireModelId: string): boolean { + const canonicalWire = canonicalAntigravityUsageModel(wireModelId.replace(/-tiered$/, "")); + return ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS.has(canonicalWire) + || ANTIGRAVITY_CLAUDE_SDK_PARAGRAPH_REJECTORS.has(canonicalAntigravityUsageModel(modelId)); +} + function stripAntigravityRejectedClaudeSdkParagraph(systemText: string): string { return systemText .split("\n\n") @@ -748,7 +785,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte // AI Studio's `-tiered` spelling is wire-only; CCA aliases may migrate to another generation. const identityModelId = provider.googleMode === "cloud-code-assist" ? routedModelId : parsed.modelId; const stripRejectedClaudeSdkParagraph = provider.googleMode === "cloud-code-assist" - && parsed.modelId === "gemini-3.7-flash"; + && rejectsClaudeSdkParagraph(parsed.modelId, routedModelId); const { systemInstruction, contents, replayedCallIds } = messagesToGeminiFormat( parsed, identityModelId, diff --git a/src/generated/model-metadata.ts b/src/generated/model-metadata.ts index dea2a49baf..73b9dee5d5 100644 --- a/src/generated/model-metadata.ts +++ b/src/generated/model-metadata.ts @@ -42,7 +42,7 @@ const DATA: Record = { "azure-openai": [["gpt-4.1",1047576,32768,"text,image",0,null,2,8,0.5,0],["gpt-4o",128000,16384,"text,image",0,null,2.5,10,1.25,0],["gpt-4o-mini",128000,16384,"text,image",0,null,0.15,0.6,0.075,0],["o3",200000,100000,"text,image",1,null,2,8,0.5,0],["o3-mini",200000,100000,"text",1,null,1.1,4.4,0.55,0]], "cerebras": [["gemma-4-31b",131072,40960,"text,image",1,null,0.99,1.49,0,0],["gpt-oss-120b",131072,40960,"text",1,null,0.35,0.75,0,0],["llama3.1-8b",32000,8000,"text",0,null,0.1,0.1,0,0],["qwen-3-235b-a22b-instruct-2507",131000,32000,"text",0,null,0.6,1.2,0,0],["qwen-3-coder-480b",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.6",131072,32768,"text",0,null,0,0,0,0],["zai-glm-4.7",131072,40960,"text",1,null,2.25,2.75,2.25,0]], "deepseek": [["deepseek-v4-flash",1048576,384000,"text",1,null,0.14,0.28,0.0028,0],["deepseek-v4-pro",1048576,384000,"text",1,null,0.435,0.87,0.003625,0]], - "google": [["deep-research-max-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["deep-research-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["gemini-1.5-flash",1000000,8192,"text,image",0,null,0.075,0.3,0.01875,0],["gemini-1.5-flash-8b",1000000,8192,"text,image",0,null,0.0375,0.15,0.01,0],["gemini-1.5-pro",1000000,8192,"text,image",0,null,1.25,5,0.3125,0],["gemini-2.0-flash",1048576,8192,"text,image",0,null,0.1,0.4,0.025,0],["gemini-2.0-flash-lite",1048576,8192,"text,image",0,null,0.075,0.3,0,0],["gemini-2.5-computer-use-preview-10-2025",131072,65536,"text,image",1,null,1.25,10,0,0],["gemini-2.5-flash",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-2.5-flash-lite",1048576,65536,"text,image",1,null,0.1,0.4,0.01,0],["gemini-2.5-flash-lite-preview-06-17",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-lite-preview-09-2025",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-preview-04-17",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-05-20",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-09-2025",1048576,65536,"text,image",1,null,0.3,2.5,0.075,0],["gemini-2.5-pro",1048576,65536,"text,image",1,null,1.25,10,0.125,0],["gemini-2.5-pro-preview-05-06",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-2.5-pro-preview-06-05",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-3-flash-preview",1048576,65536,"text,image",1,null,0.5,3,0.05,0],["gemini-3-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-flash-lite",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-lite-image",65536,65536,"text,image",1,null,0.25,30,0,0],["gemini-3.1-flash-lite-preview",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-live-preview",131072,65536,"text,image",1,null,0.75,4.5,0,0],["gemini-3.1-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-pro-preview-customtools",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.5-flash",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-3.5-flash-lite",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-3.6-flash",1048576,65536,"text,image",1,null,1.5,7.5,0.15,0],["gemini-3.7-flash",1048576,65536,"text,image",1],["gemini-flash-latest",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-flash-lite-latest",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-live-2.5-flash",128000,8000,"text,image",1,null,0.5,2,0,0],["gemini-live-2.5-flash-preview-native-audio",131072,65536,"text",1,null,0.5,2,0,0],["gemini-robotics-er-1.6-preview",131072,65536,"text,image",1,null,1,5,0,0],["gemma-3-27b-it",131072,8192,"text,image",0,null,0,0,0,0],["gemma-4-26b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-26b-a4b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-26b-it",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-E2B-it",131072,8192,"text,image",1,null,0,0,0,0],["gemma-4-E4B-it",131072,8192,"text,image",1,null,0,0,0,0]], + "google": [["deep-research-max-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["deep-research-preview-04-2026",131072,65536,"text,image",1,null,2,12,0.2,0],["gemini-1.5-flash",1000000,8192,"text,image",0,null,0.075,0.3,0.01875,0],["gemini-1.5-flash-8b",1000000,8192,"text,image",0,null,0.0375,0.15,0.01,0],["gemini-1.5-pro",1000000,8192,"text,image",0,null,1.25,5,0.3125,0],["gemini-2.0-flash",1048576,8192,"text,image",0,null,0.1,0.4,0.025,0],["gemini-2.0-flash-lite",1048576,8192,"text,image",0,null,0.075,0.3,0,0],["gemini-2.5-computer-use-preview-10-2025",131072,65536,"text,image",1,null,1.25,10,0,0],["gemini-2.5-flash",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-2.5-flash-lite",1048576,65536,"text,image",1,null,0.1,0.4,0.01,0],["gemini-2.5-flash-lite-preview-06-17",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-lite-preview-09-2025",1048576,65536,"text,image",1,null,0.1,0.4,0.025,0],["gemini-2.5-flash-preview-04-17",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-05-20",1048576,65536,"text,image",1,null,0.15,0.6,0.0375,0],["gemini-2.5-flash-preview-09-2025",1048576,65536,"text,image",1,null,0.3,2.5,0.075,0],["gemini-2.5-pro",1048576,65536,"text,image",1,null,1.25,10,0.125,0],["gemini-2.5-pro-preview-05-06",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-2.5-pro-preview-06-05",1048576,65536,"text,image",1,null,1.25,10,0.31,0],["gemini-3-flash-preview",1048576,65536,"text,image",1,null,0.5,3,0.05,0],["gemini-3-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-flash-lite",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-lite-image",65536,65536,"text,image",1,null,0.25,30,0,0],["gemini-3.1-flash-lite-preview",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-3.1-flash-live-preview",131072,65536,"text,image",1,null,0.75,4.5,0,0],["gemini-3.1-pro-preview",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.1-pro-preview-customtools",1048576,65536,"text,image",1,null,2,12,0.2,0],["gemini-3.5-flash",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-3.5-flash-lite",1048576,65536,"text,image",1,null,0.3,2.5,0.03,0],["gemini-3.6-flash",1048576,65536,"text,image",1,null,1.5,7.5,0.15,0],["gemini-3.7-flash",1048576,65536,"text,image",1],["gemini-3.8-flash",1048576,65536,"text,image",1],["gemini-flash-latest",1048576,65536,"text,image",1,null,1.5,9,0.15,0],["gemini-flash-lite-latest",1048576,65536,"text,image",1,null,0.25,1.5,0.025,0],["gemini-live-2.5-flash",128000,8000,"text,image",1,null,0.5,2,0,0],["gemini-live-2.5-flash-preview-native-audio",131072,65536,"text",1,null,0.5,2,0,0],["gemini-robotics-er-1.6-preview",131072,65536,"text,image",1,null,1,5,0,0],["gemma-3-27b-it",131072,8192,"text,image",0,null,0,0,0,0],["gemma-4-26b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-26b-a4b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-26b-it",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b",256000,8192,"text,image",1,null,0,0,0,0],["gemma-4-31b-it",262144,32768,"text,image",1,null,0,0,0,0],["gemma-4-E2B-it",131072,8192,"text,image",1,null,0,0,0,0],["gemma-4-E4B-it",131072,8192,"text,image",1,null,0,0,0,0]], "minimax": [["MiniMax-M2",196608,128000,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.1",204800,131072,"text",1,null,0.3,1.2,0,0],["MiniMax-M2.5",204800,131072,"text",1,null,0.3,1.2,0.03,0.375],["MiniMax-M2.5-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["MiniMax-M2.5-lightning",204800,32000,"text",1,null,0.3,2.4,0,0],["MiniMax-M2.7",204800,131072,"text",1,null,0.3,1.2,0.06,0.375],["MiniMax-M2.7-highspeed",204800,131072,"text",1,null,0.6,2.4,0.06,0.375],["minimax-m3",512000,128000,"text,image",1,null,0.6,2.4,0.12,0],["MiniMax-M3",1000000,128000,"text,image,video",1,null,0.3,1.2,0.06,0]], "mistral": [["codestral-latest",256000,4096,"text",0,null,0.3,0.9,0,0],["devstral-2512",262144,262144,"text",0,null,0.4,2,0,0],["devstral-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-medium-2507",128000,128000,"text",0,null,0.4,2,0,0],["devstral-medium-latest",262144,262144,"text",0,null,0.4,2,0,0],["devstral-small-2505",128000,128000,"text",0,null,0.1,0.3,0,0],["devstral-small-2507",128000,128000,"text",0,null,0.1,0.3,0,0],["labs-devstral-small-2512",256000,256000,"text,image",0,null,0,0,0,0],["magistral-medium-latest",128000,16384,"text",1,null,2,5,0,0],["magistral-small",128000,128000,"text",1,null,0.5,1.5,0,0],["ministral-3b-latest",128000,128000,"text",0,null,0.04,0.04,0,0],["ministral-8b-latest",128000,128000,"text",0,null,0.1,0.1,0,0],["mistral-large-2411",131072,16384,"text",0,null,2,6,0,0],["mistral-large-2512",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-large-latest",262144,262144,"text,image",0,null,0.5,1.5,0,0],["mistral-medium-2505",131072,131072,"text,image",0,null,0.4,2,0,0],["mistral-medium-2508",262144,262144,"text,image",0,null,0.4,2,0,0],["mistral-medium-2604",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-medium-latest",262144,262144,"text,image",1,null,1.5,7.5,0,0],["mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["mistral-small-2506",128000,16384,"text,image",0,null,0.1,0.3,0,0],["mistral-small-2603",256000,256000,"text,image",1,null,0.15,0.6,0,0],["mistral-small-latest",256000,256000,"text,image",1,null,0.15,0.6,0,0],["open-mistral-7b",8000,8000,"text",0,null,0.25,0.25,0,0],["open-mistral-nemo",128000,128000,"text",0,null,0.15,0.15,0,0],["open-mixtral-8x22b",64000,64000,"text",0,null,2,6,0,0],["open-mixtral-8x7b",32000,32000,"text",0,null,0.7,0.7,0,0],["pixtral-12b",128000,128000,"text,image",0,null,0.15,0.15,0,0],["pixtral-large-latest",128000,128000,"text,image",0,null,2,6,0,0]], "moonshot": [["kimi-k2.5",262144,65536,"text,image",1,null,0,0,0,0]], diff --git a/src/providers/antigravity-models.ts b/src/providers/antigravity-models.ts index 9b9045cb9b..9b01c69cd1 100644 --- a/src/providers/antigravity-models.ts +++ b/src/providers/antigravity-models.ts @@ -13,14 +13,27 @@ import { isModelCacheGenerationCurrent } from "../codex/model-cache"; // ── Wire IDs (what CCA :fetchAvailableModels returns) ── /** Current Antigravity Flash generation. */ -const GEMINI_FLASH_CURRENT = "gemini-3.7-flash"; +const GEMINI_FLASH_CURRENT = "gemini-3.8-flash"; /** - * Wire ID that CCA actually accepts for the current Flash generation. - * Google renamed the model to include a `-tiered` suffix; the picker-visible - * ID stays `gemini-3.7-flash` (stripped by `pickerModelIdForDiscoveredWireId`). + * Previous Flash generation — still served, still picker-visible. + * + * 3.6 vanished from CCA the moment 3.7 shipped, which is why RETIRED_FLASH_TIERS exists. 3.8 + * did not do that: Google documents 3.7 Flash as "remains fully supported", and a 2026-09-03 + * :fetchAvailableModels call returns 3.8, 3.7 AND 3.6 wire ids together. Retiring 3.7 here + * would strand a model the backend is actively serving. + */ +const GEMINI_FLASH_PREVIOUS = "gemini-3.7-flash"; + +/** + * Wire ID that CCA accepts for the RETIRED-tier redirect target (currently 3.7). + * + * Google renamed 3.7 to carry a `-tiered` suffix; the picker-visible ID stays + * `gemini-3.7-flash` (stripped by `pickerModelIdForDiscoveredWireId`). This constant is named + * for its ROLE, not for the current generation: 3.8 is current and has no `-tiered` id, so a + * name like GEMINI_FLASH_WIRE_ID would now point readers at the wrong model. */ -const GEMINI_FLASH_WIRE_ID = "gemini-3.7-flash-tiered"; +const GEMINI_RETIRED_FLASH_TARGET_WIRE_ID = "gemini-3.7-flash-tiered"; /** * Retired Flash ids → the reasoning tier they used to encode. @@ -60,6 +73,9 @@ const ANTIGRAVITY_WIRE_MODELS = [ ]; const ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID: Record = { + "gemini-3.8-flash-low": "gemini-3.8-flash", + "gemini-3.8-flash-medium": "gemini-3.8-flash", + "gemini-3.8-flash-high": "gemini-3.8-flash", "gemini-3.1-pro-low": "gemini-3.1-pro", "gemini-pro-agent": "gemini-3.1-pro", }; @@ -143,6 +159,9 @@ function collapsesIntoKnownPickerModel(candidateId: string): boolean { // Gemini models: effort → wire model suffix (official agy UI pattern). // Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern). export const ANTIGRAVITY_MODEL_EFFORTS: Record = { + // No `minimal`: Google documents it as an error for this generation, and CCA exposes only + // the three tiers. + "gemini-3.8-flash": ["low", "medium", "high"], "gemini-3.7-flash": ["low", "medium", "high"], "gemini-3.1-pro": ["low", "high"], "claude-sonnet-4-6": ["low", "medium", "high", "max"], @@ -151,12 +170,32 @@ export const ANTIGRAVITY_MODEL_EFFORTS: Record = { // ── Effort → wire model map for Gemini base models ── const ANTIGRAVITY_EFFORT_WIRE_MAP: Record> = { + // 3.8 publishes one wire id per tier and no `-tiered` row, so its efforts ride the suffix. + // This is the 3.6 shape, not the 3.7 one. + "gemini-3.8-flash": { + low: "gemini-3.8-flash-low", + medium: "gemini-3.8-flash-medium", + high: "gemini-3.8-flash-high", + }, "gemini-3.1-pro": { low: "gemini-3.1-pro-low", high: "gemini-pro-agent", }, }; +/** + * Base models whose every effort maps to a wire id that ALREADY encodes the tier. + * + * Sending `thinkingLevel` beside such a suffix states the effort twice, and CCA does not reject + * the contradiction — a `-low` wire id paired with `HIGH` returns 200, so the tier that actually + * ran becomes unknowable from the response. Membership also makes static resolution + * byte-identical to the discovery path, which never emits a thinking level. + * + * `gemini-3.1-pro` is deliberately absent: its `high` rung is `gemini-pro-agent`, which carries + * no tier suffix, so there the level is the only thing naming the effort. + */ +const ANTIGRAVITY_SUFFIX_TIER_MODELS = new Set(["gemini-3.8-flash"]); + function completeDiscoveredEffortWireModelIds( pickerId: string, available: ReadonlyMap>, @@ -178,6 +217,8 @@ function completeDiscoveredEffortWireModelIds( // ── Default effort per Gemini base model ── const ANTIGRAVITY_DEFAULT_EFFORT: Record = { + // Google's documented thinking_level default, and the tier CCA marks `recommended`. + "gemini-3.8-flash": "medium", "gemini-3.1-pro": "high", }; @@ -198,7 +239,7 @@ const ANTIGRAVITY_THINKING_LEVELS = new Set(["low", "medium", "high"]); * Models not listed here use themselves as the wire ID. */ const ANTIGRAVITY_PICKER_TO_WIRE: Record = { - "gemini-3.7-flash": GEMINI_FLASH_WIRE_ID, + "gemini-3.7-flash": GEMINI_RETIRED_FLASH_TARGET_WIRE_ID, }; /** Map a picker-visible base model to its CCA wire ID. Identity when no mapping exists. */ @@ -230,7 +271,7 @@ const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record = { // because `parseAntigravityAvailableModels` uses THIS map to keep a stale CCA // payload from republishing a dead wire id as a picker row. ...Object.fromEntries( - Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_FLASH_WIRE_ID]), + Object.keys(RETIRED_FLASH_TIERS).map(retired => [retired, GEMINI_RETIRED_FLASH_TARGET_WIRE_ID]), ), }; @@ -242,6 +283,7 @@ export const ANTIGRAVITY_MODEL_ALIASES: Record = { // Picker-visible: collapsed base models only. export const ANTIGRAVITY_MODELS = [ GEMINI_FLASH_CURRENT, + GEMINI_FLASH_PREVIOUS, "gemini-3.1-pro", "gemini-3.1-flash-image", "claude-sonnet-4-6", @@ -255,6 +297,9 @@ function isKnownAntigravityPickerModelId(value: string): boolean { // Context windows from the upstream `:fetchAvailableModels` maxTokens per model. const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record = { + "gemini-3.8-flash-low": 1_048_576, + "gemini-3.8-flash-medium": 1_048_576, + "gemini-3.8-flash-high": 1_048_576, "gemini-3.7-flash-tiered": 1_048_576, "gemini-3.1-pro-low": 1_048_576, "gemini-pro-agent": 1_048_576, @@ -266,6 +311,7 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record = { export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record = { // Collapsed base IDs — explicit entries for the picker. + "gemini-3.8-flash": 1_048_576, "gemini-3.7-flash": 1_048_576, "gemini-3.1-pro": 1_048_576, // Wire IDs and aliases via derivation. @@ -283,6 +329,7 @@ export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record = { // carries only text and image parts (`OcxImageContent`, src/types.ts) and the Codex // catalog normalizes `input_modalities` against a closed enum. Advertising a modality // the wire cannot carry would be a promise we break at request time. + "gemini-3.8-flash": ["text", "image"], "gemini-3.7-flash": ["text", "image"], "gemini-3.1-pro": ["text", "image"], "gemini-3.1-flash-image": ["text", "image"], @@ -609,13 +656,15 @@ export function resolveAntigravityEffortWireModel( }; } - // Rule 0: retired Flash id — Google has taken the wire id offline, so route to the - // current generation and carry the tier the retired id encoded. This runs BEFORE the - // suffix check because those ids are aliases, and rule 1 would drop the tier. + // Rule 0: retired Flash id — Google has taken the wire id offline, so route to the 3.7 + // redirect target and carry the tier the retired id encoded. (3.7, not "the current + // generation": 3.8 is current but these ids were retired onto 3.7, which is still served.) + // This runs BEFORE the suffix check because those ids are aliases, and rule 1 would drop + // the tier. const retiredTier = retiredAntigravityFlashTier(modelId); if (retiredTier) { return { - wireModelId: GEMINI_FLASH_WIRE_ID, + wireModelId: GEMINI_RETIRED_FLASH_TARGET_WIRE_ID, thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? retiredTier : retiredTier, }; } @@ -638,8 +687,17 @@ export function resolveAntigravityEffortWireModel( // Rule 2/3: mapped Gemini base model. const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId]; if (effortMap) { - if (effort && effort in effortMap) { - return { wireModelId: effortMap[effort]!, thinkingLevel: effort }; + const suffixTiered = ANTIGRAVITY_SUFFIX_TIER_MODELS.has(modelId); + // Normalize FIRST for suffix-tiered models. The discovery path clamps max/xhigh/ultra to + // `high` before its own lookup, so a static path that skipped the clamp answered `medium` + // for the same request: one input, two tiers, decided by whether discovery happened to run. + const requested = suffixTiered && effort + ? resolveAntigravityThinkingLevel(effort) ?? effort + : effort; + if (requested && requested in effortMap) { + const wireModelId = effortMap[requested]!; + // The suffix already names the tier; see ANTIGRAVITY_SUFFIX_TIER_MODELS. + return suffixTiered ? { wireModelId } : { wireModelId, thinkingLevel: requested }; } const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]!; return { wireModelId: effortMap[defaultEffort]! }; diff --git a/src/providers/free-directory.ts b/src/providers/free-directory.ts index 2e935c40e4..ab6e9b2389 100644 --- a/src/providers/free-directory.ts +++ b/src/providers/free-directory.ts @@ -82,7 +82,10 @@ const CONNECTABLE: Record = { "cloudflare-ai": openAi("https://api.cloudflare.com/client/v4/accounts/{account_id}/ai/v1", "https://dash.cloudflare.com/?to=/:account/ai/workers-ai", { supportLevel: "supported", verification: "official", documentationUrl: "https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/", discovery: "static", liveModels: false, models: ["@cf/meta/llama-3.3-70b-instruct-fp8-fast", "@cf/qwen/qwq-32b"] }), cohere: openAi("https://api.cohere.com/compatibility/v1", "https://dashboard.cohere.com/api-keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.cohere.com/reference/list-models", modelsUrl: "https://api.cohere.com/compatibility/v1/models" }), friendliai: openAi("https://api.friendli.ai/serverless/v1", "https://suite.friendli.ai", { modelsUrl: "https://api.friendli.ai/serverless/v1/models" }), - gemini: { baseUrl: "https://generativelanguage.googleapis.com", dashboardUrl: "https://aistudio.google.com/apikey", adapter: "google", authKind: "key", supportLevel: "supported", verification: "official", documentationUrl: "https://ai.google.dev/api/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true, googleMode: "ai-studio", models: ["gemini-3.7-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"] }, + // `lastVerified` is row-specific here: the model list was re-checked against ai.google.dev + // on 2026-09-03 when 3.8 was added. Bumping the shared LAST_VERIFIED instead would stamp + // that date on every other provider row, none of which was re-checked. + gemini: { baseUrl: "https://generativelanguage.googleapis.com", dashboardUrl: "https://aistudio.google.com/apikey", adapter: "google", authKind: "key", supportLevel: "supported", verification: "official", documentationUrl: "https://ai.google.dev/api/models", lastVerified: "2026-09-03", discovery: "live", liveModels: true, googleMode: "ai-studio", models: ["gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"] }, "github-models": openAi("https://models.github.ai/inference", "https://github.com/settings/tokens", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.github.com/en/github-models/prototyping-with-ai-models", discovery: "static", liveModels: false, models: ["openai/gpt-4.1", "meta/llama-4-scout-17b-16e-instruct"] }), groq: openAi("https://api.groq.com/openai/v1", "https://console.groq.com/keys", { supportLevel: "supported", verification: "official", documentationUrl: "https://console.groq.com/docs/api-reference#models" }), hackclub: openAi("https://ai.hackclub.com/proxy/v1", "https://ai.hackclub.com", { modelsUrl: "https://ai.hackclub.com/proxy/v1/models" }), diff --git a/src/providers/registry.ts b/src/providers/registry.ts index 3f71a1a8a7..1b9ddf4d5b 100644 --- a/src/providers/registry.ts +++ b/src/providers/registry.ts @@ -1736,13 +1736,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // devlog/_plan/260710_provider_hardening/001_research_frontier.md. { id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true, - dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"], - modelContextWindows: { "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576, "gemini-3.7-flash": 1_048_576 }, - modelInputModalities: { "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"], "gemini-3.7-flash": ["text", "image"] }, + dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"], + modelContextWindows: { "gemini-3.8-flash": 1_048_576, "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576, "gemini-3.7-flash": 1_048_576 }, + modelInputModalities: { "gemini-3.8-flash": ["text", "image"], "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"], "gemini-3.7-flash": ["text", "image"] }, modelReasoningEfforts: { + // 3.7 and 3.8 omit `minimal`: Google documents it as a validation error on both model + // pages, so advertising it hands the user a rung the API rejects. 3.5/3.6 keep theirs — + // their pages still list it, and this unit has no evidence to change them. + "gemini-3.8-flash": ["low", "medium", "high"], + "gemini-3.7-flash": ["low", "medium", "high"], "gemini-3.6-flash": ["minimal", "low", "medium", "high"], "gemini-3.5-flash": ["minimal", "low", "medium", "high"], - "gemini-3.7-flash": ["minimal", "low", "medium", "high"], "gemini-3.1-pro-preview": ["low", "medium", "high"], }, jawcodeBundle: "google", extraMetadataAliases: ["gemini"], @@ -1750,7 +1754,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [ // 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API // evidence from ai.google.dev does not establish Vertex publisher availability. { id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] }, - { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.7-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] }, + { id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", allowBaseUrlOverride: true, dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, liveModels: true, defaultModel: "gemini-3.8-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelInputModalities: ANTIGRAVITY_MODEL_INPUT_MODALITIES, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] }, { id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" }, { id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, { id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" }, diff --git a/src/usage/expected-prices.ts b/src/usage/expected-prices.ts index dfa26fe174..fee0af8dd6 100644 --- a/src/usage/expected-prices.ts +++ b/src/usage/expected-prices.ts @@ -58,6 +58,10 @@ const GEMINI_36_FLASH: Cost4 = { input: 1.5, output: 7.5, cacheRead: 0.15, cache // through 2026-12-31, stepping up to $1.50 / $7.50 on 2027-01-01. Revisit this row // then — the promotional rate is dated on the pricing page, not open-ended. const GEMINI_37_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }; +// Gemini 3.8 Flash carries the same published promotional shape as 3.7 through 2026-12-31, +// rising to $1.50 / $7.50 on 2027-01-01. A SEPARATE constant on purpose: equal today, but +// aliasing them would silently drag 3.8 along if 3.7's row is ever re-verified differently. +const GEMINI_38_FLASH: Cost4 = { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }; const MINIMAX_M21_HIGHSPEED: Cost4 = { input: 0.6, output: 2.4, cacheRead: 0.03, cacheWrite: 0.375 }; const KIMI_K3: Cost4 = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3 }; const KIMI_K27_CODE: Cost4 = { input: 0.95, output: 4, cacheRead: 0.19, cacheWrite: 0.95 }; @@ -81,6 +85,7 @@ const ANTHROPIC_PRICING = "https://platform.claude.com/docs/en/about-claude/pric const GEMINI_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-07-22); cacheWrite=0: storage is billed per-hour, not per-token"; const GEMINI_37_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-08-14); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token"; +const GEMINI_38_PRICING = "https://ai.google.dev/gemini-api/docs/pricing (2026-09-03); promotional rate through 2026-12-31, rises to 1.50/7.50 on 2027-01-01; cacheWrite=0: storage is billed per-hour, not per-token"; const MINIMAX_PRICING = "https://platform.minimax.io/docs/guides/pricing-paygo"; const OPENAI_GPT56_PRICING = "https://developers.openai.com/api/docs/pricing"; const DEEPSEEK_PRICING = "https://api-docs.deepseek.com/quick_start/pricing-details-usd; V4 Flash alias transition scheduled 2026-07-24 — re-verify after"; @@ -123,6 +128,10 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ // 3.7 Flash rides CCA, whose billing equivalence to the Developer API list price is // not published, so this is `verified-derived` rather than `verified`: the number is // proven, the claim that Antigravity charges it is inferred. + { provider: "google-antigravity", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: `derived: Gemini 3.8 Flash promotional rate through 2026-12-31 ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "gemini-3.8-flash-low", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "gemini-3.8-flash-medium", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "gemini-3.8-flash-high", cost4: GEMINI_38_FLASH, source: `derived: gemini-3.8-flash ${GEMINI_38_PRICING}`, verifiedAt: "2026-09-03", status: "verified-derived" }, { provider: "google-antigravity", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: `derived: Gemini 3.7 Flash promotional rate through 2026-12-31 ${GEMINI_37_PRICING}`, verifiedAt: "2026-08-14", status: "verified-derived" }, // Retained after the 3.6 retirement: historical usage.jsonl rows still carry these // ids, and dropping the row would silently zero the cost of requests already made. @@ -160,6 +169,7 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ { provider: "google", modelId: "gemini-3.6-flash", cost4: GEMINI_36_FLASH, source: GEMINI_PRICING, verifiedAt: "2026-07-22", status: "verified" }, // Developer API row: the price IS published for this surface, so `verified`. { provider: "google", modelId: "gemini-3.7-flash", cost4: GEMINI_37_FLASH, source: GEMINI_37_PRICING, verifiedAt: "2026-08-14", status: "verified" }, + { provider: "google", modelId: "gemini-3.8-flash", cost4: GEMINI_38_FLASH, source: GEMINI_38_PRICING, verifiedAt: "2026-09-03", status: "verified" }, { provider: "google-antigravity", modelId: "gemini-3.1-pro-preview", cost4: GEMINI_31_PRO, source: GEMINI_PRICING, verifiedAt: "2026-07-20", status: "verified" }, // Antigravity-bundled third-party models — derived from the underlying vendor's // official API price (Antigravity itself bills via subscription quota). diff --git a/src/web-search/index.ts b/src/web-search/index.ts index 8dad3b3d73..719c22efac 100644 --- a/src/web-search/index.ts +++ b/src/web-search/index.ts @@ -23,7 +23,7 @@ const DEFAULT_ANTHROPIC_SIDECAR_MODEL = "claude-sonnet-5"; // Default Grok model for the xai-backed sidecar (probe-verified with hosted tools, devlog 003). const DEFAULT_XAI_SIDECAR_MODEL = "grok-4.6"; // Default Gemini model for the gemini-backed sidecar (CCA grounding probe, devlog 002). -const DEFAULT_GEMINI_SIDECAR_MODEL = "gemini-3.7-flash"; +const DEFAULT_GEMINI_SIDECAR_MODEL = "gemini-3.8-flash"; // "low" is the lightest effort the ChatGPT backend allows with web_search ("minimal" is rejected: // "tools cannot be used with reasoning.effort 'minimal'") — keeps the sidecar fast/cheap. const DEFAULT_SIDECAR_REASONING = "low"; diff --git a/tests/codex-catalog.test.ts b/tests/codex-catalog.test.ts index 993887c645..4ae9926b30 100644 --- a/tests/codex-catalog.test.ts +++ b/tests/codex-catalog.test.ts @@ -3798,7 +3798,7 @@ describe("Codex catalog routed normalization", () => { expect(fetchCalls).toBe(0); expect(ids).toEqual([...(provider.models ?? [])].sort()); - expect(ids).toHaveLength(6); + expect(ids).toHaveLength(7); expect(getProviderDiscoveryStatus(providerName)).toBeUndefined(); markProviderDiscoveryFailed(providerName, { reason: "http", httpStatus: 404 }); diff --git a/tests/gemini-37-flash-migration.test.ts b/tests/gemini-37-flash-migration.test.ts index c577c76060..968de54fb7 100644 --- a/tests/gemini-37-flash-migration.test.ts +++ b/tests/gemini-37-flash-migration.test.ts @@ -35,16 +35,26 @@ const RETIRED_TIERS: Record = { }; describe("Gemini 3.7 Flash replaces 3.6 on Antigravity", () => { - test("3.7 is the only picker-visible Flash model", () => { + test("no retired Flash id is picker-visible", () => { expect(ANTIGRAVITY_MODELS).toContain("gemini-3.7-flash"); for (const retired of Object.keys(RETIRED_TIERS)) { expect(ANTIGRAVITY_MODELS).not.toContain(retired); } }); + test("3.7 survives the 3.8 launch instead of being retired with it", () => { + // Google documents 3.7 Flash as "remains fully supported" and CCA still serves it, so + // unlike the 3.6 generation it must NOT join the retired map when its successor ships. + // A regression here would strand every saved 3.7 selection on a redirect it never needed. + expect(RETIRED_TIERS).not.toHaveProperty("gemini-3.7-flash"); + expect(ANTIGRAVITY_MODELS).toContain("gemini-3.7-flash"); + expect(ANTIGRAVITY_MODEL_EFFORTS["gemini-3.7-flash"]).toEqual(["low", "medium", "high"]); + }); + test("the provider default points at the live model", () => { const entry = PROVIDER_REGISTRY.find(row => row.id === "google-antigravity"); - expect(entry?.defaultModel).toBe("gemini-3.7-flash"); + expect(entry?.defaultModel).toBe("gemini-3.8-flash"); + expect(entry?.models).toContain("gemini-3.8-flash"); expect(entry?.models).toContain("gemini-3.7-flash"); }); @@ -323,3 +333,85 @@ describe("the -tiered wire rename reaches the request path", () => { } }); }); + +// Gemini 3.8 arrives on top of 3.7 rather than replacing it: Google documents 3.7 as still +// fully supported and CCA serves 3.5/3.6/3.7/3.8 together. It also publishes three suffixed +// wire ids and no `-tiered` row, so its tiers ride the suffix — the 3.6 shape, not the 3.7 one. +describe("Gemini 3.8 Flash lands additively with a suffix ladder", () => { + test("3.8 is picker-visible with its own capability records", () => { + expect(ANTIGRAVITY_MODELS).toContain("gemini-3.8-flash"); + expect(ANTIGRAVITY_MODEL_CONTEXT_WINDOWS["gemini-3.8-flash"]).toBe(1_048_576); + expect(ANTIGRAVITY_MODEL_EFFORTS["gemini-3.8-flash"]).toEqual(["low", "medium", "high"]); + // Google documents video/audio/PDF, but this proxy transports only text and image parts. + expect(ANTIGRAVITY_MODEL_INPUT_MODALITIES["gemini-3.8-flash"]).toEqual(["text", "image"]); + // `minimal` errors on this generation, so it must not appear in the ladder. + expect(ANTIGRAVITY_MODEL_EFFORTS["gemini-3.8-flash"]).not.toContain("minimal"); + }); + + test("each effort routes to its own wire id and carries no thinking level", () => { + // The suffix already names the tier. Sending thinkingLevel beside it states the effort + // twice, and CCA accepts the contradiction (a `-low` wire id with HIGH returns 200), so a + // mismatch would silently run at an unknown tier instead of failing loudly. + for (const [effort, wireModelId] of [ + ["low", "gemini-3.8-flash-low"], + ["medium", "gemini-3.8-flash-medium"], + ["high", "gemini-3.8-flash-high"], + ] as const) { + expect(resolveAntigravityEffortWireModel("gemini-3.8-flash", effort)) + .toEqual({ wireModelId }); + } + }); + + test("an unset effort falls to the documented medium default", () => { + expect(resolveAntigravityEffortWireModel("gemini-3.8-flash")) + .toEqual({ wireModelId: "gemini-3.8-flash-medium" }); + }); + + test("efforts above the CCA ladder clamp to high rather than falling back to medium", () => { + for (const effort of ["xhigh", "max", "ultra"]) { + expect(resolveAntigravityEffortWireModel("gemini-3.8-flash", effort)) + .toEqual({ wireModelId: "gemini-3.8-flash-high" }); + } + }); + + test("3.8 usage rows aggregate onto the base while retired ids keep their own identity", () => { + for (const wire of ["gemini-3.8-flash-low", "gemini-3.8-flash-medium", "gemini-3.8-flash-high"]) { + expect(canonicalAntigravityUsageModel(wire)).toBe("gemini-3.8-flash"); + } + // Retirement changes what we CALL, not what we RECORD: historical spend must not migrate. + expect(canonicalAntigravityUsageModel("gemini-3.6-flash-high")).toBe("gemini-3.6-flash-high"); + expect(canonicalAntigravityUsageModel("gemini-3.7-flash")).toBe("gemini-3.7-flash"); + }); + + test("retired 3.6 and 3.5 ids still redirect to 3.7 with their recorded tier", () => { + // 3.8 becoming current must not re-point the retirement target: those ids were retired + // onto 3.7, and 3.7 is still served. + expect(resolveAntigravityEffortWireModel("gemini-3.6-flash-high")) + .toEqual({ wireModelId: "gemini-3.7-flash-tiered", thinkingLevel: "high" }); + expect(resolveAntigravityEffortWireModel("gemini-3.5-flash-extra-low")) + .toEqual({ wireModelId: "gemini-3.7-flash-tiered", thinkingLevel: "low" }); + }); + + test("3.7 keeps its single-wire tiered routing untouched", () => { + expect(resolveAntigravityEffortWireModel("gemini-3.7-flash", "high")) + .toEqual({ wireModelId: "gemini-3.7-flash-tiered", thinkingLevel: "high" }); + }); + + test("Antigravity 3.8 cost resolves to the derived overlay, not a bundled verified price", () => { + // Declaring the overlay is not enough. Bundled generated metadata is consulted FIRST and + // returns status "verified", so a cost block on the google/gemini-3.8-flash source row + // would shadow this overlay and assert a CCA billing equivalence Google never published. + // The source record omits cost precisely so this lookup lands here. + const matched = resolveMatchedPrice("google-antigravity", "gemini-3.8-flash"); + expect(matched?.status).toBe("verified-derived"); + expect(matched?.cost4).toEqual({ input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }); + expect(matched?.source).not.toBe("jawcode"); + }); + + test("the direct Google surface may claim a verified 3.8 price", () => { + // The Developer API price IS published for this surface, so unlike the CCA row it is not + // an inference. + expect(resolveMatchedPrice("google", "gemini-3.8-flash")?.cost4) + .toEqual({ input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0 }); + }); +}); diff --git a/tests/gemini-web-search.test.ts b/tests/gemini-web-search.test.ts index 0f0bd782a1..8dedb42063 100644 --- a/tests/gemini-web-search.test.ts +++ b/tests/gemini-web-search.test.ts @@ -10,6 +10,7 @@ mock.module("../src/oauth/store", () => ({ import { mapCcaGroundedResponse } from "../src/web-search/gemini-executor"; import { findGeminiSidecarProvider, planWebSearch } from "../src/web-search"; +import { resolveAntigravityEffortWireModel } from "../src/providers/antigravity-models"; import { parseRequest } from "../src/responses/parser"; import type { OcxConfig, OcxProviderConfig } from "../src/types"; @@ -72,12 +73,23 @@ describe("mapCcaGroundedResponse (002 live capture shape)", () => { describe("planWebSearch gemini arm (L8)", () => { const healthy = { accounts: [{ id: "a1", credential: { projectId: "proj-1" } }], activeAccountId: "a1" }; - test("explicit gemini + OAuth + projectId -> plan with geminiSidecar and 3.7-flash default", () => { + test("explicit gemini + OAuth + projectId -> plan with geminiSidecar and 3.8-flash default", () => { accountSets = { "google-antigravity": healthy }; const plan = planWebSearch(config({ webSearchSidecar: { backend: "gemini" } }), parsedWithWebSearch(), false, routed, "model", undefined); expect(plan?.backend).toBe("gemini"); expect(plan?.geminiSidecar?.providerName).toBe("google-antigravity"); - expect(plan?.settings.model).toBe("gemini-3.7-flash"); + // Tracks the Antigravity default: the sidecar runs google_search grounding over the same + // CCA transport, so a sidecar pinned to the previous generation would drift from it. + expect(plan?.settings.model).toBe("gemini-3.8-flash"); + }); + + test("a 3.8 sidecar call rides the suffix wire id with no thinking level", async () => { + // The 3.7 case below sends gemini-3.7-flash-tiered plus thinkingLevel because that model + // carries its tier in the request. 3.8 carries it in the wire id instead, so the envelope + // must differ — this asserts the suffix-tier decision actually reached the sidecar path. + const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel("gemini-3.8-flash", "low"); + expect(wireModelId).toBe("gemini-3.8-flash-low"); + expect(thinkingLevel).toBeUndefined(); }); test.each([ diff --git a/tests/google-adapter.test.ts b/tests/google-adapter.test.ts index e7311eae62..295c92dd67 100644 --- a/tests/google-adapter.test.ts +++ b/tests/google-adapter.test.ts @@ -282,12 +282,54 @@ describe("google adapter — Antigravity system prompt compatibility", () => { expect(systemInstructionText(ccaEnvelope.request)).not.toContain(REJECTED_CLAUDE_SDK_PARAGRAPH); }); - test("preserves the paragraph for another Cloud Code Assist model", async () => { + test("removes the rejected paragraph for CCA Gemini 3.8 Flash too", async () => { + // Probed 2026-09-03: 3.8 answers 429 RESOURCE_EXHAUSTED while this paragraph survives + // into systemInstruction, and 200 once stripped. Since 3.8 is now the default, a + // 3.7-only guard would 429 every Claude-Agent-shaped request and report it as quota. + const parsed = systemPromptParsed("gemini-3.8-flash"); + const envelope = JSON.parse((await createGoogleAdapter(ccaProvider).buildRequest(parsed)).body) as { + request: Record; + }; + + expect(systemInstructionText(envelope.request)).not.toContain(REJECTED_CLAUDE_SDK_PARAGRAPH); + }); + + test("removes it for a raw 3.8 suffix selector published by a partial ladder", async () => { + // When CCA returns an incomplete tier set the picker publishes raw suffix ids, so + // parsed.modelId can be the wire id rather than the collapsed base — exactly the ids the + // 429 probe used. A base-only membership test would lose the guard precisely when CCA is + // already degraded. + for (const suffixId of ["gemini-3.8-flash-low", "gemini-3.8-flash-medium", "gemini-3.8-flash-high"]) { + const built = await createGoogleAdapter(ccaProvider).buildRequest(systemPromptParsed(suffixId)); + const envelope = JSON.parse(built.body) as { request: Record }; + + expect(systemInstructionText(envelope.request)).not.toContain(REJECTED_CLAUDE_SDK_PARAGRAPH); + } + }); + + test("removes it for a RETIRED id that rule 0 redirects onto the rejecting generation", async () => { + // A saved gemini-3.6-flash selection does not call 3.6 — rule 0 routes it to + // gemini-3.7-flash-tiered, which rejects the paragraph (probed at 429 with it intact). + // Retired ids deliberately keep their own identity for usage accounting, so they never + // canonicalize into the generation they actually reach; judging the SELECTOR would leave + // every saved 3.6/3.5 config broken. This is why the guard reads the routed wire id. const parsed = systemPromptParsed("gemini-3.6-flash"); const envelope = JSON.parse((await createGoogleAdapter(ccaProvider).buildRequest(parsed)).body) as { request: Record; }; + expect(systemInstructionText(envelope.request)).not.toContain(REJECTED_CLAUDE_SDK_PARAGRAPH); + }); + + test("preserves the paragraph for a Cloud Code Assist model that does not reject it", async () => { + // Membership is probe-established per generation, so a model with no recorded rejection + // keeps its system prompt byte-identical. Claude-on-Antigravity is the natural control: + // the paragraph is literally true for it. + const parsed = systemPromptParsed("claude-sonnet-4-6"); + const envelope = JSON.parse((await createGoogleAdapter(ccaProvider).buildRequest(parsed)).body) as { + request: Record; + }; + expect(systemInstructionText(envelope.request)).toContain(REJECTED_CLAUDE_SDK_PARAGRAPH); }); diff --git a/tests/google-antigravity-wire.test.ts b/tests/google-antigravity-wire.test.ts index d00ba03b8a..65d74be116 100644 --- a/tests/google-antigravity-wire.test.ts +++ b/tests/google-antigravity-wire.test.ts @@ -76,9 +76,11 @@ describe("antigravity CCA envelope", () => { expect(req.url).toBe("https://daily-cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"); }); - test("exposes Gemini 3.7 Flash while retired Flash ids resolve to it", async () => { - // Collapsed picker: base models only. + test("exposes Gemini 3.8 and 3.7 Flash while retired Flash ids resolve to 3.7", async () => { + // Collapsed picker: base models only. 3.8 leads because CCA ranks it first in the + // Recommended sort; 3.7 stays because Google still serves it. expect(ANTIGRAVITY_MODELS).toEqual([ + "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.1-pro", "gemini-3.1-flash-image", @@ -127,6 +129,38 @@ describe("antigravity CCA envelope", () => { } }); + test("static and discovered resolution agree for every 3.8 effort", () => { + // These two paths answered differently for the same input: discovery clamped max/xhigh/ + // ultra to `high` before its lookup, while static resolution failed the `in effortMap` + // test and fell back to the medium default. Same request, two tiers, decided by whether + // discovery happened to have run. Asserting the paths SEPARATELY is what let that hide, + // so this compares them directly. + const modelIds = ["gemini-3.8-flash-low", "gemini-3.8-flash-medium", "gemini-3.8-flash-high"]; + const rows = parseAntigravityAvailableModels({ + models: Object.fromEntries(modelIds.map(id => [id, { maxTokens: 1_048_576 }])), + agentModelSorts: [{ groups: [{ modelIds }] }], + })!; + expect(rows.map(model => model.id)).toEqual(["gemini-3.8-flash"]); + + const baseUrl = "https://cca-38-parity.example"; + registerAntigravityDiscoveredWireModels(baseUrl, rows); + for (const effort of [undefined, "low", "medium", "high", "xhigh", "max", "ultra"]) { + expect(resolveAntigravityEffortWireModel("gemini-3.8-flash", effort, baseUrl)) + .toEqual(resolveAntigravityEffortWireModel("gemini-3.8-flash", effort)); + } + }); + + test("a partial 3.8 ladder stays as raw wire rows instead of a half-collapsed model", () => { + // A collapsed row promises three rungs. Publishing it from two would advertise a tier the + // backend did not offer, so the degradation is deliberate: raw suffix ids remain routable. + const modelIds = ["gemini-3.8-flash-low", "gemini-3.8-flash-high"]; + const rows = parseAntigravityAvailableModels({ + models: Object.fromEntries(modelIds.map(id => [id, { maxTokens: 1_048_576 }])), + agentModelSorts: [{ groups: [{ modelIds }] }], + })!; + expect(rows.map(model => model.id)).toEqual(modelIds); + }); + test("collapses a complete CCA Gemini tier set but retains partial sets as wire IDs", () => { const payload = (modelIds: string[]) => ({ models: Object.fromEntries(modelIds.map(id => [id, { maxTokens: 1_048_576 }])), diff --git a/tests/google-hardening.test.ts b/tests/google-hardening.test.ts index 509f61e606..a187b9c3d2 100644 --- a/tests/google-hardening.test.ts +++ b/tests/google-hardening.test.ts @@ -774,7 +774,12 @@ describe("google provider hardening", () => { const vertex = PROVIDER_REGISTRY.find(entry => entry.id === "google-vertex"); expect(google?.defaultModel).toBe("gemini-3.5-flash"); - expect(google?.models).toEqual(["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"]); + expect(google?.models).toEqual(["gemini-3.8-flash", "gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview", "gemini-3.7-flash"]); + expect(google?.modelContextWindows?.["gemini-3.8-flash"]).toBe(1_048_576); + expect(google?.modelInputModalities?.["gemini-3.8-flash"]).toEqual(["text", "image"]); + // `minimal` is a documented validation error on this generation, so the ladder must not + // inherit the shape its 3.5/3.6/3.7 neighbours carry. + expect(google?.modelReasoningEfforts?.["gemini-3.8-flash"]).toEqual(["low", "medium", "high"]); expect(google?.modelContextWindows?.["gemini-3.6-flash"]).toBe(1_048_576); expect(google?.modelContextWindows?.["gemini-3.5-flash"]).toBe(1_000_000); expect(google?.modelContextWindows?.["gemini-3.7-flash"]).toBe(1_048_576); @@ -787,8 +792,10 @@ describe("google provider hardening", () => { expect(google?.modelReasoningEfforts?.["gemini-3.5-flash"]).toEqual([ "minimal", "low", "medium", "high", ]); + // 3.7 and 3.8 exclude `minimal`: Google documents it as a validation error on both model + // pages, so advertising it would offer a rung the API rejects. expect(google?.modelReasoningEfforts?.["gemini-3.7-flash"]).toEqual([ - "minimal", "low", "medium", "high", + "low", "medium", "high", ]); expect(google?.modelReasoningEfforts?.["gemini-3.1-pro-preview"]).toEqual([ "low", "medium", "high", diff --git a/tests/google-models-listing.test.ts b/tests/google-models-listing.test.ts index 39900f8159..5a36700063 100644 --- a/tests/google-models-listing.test.ts +++ b/tests/google-models-listing.test.ts @@ -357,7 +357,7 @@ describe("google models listing via catalog", () => { expect(seen[0].url).toBe("https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000"); expect(seen[0].headers["x-goog-api-key"]).toBe("gk-123"); const ids = models.filter(m => m.provider === "google").map(m => m.id); - expect(ids).toEqual(["gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-3.7-flash"]); + expect(ids).toEqual(["gemini-3.1-pro-preview", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash"]); expect(ids).not.toContain("gemini-3-pro"); expect(ids).not.toContain("gemini-3-flash"); expect(getStaleCached("google")).toBeNull(); diff --git a/tests/oauth-provider-reconcile.test.ts b/tests/oauth-provider-reconcile.test.ts index 08e94c1dda..f57c70b6d5 100644 --- a/tests/oauth-provider-reconcile.test.ts +++ b/tests/oauth-provider-reconcile.test.ts @@ -79,8 +79,9 @@ describe("OAuth provider reconciliation", () => { expect(reconcileOAuthProviders(config)).toBe(true); const provider = config.providers["google-antigravity"]; - expect(provider.defaultModel).toBe("gemini-3.7-flash"); + expect(provider.defaultModel).toBe("gemini-3.8-flash"); expect(provider.models).toEqual([ + "gemini-3.8-flash", "gemini-3.7-flash", "gemini-3.1-pro", "gemini-3.1-flash-image", @@ -93,6 +94,7 @@ describe("OAuth provider reconciliation", () => { expect(provider.models).not.toContain("gemini-3.6-flash-low"); expect(provider.models).not.toContain("gemini-3.6-flash-medium"); expect(provider.models).not.toContain("gemini-3.6-flash-high"); + expect(provider.modelContextWindows?.["gemini-3.8-flash"]).toBe(1_048_576); expect(provider.modelContextWindows?.["gemini-3.7-flash"]).toBe(1_048_576); expect(provider.liveModels).toBe(true); expect(provider.project).toBe("config-project-sentinel"); @@ -104,7 +106,7 @@ describe("OAuth provider reconciliation", () => { }); const persisted = loadConfig(); - expect(persisted.providers["google-antigravity"]?.defaultModel).toBe("gemini-3.7-flash"); + expect(persisted.providers["google-antigravity"]?.defaultModel).toBe("gemini-3.8-flash"); expect(persisted.providers["google-antigravity"]?.liveModels).toBe(true); expect(reconcileOAuthProviders(config)).toBe(false); }); @@ -139,7 +141,40 @@ describe("OAuth provider reconciliation", () => { upsertOAuthProvider(config, "google-antigravity"); expect(config.providers["google-antigravity"].liveModels).toBe(true); - expect(config.providers["google-antigravity"].models).toHaveLength(6); + expect(config.providers["google-antigravity"].models).toHaveLength(7); + }); + + test("an explicit 3.7 default survives the 3.8 launch while its capabilities refresh", () => { + // The 3.5 case above starts from a RETIRED id, so it only exercises the stale-default + // healing branch. This one is the opposite claim, and the one that matters for an + // additive rollout: a user who deliberately chose 3.7 must still be on 3.7 afterwards. + // Google still serves it, so healing it onto 3.8 would be silently overriding a choice. + saveCredential("google-antigravity", { access: "a", refresh: "r", projectId: "p" }); + const config = { + port: 10100, + defaultProvider: "google-antigravity", + providers: { + "google-antigravity": { + adapter: "google", + baseUrl: "https://daily-cloudcode-pa.googleapis.com", + authMode: "oauth", + googleMode: "cloud-code-assist", + defaultModel: "gemini-3.7-flash", + models: ["gemini-3.7-flash"], + liveModels: true, + }, + }, + } satisfies OcxConfig; + + reconcileOAuthProviders(config); + const provider = config.providers["google-antigravity"]; + + expect(provider.defaultModel).toBe("gemini-3.7-flash"); + expect(provider.models).toContain("gemini-3.7-flash"); + expect(provider.models).toContain("gemini-3.8-flash"); + // Capability records still refresh from the registry — preservation is about the + // user's CHOICE, not about freezing the row. + expect(provider.modelReasoningEfforts?.["gemini-3.8-flash"]).toEqual(["low", "medium", "high"]); }); test("preserves an explicit Antigravity static opt-out without the legacy migration marker", () => { diff --git a/tests/provider-registry-parity.test.ts b/tests/provider-registry-parity.test.ts index 5ea6539dd6..573af1b27b 100644 --- a/tests/provider-registry-parity.test.ts +++ b/tests/provider-registry-parity.test.ts @@ -760,7 +760,7 @@ describe("provider registry parity", () => { expect(antigravityRegistry?.liveModels).toBe(true); expect(providerConfigSeed(antigravityRegistry!).liveModels).toBe(true); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.liveModels).toBe(true); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.defaultModel).toBe("gemini-3.7-flash"); + expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.defaultModel).toBe("gemini-3.8-flash"); // Collapsed picker: base models only, no effort-suffix variants. expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.7-flash"); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.1-pro"); @@ -768,13 +768,16 @@ describe("provider registry parity", () => { expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("claude-opus-4-6-thinking"); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gpt-oss-120b-medium"); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.1-flash-image"); - expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toHaveLength(6); + expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toContain("gemini-3.8-flash"); + expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.models).toHaveLength(7); // Effort ladders on collapsed base models. + expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelReasoningEfforts?.["gemini-3.8-flash"]).toEqual(["low", "medium", "high"]); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelReasoningEfforts?.["gemini-3.7-flash"]).toEqual(["low", "medium", "high"]); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelReasoningEfforts?.["gemini-3.1-pro"]).toEqual(["low", "high"]); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelReasoningEfforts?.["claude-opus-4-6-thinking"]).toEqual(["low", "medium", "high", "max"]); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelReasoningEfforts?.["claude-sonnet-4-6"]).toEqual(["low", "medium", "high", "max"]); // Context windows on collapsed base models. + expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelContextWindows?.["gemini-3.8-flash"]).toBe(1_048_576); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelContextWindows?.["gemini-3.7-flash"]).toBe(1_048_576); expect(OAUTH_PROVIDERS["google-antigravity"].providerConfig.modelContextWindows?.["gemini-3.1-pro"]).toBe(1_048_576); // Suffix and compat IDs are NOT in the picker list. diff --git a/tests/usage-cost.test.ts b/tests/usage-cost.test.ts index c840837b91..14d839894a 100644 --- a/tests/usage-cost.test.ts +++ b/tests/usage-cost.test.ts @@ -297,8 +297,8 @@ describe("resolveMatchedPrice", () => { expect(resolveMatchedPrice("openrouter", "anthropic-claude-3.5-sonnet")).toBeNull(); }); - test("16. shipped overlay membership: 59 keys, including canonical Fable 5.1, Opus 5 and compatibility prices", () => { - expect(EXPECTED_PRICE_OVERLAYS.length).toBe(59); + test("16. shipped overlay membership: 64 keys, including canonical Fable 5.1, Opus 5 and compatibility prices", () => { + expect(EXPECTED_PRICE_OVERLAYS.length).toBe(64); expect(EXPECTED_PRICE_OVERLAYS.some(row => row.status === "unverified")).toBe(false); const keys = new Set(EXPECTED_PRICE_OVERLAYS.map(row => `${row.provider}/${row.modelId}`)); for (const expected of [ @@ -315,6 +315,11 @@ describe("resolveMatchedPrice", () => { "minimax-cn/MiniMax-M2.1-highspeed", "deepseek/deepseek-chat", "deepseek/deepseek-reasoner", + "google-antigravity/gemini-3.8-flash", + "google-antigravity/gemini-3.8-flash-low", + "google-antigravity/gemini-3.8-flash-medium", + "google-antigravity/gemini-3.8-flash-high", + "google/gemini-3.8-flash", "google-antigravity/gemini-3.1-pro-low", "google-antigravity/gemini-3.1-pro-high", "google-antigravity/gemini-pro-agent",