diff --git a/devlog/_plan/261003_release_lane_c/000_plan.md b/devlog/_plan/261003_release_lane_c/000_plan.md new file mode 100644 index 00000000000..863565a31ec --- /dev/null +++ b/devlog/_plan/261003_release_lane_c/000_plan.md @@ -0,0 +1,47 @@ +# Preserve discovered Antigravity routing and complete Ollama replay batches + +Lane C will preserve the already-landed Antigravity discovery and snapshot contracts, carry only evidence-supported remaining Claude 5.5 behavior, and repair Ollama commentary replay in a separate PR. The reported Antigravity 404 has not yet been causally reproduced. Coordinator ROADMAP LOCKED was received after its docs-only cycle at 2152fdfeb8; local reflection and independent audit still precede implementation. + +Reader: the release coordinator deciding which independently reviewed changes are safe to integrate; source PRs and release authority remain with the coordinator. + +## Loop contract + +- Archetype/trigger: satisfy-spec, delegated release-stabilization Lane C; C3 provider contracts, C4 care for security-boundary review. +- Goal: attributed independent PRs with successful applicable exact-head CI, resolved valid reviews, and an honest source-disposition report. +- Non-goals: merge/release/source-PR closure, contributor-branch rewrites, installed-app replacement, account/credential mutation, new paid resources, guessed upstream IDs or automatic generation migration. +- Verifier: focused tests named in each decade doc; typecheck, privacy, structure, layout/file-size; applicable PR CI at its current head. Runtime checks are NOT RUN during the initial P/A/docs gate. Package scripts inspected at package.json:50-59; explicit test paths below observe changed behavior. Prose quality remains independent human/agent review, not a runtime-test claim. +- Stop: DONE only after safe changes are published, checks genuinely pass, reviews resolve and full handoff exists. NOOP requires fresh proof content already landed. Missing upstream evidence is a reported limitation and may prevent issue-resolution claims; unresolved unsafe routing is BLOCKED/NEEDS_HUMAN, not fixed. No token/time budget was set, so BUDGET_EXHAUSTED cannot be inferred from waiting. +- Memory/evidence: this unit; ignored .tmp/release-stabilization/report.md and receipt logs. Unpublished security notes belong only in ignored scratch. +- Escalation: new write ownership, destructive operations, missing upstream evidence needed for static routing, or unresolved maintainer objection. Continue independent Ollama work if Antigravity has an external blocker. +- Resources: existing repository tools/GitHub identity; post-unlock bounded provider probes using existing intended credentials only, no refresh/config mutation and no credential output. No user token/cost/wall-clock cap. Commands use bounded process handles. + +## Dependency map and publication + +1. roadmap: docs-only proposal, same-architect reflection, independent A audit, explicit coordinator unlock, docs-only B/C/D. +2. antigravity: consume 010; revalidate #6501/current dev and source #6497, collect bounded upstream evidence, implement the audited safe subset, run its ordinary independent code/security review and focused verification, then publish the coherent slice on codex/release-261003-c with its own required PR CI. +3. ollama: consume 020 independently of Antigravity runtime; execute sequentially in this worktree on codex/release-261003-c-ollama cut from fresh dev, preserving both branch heads. Publish after its own independent review and verification without waiting for the other slice. No parallel branch-changing subagents. +4. handoff: consume 030; collect each published slice's exact-head CI and resolved review evidence, then give the coordinator both heads/source dispositions. The coordinator owns serial merges, lane=all CI, and the full parallel adversarial phase on its integrated candidate before deployment. + +These are independent PRs, not a native stack. Both source PRs currently target dev in contributor forks; successful stack API inspection returned [] for each. + +## Evidence and consultation + +Baseline: origin/dev cb2d1736a858a69b3d95944d531f48b93c2f56a5; #6501 merged as 3bae88cce7400e47a5e69f9fd28749ed25f919f6. #6497 head 742bd6fa33f89fc66d112bafc93bf6753c62a52f; #6509 head de5bb1f215c613670e03585e918a344c510ba97a. #6502 remains open and reports a saved suffix 404 without a wire trace. Source JSON and review snapshots are in ignored scratch. + +V1 architect handle 01a1020f-0150-7a62-ba3e-e2b7bad4494a used a fresh context with inherited model/effort; no model-family independence claimed. Proposal D1-D6 accepted with D2 amended: static 5.5 routing is excluded from the executable patch until a read-only bounded probe supplies exact destination-specific wire evidence and a revised plan is reflected/audited. D3 holds 4.6 migration; D4 carries deterministic usage normalization and derived cost attribution; D5 preserves the local pending-batch owner; D6 expands negative and immutability coverage. Proposal cited the earlier base b82b39018b; main rechecked the relevant unchanged spans after moving to cb2d1736a8. + +Alternatives rejected: wholesale #6497 cherry-pick conflicts with landed discovery and changes saved generation/effort without adequate evidence; generic suffix stripping invents historical usage mappings; one combined PR imposes a false dependency; global history reorder weakens per-batch validation. + +Same-architect reflection: ALIGNED D1-D6, no material design gaps. The independent roadmap audit subsequently passed; implementation/publication evidence is recorded in each PR's Verification section. + +Coordinator gate: the final parallel inherited-subagent regression phase applies to the integrated candidate before deployment. Each lane slice retains its ordinary independent review, local verification and exact-head PR CI. ROADMAP LOCKED released the initial runtime/push/CI gate; no merge/release authority was added. + +Vendor-price evidence opened 2026-10-03: https://platform.claude.com/docs/en/about-claude/pricing lists Sonnet 5.5 input/output/cache-read/5-minute-cache-write 2/10/0.2/2.5 and Opus 5.5 4/20/0.2/5 USD/MTok. This supports underlying reference prices only, not CCA billing. Initial baseline test could not load zod/v4; frozen-lockfile dependency install with lifecycle scripts disabled completed without a lockfile change. + +Post-unlock bounded native evidence: configured daily-cloudcode endpoint returned HTTP 200 discovery with six Claude 5.5 low/medium/high IDs, each maxTokens 1,000,000 and image support. Current unmodified adapter returned HTTP 200/candidates for saved claude-sonnet-5-5-high with conflicting low effort; wire remained high and no thinkingConfig was sent. No token refresh, account writes or installed-app replacement. This single-account evidence cannot establish the original Windows/multi-account cause. #6497 static metadata of 250,000 differs from this live response; no static routing or metadata carry is justified for the observed working path. + +The roadmap audit accepted the explicit eight-row membership assertion update (baseline 152 to 160). Its initial publication hold was superseded by the coordinator's integrated-candidate clarification; the dependency map above states the operative workflow. + +Roadmap A round 2: independent reviewer 01a10216-e0dd-7dc1-8e81-805e9fe2cfcb PASS, no blockers. Fresh-reader check understood safe scope and remaining verification boundaries. Docs-only B finalized this roadmap; next cycle executes 010, preserving current routing. Baselines: usage-cost 102 pass; Ollama native 26 pass; reviewer independently ran discovered families 9 pass. Ordinary bun-run gates exposed the skipped local Bun package postinstall; use the installed Bun binary directly for equivalent checker entrypoints, preserving dependencies and global install. + +Each independently reviewed and locally verified slice may publish immediately and run its own PR CI. Antigravity is not delayed behind Ollama; the lane reports evidence incrementally. diff --git a/devlog/_plan/261003_release_lane_c/010_antigravity.md b/devlog/_plan/261003_release_lane_c/010_antigravity.md new file mode 100644 index 00000000000..f1ea3890f06 --- /dev/null +++ b/devlog/_plan/261003_release_lane_c/010_antigravity.md @@ -0,0 +1,124 @@ +# Claude 5.5 usage attribution while preserving discovered wire routing + +Depends on: audited roadmap and explicit ROADMAP LOCKED. Revalidate current dev and #6497 before B. No new public type/enum or persistence field is introduced. + +## Exact file map + +MODIFY src/providers/antigravity-models.ts only in ANTIGRAVITY_USAGE_BASE_BY_ID (currently :754-775). Preserve discoveredAntigravityEffortWireModelId precedence (:667), snapshot scoping, partial-family behavior and existing 4.6 IDs. Add only deterministic known Claude 5.5 base/tier usage identities before return rev: + +```diff ++ for (const base of ["claude-sonnet-5-5", "claude-opus-5-5"]) { ++ rev[base] = base; ++ for (const effort of ANTIGRAVITY_DISCOVERY_EFFORTS) rev[`${base}-${effort}`] = base; ++ } + return rev; +``` + +MODIFY src/usage/expected-prices.ts after existing Antigravity 4.6 rows (:367-374): adapt the eight 5.5 rows from #6497, retaining existing CLAUDE_SONNET_55/CLAUDE_OPUS_55 constants; ALL eight status fields are verified-derived. Source text says derived Anthropic reference price, not CCA billing. Verify the cited underlying vendor price before carrying a fresh verifiedAt date; do not invent current-price evidence. Preserve 4.6 historical overlay rows. + +MODIFY tests/usage/usage-cost.test.ts membership assertion at :466 from 152 to 160 and include all eight new keys in its reviewed expected set, then adjacent existing Claude 5.5 price cases: add google-antigravity derived source assertions for the base and each tier with literal expected tuples (Sonnet 2/10/0.2/2.5; Opus 4/20/0.2/5, confirmed in the official pricing table on 2026-10-03). Assert 4.6 identity/cost unchanged and unknown future suffix identity unchanged; use tests/usage/usage-summary.test.ts if durable aggregation-level coverage is missing. Check file-size cap before adding; if at cap use NEW tests/usage/usage-antigravity-55.test.ts and register it in both scripts/test-layout/layout.json and tests/fixtures/test-layout-expected.json with the existing usage domain shape. No lowered caps/skips. + +MODIFY structure/providers-and-adapters.md :557-559: append that deterministic known 5.5 usage normalization is independent of discovery and does not migrate saved routing. MODIFY docs-site/src/content/docs/guides/providers.md at Antigravity guidance: reference costs are derived estimates; use discovered models/efforts; do not claim #6502 resolved without a successful matching request. Review all manifest owners for src/providers and src/usage; amend only affected prose. + +## Routing evidence decision before implementation + +Inspect the existing intended Antigravity discovery path and credential availability without printing secrets. After unlock, bound model discovery and at most a minimal request for the reported saved Sonnet 5.5-high selection; retain only wire IDs/effort/status and redacted endpoint identity. No account configuration or installed-service restart. If credentials are absent or discovery/request fails, record that exact limitation and leave #6502 unresolved. Existing #6501 controlled fixture proof does not prove live entitlement or backend availability. + +Static 5.5 fallback, 4.6 retirement, inferred context/image metadata, and cross-generation aliases are NOT in the patch above. Adding static fallback requires evidence plus P/A amendment with exact maps/tests. That amendment must cover medium default, xhigh/max/ultra clamp, suffix override, complete/partial/empty discovery and snapshot invalidation using an unbundled family so a new fallback cannot make invalidation coverage tautological. + +## Acceptance and commands (planned; NOT RUN at initial gate) + +- All known 5.5 base/low/medium/high usage spellings normalize deterministically without requiring registration; unknown model suffixes remain exact; historical 4.6 stays itself. Both src/usage/cost.ts and src/usage/summary.ts consume canonicalAntigravityUsageModel. src/adapters/google.ts:95 also calls it for a Gemini-only rejection predicate; Claude normalization must leave that behavior unchanged. +- Exact discovered family remains authoritative; explicit high plus conflicting low effort yields high wire and no thinkingConfig. Complete/partial/empty discovery, restart/outage and home/destination/generation isolation keep existing tests green. +- Eight reference-price rows carry verified-derived; literal tuple assertions and a source assertion prove provenance. No charge/billing claim. +- Run each affected file in its own Bun process: tests/adapters/google/antigravity-discovered-families.test.ts, tests/adapters/google/google-antigravity-wire.test.ts, tests/adapters/google/antigravity-static-catalog.test.ts, tests/providers/provider-antigravity-effort-families.test.ts, tests/providers/provider-antigravity-family-catalog.test.ts, tests/providers/provider-antigravity-wire-snapshot.test.ts, tests/usage/usage-cost.test.ts, tests/usage/usage-summary.test.ts, tests/providers/provider-registry-parity.test.ts. +- Run bun run typecheck; bun run privacy:scan; bun run structure:check; focused layout and file-size guards; bun run test:changed after examining its scope. Full local suite exception: concurrent stabilization worktrees share resources; record focused commands/counts and remaining platform/CI coverage in PR. +- Independent implementation/security review, actual applicable exact-head PR CI and maintained source credit precede review readiness. + +Credit for adapted source: Co-authored-by: Prince . Source #6497 stays open; report the withheld routing/migration delta explicitly. + +## Execution revalidation + +Previous D concluded: docs-only roadmap complete at 12d0ba48d0; next execute 010 usage/pricing only and preserve current routing. This cycle follows that direction. Live discovery/inference at the baseline supports retaining routing, and official vendor pricing supports the two reference tuples. Baseline usage-cost 102/0; direct installed-Bun tsc/structure/privacy all exit0. usage-cost is uncapped, but usage-summary is exactly at its 2069-line cap. Add the already-planned tests/usage/usage-antigravity-55.test.ts sibling and both layout registrations; do not edit usage-summary. Coordinator clarified that its own integrated candidate gets final parallel review; this slice may publish after scoped independent review and local verification. + +Focused architect 01a1021d-d929-7f41-bad4-e49df00f9633 D1-D6 accepted. Add estimateRequestCost estimated=true assertion, pool provider pricing, required top-level/per-day grouping with literal request/token/cost totals, and unchanged historical/unknown identities. Also update the pricing paragraph in structure/dashboard-and-usage.md. No wire changes. New sibling uses existing summarizeUsage and PersistedUsageEntry interfaces; for each family, low/medium/high rows plus one base row with a resolved high tier produce four requests per family, 4400 tokens and Sonnet 0.012 / Opus 0.024 reference cost at 1000 input+100 output each. Both aggregate and per-day rows must match those literals. Historical 4.6 and unknown future suffix rows remain separate. + +## Complete new sibling test content + +NEW tests/usage/usage-antigravity-55.test.ts (the existing cost file changes only membership; focused new assertions live together here): + +```typescript +import { describe, expect, test } from "bun:test"; +import { canonicalAntigravityUsageModel } from "../../src/providers/antigravity-models"; +import { estimateRequestCost, resolveMatchedPrice } from "../../src/usage/cost"; +import { findExpectedPriceOverlay } from "../../src/usage/expected-prices"; +import type { PersistedUsageEntry } from "../../src/usage/log"; +import { summarizeUsage } from "../../src/usage/summary"; + +const families = [ + { base: "claude-sonnet-5-5", cost4: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, total: 0.012 }, + { base: "claude-opus-5-5", cost4: { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 }, total: 0.024 }, +]; +const now = Date.UTC(2026, 9, 3, 12); + +function row(model: string, index: number, resolvedModel?: string): PersistedUsageEntry { + return { + requestId: `antigravity-fixture-${index}`, timestamp: now - index, + provider: "google-antigravity", model, ...(resolvedModel ? { resolvedModel } : {}), + status: 200, durationMs: 1, usageStatus: "reported", + usage: { inputTokens: 1000, outputTokens: 100 }, totalTokens: 1100, + }; +} + +describe("Antigravity Claude 5.5 usage", () => { + for (const { base, cost4, total } of families) { + test(`${base} has deterministic identity and derived reference prices without discovery`, () => { + for (const id of [base, `${base}-low`, `${base}-medium`, `${base}-high`]) { + expect(canonicalAntigravityUsageModel(id)).toBe(base); + expect(findExpectedPriceOverlay("google-antigravity", id)).toMatchObject({ + provider: "google-antigravity", modelId: id, cost4, status: "verified-derived", + }); + for (const provider of ["google-antigravity", "google-antigravity-pabcdef"]) { + const price = resolveMatchedPrice(provider, id); + expect(price).toMatchObject({ cost4, status: "verified-derived", source: "expected" }); + expect(price?.sourceRef).toContain("derived:"); + expect(price?.sourceRef).toContain("platform.claude.com/docs/en/about-claude/pricing"); + const estimate = estimateRequestCost({ provider, model: id, usageStatus: "reported", usage: { inputTokens: 1000, outputTokens: 100 } }); + expect(estimate?.estimated).toBe(true); + expect(estimate?.cost.total).toBeCloseTo(total / 4, 10); + } + } + }); + + test(`${base} aggregates tiers and resolved IDs in model and day summaries`, () => { + const entries = [row(`${base}-low`, 1), row(`${base}-medium`, 2), row(`${base}-high`, 3), row(base, 4, `${base}-high`)]; + const summary = summarizeUsage(entries, "all", now); + expect(summary.models).toHaveLength(1); + expect(summary.models[0]).toMatchObject({ provider: "google-antigravity", model: base, requests: 4, totalTokens: 4400 }); + expect(summary.models[0]?.resolvedModel).toBeUndefined(); + expect(summary.models[0]?.estimatedCostUsd).toBeCloseTo(total, 10); + const days = summary.days.filter(day => day.requests > 0); + expect(days).toHaveLength(1); + expect(days[0]?.models).toHaveLength(1); + expect(days[0]?.models[0]).toMatchObject({ model: base, requests: 4, totalTokens: 4400 }); + expect(days[0]?.models[0]?.estimatedCostUsd).toBeCloseTo(total, 10); + expect(summary.summary.estimatedCostUsd).toBeCloseTo(total, 10); + }); + } + + test("historical Claude and unknown suffixes retain their exact usage identities", () => { + const ids = ["claude-sonnet-4-6", "claude-opus-4-6-thinking", "claude-sonnet-6-0-high", "claude-sonnet-5-5-ultra"]; + for (const id of ids) expect(canonicalAntigravityUsageModel(id)).toBe(id); + const summary = summarizeUsage(ids.map((id, i) => row(id, i + 1)), "all", now); + expect(summary.models.map(model => model.model).sort()).toEqual([...ids].sort()); + expect(resolveMatchedPrice("google-antigravity", "claude-sonnet-4-6")?.cost4) + .toEqual({ input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }); + expect(resolveMatchedPrice("google-antigravity", "claude-opus-4-6-thinking")?.cost4) + .toEqual({ input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }); + }); +}); +``` + +Same-architect final reflection: ALIGNED D1-D6 after correcting fixture count to four; no material gaps. + +B fixture correction: summarizeUsage all-range deliberately includes a leading empty day (dayCountForAllRange uses ceil(delta/day)+1). Assert exactly one nonempty day and its full model totals; no production change for an incorrect fixture assumption. New tests first failed four behavior cases before the runtime change, then all five passed after the mapping/overlays and this calendar-independent assertion. diff --git a/devlog/_plan/261003_release_lane_c/020_ollama.md b/devlog/_plan/261003_release_lane_c/020_ollama.md new file mode 100644 index 00000000000..bafe97fbb90 --- /dev/null +++ b/devlog/_plan/261003_release_lane_c/020_ollama.md @@ -0,0 +1,304 @@ +# Keep unresolved native batches open across commentary + +Depends on: roadmap/unlock only; runtime independent of 010. Carry #6509 at de5bb1f215c613670e03585e918a344c510ba97a on a separate own branch from current dev. + +MODIFY the four files in the exact proposed source diff below, adapting only current-base conflicts. PendingToolBatch.unresolvedCount is created from wireCalls.length, decremented only after validated result attachment and consumed only by the no-new-call assistant deferral branch. It is request-local internal state: serialization/deserialization N/A; no persisted or public API field. + +The new branch activates with an open batch, unresolvedCount > 0, assistant role and no extracted calls. It defers assistant text/thinking until flushPending emits genuine results in original call order and then releaseDeferred emits conversation arrival order. New tool-call batches and EOF settle missing results with existing unknown markers. Fully resolved batches flush before normal assistant turns. Validation remains before decrement: unknown/duplicate/name/namespace results reject; invalid/reused call IDs retain their existing rules. No parsed input mutation. + +## Negative verification delta beyond source PR + +MODIFY tests/providers/ollama/ollama-native.test.ts (the existing uncapped file; no additional registry entry is needed): construct batches with commentary interleaved before orphan IDs, duplicate result IDs, mismatched tool names and mismatched namespaces; each must throw the existing error. Add old-batch result after a new tool-call batch (must reject), unresolved EOF (must emit exactly one unknown result before commentary), fully resolved result then commentary (must not defer past subsequent conversation), reverse-arrival results in a multi-call batch (must output call order), and deep-frozen parsed context with nested content/call argument objects (must build unchanged). Assert output payload order and values rather than internal counters. Keep all existing malformed-input tests. + +Commands planned, NOT RUN under initial gate: bun test tests/providers/ollama/ollama-native.test.ts; native-parser/native-v4/native-reasoning-wire/native-structured-output files in separate Bun processes; tests/adapters/adapter-tool-conformance.test.ts and adapter-buffered-tool-conformance.test.ts; typecheck/privacy/structure/layout/file-size/test:changed gates. Document full-local-suite exception for concurrent worktrees. No live Ollama service is assumed; absence is an explicit evidence limit. + +Review all src/adapters ownership docs; preserve OpenAI behavior and touch only the Ollama paragraph in chat-compat. User docs describe observed contract, never promise caller-side physical tool execution. Independent code/security review and applicable exact-head CI are required. Source #6509 stays open. + +Credit: Co-authored-by: potota90 <85318310+adtumk@users.noreply.github.com>. + +## Source-PR snapshot and final regression evidence + +The embedded diff below records original #6509, before the carry's additional adversarial tests. Its JSON snapshot assertion is not the deep-freeze proof. The implemented obligation lives in separate [PR #6519](https://github.com/lidge-jun/opencodex/pull/6519): [deep-frozen replay regression at commit 917fa41229, line 589](https://github.com/lidge-jun/opencodex/blob/917fa4122977b7d890ba1115724e2b5a9a7940cd/tests/providers/ollama/ollama-native.test.ts#L589). That test recursively freezes the entire parsed request, including content arrays and the non-empty `{ cmd: "pwd" }` call arguments, before building. It verifies unchanged input plus output call/deferred ordering, thinking and argument values. The ten additional cases and all 42 native tests passed there. No Ollama runtime or test changes are included in the Antigravity pricing PR. + +## Original source diff (before carry additions) + +```diff +diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md +index a765419710..c616f58a0e 100644 +--- a/docs-site/src/content/docs/reference/adapters.md ++++ b/docs-site/src/content/docs/reference/adapters.md +@@ -118,6 +118,10 @@ be configured on a separately named custom or self-hosted Ollama provider with + is refused rather than mis-sent, and remote image URLs are not fetched. + - **Tools:** declared in Ollama's native shape, streamed tool calls are whole-call records with + object-valued `arguments`, and tool-result replay is paired strictly by call id and tool name. ++ Codex may record assistant commentary before a pending call's results. Text/thinking with no ++ new tool calls is deferred until the batch is settled, so genuine results remain beside their ++ originating calls. A new tool-call batch still settles the preceding one; missing results retain ++ an explicit unknown-status marker, and orphan or duplicate results remain invalid. + `tool_choice: "none"` and `auto` behave normally; **`required` or an exact named choice fails + closed**, because Ollama's `/api/chat` has no `tool_choice` field to enforce it with. + - **Structured output is refused on canonical Ollama Cloud.** Ollama currently documents structured +diff --git a/src/adapters/ollama-native.ts b/src/adapters/ollama-native.ts +index 2f95c5dbdc..b180d1f1d5 100644 +--- a/src/adapters/ollama-native.ts ++++ b/src/adapters/ollama-native.ts +@@ -77,6 +77,7 @@ interface PendingToolCall { + interface PendingToolBatch { + calls: PendingToolCall[]; + byId: Map; ++ unresolvedCount: number; + } + + interface NativeStreamToolCall { +@@ -302,6 +303,12 @@ function assistantTextThinkingAndCalls(message: OcxAssistantMessage): { + }; + } + ++/** ++ * Build native replay messages without mutating the parsed history. Keep tool results ++ * beside their originating batch, deferring intervening conversation until settlement. ++ * Reserve replayed call IDs for response translation and mark missing results explicitly. ++ * @throws When call IDs are invalid or results are orphaned, duplicated, or mismatched. ++ */ + function buildNativeMessages( + parsed: OcxParsedRequest, + reservedToolCallIds: Set, +@@ -323,12 +330,14 @@ function buildNativeMessages( + // early. The openai-chat adapter defers them the same way; refusing the replay killed the turn. + let deferred: OllamaNativeMessage[] = []; + ++ /** Emit held conversation messages in their recorded arrival order after settlement. */ + const releaseDeferred = (): void => { + if (deferred.length === 0) return; + messages.push(...deferred); + deferred = []; + }; + ++ /** Settle the open batch with genuine results or unknown markers, then release deferred messages. */ + const flushPending = (): void => { + if (!pending) return; + for (const call of pending.calls) { +@@ -376,13 +385,14 @@ function buildNativeMessages( + throw new Error(`ollama-native tool result ${message.toolCallId} names the wrong originating tool`); + } + call.result = message; ++ pending.unresolvedCount--; + continue; + } + + // Native Ollama requires the whole assistant tool-call turn followed by its tool results. A + // conversational message that arrives while the batch is still open is held aside instead of + // closing it, so the call keeps its results adjacent; it is released right after the batch +- // flushes. Anything else (a new assistant turn) settles the batch first. ++ // flushes. A new assistant tool-call batch settles the preceding batch first. + if (pending) { + if (message.role === "user" || message.role === "developer") { + const translated = message.role === "user" +@@ -393,6 +403,19 @@ function buildNativeMessages( + : { role: "system", content: translated.content }); + continue; + } ++ if (message.role === "assistant" && pending.unresolvedCount > 0) { ++ const extracted = assistantTextThinkingAndCalls(message); ++ if (extracted.calls.length === 0) { ++ // Commentary can follow the call items but precede their recorded results. ++ // Keep the batch open and release its text/thinking after the actual results. ++ deferred.push({ ++ role: "assistant", ++ content: extracted.content, ++ ...(extracted.thinking ? { thinking: extracted.thinking } : {}), ++ }); ++ continue; ++ } ++ } + flushPending(); + } + +@@ -443,7 +466,11 @@ function buildNativeMessages( + }; + messages.push(native); + if (wireCalls.length > 0) { +- pending = { calls: wireCalls, byId: new Map(wireCalls.map(call => [call.id, call])) }; ++ pending = { ++ calls: wireCalls, ++ byId: new Map(wireCalls.map(call => [call.id, call])), ++ unresolvedCount: wireCalls.length, ++ }; + } + break; + } +diff --git a/structure/providers/chat-compat.md b/structure/providers/chat-compat.md +index a469a3c8ef..740bb7a115 100644 +--- a/structure/providers/chat-compat.md ++++ b/structure/providers/chat-compat.md +@@ -122,13 +122,13 @@ and synthesizing explicit "no tool result was recorded" answers only when no rea + (Kimi/Moonshot 400 `ocx-mrqaiw05-269`; unit `devlog/_fin/260718_dangling_toolcall_hardening`). + + The native Ollama wire carries the same contract. `src/adapters/ollama-native.ts` +-`buildNativeMessages` defers `user`/`developer` messages that arrive while a batch is open and +-releases them after the tool messages, and answers a call with no result anywhere in the replayed +-history with the same `[ocx] no tool result was recorded for ""` marker. The shape it +-absorbs is ordinary Codex history, not a malformed one: Codex records mid-turn items (a +-`PostToolUse` hook verdict, a context notice) between an assistant `tool_calls` message and that +-call's own result. The strict pair checks (orphan result, duplicate result, result naming another +-tool) still throw on both wires (#4842). ++`buildNativeMessages` defers `user`/`developer` messages while a batch is open; assistant text/thinking ++with no new tool calls is also deferred while results remain outstanding. Deferred messages retain ++arrival order after recorded results. An unresolved counter avoids rescanning calls per message. ++A new tool-call batch settles its predecessor; completed batches keep subsequent messages in place. ++Codex can record hook notices or commentary between calls and results. Missing results keep the ++`[ocx] no tool result was recorded for ""` marker; orphan IDs, duplicates, and mismatched names ++still throw (#4842). `tests/providers/ollama/ollama-native.test.ts` covers replay and compaction. + + Forward-mode OpenAI passthrough also repairs replayed `call_id` values longer than the Responses + API's 64-character limit. Sidechat/fork replay can namespace routed-provider ids beyond that limit, +diff --git a/tests/providers/ollama/ollama-native.test.ts b/tests/providers/ollama/ollama-native.test.ts +index 44a09e8a8c..c17e5c1967 100644 +--- a/tests/providers/ollama/ollama-native.test.ts ++++ b/tests/providers/ollama/ollama-native.test.ts +@@ -9,12 +9,14 @@ import { getProviderRegistryEntry } from "../../../src/providers/registry"; + import { withStubbedProviderFetch } from "../../helpers/catalog-provider-fetch"; + import type { OcxParsedRequest, OcxProviderConfig } from "../../../src/types"; + ++/** Discover routed test models with provider fetches stubbed to avoid live requests. */ + const gatherRoutedModels: typeof gatherRoutedModelsDirect = (config, options) => + gatherRoutedModelsDirect(withStubbedProviderFetch(config), options); + + /** The four ids this transport is maintained against. */ + const TARGETS = ["glm-5.3-flash", "deepseek-v4-flash:0731", "glm-5.2", "kimi-k3"] as const; + ++/** Configure a native Ollama test provider with inert credentials and caller overrides. */ + function ollamaProvider(overrides: Partial = {}): OcxProviderConfig { + return { + adapter: "ollama-native", +@@ -28,6 +30,7 @@ function ollamaProvider(overrides: Partial = {}): OcxProvider + } as OcxProviderConfig; + } + ++/** Build a minimal streamed replay request from synthetic messages and caller options. */ + function parsedWith( + messages: unknown[], + options: Record = {}, +@@ -285,6 +288,120 @@ describe("ollama-native — request shape", () => { + expect(messages[3].content).toBe("[hook] design findings requiring review"); + }); + ++ test("assistant commentary before parallel results keeps the original batch open", async () => { ++ const adapter = createOllamaNativeAdapter(ollamaProvider()); ++ const { body } = await adapter.buildRequest(parsedWith([ ++ { ++ role: "assistant", ++ content: [ ++ { type: "toolCall", id: "call_commentary_first", name: "exec", arguments: { input: "text('first')" } }, ++ { type: "toolCall", id: "call_commentary_second", name: "exec", arguments: { input: "text('second')" } }, ++ ], ++ timestamp: 1, ++ }, ++ { role: "assistant", content: [{ type: "text", text: "checking both results" }], timestamp: 2 }, ++ { role: "toolResult", toolCallId: "call_commentary_second", toolName: "exec", content: "second result", isError: false, timestamp: 3 }, ++ { role: "toolResult", toolCallId: "call_commentary_first", toolName: "exec", content: "first result", isError: false, timestamp: 4 }, ++ ])); ++ const messages = JSON.parse(String(body)).messages; ++ expect(messages.map((message: { role: string }) => message.role)) ++ .toEqual(["assistant", "tool", "tool", "assistant"]); ++ expect(messages[1]).toMatchObject({ tool_call_id: "call_commentary_first", content: "first result" }); ++ expect(messages[2]).toMatchObject({ tool_call_id: "call_commentary_second", content: "second result" }); ++ expect(messages[3].content).toBe("checking both results"); ++ }); ++ ++ test("assistant commentary after one parallel result preserves the remaining genuine result", async () => { ++ const adapter = createOllamaNativeAdapter(ollamaProvider()); ++ const { body } = await adapter.buildRequest(parsedWith([ ++ { ++ role: "assistant", ++ content: [ ++ { type: "toolCall", id: "call_partial_first", name: "exec", arguments: {} }, ++ { type: "toolCall", id: "call_partial_second", name: "exec", arguments: {} }, ++ ], ++ timestamp: 1, ++ }, ++ { role: "toolResult", toolCallId: "call_partial_first", toolName: "exec", content: "first done", isError: false, timestamp: 2 }, ++ { role: "assistant", content: [{ type: "text", text: "waiting for the second result" }], timestamp: 3 }, ++ { role: "toolResult", toolCallId: "call_partial_second", toolName: "exec", content: "second done", isError: false, timestamp: 4 }, ++ ])); ++ const messages = JSON.parse(String(body)).messages; ++ expect(messages.map((message: { role: string }) => message.role)) ++ .toEqual(["assistant", "tool", "tool", "assistant"]); ++ expect(messages[1].content).toBe("first done"); ++ expect(messages[2].content).toBe("second done"); ++ expect(messages[3].content).toBe("waiting for the second result"); ++ }); ++ ++ test("deferred assistant text and thinking retain their order without mutating parsed history", async () => { ++ const parsed = parsedWith([ ++ { role: "assistant", content: [{ type: "toolCall", id: "call_thinking", name: "exec", arguments: {} }], timestamp: 1 }, ++ { role: "developer", content: "context notice", timestamp: 2 }, ++ { role: "assistant", content: [{ type: "text", text: "first comment" }, { type: "thinking", thinking: "synthetic reasoning" }], timestamp: 3 }, ++ { role: "assistant", content: [{ type: "text", text: "second comment" }], timestamp: 4 }, ++ { role: "toolResult", toolCallId: "call_thinking", toolName: "exec", content: "recorded result", isError: false, timestamp: 5 }, ++ ]); ++ const original = JSON.stringify(parsed); ++ const { body } = await createOllamaNativeAdapter(ollamaProvider()).buildRequest(parsed); ++ const messages = JSON.parse(String(body)).messages; ++ expect(messages.map((message: { role: string }) => message.role)) ++ .toEqual(["assistant", "tool", "system", "assistant", "assistant"]); ++ expect(messages[1].content).toBe("recorded result"); ++ expect(messages[2].content).toBe("context notice"); ++ expect(messages[3]).toMatchObject({ content: "first comment", thinking: "synthetic reasoning" }); ++ expect(messages[4].content).toBe("second comment"); ++ expect(JSON.stringify(parsed)).toBe(original); ++ }); ++ ++ test("assistant commentary after a complete batch keeps its existing wire position", async () => { ++ const { body } = await createOllamaNativeAdapter(ollamaProvider()).buildRequest(parsedWith([ ++ { role: "assistant", content: [{ type: "toolCall", id: "call_complete", name: "exec", arguments: {} }], timestamp: 1 }, ++ { role: "toolResult", toolCallId: "call_complete", toolName: "exec", content: "done", isError: false, timestamp: 2 }, ++ { role: "assistant", content: [{ type: "text", text: "completed comment" }], timestamp: 3 }, ++ { role: "user", content: "next turn", timestamp: 4 }, ++ ])); ++ const messages = JSON.parse(String(body)).messages; ++ expect(messages.map((message: { role: string }) => message.role)) ++ .toEqual(["assistant", "tool", "assistant", "user"]); ++ expect(messages[1].content).toBe("done"); ++ expect(messages[2].content).toBe("completed comment"); ++ expect(messages[3].content).toBe("next turn"); ++ }); ++ ++ test("a new tool-call batch still settles an unresolved batch after assistant commentary", async () => { ++ const { body } = await createOllamaNativeAdapter(ollamaProvider()).buildRequest(parsedWith([ ++ { role: "assistant", content: [{ type: "toolCall", id: "call_unfinished", name: "exec", arguments: {} }], timestamp: 1 }, ++ { role: "assistant", content: [{ type: "text", text: "before the next batch" }], timestamp: 2 }, ++ { role: "assistant", content: [{ type: "toolCall", id: "call_next", name: "exec", arguments: {} }], timestamp: 3 }, ++ { role: "toolResult", toolCallId: "call_next", toolName: "exec", content: "next result", isError: false, timestamp: 4 }, ++ ])); ++ const messages = JSON.parse(String(body)).messages; ++ expect(messages.map((message: { role: string }) => message.role)) ++ .toEqual(["assistant", "tool", "assistant", "assistant", "tool"]); ++ expect(messages[1].tool_call_id).toBe("call_unfinished"); ++ expect(messages[1].content).toContain("execution status unknown"); ++ expect(messages[2].content).toBe("before the next batch"); ++ expect(messages[4]).toMatchObject({ tool_call_id: "call_next", content: "next result" }); ++ }); ++ ++ test("routed compaction preserves a result recorded after assistant commentary", async () => { ++ const parsed = parsedWith([ ++ { role: "assistant", content: [{ type: "toolCall", id: "call_compaction", name: "exec", arguments: {} }], timestamp: 1 }, ++ { role: "assistant", content: [{ type: "text", text: "commentary before compaction" }], timestamp: 2 }, ++ { role: "toolResult", toolCallId: "call_compaction", toolName: "exec", content: "genuine result", isError: false, timestamp: 3 }, ++ { role: "user", content: "summarize this history", timestamp: 4 }, ++ ], { toolChoice: "none" }); ++ parsed._compactionRequest = true; ++ const { body } = await createOllamaNativeAdapter(ollamaProvider()).buildRequest(parsed); ++ const request = JSON.parse(String(body)); ++ expect(request.messages.map((message: { role: string }) => message.role)) ++ .toEqual(["assistant", "tool", "assistant", "user"]); ++ expect(request.messages[1]).toMatchObject({ tool_call_id: "call_compaction", content: "genuine result" }); ++ expect(request.messages[2].content).toBe("commentary before compaction"); ++ expect(request.tools).toBeUndefined(); ++ }); ++ + test("a deferred user message keeps its text and images after the tool result", async () => { + const adapter = createOllamaNativeAdapter(ollamaProvider()); + const png = "data:image/png;base64,iVBORw0KGgo="; + +``` diff --git a/devlog/_plan/261003_release_lane_c/030_handoff.md b/devlog/_plan/261003_release_lane_c/030_handoff.md new file mode 100644 index 00000000000..f505769c615 --- /dev/null +++ b/devlog/_plan/261003_release_lane_c/030_handoff.md @@ -0,0 +1,13 @@ +# Deliver two independently verified heads to the coordinator + +Depends on: both implementation outcomes, including an explicitly unresolved Antigravity live issue when access prevents causal verification. MODIFY this unit with each cycle conclusion and actual evidence; NEW ignored .tmp/release-stabilization/report.md with exact own branch/head and PR URL, source heads and dispositions, local test commands/counts, hosted event/head/check SHA/run ID/attempt/conclusion, review outcomes and native limits. No runtime changes in this phase. + +Each slice publishes after its ordinary independent code/security review and focused verification; main resolves valid findings and re-verifies that slice. Publication does not wait for the other slice. The coordinator owns the separate full parallel adversarial regression phase on the integrated candidate before deployment. This handoff collects both slices' evidence after their independent publication. + +Before publishing, fetch current dev/source heads and compare patch IDs/changed paths to avoid duplicate landed content. Preserve source authors in Co-authored-by trailers; use every repository PR template section. Both ordinary PRs target dev, do not alter source branches or close source PRs. The first PR carries its own documentation; the second excludes unrelated Antigravity changes. Verify native stack membership read-only; never register one. + +At C, read expected workflow job conditions and compare to observed check-runs. Required tests must actually run successfully against the applicable head or its verified PR merge ref. Absent/pending/skipped/cancelled/older-head checks are not passing. Lane may dispatch only its required PR CI after ROADMAP LOCKED; coordinator owns lane=all and all merges. Do not cancel other lanes' jobs. Inspect failing logs before repairs/reruns and preserve outstanding maintainer objections. + +Before completion, review every source item: #6501 preserved; #6497 adapted/deferred chunks and credit; #6502 reproduced/fixed or unresolved with actual access/status evidence; #6509 adopted delta and added validation proof. A fresh reader must be able to identify safe integration heads and remaining blockers from the report. Unmet checks or unresolved material reviews prevent DONE. No user-set time/token cap; waiting is not exhaustion. + +Criterion c-4 collects both exact-head PR CI receipts and resolved reviews for final handoff; it does not impose a cross-slice publication prerequisite. diff --git a/docs-site/src/content/docs/guides/providers.md b/docs-site/src/content/docs/guides/providers.md index 9adefb1f465..1d1384eecc5 100644 --- a/docs-site/src/content/docs/guides/providers.md +++ b/docs-site/src/content/docs/guides/providers.md @@ -210,7 +210,7 @@ ocx logout | `devin` | `devin` | `https://server.codeium.com` | Experimental unofficial Cognition/Devin bridge. Login first imports the credential the installed Devin CLI already holds (`devin auth login` writes a `devin-session-token` to its own `credentials.toml`); when none is present it opens Auth0 browser sign-in and exchanges the pasted token via Cognition's `RegisterUser` for a long-lived API key. `ocx login devin-cli` remains as a deprecated alias. Models are discovered per account with `GetCascadeModelConfigs`. Account quota comes from `GetUserStatus` under one eight-second request and body deadline: dated daily and weekly windows, plus the monthly prompt and flex credit pool for a credit-billed plan or an unknown strategy with both reset dates absent when a balance field is present. Negative used credits omit the monthly window; valid zero available credits mark it exhausted. A timed-out probe keeps the last good quota. Not shown in the dashboard preset by default. Chat and usage reporting are verified against a live account across three models. | | `github-copilot` | `openai-chat` | `https://api.githubcopilot.com` | Experimental. GitHub device flow + `copilot_internal` exchange (VS Code OAuth client). Requires an active Copilot subscription; not an official third-party API. | -Google Antigravity groups a model's complete `low` / `medium` / `high` variants into one entry with an effort selector. This includes Claude Opus and Sonnet 5.5 and future versions discovered during automatic refresh. Partial sets remain separate. Existing enabled/disabled choices carry into the grouped entry; genuinely new models still follow your New model policy. Saved suffix IDs continue to request their original tier. Grouped effort routing survives a proxy restart when model discovery is temporarily unavailable. A running Codex session may need to reload its model list after catalog refresh. +Google Antigravity groups a model's complete `low` / `medium` / `high` variants into one entry with an effort selector. This includes Claude Opus and Sonnet 5.5 and future versions discovered during automatic refresh. Partial sets remain separate. Existing enabled/disabled choices carry into the grouped entry; genuinely new models still follow your New model policy. Saved suffix IDs continue to request their original tier. Grouped effort routing survives a proxy restart when model discovery is temporarily unavailable. A running Codex session may need to reload its model list after catalog refresh. Usage groups known Claude Sonnet and Opus 5.5 tier IDs under their base model, including historical tier records. Their displayed costs are derived Anthropic reference estimates, not Antigravity subscription charges. Google Antigravity account and provider quota probes use fixed Google accounting endpoints, including the models fallback. They support transparent Fake-IP DNS for those destinations while retaining TLS verification, redirect rejection and private-address checks. A custom provider base URL changes model requests, not quota destinations; `NO_PROXY` continues to select the direct-route policy. diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index f9e9bda9b2f..5f16a268e26 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -2,6 +2,7 @@ "version": 1, "root": "tests", "explicit": { + "usage-antigravity-55.test.ts": "usage", "provider-antigravity-wire-snapshot.test.ts": "providers", "antigravity-discovered-families.test.ts": "adapters/google", "provider-antigravity-effort-families.test.ts": "providers", @@ -57,42 +58,11 @@ "openai-chat-tool-call-id-remint.test.ts": "adapters/openai", "coding-agent-json-lines-scaling.test.ts": "providers", "usage-snapshot-digest-reuse.test.ts": "usage", - "release-desktop-scripts.test.ts": "ci-workflows", - "installed-gate-drivers.test.ts": "ci-workflows", - "gui-desktop-sidecar-script.test.ts": "gui", - "standalone-build-script.test.ts": "gui", - "standalone-service.test.ts": "service", - "standalone.test.ts": "lib", "worker-embed.test.ts": "lib", "worker-embed-coverage.test.ts": "lib", - "server-combo-held-response.test.ts": "server", - "key-attribution.test.ts": "usage", "jev-stats.test.ts": "usage", - "provider-send-path-import.test.ts": "server", - "socks5-fetch.test.ts": "lib", - "socks5-upload-lifecycle.test.ts": "lib", - "provider-egress.test.ts": "lib", - "provider-egress-outbound.test.ts": "providers", - "provider-egress-fetch.test.ts": "responses", - "provider-egress-management-validation.test.ts": "server", - "start-args.test.ts": "cli", - "start-ownership-publication.test.ts": "cli", - "responses-core-modules.test.ts": "responses", "responses-devin-401-replay.test.ts": "responses", "responses-grok-devin-preflight.test.ts": "responses", - "responses-passthrough-transient-policy.test.ts": "responses", - "responses-spend-ledger-wiring.test.ts": "responses", - "responses-send-budget-errors.test.ts": "responses", - "responses-4546-incident-regression.test.ts": "responses", - "chat-responses-control-integration.test.ts": "responses", - "coding-agent-tool-result-images.test.ts": "adapters", - "cold-spawn-warmup.test.ts": "ci-workflows", - "stepfun-provider.test.ts": "providers", - "warmup-registration.test.ts": "ci-workflows", - "hub-usage.test.ts": "server", - "client-hub-usage.test.ts": "clients", - "cli-usage-hub.test.ts": "cli", - "cli-companion.test.ts": "cli", "deepseek-quota-currency.test.ts": "providers", "mimo-token-plan-capacity.test.ts": "providers", "command-code-tool-text-prose-split.test.ts": "providers", diff --git a/src/providers/antigravity-models.ts b/src/providers/antigravity-models.ts index a67bfb3f491..5ad7403aa15 100644 --- a/src/providers/antigravity-models.ts +++ b/src/providers/antigravity-models.ts @@ -770,6 +770,11 @@ const ANTIGRAVITY_USAGE_BASE_BY_ID: Record = (() => { // onto a model that did not exist then, and away from the 3.6 price row that still // prices it correctly. Retirement changes what we CALL, not what we RECORD. for (const retired of Object.keys(RETIRED_FLASH_TIERS)) rev[retired] = retired; + // Usage identity is stable even before discovery; routing keeps its own live evidence. + for (const base of ["claude-sonnet-5-5", "claude-opus-5-5"]) { + rev[base] = base; + for (const effort of ANTIGRAVITY_DISCOVERY_EFFORTS) rev[`${base}-${effort}`] = base; + } // Visible aliases that only appear in ANTIGRAVITY_VISIBLE_MODEL_ALIASES are already // included via ANTIGRAVITY_MODEL_ALIASES. Identity bases without effort maps remain. return rev; diff --git a/src/usage/expected-prices.ts b/src/usage/expected-prices.ts index 717e773b76f..94a8c6950e2 100644 --- a/src/usage/expected-prices.ts +++ b/src/usage/expected-prices.ts @@ -370,6 +370,15 @@ export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ { provider: "google-antigravity", modelId: "claude-opus-4-6-thinking", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" }, // Alias without the Antigravity "-thinking" suffix (if logs/UI ever surface it). { provider: "google-antigravity", modelId: "claude-opus-4-6", cost4: CLAUDE_OPUS_46, source: `anthropic official Claude Opus 4.6 ${ANTHROPIC_PRICING}`, verifiedAt: "2026-07-23", status: "verified" }, + // Antigravity uses subscription quota; these are derived Anthropic reference estimates. + { provider: "google-antigravity", modelId: "claude-sonnet-5-5", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "claude-sonnet-5-5-low", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "claude-sonnet-5-5-medium", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "claude-sonnet-5-5-high", cost4: CLAUDE_SONNET_55, source: `derived: Anthropic Claude Sonnet 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "claude-opus-5-5", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "claude-opus-5-5-low", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "claude-opus-5-5-medium", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, + { provider: "google-antigravity", modelId: "claude-opus-5-5-high", cost4: CLAUDE_OPUS_55, source: `derived: Anthropic Claude Opus 5.5 reference price; ${ANTHROPIC_PRICING}`, verifiedAt: "2026-10-03", status: "verified-derived" }, { provider: "google-antigravity", modelId: "gpt-oss-120b-medium", cost4: { input: 0.03, output: 0.15, cacheRead: 0, cacheWrite: 0 }, source: "derived: gpt-oss-120b open-weights — OpenRouter advertised lowest https://openrouter.ai/openai/gpt-oss-120b/providers", verifiedAt: "2026-07-20", status: "verified-derived" }, // Kimi / Moonshot — official price tables are now published (2026-07-20 re-check; // previously empty). kimi = Kimi Code OAuth surface, moonshot = CN key surface, diff --git a/structure/dashboard-and-usage.md b/structure/dashboard-and-usage.md index 24aed6d89ed..55909c49398 100644 --- a/structure/dashboard-and-usage.md +++ b/structure/dashboard-and-usage.md @@ -334,7 +334,7 @@ selectors, a vendor-only inferred price is unavailable; exact provider and user eligible. Missing trace evidence is not reconstructed from today's configuration. Provider-detail model shares use that provider's token total, not the global total. Unknown reserved `policy/` selectors are rejected before upstream dispatch; historical rows remain unchanged. -Expected-price overlays are estimates, not billing reproductions: the Z.AI GLM rows (`zai`, `zhipu-bigmodel`, `zhipu-bigmodel-coding`, `zhipu-bigmodel-responses`) display the published z.ai USD list price on surfaces that actually bill by Coding Plan subscription or CNY-tiered domestic PAYG, and every such row is marked `verified-derived` so the estimate flag reaches the UI. +Expected-price overlays are estimates, not billing reproductions: the Z.AI GLM rows (`zai`, `zhipu-bigmodel`, `zhipu-bigmodel-coding`, `zhipu-bigmodel-responses`) display the published z.ai USD list price on surfaces that actually bill by Coding Plan subscription or CNY-tiered domestic PAYG, and every such row is marked `verified-derived` so the estimate flag reaches the UI. Antigravity Claude Sonnet and Opus 5.5 base/tier overlays in `src/usage/expected-prices.ts` likewise use Anthropic reference prices with `verified-derived`, so even reported tokens retain an estimated-cost flag. The management API retains the compact accumulator plus bounded query summaries; it never retains normalized per-request rows after a response. File identity changes, shrinkage, same-size metadata diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index 980bf26f4f5..f88d6acac47 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -557,3 +557,5 @@ Preflight heartbeat retention keeps `replayUnsafe` sticky in the replayed tail, Antigravity discovery in `src/providers/antigravity-models.ts` collapses complete low/medium/high wire families without a version allowlist. Partial families retain exact wire IDs. The adapter sends the selected suffix without a second thinking level; saved suffix requests keep their tier. Existing Claude 4.6 single-wire thinking levels and Gemini irregular aliases keep their contracts. Grouped context is the minimum known across every tier; image support requires agreement, with an explicit negative taking precedence. Mappings are scoped to config home, normalized destination (including path case), and accepted cache generation. `src/providers/antigravity-wire-snapshot.ts` atomically saves bounded exact suffix maps before synthetic catalog rows are published. Hash-named files contain no destination URLs or credentials. First-use restoration after restart supplies routing aliases only; current live discovery replaces the snapshot, and invalidation tombstones prevent stale restoration. A failed snapshot write leaves the previous coherent discovery result. Antigravity catalog rows carry the observed complete effort map from `src/providers/antigravity-models.ts` through the in-memory cache. `src/providers/antigravity-effort-families.ts` uses that evidence to group public rows and project suffix allowlists into the base selection, including both final catalog merge callers. Internal retained suffix rows remain available to combo resolution. Authoritative new-model reconciliation normalizes both the prior baseline and policy input before detecting arrivals: an existing suffix family is not a new model. All-disabled tiers transfer a base disable once; a known base keeps the operator's later choice. Genuinely new families still follow new-model policy. Saved defaults, combo targets, suffix disables and provider selections remain unchanged. A discovered ladder advertises only its wire-backed efforts unless explicitly overridden; unknown single-wire models gain no invented ladder. + +Known Claude Sonnet and Opus 5.5 base and low/medium/high IDs share a deterministic usage identity in `src/providers/antigravity-models.ts`, independently of live discovery. This accounting map does not migrate saved routing selections; Claude 4.6 and unknown suffix identities remain unchanged. diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index ee3209290bb..cbf48f861da 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -1,5 +1,5 @@ { - "provider-antigravity-wire-snapshot.test.ts": "providers", + "usage-antigravity-55.test.ts": "usage", "provider-antigravity-wire-snapshot.test.ts": "providers", "antigravity-discovered-families.test.ts": "adapters/google", "provider-antigravity-effort-families.test.ts": "providers", "provider-antigravity-family-catalog.test.ts": "providers", @@ -70,42 +70,11 @@ "openai-chat-tool-call-id-remint.test.ts": "adapters/openai", "coding-agent-json-lines-scaling.test.ts": "providers", "usage-snapshot-digest-reuse.test.ts": "usage", - "release-desktop-scripts.test.ts": "ci-workflows", - "installed-gate-drivers.test.ts": "ci-workflows", - "gui-desktop-sidecar-script.test.ts": "gui", - "standalone-build-script.test.ts": "gui", - "standalone-service.test.ts": "service", - "standalone.test.ts": "lib", "worker-embed.test.ts": "lib", "worker-embed-coverage.test.ts": "lib", - "server-combo-held-response.test.ts": "server", - "key-attribution.test.ts": "usage", "jev-stats.test.ts": "usage", - "provider-send-path-import.test.ts": "server", - "socks5-fetch.test.ts": "lib", - "socks5-upload-lifecycle.test.ts": "lib", - "provider-egress.test.ts": "lib", - "provider-egress-outbound.test.ts": "providers", - "provider-egress-fetch.test.ts": "responses", - "provider-egress-management-validation.test.ts": "server", - "start-args.test.ts": "cli", - "start-ownership-publication.test.ts": "cli", - "responses-core-modules.test.ts": "responses", "responses-devin-401-replay.test.ts": "responses", "responses-grok-devin-preflight.test.ts": "responses", - "responses-passthrough-transient-policy.test.ts": "responses", - "responses-spend-ledger-wiring.test.ts": "responses", - "responses-send-budget-errors.test.ts": "responses", - "responses-4546-incident-regression.test.ts": "responses", - "chat-responses-control-integration.test.ts": "responses", - "coding-agent-tool-result-images.test.ts": "adapters", - "cold-spawn-warmup.test.ts": "ci-workflows", - "stepfun-provider.test.ts": "providers", - "warmup-registration.test.ts": "ci-workflows", - "hub-usage.test.ts": "server", - "client-hub-usage.test.ts": "clients", - "cli-usage-hub.test.ts": "cli", - "cli-companion.test.ts": "cli", "abort-idle-deadline.test.ts": "lib", "tool-envelope-echo-whole-line.test.ts": "adapters", "abort-race.test.ts": "adapters", diff --git a/tests/usage/usage-antigravity-55.test.ts b/tests/usage/usage-antigravity-55.test.ts new file mode 100644 index 00000000000..ef0fe79d9dd --- /dev/null +++ b/tests/usage/usage-antigravity-55.test.ts @@ -0,0 +1,69 @@ +import { describe, expect, test } from "bun:test"; +import { canonicalAntigravityUsageModel } from "../../src/providers/antigravity-models"; +import { estimateRequestCost, resolveMatchedPrice } from "../../src/usage/cost"; +import { findExpectedPriceOverlay } from "../../src/usage/expected-prices"; +import type { PersistedUsageEntry } from "../../src/usage/log"; +import { summarizeUsage } from "../../src/usage/summary"; + +const families = [ + { base: "claude-sonnet-5-5", cost4: { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, total: 0.012 }, + { base: "claude-opus-5-5", cost4: { input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 }, total: 0.024 }, +]; +const now = Date.UTC(2026, 9, 3, 12); + +function row(model: string, index: number, resolvedModel?: string): PersistedUsageEntry { + return { + requestId: `antigravity-fixture-${index}`, timestamp: now - index, + provider: "google-antigravity", model, ...(resolvedModel ? { resolvedModel } : {}), + status: 200, durationMs: 1, usageStatus: "reported", + usage: { inputTokens: 1000, outputTokens: 100 }, totalTokens: 1100, + }; +} + +describe("Antigravity Claude 5.5 usage", () => { + for (const { base, cost4, total } of families) { + test(`${base} has deterministic identity and derived reference prices without discovery`, () => { + for (const id of [base, `${base}-low`, `${base}-medium`, `${base}-high`]) { + expect(canonicalAntigravityUsageModel(id)).toBe(base); + expect(findExpectedPriceOverlay("google-antigravity", id)).toMatchObject({ + provider: "google-antigravity", modelId: id, cost4, status: "verified-derived", + }); + for (const provider of ["google-antigravity", "google-antigravity-pabcdef"]) { + const price = resolveMatchedPrice(provider, id); + expect(price).toMatchObject({ cost4, status: "verified-derived", source: "expected" }); + expect(price?.sourceRef).toContain("derived:"); + expect(price?.sourceRef).toContain("platform.claude.com/docs/en/about-claude/pricing"); + const estimate = estimateRequestCost({ provider, model: id, usageStatus: "reported", usage: { inputTokens: 1000, outputTokens: 100 } }); + expect(estimate?.estimated).toBe(true); + expect(estimate?.cost.total).toBeCloseTo(total / 4, 10); + } + } + }); + + test(`${base} aggregates tiers and resolved IDs in model and day summaries`, () => { + const entries = [row(`${base}-low`, 1), row(`${base}-medium`, 2), row(`${base}-high`, 3), row(base, 4, `${base}-high`)]; + const summary = summarizeUsage(entries, "all", now); + expect(summary.models).toHaveLength(1); + expect(summary.models[0]).toMatchObject({ provider: "google-antigravity", model: base, requests: 4, totalTokens: 4400 }); + expect(summary.models[0]?.resolvedModel).toBeUndefined(); + expect(summary.models[0]?.estimatedCostUsd).toBeCloseTo(total, 10); + const days = summary.days.filter(day => day.requests > 0); + expect(days).toHaveLength(1); + expect(days[0]?.models).toHaveLength(1); + expect(days[0]?.models[0]).toMatchObject({ model: base, requests: 4, totalTokens: 4400 }); + expect(days[0]?.models[0]?.estimatedCostUsd).toBeCloseTo(total, 10); + expect(summary.summary.estimatedCostUsd).toBeCloseTo(total, 10); + }); + } + + test("historical Claude and unknown suffixes retain their exact usage identities", () => { + const ids = ["claude-sonnet-4-6", "claude-opus-4-6-thinking", "claude-sonnet-6-0-high", "claude-sonnet-5-5-ultra"]; + for (const id of ids) expect(canonicalAntigravityUsageModel(id)).toBe(id); + const summary = summarizeUsage(ids.map((id, i) => row(id, i + 1)), "all", now); + expect(summary.models.map(model => model.model).sort()).toEqual([...ids].sort()); + expect(resolveMatchedPrice("google-antigravity", "claude-sonnet-4-6")?.cost4) + .toEqual({ input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 }); + expect(resolveMatchedPrice("google-antigravity", "claude-opus-4-6-thinking")?.cost4) + .toEqual({ input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 }); + }); +}); diff --git a/tests/usage/usage-cost.test.ts b/tests/usage/usage-cost.test.ts index 8606aa5b570..b5abcf2f243 100644 --- a/tests/usage/usage-cost.test.ts +++ b/tests/usage/usage-cost.test.ts @@ -462,8 +462,8 @@ describe("resolveMatchedPrice", () => { } }); - test("16. shipped overlay membership: 152 keys, including canonical Fable 5.1, Opus 5, Opus 5.5, Sonnet 5.5, GPT-6.1 Sol, OpenCode Go and compatibility prices", () => { - expect(EXPECTED_PRICE_OVERLAYS.length).toBe(152); + test("16. shipped overlay membership: 160 keys, including canonical Fable 5.1, Opus 5, Opus 5.5, Sonnet 5.5, GPT-6.1 Sol, OpenCode Go and compatibility prices", () => { + expect(EXPECTED_PRICE_OVERLAYS.length).toBe(160); 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 [ @@ -522,6 +522,14 @@ describe("resolveMatchedPrice", () => { "google-antigravity/gemini-3.5-flash-high", "google-antigravity/gemini-3-flash-agent", "google-antigravity/gemini-3.1-pro-preview", + "google-antigravity/claude-sonnet-5-5", + "google-antigravity/claude-sonnet-5-5-low", + "google-antigravity/claude-sonnet-5-5-medium", + "google-antigravity/claude-sonnet-5-5-high", + "google-antigravity/claude-opus-5-5", + "google-antigravity/claude-opus-5-5-low", + "google-antigravity/claude-opus-5-5-medium", + "google-antigravity/claude-opus-5-5-high", "google-antigravity/claude-sonnet-4-6", "google-antigravity/claude-opus-4-6-thinking", "google-antigravity/claude-opus-4-6",