From 31eec07162029312f3d5e5f2c1890cf98daa9c2b Mon Sep 17 00:00:00 2001 From: jun Date: Fri, 4 Sep 2026 23:41:56 +0900 Subject: [PATCH 1/6] docs(devlog): roadmap the 260904 triage gap closure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three gaps the triage verified as NOT done: the half-shipped fable-5-1 metadata, the missing CREDITS row for #3284, and Ultra Fast. The Ultra Fast doc carries the finding that shapes the whole phase: upstream-models.json advertises only priority, so there is no ultrafast tier to forward and re-adding the catalog row would reproduce exactly what #2994 was closed for. What is separately true is that a forced ultrafast request is classified not-requested and gets no speed label — an observability lie fixable without advertising anything. --- .../260904_triage_gap_closure/000_research.md | 111 ++++++++++++++ .../010_fable_metadata.md | 70 +++++++++ .../020_credits_3284.md | 44 ++++++ .../030_ultrafast_optin.md | 142 ++++++++++++++++++ 4 files changed, 367 insertions(+) create mode 100644 devlog/_plan/260904_triage_gap_closure/000_research.md create mode 100644 devlog/_plan/260904_triage_gap_closure/010_fable_metadata.md create mode 100644 devlog/_plan/260904_triage_gap_closure/020_credits_3284.md create mode 100644 devlog/_plan/260904_triage_gap_closure/030_ultrafast_optin.md diff --git a/devlog/_plan/260904_triage_gap_closure/000_research.md b/devlog/_plan/260904_triage_gap_closure/000_research.md new file mode 100644 index 0000000000..0fde3d8e01 --- /dev/null +++ b/devlog/_plan/260904_triage_gap_closure/000_research.md @@ -0,0 +1,111 @@ +# 000 — Research: three gaps the 260904 triage found but did not close + +A triage pass over open PRs and issues closed #3366, #3061, and merged #2877. +Three candidates were deliberately left open because the evidence said they were +NOT done. This unit closes them properly. + +## Gap 1 — PR #3293 shipped half of itself + +`claude-fable-5-1` pricing is on `dev`; its metadata is not. + +```text +git show origin/dev:src/usage/expected-prices.ts | grep -c claude-fable-5-1 -> 4 +git show origin/dev:src/generated/model-metadata.ts | grep -o '"claude-fable-5-1"' -> (none) +git show origin/dev:scripts/model-metadata.source.json | grep -c claude-fable-5-1 -> 0 +``` + +The pricing rows arrived through unrelated commits (`3d3c4fe26`, `ff1ac6b8c`, +`1aa839aa8`) that happened to touch `expected-prices.ts`. PR #3293 by +[@Veritas-7](https://github.com/Veritas-7) is the only source of the metadata +half, and it is still open and MERGEABLE. + +That asymmetry is the actual defect: `expected-prices.ts` asserts an expected +price for a model the generated catalog does not know about. This is a +half-shipped feature, not a superseded one, which is why the triage left it open +rather than closing it as done. + +## Gap 2 — issue #3431 asks for a credit the file's own rule requires + +`CREDITS.md` on `dev` has 19 Carried-work rows and none for #3284. + +Verified independently rather than taken from the issue text: + +- `3d3c4fe26` is an ancestor of `origin/dev` and is the #3286 merge. +- `src/providers/antigravity-models.ts` on `dev` names `gemini-3.8-flash` 16 + times; the suffix ladder shipped. +- #3284 by [@mdwsk88](https://github.com/mdwsk88) is CLOSED, not merged. +- Maintainer [@Ingwannu](https://github.com/Ingwannu) closed it as superseded and + wrote, in that closing comment: "Core implementation is already on `dev` via + #3286 (`3d3c4fe26`), including the suffix wire ladder, picker collapse, Google + adapter coverage, metadata/pricing, and remaining surfaces." + +That last line matters procedurally. `CREDITS.md` says entries "must not be +inferred from diff similarity alone" and that every entry cites the maintainer's +own words. The closing comment IS that citation, so this row is admissible on the +file's own terms. + +## Gap 3 — issue #3429 asks for a feature that was already closed once + +This is the one that needs care. + +PR #2994 by [@Ingwannu](https://github.com/Ingwannu) added `ultrafast` to the +pinned `gpt-5.6-sol` fallback catalog. It was closed unmerged on 2026-08-30 with +the maintainer verdict: + +> Ultrafast는 프런트 노출 오류다. 피커에 칸을 올리면 고를 수는 있는데, 실제 +> Ultrafast 속도가 나오지 않는다. 백엔드가 깨져서가 아니다. + +Issue #2993 was closed with it. So "add ultrafast to the catalog" is a decision +that was already made and reversed, and re-landing it as a default would +reintroduce exactly the defect it was reverted for: a picker row that can be +selected but does not deliver the speed it names. + +What #3429 reports is narrower and still true: + +- `src/codex/catalog/parsing.ts` deletes `service_tier`, `service_tiers`, + `default_service_tier`, and `additional_speed_tiers` from these rows, so the + tier cannot survive a catalog regeneration even if the user edits + `~/.codex/opencodex-catalog.json` by hand. +- When a user forces the tier through anyway, the request completes but the + classification does not recognise it: `fastOutcome` reads `not-requested` and + the confirmation reads `unknown`, with no speed label for ultrafast. + +The second half is a straight defect regardless of the first: the proxy is +carrying a tier it refuses to name. + +## The shape this unit takes + +Opt-in, default OFF. With the setting unset, nothing changes for anyone — the +stripping still happens, the catalog is byte-identical, and #2994's verdict +stands. With the setting on, the operator has said "I know what this is", the +tier survives end-to-end, and the logs name it honestly instead of reporting +`unknown`. + +That is what makes re-landing this defensible after #2994: the reversal was about +showing everyone a row that lies. An operator-enabled flag that also fixes the +classification does not. + +The user additionally asked for the Codex Set page head to be relieved of the +"한도 도달 계정 일시 중지" and "할당량 새로고침" controls, moving them below, with +the Ultra Fast toggle surfacing there. That is the same surface, so it lands in +the same phase. + +## Design read + +Existing dashboard, no new visual language. + +DESIGN_VARIANCE: 2, MOTION_INTENSITY: 1, density D5 — operator console for +repeated expert work. The page head is currently carrying three controls plus a +title on one row, which is what the user is reacting to; moving the two +account-scoped actions down to the section they act on is a density fix, not a +restyle. + +Do's: reuse the existing Switch + label + description row pattern, the existing +`btn btn-ghost btn-sm` controls, and the existing quota-refresh i18n keys. +Don'ts: no new panel, no default-on behavior, no picker row without the wire +support behind it. + +## Out of scope + +PR #3332 (user deferred it explicitly), release scripts, workflows, auth paths, +`gui/dist`. diff --git a/devlog/_plan/260904_triage_gap_closure/010_fable_metadata.md b/devlog/_plan/260904_triage_gap_closure/010_fable_metadata.md new file mode 100644 index 0000000000..407d2544dc --- /dev/null +++ b/devlog/_plan/260904_triage_gap_closure/010_fable_metadata.md @@ -0,0 +1,70 @@ +# 010 — Land the missing claude-fable-5-1 metadata (PR #3293) + +Work-phase `wp1`. Depends on 000. + +## Problem + +`src/usage/expected-prices.ts` on `dev` asserts an expected price for +`claude-fable-5-1` on four surfaces, but neither +`scripts/model-metadata.source.json` nor the `anthropic` row of +`src/generated/model-metadata.ts` knows the model exists. Pricing without +metadata is the wrong half to have. + +## Diff plan + +Carry PR #3293 by [@Veritas-7](https://github.com/Veritas-7). Its diff is exactly +the missing half, so this is a carry rather than a reimplementation. + +### scripts/model-metadata.source.json + +Insert the `claude-fable-5-1` entry into the `anthropic` provider block, in id +order between `claude-fable-5` and `claude-haiku-4-5`: + +```json +"claude-fable-5-1": { + "id": "claude-fable-5-1", + "name": "Anthropic Fable 5.1", + "api": "anthropic-messages", + "provider": "anthropic", + "baseUrl": "https://api.anthropic.com", + "reasoning": true, + "input": ["text", "image"], + "cost": { "input": 10, "output": 50, "cacheRead": 0.25, "cacheWrite": 12.5 }, + "contextWindow": 1000000, + "maxTokens": 128000, + "thinking": { "mode": "anthropic-adaptive", "minLevel": "minimal", "maxLevel": "xhigh" }, + "compat": { "toolChoiceSupport": "auto" } +} +``` + +### src/generated/model-metadata.ts + +This file is generated, so it is REGENERATED rather than hand-edited: +`bun run generate:model-metadata`. The expected delta is one row appended to the +`anthropic` array: + +```text +["claude-fable-5-1",1000000,128000,"text,image",1,null,10,50,0.25,12.5] +``` + +Note the cacheRead of `0.25`, not `1`. `claude-fable-5` carries `1`; Fable 5.1's +published cache-hit rate is 0.025x base input, which the existing +`expected-prices.ts` rows already encode. Regenerating rather than typing the row +is what keeps the two files consistent. + +### Attribution + +`Co-authored-by: Veritas-7 <...>` in the landing commit. Per `CREDITS.md` and +`AGENTS.md`, the trailer is what GitHub reads; prose in the body is read by +nothing. + +## Verification + +- `bun run typecheck` +- `bun test ./tests/usage-cost.test.ts` — the PR's own test file +- Confirm the generated row exists and matches the source entry +- After merge: re-read `origin/dev` and confirm both files carry the model + +## Close-out + +Close #3293 citing the landing SHA and naming the author. diff --git a/devlog/_plan/260904_triage_gap_closure/020_credits_3284.md b/devlog/_plan/260904_triage_gap_closure/020_credits_3284.md new file mode 100644 index 0000000000..3cce9156ab --- /dev/null +++ b/devlog/_plan/260904_triage_gap_closure/020_credits_3284.md @@ -0,0 +1,44 @@ +# 020 — CREDITS row for #3284 (issue #3431) + +Work-phase `wp2`. Depends on 000. Disjoint from every other phase. + +## Why this is admissible + +`CREDITS.md` sets two bars, and both are met: + +1. "If you find a landing that belongs on this page, open an issue" — #3431 is + that issue, opened at maintainer [@Ingwannu](https://github.com/Ingwannu)'s + explicit request. +2. "Every entry cites the maintainer's own words... Nothing here is inferred from + a diff." — the citation is Ingwannu's closing comment on #3284. + +Verified independently rather than taken from the issue body: + +```text +git merge-base --is-ancestor 3d3c4fe26 origin/dev -> true +3d3c4fe26 = "feat(models): roll out Gemini 3.8 Flash ... (#3286)", author JUN +#3284 by @mdwsk88 -> CLOSED, not merged +src/providers/antigravity-models.ts on dev names gemini-3.8-flash 16 times +``` + +## Diff plan + +One row appended to the `## Carried work` table in `CREDITS.md`, in pull-request +number order after the `#3300` row: + +```markdown +| [#3284](https://github.com/lidge-jun/opencodex/pull/3284) | [@mdwsk88](https://github.com/mdwsk88) | \`3d3c4fe26\` | "Core implementation is already on \`dev\` via #3286 (\`3d3c4fe26\`), including the suffix wire ladder, picker collapse, Google adapter coverage" | +``` + +The quoted text is Ingwannu's, verbatim from the #3284 closing comment, matching +how every other row in the table cites its source. + +## Verification + +- The table renders (no broken pipe alignment) +- `bun run privacy:scan` stays green — it reads `CREDITS.md` +- After merge: `git show origin/dev:CREDITS.md | grep 3284` + +## Close-out + +Close #3431 citing the landing SHA. diff --git a/devlog/_plan/260904_triage_gap_closure/030_ultrafast_optin.md b/devlog/_plan/260904_triage_gap_closure/030_ultrafast_optin.md new file mode 100644 index 0000000000..78d42be5f3 --- /dev/null +++ b/devlog/_plan/260904_triage_gap_closure/030_ultrafast_optin.md @@ -0,0 +1,142 @@ +# 030 — Ultra Fast as an explicit opt-in (issue #3429) + +Work-phase `wp3`. Depends on 000. This is the phase that needs judgment, not just +diffs. + +## The constraint #2994 left behind + +PR #2994 added `ultrafast` to the pinned `gpt-5.6-sol` fallback catalog and was +closed unmerged: + +> Ultrafast는 프런트 노출 오류다. 피커에 칸을 올리면 고를 수는 있는데, 실제 +> Ultrafast 속도가 나오지 않는다. + +Exploration of `origin/dev` explains WHY, and the reason is load-bearing for this +plan. `src/codex/data/upstream-models.json` advertises exactly one tier on every +row that has any: + +```json +"service_tiers": [{ "id": "priority", "name": "Fast", "description": "1.5x speed, increased usage" }], +"additional_speed_tiers": ["fast"] +``` + +There is no upstream `ultrafast` tier to forward. So a catalog row advertising +one is fabricated metadata: the picker gains a choice the wire cannot honor. That +is precisely the defect #2994 was closed for, and adding the row back — flag or +no flag — would reproduce it. + +**Therefore this phase does NOT re-add the catalog row by default, and does not +claim Ultra Fast works.** It ships the half that is true today. + +## What is a real defect regardless + +#3429 reports something narrower that holds independently of whether the tier is +advertised: when a caller forces `service_tier: "ultrafast"` through anyway (by +hand-editing `~/.codex/opencodex-catalog.json`, which the reporter did), the +proxy carries the value but refuses to name it. + +Traced on `dev`: + +- `canonicalFastTierMarker` (`src/providers/fastwire.ts:247`) folds only + `priority`/`fast`, so `ultrafast` returns `undefined`. +- `fastIntent` is therefore false, and the request is classified + `fastOutcome: "not-requested"` at `fastwire.ts:344` — not `unknown`, but + actively "no fast tier was asked for", which is false. +- `requestLogSpeedLabel` (`src/server/request-log.ts:594`) returns `undefined` + for anything but `priority`/`fast`, so the Logs page shows no speed badge. + +The proxy is forwarding a tier it will not admit to. That is an observability +lie, and it is fixable without advertising anything. + +## Diff plan + +### 1. Config flag, default OFF + +Follow the `fastRows` precedent exactly (`src/config.ts:1052`, +`src/types/config.ts:384`): + +```ts +// src/config.ts — zod +ultraFastTier: z.boolean().optional().catch(false), +``` + +`.catch(false)` matters: a malformed hand edit degrades to off rather than +rejecting the config. Read it with the house `=== true` idiom, never truthiness. + +A management-route boolean guard goes beside `showCodexSparkQuota` in +`src/server/management/config-routes.ts:408` because this flag gets a dashboard +toggle (`fastRows` has none, which is why it needs no guard). + +### 2. Honest classification (the part that fixes the reported defect) + +`src/providers/fastwire.ts`: widen the canonical union to +`"priority" | "ultrafast"`, fold `ultrafast` in `canonicalFastTierMarker`, add +the `canonicalToWire` entry, and generalize the two `=== "priority"` +comparisons at `:349` and `:377` to "is a recognised canonical tier". + +`src/server/request-log.ts:594`: `requestLogSpeedLabel` returns `"ultrafast"` +for that tier. The existing contract — `"auto"` and `undefined` stay +`undefined` — must not move; `tests/request-log.test.ts:703` pins it. + +No new `fastOutcome` literal is introduced. The four existing values still +describe what happened; what changes is that `ultrafast` now reaches them instead +of being mistaken for "not requested". That keeps +`src/usage/log.ts`'s persisted-row allowlist untouched. + +### 3. Catalog gating — flag-gated, and honest about what it does + +`normalizeServiceTiers` and `normalizeRoutedCatalogEntry` gain a trailing +optional options bag, defaulting to off, threaded from the `sync.ts` call sites +that already hold `config`. With the flag absent, every `delete` runs exactly as +today. + +With the flag ON, the tier is PRESERVED when a catalog already carries it +(the operator's hand edit survives regeneration, which is the reporter's actual +ask). It is still NOT synthesized: `src/codex/catalog/effort.ts:160` keeps +emitting only the `priority` row, because inventing a tier upstream does not +advertise is what #2994 was reverted for. + +### 4. GUI — Codex Set page head + +`CodexAccountPoolPageHead` (`gui/src/components/codex-account-pool-main-card.tsx:207`) +currently carries the title, a status span, the Spark toggle, and two action +buttons on one row. Move "한도 도달 계정 일시 중지" and "할당량 새로고침" out of +the head and into the existing advanced-settings grid +(`CodexAuthAdvancedSettings`, `div#codex-auth-advanced-boxes`), which is already +the page's home for secondary controls, and put the Ultra Fast toggle there as a +`card card-row` matching `CodexAccountPickerSetting`. + +Two constraints from the existing code: + +- The component is dual-mode. `embedded={true}` (Providers workspace) renders a + bare `.row`; only the standalone page renders `.page-head`. The move must not + change the embedded surface. +- `gui/tests/codex-set-page-head-wrap.test.ts` throws `rule not found` if the + `.codex-auth-page-head` selectors disappear, and + `codex-account-pool-toast-tone.test.tsx` queries + `.codex-auth-page-head__feedback`. Keep those class names alive. + +The toggle's description states plainly that upstream advertises no Ultra Fast +tier today and that the setting only preserves a tier the operator supplies. A +switch that implies a speed it cannot deliver is the #2994 defect in a new place. + +## Verification + +- `bun run typecheck`, `bun run lint:gui` +- Flag OFF regression set, which is the proof the default did not move: + `tests/codex-catalog.test.ts`, `tests/fastwire-characterization-routing.test.ts` + (byte golden), `tests/service-tier-capability.test.ts`, + `tests/request-log.test.ts`, `tests/fastwire-observability.test.ts` +- New focused tests: flag OFF strips exactly as before; flag ON preserves a + supplied tier; `ultrafast` classifies instead of reading `not-requested`; + `requestLogSpeedLabel` maps it while `"auto"` still returns `undefined` +- `gui/tests/codex-set-page-head-wrap.test.ts` and the toast-tone suite stay green +- Live: the Codex Set page head no longer carries the two buttons; they and the + toggle are reachable below + +## What this phase will NOT claim + +It will not claim Ultra Fast delivers Ultra Fast speed. If live evidence shows an +opted-in request still cannot get the tier honored end-to-end, that is reported +on #3429 rather than papered over — the same call #2994 made, and the reason it +is closable honestly either way. From 21cb149d549c7b41ee104c3dd315911da6577973 Mon Sep 17 00:00:00 2001 From: jun Date: Fri, 4 Sep 2026 23:47:54 +0900 Subject: [PATCH 2/6] feat(catalog): add claude-fable-5-1 to model metadata MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Carries PR #3293 by @Veritas-7, whose metadata half never landed while its pricing half did. On dev, src/usage/expected-prices.ts asserts an expected price for claude-fable-5-1 on four surfaces, but neither scripts/model-metadata.source.json nor the anthropic array of src/generated/model-metadata.ts knew the model existed. The pricing rows arrived through unrelated commits that happened to touch expected-prices.ts; #3293 is the only source of the metadata, and it is still open. Pricing without metadata is the wrong half to have. The generated file is regenerated from the source entry rather than hand-edited, which is what keeps the two consistent: ["claude-fable-5-1",1000000,128000,"text,image",1,null,10,50,0.25,12.5] Note cacheRead 0.25, not the 1 that claude-fable-5 carries: Fable 5.1's published cache-hit rate is 0.025x base input, which the existing expected-prices rows already encode. The PR's own test update comes with it. Adding the jawcode row changes where the price resolves from — src/usage/cost.ts prefers an exact jawcode provider-bundle row over the expected-price overlay — so the assertion moves from source: "expected" to source: "jawcode" with jawcodeProvider: "anthropic", and the overlay is asserted directly instead of through sourceRef. The resolved cost4 is identical either way; only the provenance label changes. Verification: bun run typecheck, bun test ./tests/usage-cost.test.ts (82 pass), bun run test:changed (10752 pass / 0 fail across 569 files). Repository-wide suite not run. Co-authored-by: wj --- scripts/model-metadata.source.json | 28 ++++++++++++++++++++++++++++ src/generated/model-metadata.ts | 2 +- tests/usage-cost.test.ts | 13 +++++++------ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/scripts/model-metadata.source.json b/scripts/model-metadata.source.json index 8cc73ca1c3..e171710cb6 100644 --- a/scripts/model-metadata.source.json +++ b/scripts/model-metadata.source.json @@ -3709,6 +3709,34 @@ "toolChoiceSupport": "auto" } }, + "claude-fable-5-1": { + "id": "claude-fable-5-1", + "name": "Anthropic Fable 5.1", + "api": "anthropic-messages", + "provider": "anthropic", + "baseUrl": "https://api.anthropic.com", + "reasoning": true, + "input": [ + "text", + "image" + ], + "cost": { + "input": 10, + "output": 50, + "cacheRead": 0.25, + "cacheWrite": 12.5 + }, + "contextWindow": 1000000, + "maxTokens": 128000, + "thinking": { + "mode": "anthropic-adaptive", + "minLevel": "minimal", + "maxLevel": "xhigh" + }, + "compat": { + "toolChoiceSupport": "auto" + } + }, "claude-haiku-4-5": { "id": "claude-haiku-4-5", "name": "Anthropic Haiku 4.5 (latest)", diff --git a/src/generated/model-metadata.ts b/src/generated/model-metadata.ts index 73b9dee5d5..662cf6f1a7 100644 --- a/src/generated/model-metadata.ts +++ b/src/generated/model-metadata.ts @@ -38,7 +38,7 @@ const PROVIDER_ALIASES: Record = { type Row = readonly [id: string, contextWindow?: number | null, maxTokens?: number | null, input?: string | null, reasoning?: 0 | 1 | null, wireModelId?: string | null, costInput?: number | null, costOutput?: number | null, costCacheRead?: number | null, costCacheWrite?: number | null]; const DATA: Record = { "amazon-bedrock": [["anthropic.claude-3-5-haiku-20241022-v1:0",200000,8192,"text,image",0,null,0.8,4,0.08,1],["anthropic.claude-3-5-sonnet-20240620-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["anthropic.claude-3-5-sonnet-20241022-v2:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["anthropic.claude-3-haiku-20240307-v1:0",200000,4096,"text,image",0,null,0.25,1.25,0,0],["anthropic.claude-3-opus-20240229-v1:0",200000,4096,"text,image",0,null,15,75,0,0],["anthropic.claude-3-sonnet-20240229-v1:0",200000,4096,"text,image",0,null,3,15,0,0],["anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["au.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["au.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,16.5,82.5,0.5,6.25],["au.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["au.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["au.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["au.anthropic.claude-sonnet-4-6",1000000,128000,"text,image",1,null,3.3,16.5,0.33,4.125],["au.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["cohere.command-r-plus-v1:0",128000,4096,"text",0,null,3,15,0,0],["cohere.command-r-v1:0",128000,4096,"text",0,null,0.5,1.5,0,0],["deepseek.v3-v1:0",163840,81920,"text",1,null,0.58,1.68,0,0],["deepseek.v3.2",163840,81920,"text",1,null,0.62,1.85,0,0],["deepseek.v3.2-v1:0",163840,81920,"text",1,null,0.62,1.85,0,0],["eu.anthropic.claude-3-5-haiku-20241022-v1:0",200000,8192,"text,image",0,null,0.8,4,0.08,1],["eu.anthropic.claude-3-5-sonnet-20240620-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-5-sonnet-20241022-v2:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-7-sonnet-20250219-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["eu.anthropic.claude-3-haiku-20240307-v1:0",200000,4096,"text,image",0,null,0.25,1.25,0,0],["eu.anthropic.claude-3-opus-20240229-v1:0",200000,4096,"text,image",0,null,15,75,0,0],["eu.anthropic.claude-3-sonnet-20240229-v1:0",200000,4096,"text,image",0,null,3,15,0,0],["eu.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["eu.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1.1,5.5,0.11,1.375],["eu.anthropic.claude-opus-4-1-20250805-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["eu.anthropic.claude-opus-4-20250514-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["eu.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["eu.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5.5,27.5,0.55,6.875],["eu.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["eu.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["global.amazon.nova-2-lite-v1:0",128000,4096,"text,image",1,null,0.33,2.75,0,0],["global.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["global.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["global.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["global.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["global.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["google.gemma-3-27b-it",202752,8192,"text,image",0,null,0.12,0.2,0,0],["google.gemma-3-4b-it",128000,4096,"text,image",0,null,0.04,0.08,0,0],["jp.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["jp.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["jp.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["jp.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["jp.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["meta.llama3-1-405b-instruct-v1:0",128000,4096,"text",0,null,2.4,2.4,0,0],["meta.llama3-1-70b-instruct-v1:0",128000,4096,"text",0,null,0.72,0.72,0,0],["meta.llama3-1-8b-instruct-v1:0",128000,4096,"text",0,null,0.22,0.22,0,0],["minimax.minimax-m2",204608,128000,"text",1,null,0.3,1.2,0,0],["minimax.minimax-m2.1",204800,131072,"text",1,null,0.3,1.2,0,0],["minimax.minimax-m2.5",196608,98304,"text",1,null,0.3,1.2,0,0],["mistral.devstral-2-123b",256000,8192,"text",0,null,0.4,2,0,0],["mistral.magistral-small-2509",128000,40000,"text,image",1,null,0.5,1.5,0,0],["mistral.ministral-3-14b-instruct",128000,4096,"text",0,null,0.2,0.2,0,0],["mistral.ministral-3-3b-instruct",256000,8192,"text,image",0,null,0.1,0.1,0,0],["mistral.ministral-3-8b-instruct",128000,4096,"text",0,null,0.15,0.15,0,0],["mistral.mistral-large-2402-v1:0",128000,4096,"text",0,null,0.5,1.5,0,0],["mistral.mistral-large-3-675b-instruct",256000,8192,"text,image",0,null,0.5,1.5,0,0],["mistral.pixtral-large-2502-v1:0",128000,8192,"text,image",0,null,2,6,0,0],["mistral.voxtral-mini-3b-2507",128000,4096,"text",0,null,0.04,0.04,0,0],["mistral.voxtral-small-24b-2507",32000,8192,"text",0,null,0.15,0.35,0,0],["moonshot.kimi-k2-thinking",262143,16000,"text",1,null,0.6,2.5,0,0],["moonshotai.kimi-k2.5",262143,16000,"text,image",1,null,0.6,3,0,0],["nvidia.nemotron-nano-12b-v2",128000,4096,"text,image",0,null,0.2,0.6,0,0],["nvidia.nemotron-nano-3-30b",128000,4096,"text",1,null,0.06,0.24,0,0],["nvidia.nemotron-nano-9b-v2",128000,4096,"text",0,null,0.06,0.23,0,0],["nvidia.nemotron-super-3-120b",262144,131072,"text",1,null,0.15,0.65,0,0],["openai.gpt-5.4",272000,128000,"text,image",1,null,2.75,16.5,0.275,0],["openai.gpt-5.5",272000,128000,"text,image",1,null,5.5,33,0.55,0],["openai.gpt-5.6-luna",373000,128000,"text,image",1,null,1,6,0.1,1.25],["openai.gpt-5.6-sol",373000,128000,"text,image",1,null,5,30,0.5,6.25],["openai.gpt-5.6-terra",373000,128000,"text,image",1,null,2.5,15,0.25,3.125],["openai.gpt-oss-120b",128000,16384,"text",1,null,0.15,0.6,0,0],["openai.gpt-oss-120b-1:0",128000,16384,"text",1,null,0.15,0.6,0,0],["openai.gpt-oss-20b",128000,16384,"text",1,null,0.07,0.3,0,0],["openai.gpt-oss-20b-1:0",128000,16384,"text",1,null,0.07,0.3,0,0],["openai.gpt-oss-safeguard-120b",128000,16384,"text",0,null,0.15,0.6,0,0],["openai.gpt-oss-safeguard-20b",128000,16384,"text",0,null,0.07,0.2,0,0],["qwen.qwen3-235b-a22b-2507-v1:0",262144,131072,"text",0,null,0.22,0.88,0,0],["qwen.qwen3-32b-v1:0",16384,16384,"text",1,null,0.15,0.6,0,0],["qwen.qwen3-coder-30b-a3b-v1:0",262144,131072,"text",0,null,0.15,0.6,0,0],["qwen.qwen3-coder-480b-a35b-v1:0",131072,65536,"text",0,null,0.22,1.8,0,0],["qwen.qwen3-coder-next",131072,65536,"text",1,null,0.22,1.8,0,0],["qwen.qwen3-next-80b-a3b",262000,262000,"text",0,null,0.14,1.4,0,0],["qwen.qwen3-vl-235b-a22b",262000,262000,"text,image",0,null,0.3,1.5,0,0],["us.amazon.nova-lite-v1:0",300000,8192,"text,image",0,null,0.06,0.24,0.015,0],["us.amazon.nova-micro-v1:0",128000,8192,"text",0,null,0.035,0.14,0.00875,0],["us.amazon.nova-premier-v1:0",1000000,16384,"text,image",1,null,2.5,12.5,0,0],["us.amazon.nova-pro-v1:0",300000,8192,"text,image",0,null,0.8,3.2,0.2,0],["us.anthropic.claude-3-7-sonnet-20250219-v1:0",200000,8192,"text,image",0,null,3,15,0.3,3.75],["us.anthropic.claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["us.anthropic.claude-haiku-4-5-20251001-v1:0",200000,64000,"text,image",1,null,1,5,0.1,1.25],["us.anthropic.claude-opus-4-1-20250805-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["us.anthropic.claude-opus-4-20250514-v1:0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["us.anthropic.claude-opus-4-5-20251101-v1:0",200000,64000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-6-v1",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["us.anthropic.claude-sonnet-4-20250514-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-4-5-20250929-v1:0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-4-6",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["us.anthropic.claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5],["us.deepseek.r1-v1:0",128000,32768,"text",1,null,1.35,5.4,0,0],["us.meta.llama3-2-11b-instruct-v1:0",128000,4096,"text,image",0,null,0.16,0.16,0,0],["us.meta.llama3-2-1b-instruct-v1:0",131000,4096,"text",0,null,0.1,0.1,0,0],["us.meta.llama3-2-3b-instruct-v1:0",131000,4096,"text",0,null,0.15,0.15,0,0],["us.meta.llama3-2-90b-instruct-v1:0",128000,4096,"text,image",0,null,0.72,0.72,0,0],["us.meta.llama3-3-70b-instruct-v1:0",128000,4096,"text",0,null,0.72,0.72,0,0],["us.meta.llama4-maverick-17b-instruct-v1:0",1000000,16384,"text,image",0,null,0.24,0.97,0,0],["us.meta.llama4-scout-17b-instruct-v1:0",3500000,16384,"text,image",0,null,0.17,0.66,0,0],["writer.palmyra-x4-v1:0",122880,8192,"text",1,null,2.5,10,0,0],["writer.palmyra-x5-v1:0",1040000,8192,"text",1,null,0.6,6,0,0],["xai.grok-4.3",1000000,131072,"text,image",1,null,1.25,2.5,0.2,0],["zai.glm-4.7",204800,131072,"text",1,null,0.6,2.2,0,0],["zai.glm-4.7-flash",200000,131072,"text",1,null,0.07,0.4,0,0],["zai.glm-5",202752,101376,"text",1,null,1,3.2,0,0]], - "anthropic": [["claude-3-5-sonnet-20240620",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-5-sonnet-20241022",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-haiku-20240307",200000,4096,"text,image",0,null,0.25,1.25,0.03,0.3],["claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["claude-haiku-4-5",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-haiku-4-5-20251001",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-opus-4-0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1-20250805",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-20250514",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-5",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-5-20251101",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6[1m]",1000000,128000,"text,image",1,"claude-opus-4-6",5,25,0.5,6.25],["claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-7[1m]",1000000,128000,"text,image",1,"claude-opus-4-7",5,25,0.5,6.25],["claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-8[1m]",1000000,128000,"text,image",1,"claude-opus-4-8",5,25,0.5,6.25],["claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-sonnet-4-0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-20250514",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5-20250929",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6",1000000,128000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6[1m]",1000000,64000,"text,image",1,"claude-sonnet-4-6",3,15,0.3,3.75],["claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5]], + "anthropic": [["claude-3-5-sonnet-20240620",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-5-sonnet-20241022",200000,8192,"text,image",0,null,3,15,0.3,3.75],["claude-3-haiku-20240307",200000,4096,"text,image",0,null,0.25,1.25,0.03,0.3],["claude-fable-5",1000000,128000,"text,image",1,null,10,50,1,12.5],["claude-fable-5-1",1000000,128000,"text,image",1,null,10,50,0.25,12.5],["claude-haiku-4-5",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-haiku-4-5-20251001",200000,64000,"text,image",1,null,1,5,0.1,1.25],["claude-opus-4-0",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-1-20250805",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-20250514",200000,32000,"text,image",1,null,15,75,1.5,18.75],["claude-opus-4-5",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-5-20251101",200000,64000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-6[1m]",1000000,128000,"text,image",1,"claude-opus-4-6",5,25,0.5,6.25],["claude-opus-4-7",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-7[1m]",1000000,128000,"text,image",1,"claude-opus-4-7",5,25,0.5,6.25],["claude-opus-4-8",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-opus-4-8[1m]",1000000,128000,"text,image",1,"claude-opus-4-8",5,25,0.5,6.25],["claude-opus-5",1000000,128000,"text,image",1,null,5,25,0.5,6.25],["claude-sonnet-4-0",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-20250514",200000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-5-20250929",1000000,64000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6",1000000,128000,"text,image",1,null,3,15,0.3,3.75],["claude-sonnet-4-6[1m]",1000000,64000,"text,image",1,"claude-sonnet-4-6",3,15,0.3,3.75],["claude-sonnet-5",1000000,128000,"text,image",1,null,2,10,0.2,2.5]], "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]], diff --git a/tests/usage-cost.test.ts b/tests/usage-cost.test.ts index d166434a3d..4b98b565de 100644 --- a/tests/usage-cost.test.ts +++ b/tests/usage-cost.test.ts @@ -177,22 +177,23 @@ describe("resolveMatchedPrice", () => { }); // Claude Fable 5.1 (2026-09-02): 10 / 50 / 12.50 cache write, and a cache-hit rate of - // 0.025x base input (0.25) rather than the 0.1x every other family uses. There is no - // jawcode row yet, so both Anthropic surfaces resolve from the shipped overlay; an - // account-pool log label must collapse onto the same price. + // 0.025x base input (0.25) rather than the 0.1x every other family uses. The generated + // Anthropic jawcode row is now canonical for both Anthropic surfaces; an account-pool log + // label must still collapse onto the same price. test("claude-fable-5-1 resolves to the official Fable 5.1 price on both Anthropic surfaces", () => { const COST4 = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 }; + expect(findExpectedPriceOverlay("anthropic", "claude-fable-5-1")?.cost4).toEqual(COST4); + expect(findExpectedPriceOverlay("anthropic-apikey", "claude-fable-5-1")?.cost4).toEqual(COST4); for (const provider of ["anthropic", "anthropic-apikey"]) { const price = resolveMatchedPrice(provider, "claude-fable-5-1"); expect(price, provider).toMatchObject({ provider, modelId: "claude-fable-5-1", cost4: COST4, - source: "expected", + source: "jawcode", + jawcodeProvider: "anthropic", status: "verified", }); - expect(price?.sourceRef).toContain("platform.claude.com"); - expect(price?.sourceRef).toContain("0.025x"); } expect(resolveMatchedPrice("anthropic-pb51d9b", "claude-fable-5-1")?.cost4).toEqual(COST4); // Cursor accepts all three spellings but pricing stores one canonical overlay row. From 2a7d502f70abb974a15a194f4f382a2c29304e24 Mon Sep 17 00:00:00 2001 From: jun Date: Fri, 4 Sep 2026 23:48:24 +0900 Subject: [PATCH 3/6] docs(credits): record #3284 as carried work MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the gap issue #3431 opened at @Ingwannu's request. The Gemini 3.8 Flash Antigravity work first submitted by @mdwsk88 in #3284 landed on dev via #3286 (3d3c4fe26), and #3284 was closed as superseded rather than merged — so the contributor graph shows nothing. CREDITS.md exists for exactly that case. The file sets two bars and both are met. It says "If you find a landing that belongs on this page, open an issue"; #3431 is that issue. It also says entries cite the maintainer's own words and are never inferred from diff similarity; the quoted text is verbatim from @Ingwannu's closing comment on #3284. Verified independently rather than taken from the issue body: 3d3c4fe26 is an ancestor of origin/dev and is the #3286 merge, #3284 is CLOSED and not merged, and src/providers/antigravity-models.ts on dev names gemini-3.8-flash 16 times, so the suffix ladder that PR argued for is what shipped. Verification: bun run privacy:scan passed (it reads CREDITS.md); table renders with the existing column alignment. --- CREDITS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CREDITS.md b/CREDITS.md index 8cff47f965..14bd5c84d5 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -55,6 +55,7 @@ Code, design, or tests from these pull requests shipped. | [#3078](https://github.com/lidge-jun/opencodex/pull/3078) | [@Veritas-7](https://github.com/Veritas-7) | `0ef04e640` | "reimplements both of your production hunks on `dev`" | | [#3142](https://github.com/lidge-jun/opencodex/pull/3142) | [@olddonkey](https://github.com/olddonkey) | `52d941640` | "That carry keeps the measurement/refusal work and ships the guard default-off" | | [#3300](https://github.com/lidge-jun/opencodex/pull/3300) | [@S0RYUASUKA](https://github.com/S0RYUASUKA) | `15b43e51c` | the same two test files made hermetic | +| [#3284](https://github.com/lidge-jun/opencodex/pull/3284) | [@mdwsk88](https://github.com/mdwsk88) | `3d3c4fe26` | "Core implementation is already on `dev` via #3286 (`3d3c4fe26`), including the suffix wire ladder, picker collapse, Google adapter coverage" | ## Report and diagnosis From 652c692ba652884ab6affbbcbd5f046f2ffab339 Mon Sep 17 00:00:00 2001 From: jun Date: Fri, 4 Sep 2026 23:58:42 +0900 Subject: [PATCH 4/6] feat(codex): name the Ultra Fast tier, and move the account actions out of the page head MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two halves of issue #3429, plus the page-head relief the same surface needed. PR #2994 added an ultrafast row to the pinned catalog and was closed unmerged: the picker gained a choice the wire could not honor. That verdict stands and is why nothing here synthesizes a catalog row. src/codex/data/upstream-models.json advertises exactly one tier — priority — on every row that has any, so an ultrafast entry would still be fabricated metadata. What is separately true is the half #3429 actually reports. A caller who supplies service_tier: "ultrafast" themselves gets the request forwarded, and then canonicalFastTierMarker folds it to undefined, fastIntent goes false, and the attempt is recorded as fastOutcome "not-requested" — the log asserting the user asked for nothing. requestLogSpeedLabel returns undefined for the same value, so the Logs speed column stays empty. The proxy was carrying a tier it refused to name. canonicalFastTierMarker now folds ultrafast to its own canonical rather than onto priority, which would have been the opposite lie: claiming a 1.5x Fast tier when the caller named a different one. There is deliberately no canonicalToWire entry, so an ultrafast attempt lands on "unknown" instead of a false "applied" — the truth is that it was requested and we cannot confirm it was honored. The ultraFastTier flag follows the fastRows precedent exactly: optional, catch(false) so a malformed hand edit degrades to off, read with === true, and off deletes the key rather than persisting a false nobody chose. Its dashboard toggle gets the boolean guard in config-routes.ts that fastRows never needed. The page head carried a title, a status line, the Spark toggle and two buttons on one row, with the account actions sitting above the cards they act on. Pause and Refresh move to their own row below the account-mode banner. The embedded Providers surface keeps them inline: it renders a bare .row with no title, so there is nothing to crowd there. Verification: bun run typecheck, bun run lint:gui, 408 focused backend tests including the byte-golden default-path guard in fastwire-characterization-routing, and the GUI head-wrap, toast-tone, controller and locale-parity suites. Live on a scratch instance: the head shows only the title and Spark toggle, the two actions render below, the toggle reads OFF by default, and enabling it writes ultraFastTier: true to config.json. Repository-wide suite not run. --- .../assets/010_codex_set_head_relocated.png | Bin 0 -> 13918 bytes gui/src/components/CodexAccountPool.tsx | 18 ++- gui/src/components/UltraFastTierSetting.tsx | 133 ++++++++++++++++++ .../codex-account-pool-main-card.tsx | 90 +++++++++--- gui/src/i18n/de.ts | 6 + gui/src/i18n/en.ts | 6 + gui/src/i18n/fr.ts | 6 + gui/src/i18n/ja.ts | 6 + gui/src/i18n/ko.ts | 6 + gui/src/i18n/ru.ts | 6 + gui/src/i18n/tr.ts | 6 + gui/src/i18n/zh-TW.ts | 6 + gui/src/i18n/zh.ts | 6 + gui/src/pages/codex-set-multiauth.tsx | 2 + gui/src/styles.css | 18 +++ .../codex-set-actions-relocation.test.ts | 64 +++++++++ src/config.ts | 3 + src/providers/fastwire.ts | 20 ++- src/server/management/config-routes.ts | 19 ++- src/server/request-log.ts | 6 + src/types/config.ts | 11 ++ tests/ultrafast-tier-honesty.test.ts | 61 ++++++++ 22 files changed, 478 insertions(+), 21 deletions(-) create mode 100644 devlog/_plan/260904_triage_gap_closure/assets/010_codex_set_head_relocated.png create mode 100644 gui/src/components/UltraFastTierSetting.tsx create mode 100644 gui/tests/codex-set-actions-relocation.test.ts create mode 100644 tests/ultrafast-tier-honesty.test.ts diff --git a/devlog/_plan/260904_triage_gap_closure/assets/010_codex_set_head_relocated.png b/devlog/_plan/260904_triage_gap_closure/assets/010_codex_set_head_relocated.png new file mode 100644 index 0000000000000000000000000000000000000000..cd7a71b43ca406ab36728fd411e4a39e6e36890d GIT binary patch literal 13918 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 literal 0 HcmV?d00001 diff --git a/gui/src/components/CodexAccountPool.tsx b/gui/src/components/CodexAccountPool.tsx index cfedd15013..db2ef62652 100644 --- a/gui/src/components/CodexAccountPool.tsx +++ b/gui/src/components/CodexAccountPool.tsx @@ -14,7 +14,7 @@ import { readJsonIfOk } from "../fetch-json"; import { CodexAccountPoolCards, CodexAccountPoolReauthBanner } from "./codex-account-pool-cards"; import { CodexAccountSwitchModal } from "./codex-account-switch-modal"; import { CodexAccountResetModal } from "./codex-account-reset-modal"; -import { CodexAccountPoolLoadStates, CodexAccountPoolMainCard, CodexAccountPoolPageHead } from "./codex-account-pool-main-card"; +import { CodexAccountPoolActions, CodexAccountPoolLoadStates, CodexAccountPoolMainCard, CodexAccountPoolPageHead } from "./codex-account-pool-main-card"; import { redeemResetCredit } from "./codex-account-pool-handlers"; import type { CodexAccountEntry } from "./codex-account-pool-types"; import { accountNeedsReauth } from "../oauth-health-display"; @@ -348,6 +348,22 @@ export default function CodexAccountPool({ apiBase, accountModeState = null, ban {banner} + {/* + Relocated out of the page head: with two accounts the head carried a title, a + status line, a toggle and two buttons on one row, and the actions sat above the + cards they act on. They belong next to the accounts. + */} + {!embedded && ( + { void refreshQuotas(); }} + onPauseExhausted={() => { void pauseExhausted(); }} + /> + )} + {/* Skeleton must sit where main/pool cards will be — never above the account-mode banner, or the strip collapses on ready and shoves the whole page up (CLS). */} (null); + const savingRef = useRef(false); + const loadGenerationRef = useRef(0); + + const load = useCallback(async () => { + // A poll landing between the optimistic flip and the PUT response must not revert the + // UI to the server's pre-save value. + if (savingRef.current) return; + const generation = ++loadGenerationRef.current; + const bounded = createBoundedFetch(15_000); + try { + const res = await fetch(`${apiBase}/api/settings`, { signal: bounded.signal }); + if (!res.ok) throw new Error("load"); + const payload = await res.json() as { ultraFastTier?: unknown }; + if (savingRef.current || generation !== loadGenerationRef.current) return; + setEnabled(payload.ultraFastTier === true); + setHydrated(true); + setLoadError(false); + } catch { + if (!savingRef.current && generation === loadGenerationRef.current) setLoadError(true); + } finally { + bounded.clear(); + } + }, [apiBase]); + + useEffect(() => { + const timeout = window.setTimeout(() => { void load(); }, 0); + const stop = startVisibilityPoll(() => { void load(); }, 30_000); + return () => { + window.clearTimeout(timeout); + stop(); + }; + }, [load]); + + const toggle = useCallback(async () => { + if (savingRef.current || !hydrated || loadError) return; + const requested = !enabled; + savingRef.current = true; + setSaving(true); + setFeedback(null); + // Optimistic, then reconciled with what the server actually stored. + setEnabled(requested); + const bounded = createBoundedFetch(15_000); + try { + const res = await fetch(`${apiBase}/api/settings`, { + method: "PUT", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ ultraFastTier: requested }), + signal: bounded.signal, + }); + if (!res.ok) throw new Error("save"); + const payload = await res.json() as { ultraFastTier?: unknown }; + const confirmed = typeof payload.ultraFastTier === "boolean" ? payload.ultraFastTier : requested; + setEnabled(confirmed); + setFeedback({ tone: "ok", message: t(confirmed ? "codexAuth.ultraFastEnabled" : "codexAuth.ultraFastDisabled") }); + } catch { + setEnabled(!requested); + setFeedback({ tone: "err", message: t("codexAuth.ultraFastFailed") }); + } finally { + bounded.clear(); + savingRef.current = false; + setSaving(false); + } + }, [apiBase, enabled, hydrated, loadError, t]); + + const controlsDisabled = saving || !hydrated || loadError; + + return ( +
+
+ {t("codexAuth.ultraFastTitle")} +
+ {loadError ? t("codexAuth.ultraFastLoadFailed") : t("codexAuth.ultraFastDesc")} +
+
+
+ {loadError && ( + + )} + +
+ {feedback && ( +
+ {feedback.message} +
+ )} +
+ ); +} diff --git a/gui/src/components/codex-account-pool-main-card.tsx b/gui/src/components/codex-account-pool-main-card.tsx index dba055fa5a..d55094afd1 100644 --- a/gui/src/components/codex-account-pool-main-card.tsx +++ b/gui/src/components/codex-account-pool-main-card.tsx @@ -234,27 +234,85 @@ export function CodexAccountPoolPageHead({ )} - - + {/* + The two account-scoped actions used to live here, beside the page title. On the + standalone page that put four controls plus a heading on one row, and the actions + sat far above the account cards they act on. They render in + CodexAccountPoolActions below instead. The embedded surface keeps them inline, + because there is no title row there to crowd. + */} + {embedded && ( + + )} ); } +/** The pause/refresh pair, shared by the embedded head and the standalone action row. */ +export function CodexAccountPoolActionButtons({ + t, + refreshingQuota, + pausingExhausted, + pauseBusy, + onRefresh, + onPauseExhausted, +}: { + t: TFn; + refreshingQuota: boolean; + pausingExhausted: boolean; + pauseBusy?: boolean; + onRefresh: () => void; + onPauseExhausted: () => void; +}) { + return ( + <> + + + + ); +} + +/** + * Standalone-page action row: the pause/refresh pair, moved out of the page head and + * placed directly above the account cards they operate on. + */ +export function CodexAccountPoolActions(props: { + t: TFn; + refreshingQuota: boolean; + pausingExhausted: boolean; + pauseBusy?: boolean; + onRefresh: () => void; + onPauseExhausted: () => void; +}) { + return ( +
+ +
+ ); +} + export function CodexAccountPoolLoadStates({ t, loadState, diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index ab13c622f5..9045e81ef1 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -1180,6 +1180,12 @@ export const de: Record = { "codexAuth.sparkQuotaHidden": "Codex-Spark-Kontingent ausgeblendet", "codexAuth.sparkQuotaFailed": "Codex-Spark-Kontingent konnte nicht geändert werden", "codexAuth.refreshQuota": "Kontingente aktualisieren", + "codexAuth.ultraFastTitle": "Ultra-Fast-Diensttarif", + "codexAuth.ultraFastDesc": "Verhindert, dass ein selbst konfigurierter ultrafast-Diensttarif beim Neuaufbau des Katalogs entfernt wird, und benennt ihn in den Anfrageprotokollen. Ultra Fast wird nicht in die Modellauswahl aufgenommen: Upstream kündigt nur Fast an, ein Eintrag würde also eine Geschwindigkeit anbieten, die die Leitung nicht liefern kann.", + "codexAuth.ultraFastLoadFailed": "Die Ultra-Fast-Einstellung konnte nicht gelesen werden.", + "codexAuth.ultraFastEnabled": "Ultra-Fast-Tarif aktiviert", + "codexAuth.ultraFastDisabled": "Ultra-Fast-Tarif deaktiviert", + "codexAuth.ultraFastFailed": "Die Ultra-Fast-Einstellung konnte nicht geändert werden", "codexAuth.refreshingQuota": "Aktualisiere…", "codexAuth.quotaRefreshed": "Kontingente aktualisiert", "codexAuth.quotaRefreshFailed": "Kontingente konnten nicht aktualisiert werden", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 54882c2527..eba44bae1c 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -1722,6 +1722,12 @@ export const en = { "codexAuth.sparkQuotaHidden": "Codex Spark quota hidden", "codexAuth.sparkQuotaFailed": "Could not change the Codex Spark quota setting", "codexAuth.refreshQuota": "Refresh quotas", + "codexAuth.ultraFastTitle": "Ultra Fast service tier", + "codexAuth.ultraFastDesc": "Keeps an ultrafast service tier you configured yourself from being stripped when the catalog is regenerated, and names it in the request logs. It does not add Ultra Fast to the model picker: upstream advertises only Fast, so a picker row would offer a speed the wire cannot deliver.", + "codexAuth.ultraFastLoadFailed": "Could not read the Ultra Fast setting.", + "codexAuth.ultraFastEnabled": "Ultra Fast tier enabled", + "codexAuth.ultraFastDisabled": "Ultra Fast tier disabled", + "codexAuth.ultraFastFailed": "Could not change the Ultra Fast setting", "codexAuth.refreshingQuota": "Refreshing...", "codexAuth.quotaRefreshed": "Quotas refreshed", "codexAuth.quotaRefreshFailed": "Failed to refresh quotas", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index fbdce1062d..56967c3ffb 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -1654,6 +1654,12 @@ export const fr: Record = { "codexAuth.sparkQuotaHidden": "Quota Codex Spark masqué", "codexAuth.sparkQuotaFailed": "Impossible de modifier le réglage du quota Codex Spark", "codexAuth.refreshQuota": "Actualiser les quotas", + "codexAuth.ultraFastTitle": "Niveau de service Ultra Fast", + "codexAuth.ultraFastDesc": "Empêche la suppression d’un niveau de service ultrafast que vous avez configuré vous-même lors de la régénération du catalogue, et le nomme dans les journaux de requêtes. Ultra Fast n’est pas ajouté au sélecteur de modèles : l’amont n’annonce que Fast, une entrée proposerait donc une vitesse que le transport ne peut pas fournir.", + "codexAuth.ultraFastLoadFailed": "Impossible de lire le réglage Ultra Fast.", + "codexAuth.ultraFastEnabled": "Niveau Ultra Fast activé", + "codexAuth.ultraFastDisabled": "Niveau Ultra Fast désactivé", + "codexAuth.ultraFastFailed": "Impossible de modifier le réglage Ultra Fast", "codexAuth.refreshingQuota": "Actualisation…", "codexAuth.quotaRefreshed": "Quotas actualisés", "codexAuth.quotaRefreshFailed": "Échec de l’actualisation des quotas", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 9f619578f0..6cde2b5610 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -1614,6 +1614,12 @@ export const ja: Record = { "codexAuth.sparkQuotaHidden": "Codex Spark 使用量を非表示にしました", "codexAuth.sparkQuotaFailed": "Codex Spark 使用量の設定を変更できませんでした", "codexAuth.refreshQuota": "クォータを更新", + "codexAuth.ultraFastTitle": "Ultra Fast サービスティア", + "codexAuth.ultraFastDesc": "自分で設定した ultrafast サービスティアがカタログ再生成時に削除されないようにし、リクエストログにそのティア名を記録します。モデルピッカーに Ultra Fast は追加しません。アップストリームは Fast しか公開しておらず、ピッカーに項目を出すと実際には出せない速度を選ばせることになるためです。", + "codexAuth.ultraFastLoadFailed": "Ultra Fast 設定を読み取れませんでした。", + "codexAuth.ultraFastEnabled": "Ultra Fast ティアを有効にしました", + "codexAuth.ultraFastDisabled": "Ultra Fast ティアを無効にしました", + "codexAuth.ultraFastFailed": "Ultra Fast 設定を変更できませんでした", "codexAuth.refreshingQuota": "更新中...", "codexAuth.quotaRefreshed": "クォータを更新しました", "codexAuth.quotaRefreshFailed": "クォータの更新に失敗しました", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 281dd3d8c4..237ca5ecda 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -1204,6 +1204,12 @@ export const ko: Record = { "codexAuth.sparkQuotaHidden": "Codex Spark 할당량을 숨겼습니다", "codexAuth.sparkQuotaFailed": "Codex Spark 할당량 설정을 바꾸지 못했습니다", "codexAuth.refreshQuota": "할당량 새로고침", + "codexAuth.ultraFastTitle": "Ultra Fast 서비스 티어", + "codexAuth.ultraFastDesc": "직접 설정한 ultrafast 서비스 티어가 카탈로그를 다시 만들 때 지워지지 않게 하고, 요청 로그에 그 티어 이름을 남깁니다. 모델 피커에 Ultra Fast를 추가하지는 않습니다. 업스트림은 Fast만 알리기 때문에, 피커에 칸을 만들면 실제로 낼 수 없는 속도를 고르게 하는 셈입니다.", + "codexAuth.ultraFastLoadFailed": "Ultra Fast 설정을 읽지 못했습니다.", + "codexAuth.ultraFastEnabled": "Ultra Fast 티어를 켰습니다", + "codexAuth.ultraFastDisabled": "Ultra Fast 티어를 껐습니다", + "codexAuth.ultraFastFailed": "Ultra Fast 설정을 바꾸지 못했습니다", "codexAuth.refreshingQuota": "새로고침 중...", "codexAuth.quotaRefreshed": "할당량을 다시 조회했습니다", "codexAuth.quotaRefreshFailed": "할당량 재조회에 실패했습니다", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 0bae1d4ff9..a1a16ebd75 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -1665,6 +1665,12 @@ export const ru: Record = { "codexAuth.sparkQuotaHidden": "Квота Codex Spark скрыта", "codexAuth.sparkQuotaFailed": "Не удалось изменить настройку квоты Codex Spark", "codexAuth.refreshQuota": "Обновить квоты", + "codexAuth.ultraFastTitle": "Уровень обслуживания Ultra Fast", + "codexAuth.ultraFastDesc": "Не даёт удалить настроенный вами уровень ultrafast при перегенерации каталога и записывает его имя в журналы запросов. Ultra Fast не добавляется в выбор моделей: вышестоящий сервис объявляет только Fast, поэтому пункт в списке предлагал бы скорость, которую канал не может обеспечить.", + "codexAuth.ultraFastLoadFailed": "Не удалось прочитать настройку Ultra Fast.", + "codexAuth.ultraFastEnabled": "Уровень Ultra Fast включён", + "codexAuth.ultraFastDisabled": "Уровень Ultra Fast выключен", + "codexAuth.ultraFastFailed": "Не удалось изменить настройку Ultra Fast", "codexAuth.refreshingQuota": "Обновление...", "codexAuth.quotaRefreshed": "Квоты обновлены", "codexAuth.quotaRefreshFailed": "Не удалось обновить квоты", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 25654fff76..09ffe51110 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -1683,6 +1683,12 @@ export const tr: Record = { "codexAuth.sparkQuotaHidden": "Codex Spark kotası gizlendi", "codexAuth.sparkQuotaFailed": "Codex Spark kotası ayarı değiştirilemedi", "codexAuth.refreshQuota": "Kotaları yenile", + "codexAuth.ultraFastTitle": "Ultra Fast hizmet katmanı", + "codexAuth.ultraFastDesc": "Kendi yapılandırdığınız ultrafast hizmet katmanının katalog yeniden oluşturulurken silinmesini önler ve istek günlüklerinde bu katmanın adını yazar. Ultra Fast’i model seçicisine eklemez: üst kaynak yalnızca Fast duyurur, bu yüzden bir satır eklemek hattın veremeyeceği bir hızı seçtirmek olurdu.", + "codexAuth.ultraFastLoadFailed": "Ultra Fast ayarı okunamadı.", + "codexAuth.ultraFastEnabled": "Ultra Fast katmanı etkinleştirildi", + "codexAuth.ultraFastDisabled": "Ultra Fast katmanı devre dışı bırakıldı", + "codexAuth.ultraFastFailed": "Ultra Fast ayarı değiştirilemedi", "codexAuth.refreshingQuota": "Yenileniyor...", "codexAuth.quotaRefreshed": "Kotalar yenilendi", "codexAuth.quotaRefreshFailed": "Kotalar yenilenemedi", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 21c2c5150f..4c63e861c4 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -1298,6 +1298,12 @@ export const zhTW: Record = { "codexAuth.sparkQuotaHidden": "已隱藏 Codex Spark 配額", "codexAuth.sparkQuotaFailed": "無法變更 Codex Spark 配額設定", "codexAuth.refreshQuota": "重新整理額度", + "codexAuth.ultraFastTitle": "Ultra Fast 服務層級", + "codexAuth.ultraFastDesc": "讓你自行設定的 ultrafast 服務層級在重新產生目錄時不被移除,並在請求記錄中寫下該層級名稱。它不會把 Ultra Fast 加入模型選擇器:上游只公布 Fast,選擇器出現該項等於讓使用者挑一個實際無法提供的速度。", + "codexAuth.ultraFastLoadFailed": "無法讀取 Ultra Fast 設定。", + "codexAuth.ultraFastEnabled": "已啟用 Ultra Fast 層級", + "codexAuth.ultraFastDisabled": "已停用 Ultra Fast 層級", + "codexAuth.ultraFastFailed": "無法變更 Ultra Fast 設定", "codexAuth.refreshingQuota": "重新整理中...", "codexAuth.quotaRefreshed": "額度已重新整理", "codexAuth.quotaRefreshFailed": "額度重新整理失敗", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 2223a66a20..4f338fdcd9 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -1197,6 +1197,12 @@ export const zh: Record = { "codexAuth.sparkQuotaHidden": "已隐藏 Codex Spark 配额", "codexAuth.sparkQuotaFailed": "无法更改 Codex Spark 配额设置", "codexAuth.refreshQuota": "刷新额度", + "codexAuth.ultraFastTitle": "Ultra Fast 服务层级", + "codexAuth.ultraFastDesc": "让你自己配置的 ultrafast 服务层级在重新生成目录时不被删除,并在请求日志中记录该层级名称。它不会把 Ultra Fast 加入模型选择器:上游只公布 Fast,选择器中出现该项等于让用户选择一个实际无法提供的速度。", + "codexAuth.ultraFastLoadFailed": "无法读取 Ultra Fast 设置。", + "codexAuth.ultraFastEnabled": "已启用 Ultra Fast 层级", + "codexAuth.ultraFastDisabled": "已禁用 Ultra Fast 层级", + "codexAuth.ultraFastFailed": "无法更改 Ultra Fast 设置", "codexAuth.refreshingQuota": "刷新中...", "codexAuth.quotaRefreshed": "额度已刷新", "codexAuth.quotaRefreshFailed": "额度刷新失败", diff --git a/gui/src/pages/codex-set-multiauth.tsx b/gui/src/pages/codex-set-multiauth.tsx index c0c4b56b95..8f76f91af5 100644 --- a/gui/src/pages/codex-set-multiauth.tsx +++ b/gui/src/pages/codex-set-multiauth.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useT } from "../i18n/shared"; import CodexAccountPool from "../components/CodexAccountPool"; import DefaultModeRequestUserInputSetting from "../components/DefaultModeRequestUserInputSetting"; +import UltraFastTierSetting from "../components/UltraFastTierSetting"; import CodexAccountPickerSetting from "../components/CodexAccountPickerSetting"; import { codexAccountModeState, type CodexAccountModeState } from "../codex-multi-state"; import { navigateHash } from "../hash-routing"; @@ -196,6 +197,7 @@ export default function CodexSetMultiauth({ apiBase }: { apiBase: string }) { advancedExtras={<> + } /> diff --git a/gui/src/styles.css b/gui/src/styles.css index 1c5147171b..027b055c3a 100644 --- a/gui/src/styles.css +++ b/gui/src/styles.css @@ -1618,6 +1618,13 @@ dialog.modal-overlay::backdrop { .codex-request-user-input-controls > .toggle { margin-left: auto; } .codex-request-user-input-feedback { flex: 1 0 100%; margin-top: -8px; color: var(--muted); font-size: var(--text-label); line-height: var(--leading-body); text-align: right; } .codex-request-user-input-feedback.is-error { color: var(--red); } +/* Ultra Fast opt-in: same card-row idiom as the picker and request-user-input switches. */ +.codex-ultrafast-card { gap: 16px; flex-wrap: wrap; } +.codex-ultrafast-copy { flex: 1 1 auto; min-width: 0; } +.codex-ultrafast-copy .card-sub { padding: 2px 0 0; } +.codex-ultrafast-controls { display: flex; align-items: flex-end; gap: 12px; flex: 0 0 auto; margin-left: auto; } +.codex-ultrafast-feedback { flex: 1 0 100%; margin-top: -8px; color: var(--muted); font-size: var(--text-label); line-height: var(--leading-body); text-align: right; } +.codex-ultrafast-feedback.is-error { color: var(--red); } .codex-account-picker-card { gap: 16px; flex-wrap: wrap; margin-top: 16px; } .codex-auth-advanced { margin-top: 8px; } .codex-auth-advanced__toggle { @@ -1809,6 +1816,17 @@ dialog.modal-overlay::backdrop { font-size: 12px; color: var(--muted); } +/* The pause/refresh pair after it moved out of the page head. Right-aligned so it still + reads as chrome for the account list below rather than a heading of its own, and it + wraps because both labels grow substantially in several locales. */ +.codex-auth-actions-row { + display: flex; + align-items: center; + flex-wrap: wrap; + justify-content: flex-end; + gap: 8px; + margin-bottom: 10px; +} /* Account actions (pause / copy doctor / pause-exhausted): clearer hover than plain btn-ghost on card surface — same raised-hover + faint border as icon/list cues. */ .codex-auth-action-btn:hover:not(:disabled) { diff --git a/gui/tests/codex-set-actions-relocation.test.ts b/gui/tests/codex-set-actions-relocation.test.ts new file mode 100644 index 0000000000..30e40a5ef4 --- /dev/null +++ b/gui/tests/codex-set-actions-relocation.test.ts @@ -0,0 +1,64 @@ +/** + * Where the Codex Set account actions live. + * + * "한도 도달 계정 일시 중지" and "할당량 새로고침" used to sit in the page head beside the + * title and the Spark toggle — four controls and a heading on one row, with the actions + * far above the account cards they operate on. They render in their own row below the + * account-mode banner now. + * + * The embedded surface is deliberately excluded: in the Providers workspace the same + * component renders a bare `.row` with no title, so there is nothing to crowd and the + * buttons stay inline. + */ +import { expect, test } from "bun:test"; + +const raw = await Bun.file(new URL("../src/components/codex-account-pool-main-card.tsx", import.meta.url)).text(); +// Comments name these controls; matching prose is not evidence about code. +const src = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); +const pool = (await Bun.file(new URL("../src/components/CodexAccountPool.tsx", import.meta.url)).text()) + .replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, ""); +const css = await Bun.file(new URL("../src/styles.css", import.meta.url)).text(); + +const headStart = src.indexOf("export function CodexAccountPoolPageHead"); +const head = src.slice(headStart, src.indexOf("export function CodexAccountPoolActionButtons", headStart)); + +test("the standalone page head no longer renders the two action buttons inline", () => { + // The head keeps the title, the feedback region and the Spark toggle; the pause and + // refresh labels are reached through the shared component only. + expect(head).not.toContain('t("codexAuth.pauseExhausted")'); + expect(head).not.toContain('t("codexAuth.refreshQuota")'); +}); + +test("the embedded surface keeps them inline, because it has no title row to crowd", () => { + expect(head).toContain("embedded && ("); + expect(head).toContain("CodexAccountPoolActionButtons"); +}); + +test("the standalone page renders them in their own row instead", () => { + expect(pool).toContain("!embedded && ("); + expect(pool).toContain("CodexAccountPoolActions"); +}); + +test("both buttons still exist and keep their disabled contract", () => { + const shared = src.slice(src.indexOf("export function CodexAccountPoolActionButtons")); + expect(shared).toContain('t("codexAuth.pauseExhausted")'); + expect(shared).toContain('t("codexAuth.refreshQuota")'); + // A refresh in flight must not let a second pause/refresh start beside it. + expect(shared.match(/disabled=\{refreshingQuota \|\| pausingExhausted \|\| !!pauseBusy\}/g)?.length).toBe(2); +}); + +test("the head classes the CSS tests pin are still rendered", () => { + // codex-set-page-head-wrap.test.ts throws "rule not found" if these disappear, and the + // toast-tone suite queries the feedback span by class. + expect(head).toContain("page-head codex-auth-page-head"); + expect(head).toContain("codex-auth-page-head__actions"); + expect(head).toContain("codex-auth-page-head__feedback"); +}); + +test("the relocated row has a layout rule that wraps", () => { + const start = css.indexOf(".codex-auth-actions-row {"); + expect(start).toBeGreaterThan(-1); + const rule = css.slice(start, css.indexOf("}", start)); + expect(rule).toContain("flex-wrap: wrap"); + expect(rule).toContain("justify-content: flex-end"); +}); diff --git a/src/config.ts b/src/config.ts index d69d292c8f..e541840671 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1053,6 +1053,9 @@ const configSchema = z.object({ // Same opt-in discipline: a malformed hand edit degrades to off rather than rejecting // every provider. fastRows: z.boolean().optional().catch(false), + // Ultra Fast is opt-in for the same reason and degrades the same way: a malformed hand + // edit turns the tier off rather than rejecting the config that carries it. + ultraFastTier: z.boolean().optional().catch(false), // Future versions remain opaque through passthrough-compatible whole-config saves. // Only version 1 grants deletion authority in the rebase path. configRebaseProvenance: z.unknown().optional(), diff --git a/src/providers/fastwire.ts b/src/providers/fastwire.ts index 7eae6b0fe1..78b5cbc09d 100644 --- a/src/providers/fastwire.ts +++ b/src/providers/fastwire.ts @@ -244,9 +244,22 @@ export function resolveFastPolicy( }; } -export function canonicalFastTierMarker(callerTier: string | undefined): "priority" | undefined { +/** + * Fold a caller's service tier onto a canonical fast marker. + * + * `ultrafast` is recognised as INTENT even though no shipped catalog advertises it and + * `DEFAULT_SERVICE_TIER_FAST_WIRE` has no wire mapping for it. That asymmetry is + * deliberate: a caller who sends `ultrafast` (which #3429's reporter did, via their own + * catalog edit) was previously folded to `undefined`, which made `fastIntent` false and + * recorded `fastOutcome: "not-requested"` — the log asserting the user asked for nothing. + * Recognising the intent without a wire mapping lands the attempt on `unknown` instead, + * which is the truth: the tier was requested, and we cannot confirm it was honored. + */ +export function canonicalFastTierMarker(callerTier: string | undefined): "priority" | "ultrafast" | undefined { const folded = callerTier?.trim().toLowerCase(); - return folded === "priority" || folded === "fast" ? "priority" : undefined; + if (folded === "priority" || folded === "fast") return "priority"; + if (folded === "ultrafast") return "ultrafast"; + return undefined; } /** Capture Fast demand before the final A1 serialization action rewrites the parsed tier view. */ @@ -298,7 +311,8 @@ export function createAdapterTierMetadata( ): AdapterTierMetadata | undefined { if (!context || !decision) return undefined; - const callerCanonicalFast = canonicalFastTierMarker(context.callerTier) === "priority"; + const callerMarker = canonicalFastTierMarker(context.callerTier); + const callerCanonicalFast = callerMarker !== undefined; const callerTierDropped = context.callerTier !== undefined && !callerCanonicalFast && wireValue === null; diff --git a/src/server/management/config-routes.ts b/src/server/management/config-routes.ts index 6fe625aa10..2cd99beb69 100644 --- a/src/server/management/config-routes.ts +++ b/src/server/management/config-routes.ts @@ -303,6 +303,9 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise; /** OpenAI provider-contract migration marker (v2 = single `openai` provider with account mode). */ diff --git a/tests/ultrafast-tier-honesty.test.ts b/tests/ultrafast-tier-honesty.test.ts new file mode 100644 index 0000000000..23fd156637 --- /dev/null +++ b/tests/ultrafast-tier-honesty.test.ts @@ -0,0 +1,61 @@ +/** + * Ultra Fast: name the tier the proxy is already carrying. + * + * PR #2994 added an `ultrafast` row to the pinned catalog and was closed unmerged with + * the verdict that the picker gained a choice the wire could not honor — + * `src/codex/data/upstream-models.json` advertises only `priority`, so the row was + * fabricated metadata. That decision stands: nothing here synthesizes a catalog row. + * + * What #3429 reported is separately true and fixable. A caller who supplies + * `service_tier: "ultrafast"` themselves gets the request forwarded, and then the proxy + * records `fastOutcome: "not-requested"` and no speed label — it asserts the user asked + * for nothing. These tests pin the corrected accounting. + */ +import { describe, expect, test } from "bun:test"; +import { canonicalFastTierMarker } from "../src/providers/fastwire"; +import { requestLogSpeedLabel } from "../src/server/request-log"; + +describe("ultrafast intent is recognised, not mistaken for silence", () => { + test("the caller marker folds ultrafast to its own canonical, not to priority", () => { + // Folding it onto "priority" would be the other lie: claiming a 1.5x Fast tier was + // requested when the caller named a different one. + expect(canonicalFastTierMarker("ultrafast")).toBe("ultrafast"); + expect(canonicalFastTierMarker("UltraFast")).toBe("ultrafast"); + expect(canonicalFastTierMarker(" ultrafast ")).toBe("ultrafast"); + }); + + test("the existing Fast spellings are unchanged", () => { + expect(canonicalFastTierMarker("priority")).toBe("priority"); + expect(canonicalFastTierMarker("fast")).toBe("priority"); + expect(canonicalFastTierMarker(" PRIORITY ")).toBe("priority"); + }); + + test("unrelated tiers still fold to undefined", () => { + // "auto" reaching a canonical marker would turn every default request into Fast intent. + expect(canonicalFastTierMarker("auto")).toBeUndefined(); + expect(canonicalFastTierMarker("default")).toBeUndefined(); + expect(canonicalFastTierMarker("ultra")).toBeUndefined(); + expect(canonicalFastTierMarker("ultra-fast")).toBeUndefined(); + expect(canonicalFastTierMarker(undefined)).toBeUndefined(); + expect(canonicalFastTierMarker("")).toBeUndefined(); + }); +}); + +describe("ultrafast gets a speed label", () => { + test("the label is its own, so Logs cannot read it as Fast", () => { + expect(requestLogSpeedLabel("ultrafast")).toBe("ultrafast"); + expect(requestLogSpeedLabel(" UltraFast ")).toBe("ultrafast"); + }); + + test("the Fast contract is untouched", () => { + expect(requestLogSpeedLabel("priority")).toBe("fast"); + expect(requestLogSpeedLabel("fast")).toBe("fast"); + }); + + test("auto and absent still produce no label", () => { + // A label here would put a speed badge on every ordinary request. + expect(requestLogSpeedLabel("auto")).toBeUndefined(); + expect(requestLogSpeedLabel(undefined)).toBeUndefined(); + expect(requestLogSpeedLabel("")).toBeUndefined(); + }); +}); From 4f56ba3f9fcd104082fd473515a09b6082afcd1c Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 00:07:08 +0900 Subject: [PATCH 5/6] fix(codex): make the Ultra Fast flag actually do something MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-review caught the flag stored but never read: `grep ultraFastTier src/` outside config/types/routes returned nothing, so the toggle persisted a preference and changed no behavior. Shipping a switch whose description promises the tier survives regeneration, while nothing consumes it, would be the #2994 failure in a new place — a control that implies an effect it does not have. normalizeRoutedCatalogEntry now consults the flag. With it OFF the four deletes run exactly as before. With it ON the row keeps an ultrafast the OPERATOR put in their own catalog, which is the reported symptom: #3429's reporter hand-edited opencodex-catalog.json and watched every regeneration delete it. Preserving is narrowed, not blanket. retainOnlyUltraFastTier keeps the ultrafast entry and drops everything else, because routed rows are stripped precisely so a clone of a native template cannot inherit OpenAI's priority tier — reopening that under an unrelated flag would be a worse bug than the one being fixed. A service_tier or default_service_tier naming a tier the row no longer carries is dropped for the same reason. And a row carrying only upstream's Fast is still stripped with the flag ON: there is no ultrafast to preserve, and inventing one is the line #2994 was closed for. The flag is read through ultraFastTierEnabled(loadConfig()) inside parsing.ts rather than threaded through deriveEntry. That function and its five call sites are pure RawEntry -> RawEntry transforms with no config parameter; plumbing one boolean through all of them would be a far larger change than the behavior it gates. Callers holding a config can still pass opts.ultraFastTier explicitly, which is what the new tests do, and a read failure degrades to OFF. Four new tests cover both states, including the two refusals: the flag never smuggles Fast onto a routed row, and it invents nothing when the operator supplied nothing. Verification: bun run typecheck, bun run lint:gui, 340 focused catalog/fastwire tests including the byte golden, 10 in ultrafast-tier-honesty, and bun run test:changed at 14419 pass / 0 fail across 785 files. --- src/codex/catalog/parsing.ts | 90 ++++++++++++++++++++++++++-- src/config.ts | 8 +++ tests/ultrafast-tier-honesty.test.ts | 57 ++++++++++++++++++ 3 files changed, 150 insertions(+), 5 deletions(-) diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index fecf49c2dd..fb29ab089a 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process"; import { createHash } from "node:crypto"; import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs"; import { delimiter, dirname, join, resolve } from "node:path"; -import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config"; +import { atomicWriteFile, expandUserPath, getConfigDir, loadConfig, ultraFastTierEnabled, websocketsEnabled } from "../../config"; import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths"; import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache"; import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth"; @@ -303,6 +303,70 @@ const NO_FAST_TIER_NATIVE_SLUGS = new Set([ "gpt-5.3-codex-spark", ]); +/** Does this row already carry an `ultrafast` tier the operator put there themselves? */ +/** + * Read the opt-in from the live config. + * + * `deriveEntry` and its five call sites are pure `RawEntry -> RawEntry` transforms with no + * config parameter, and threading one through all of them to carry a single boolean would + * be a much larger change than the behavior it gates. Callers that already hold a config + * can still pass `opts.ultraFastTier` explicitly, which is what the tests do; this is the + * fallback for the sync path. Failure reads as OFF, matching `.catch(false)`. + */ +function ultraFastTierOptIn(): boolean { + try { + return ultraFastTierEnabled(loadConfig()); + } catch { + return false; + } +} + +function entryDeclaresUltraFast(entry: RawEntry): boolean { + const tiers = entry.service_tiers; + const declaredInTiers = Array.isArray(tiers) && tiers.some(tier => ( + !!tier && typeof tier === "object" && "id" in tier + && String((tier as { id?: unknown }).id).trim().toLowerCase() === "ultrafast" + )); + const speeds = entry.additional_speed_tiers; + const declaredInSpeeds = Array.isArray(speeds) && speeds.some(speed => ( + typeof speed === "string" && speed.trim().toLowerCase() === "ultrafast" + )); + return declaredInTiers || declaredInSpeeds; +} + +/** + * Keep the operator's `ultrafast` and drop everything else. + * + * A routed row must not inherit the native template's `priority` tier, which is the whole + * reason this strip exists. Preserving the supplied tier without this narrowing would + * smuggle Fast onto third-party providers under an unrelated flag. + */ +function retainOnlyUltraFastTier(entry: RawEntry): void { + const tiers = entry.service_tiers; + const keptTiers = Array.isArray(tiers) + ? tiers.filter(tier => ( + !!tier && typeof tier === "object" && "id" in tier + && String((tier as { id?: unknown }).id).trim().toLowerCase() === "ultrafast" + )) + : []; + if (keptTiers.length > 0) entry.service_tiers = keptTiers; + else delete entry.service_tiers; + + const speeds = entry.additional_speed_tiers; + const keptSpeeds = Array.isArray(speeds) + ? speeds.filter(speed => typeof speed === "string" && speed.trim().toLowerCase() === "ultrafast") + : []; + if (keptSpeeds.length > 0) entry.additional_speed_tiers = keptSpeeds; + else delete entry.additional_speed_tiers; + + // A default of `priority` on a row that now only offers ultrafast would name a tier the + // row no longer carries. + if (String(entry.service_tier ?? "").trim().toLowerCase() !== "ultrafast") delete entry.service_tier; + if (String(entry.default_service_tier ?? "").trim().toLowerCase() !== "ultrafast") { + delete entry.default_service_tier; + } +} + export function normalizeServiceTiers(entry: RawEntry): RawEntry { // Strip service tiers for models that do not actually support the Fast tier. if (typeof entry.slug === "string" && NO_FAST_TIER_NATIVE_SLUGS.has(entry.slug)) { @@ -612,6 +676,7 @@ export function normalizeRoutedCatalogEntry( entry: RawEntry, parallelToolCalls = false, toolMode?: "code_mode_only" | "shell" | string, + opts?: { ultraFastTier?: boolean }, ): RawEntry { delete entry.model_messages; delete entry.tool_mode; @@ -619,10 +684,25 @@ export function normalizeRoutedCatalogEntry( delete entry.multi_agent_version; delete entry.use_responses_lite; delete entry.supports_websockets; - delete entry.additional_speed_tiers; - delete entry.service_tier; - delete entry.service_tiers; - delete entry.default_service_tier; + /* + * Tier metadata is stripped from routed rows because a row cloned from a native template + * would otherwise hand a third-party provider OpenAI's tiers. + * + * The opt-in carves out exactly one case: an `ultrafast` the OPERATOR put in their own + * catalog. #3429's reporter added it by hand and watched a regeneration delete it every + * time. Preserving what they wrote is not the same as advertising a tier — upstream + * publishes only `priority`, and synthesizing an ultrafast row is what PR #2994 was + * closed for. So this keeps a supplied tier and still never invents one. + */ + const keepUltraFast = (opts?.ultraFastTier ?? ultraFastTierOptIn()) && entryDeclaresUltraFast(entry); + if (!keepUltraFast) { + delete entry.additional_speed_tiers; + delete entry.service_tier; + delete entry.service_tiers; + delete entry.default_service_tier; + } else { + retainOnlyUltraFastTier(entry); + } // Routed rows cloned from native templates must not inherit OpenAI-only summary delivery. // Explicit provider/model metadata is re-applied after this normalization step. delete entry.supports_reasoning_summaries; diff --git a/src/config.ts b/src/config.ts index e541840671..2a1af85020 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3118,6 +3118,14 @@ export function websocketsEnabled(config: Pick): boolea return config.websockets === true; } +/** + * Opt-in Ultra Fast, read with the house `=== true` idiom so an absent key and a + * malformed one both mean off. + */ +export function ultraFastTierEnabled(config: Pick): boolean { + return config.ultraFastTier === true; +} + // --------------------------------------------------------------------------- // Hand-edit protection for the `claudeCode` subtree (devlog 260726_claude_auth_auto/040 H1). // diff --git a/tests/ultrafast-tier-honesty.test.ts b/tests/ultrafast-tier-honesty.test.ts index 23fd156637..9115772d3c 100644 --- a/tests/ultrafast-tier-honesty.test.ts +++ b/tests/ultrafast-tier-honesty.test.ts @@ -14,6 +14,7 @@ import { describe, expect, test } from "bun:test"; import { canonicalFastTierMarker } from "../src/providers/fastwire"; import { requestLogSpeedLabel } from "../src/server/request-log"; +import { normalizeRoutedCatalogEntry } from "../src/codex/catalog/parsing"; describe("ultrafast intent is recognised, not mistaken for silence", () => { test("the caller marker folds ultrafast to its own canonical, not to priority", () => { @@ -59,3 +60,59 @@ describe("ultrafast gets a speed label", () => { expect(requestLogSpeedLabel("")).toBeUndefined(); }); }); + +/** + * The catalog half. The flag PRESERVES a tier the operator supplied; it never invents one, + * which is the line PR #2994 was closed for crossing. + */ +describe("routed rows and the ultrafast opt-in", () => { + const operatorRow = () => ({ + slug: "kimi/k3", + service_tier: "ultrafast", + default_service_tier: "ultrafast", + service_tiers: [ + { id: "priority", name: "Fast", description: "1.5x speed, increased usage" }, + { id: "ultrafast", name: "Ultra Fast", description: "operator supplied" }, + ], + additional_speed_tiers: ["fast", "ultrafast"], + }); + + test("with the flag OFF every tier field is stripped, exactly as before", () => { + const entry = normalizeRoutedCatalogEntry(operatorRow(), false, undefined, { ultraFastTier: false }); + expect(entry.service_tier).toBeUndefined(); + expect(entry.service_tiers).toBeUndefined(); + expect(entry.default_service_tier).toBeUndefined(); + expect(entry.additional_speed_tiers).toBeUndefined(); + }); + + test("with the flag ON the operator's ultrafast survives regeneration", () => { + // The whole reported symptom: a hand-edited catalog lost the tier on every sync. + const entry = normalizeRoutedCatalogEntry(operatorRow(), false, undefined, { ultraFastTier: true }); + expect(entry.service_tiers).toEqual([{ id: "ultrafast", name: "Ultra Fast", description: "operator supplied" }]); + expect(entry.additional_speed_tiers).toEqual(["ultrafast"]); + expect(entry.service_tier).toBe("ultrafast"); + expect(entry.default_service_tier).toBe("ultrafast"); + }); + + test("the flag never smuggles Fast onto a routed row", () => { + // Routed rows are stripped because a clone of a native template would otherwise inherit + // OpenAI's priority tier. Preserving ultrafast must not reopen that. + const entry = normalizeRoutedCatalogEntry(operatorRow(), false, undefined, { ultraFastTier: true }); + const ids = (entry.service_tiers as Array<{ id: string }>).map(tier => tier.id); + expect(ids).not.toContain("priority"); + expect(entry.additional_speed_tiers).not.toContain("fast"); + }); + + test("the flag invents nothing when the operator supplied no ultrafast", () => { + // A row carrying only the upstream Fast tier is stripped whether the flag is on or off: + // upstream advertises no ultrafast, so there is nothing to preserve. + const fastOnly = { + slug: "kimi/k3", + service_tiers: [{ id: "priority", name: "Fast" }], + additional_speed_tiers: ["fast"], + }; + const entry = normalizeRoutedCatalogEntry(fastOnly, false, undefined, { ultraFastTier: true }); + expect(entry.service_tiers).toBeUndefined(); + expect(entry.additional_speed_tiers).toBeUndefined(); + }); +}); From f626a12daf048d017053762fe2de7372a0a01c73 Mon Sep 17 00:00:00 2001 From: jun Date: Sat, 5 Sep 2026 00:14:45 +0900 Subject: [PATCH 6/6] fix(fastwire): forward an unmapped canonical tier instead of dropping it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review found that the previous commit made the reported problem worse, and that every suite still passed while it did. Recognising ultrafast as a canonical marker routed it into the canonical-wire lookup in decideTier. That lookup is keyed by canonicalToWire, which maps only priority, so an unmapped canonical fell straight through to { kind: "drop" }. Measured before and after: before ultrafast -> forward-caller wire service_tier=ultrafast after ultrafast -> drop wire service_tier=(absent) So recognition was strictly worse than leaving the tier unrecognised: it used to be a foreign tier, and foreignCallerTiers "verbatim" forwarded it untouched. The operator's hand-configured tier stopped reaching the provider entirely, and the new "ultrafast" speed label became unreachable on the Responses path because tierValueAfterDecision had already cleared the value. That is the exact symptom #3429 reported, reintroduced by the fix for it. An unmapped canonical now falls through to the foreign-tier rules rather than dropping. Second finding, same root cause: callerCanonicalFast was widened to "any marker", which made a fastMode:false request from an ultrafast caller record callerFastSuppressedByConfig. The Fast toggle did not suppress a 1.5x Fast request; it turned away a differently-named one. The predicate is back to === "priority" for the drop/suppression facts, and only fastIntent carries the wider fast-family question. Third: ultraFastTierOptIn called loadConfig() per catalog row, and normalizeRoutedCatalogEntry runs in a per-entry sync loop — that is a chmod, three secret hardenings, a file read and a full Zod parse per row. It is memoized with a 5s TTL plus a reset seam; callers holding a config still pass opts.ultraFastTier and bypass it entirely. Fourth: the expected-prices comment claiming claude-fable-5-1 has no jawcode row was made stale by 21cb149d5, which added exactly that row. Five new tests cover what the previous suite could not see: the wire decision itself, and the suppression-vs-dropped distinction. The old tests all passed against the broken behavior because none of them asserted decideTier. Verification: bun run typecheck, bun run lint:gui, 720 focused fastwire/catalog/ request-log/usage tests, and bun run test:changed at 14466 pass / 0 fail across 787 files. --- src/codex/catalog/parsing.ts | 33 +++++++++---- src/providers/fastwire.ts | 25 ++++++++-- src/usage/expected-prices.ts | 8 ++-- tests/ultrafast-tier-honesty.test.ts | 69 +++++++++++++++++++++++++++- 4 files changed, 118 insertions(+), 17 deletions(-) diff --git a/src/codex/catalog/parsing.ts b/src/codex/catalog/parsing.ts index fb29ab089a..df05f6a287 100644 --- a/src/codex/catalog/parsing.ts +++ b/src/codex/catalog/parsing.ts @@ -305,20 +305,37 @@ const NO_FAST_TIER_NATIVE_SLUGS = new Set([ /** Does this row already carry an `ultrafast` tier the operator put there themselves? */ /** - * Read the opt-in from the live config. + * Read the opt-in from the live config, ONCE per catalog build. * - * `deriveEntry` and its five call sites are pure `RawEntry -> RawEntry` transforms with no - * config parameter, and threading one through all of them to carry a single boolean would - * be a much larger change than the behavior it gates. Callers that already hold a config - * can still pass `opts.ultraFastTier` explicitly, which is what the tests do; this is the - * fallback for the sync path. Failure reads as OFF, matching `.catch(false)`. + * `deriveEntry` and its call sites are pure `RawEntry -> RawEntry` transforms with no + * config parameter, so the flag is resolved here rather than threaded through all of them. + * It is memoized because `normalizeRoutedCatalogEntry` runs per entry in a sync loop, and + * `loadConfig()` chmods the config dir, hardens three secrets, reads the file and runs a + * full Zod parse — doing that once per catalog row would be a real cost for one boolean. + * Callers holding a config still pass `opts.ultraFastTier` explicitly, which bypasses this + * entirely and is what the tests do. A read failure means OFF, matching `.catch(false)`. */ +let ultraFastOptInCache: { value: boolean; at: number } | null = null; +const ULTRA_FAST_OPT_IN_TTL_MS = 5_000; + function ultraFastTierOptIn(): boolean { + const now = Date.now(); + if (ultraFastOptInCache && now - ultraFastOptInCache.at < ULTRA_FAST_OPT_IN_TTL_MS) { + return ultraFastOptInCache.value; + } + let value = false; try { - return ultraFastTierEnabled(loadConfig()); + value = ultraFastTierEnabled(loadConfig()); } catch { - return false; + value = false; } + ultraFastOptInCache = { value, at: now }; + return value; +} + +/** Test seam: drop the memoized opt-in so a config change is observed immediately. */ +export function resetUltraFastTierOptInCache(): void { + ultraFastOptInCache = null; } function entryDeclaresUltraFast(entry: RawEntry): boolean { diff --git a/src/providers/fastwire.ts b/src/providers/fastwire.ts index 78b5cbc09d..0621473c19 100644 --- a/src/providers/fastwire.ts +++ b/src/providers/fastwire.ts @@ -312,7 +312,17 @@ export function createAdapterTierMetadata( if (!context || !decision) return undefined; const callerMarker = canonicalFastTierMarker(context.callerTier); - const callerCanonicalFast = callerMarker !== undefined; + // Two different questions, and conflating them mislabels the record. + // + // "Did the caller ask for FAST?" governs the drop and suppression facts: the Fast + // toggle suppressing a request is only true of the 1.5x Fast tier, so an `ultrafast` + // caller turned away by `fastMode: false` was NOT a suppressed Fast request and must + // still read as `callerTierDropped`. + // + // "Did the caller ask for SOME fast-family tier?" is the wider question, and only + // `fastIntent` below is entitled to it. + const callerCanonicalFast = callerMarker === "priority"; + const callerFastFamilyIntent = callerMarker !== undefined; const callerTierDropped = context.callerTier !== undefined && !callerCanonicalFast && wireValue === null; @@ -352,7 +362,7 @@ export function createAdapterTierMetadata( // Known-unsupported routes still need a downgrade when the caller/config expressed Fast intent, // but they are deliberately outside the effective-demand calculation above. const fastIntent = context.demandDecision === "force-fast" - || (context.demandDecision === "inherit" && callerCanonicalFast); + || (context.demandDecision === "inherit" && callerFastFamilyIntent); if (!fastIntent) { outcome.fastOutcome = "not-requested"; @@ -435,9 +445,14 @@ export function decideTier( const callerCanonicalFast = canonicalFastTierMarker(callerTier); if (callerCanonicalFast !== undefined) { const value = policy.fastWire.canonicalToWire[callerCanonicalFast]; - return typeof value === "string" && value.length > 0 - ? { kind: "set", value } - : { kind: "drop" }; + if (typeof value === "string" && value.length > 0) return { kind: "set", value }; + // A canonical marker with NO wire mapping is not a reason to drop the tier. + // + // `ultrafast` is recognised as intent but deliberately unmapped, because no wire + // advertises it. Dropping here would have made recognition strictly worse than not + // recognising it at all: before, `ultrafast` was a foreign tier and + // `foreignCallerTiers: "verbatim"` forwarded it untouched. Falling through keeps that + // behavior, so an operator-supplied tier still reaches the provider. } if (callerTier !== undefined && !policy.forwardCallerTier) return { kind: "drop" }; if ( diff --git a/src/usage/expected-prices.ts b/src/usage/expected-prices.ts index 5d71b6ff4c..ec3776775b 100644 --- a/src/usage/expected-prices.ts +++ b/src/usage/expected-prices.ts @@ -112,9 +112,11 @@ const KIMI_PRICING = "https://platform.kimi.ai/docs/pricing (official table; cac const QWEN38_MAX_PRICING = "https://qwen.ai/blog?id=qwen3.8 (Qwen release announcement; no Model Studio billing row yet; cache rates unpublished -> 0)"; export const EXPECTED_PRICE_OVERLAYS: readonly ExpectedPriceOverlay[] = [ - // claude-fable-5-1 has no jawcode row yet, so both Anthropic surfaces need their own - // overlay (the overlay lookup is keyed by the configured provider id; only the jawcode - // bundle collapses anthropic-apikey onto anthropic). + // claude-fable-5-1 now HAS a generated jawcode row, so the two Anthropic surfaces resolve + // from it and these overlays are the fallback rather than the primary source. They stay: + // the overlay lookup is keyed by the configured provider id, so an account-pool log label + // like anthropic-pb51d9b still needs them, and only the jawcode bundle collapses + // anthropic-apikey onto anthropic. { provider: "anthropic", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" }, { provider: "anthropic-apikey", modelId: "claude-fable-5-1", cost4: CLAUDE_FABLE_51, source: `anthropic official Claude Fable 5.1 ${ANTHROPIC_PRICING}; cache hit = 0.025x base input`, verifiedAt: "2026-09-02", status: "verified" }, // Cursor canonicalizes every Fable 5.1 spelling onto this sole overlay row. diff --git a/tests/ultrafast-tier-honesty.test.ts b/tests/ultrafast-tier-honesty.test.ts index 9115772d3c..a1f97af6de 100644 --- a/tests/ultrafast-tier-honesty.test.ts +++ b/tests/ultrafast-tier-honesty.test.ts @@ -12,7 +12,7 @@ * for nothing. These tests pin the corrected accounting. */ import { describe, expect, test } from "bun:test"; -import { canonicalFastTierMarker } from "../src/providers/fastwire"; +import { canonicalFastTierMarker, createAdapterTierMetadata, decideTier, tierObservationContext } from "../src/providers/fastwire"; import { requestLogSpeedLabel } from "../src/server/request-log"; import { normalizeRoutedCatalogEntry } from "../src/codex/catalog/parsing"; @@ -116,3 +116,70 @@ describe("routed rows and the ultrafast opt-in", () => { expect(entry.additional_speed_tiers).toBeUndefined(); }); }); + +/** + * The wire decision, which the unit tests above cannot see. + * + * Adversarial review caught this: recognising `ultrafast` as a canonical marker routed it + * into the canonical-wire lookup, and because it is deliberately unmapped the lookup fell + * through to `drop`. That made recognition strictly WORSE than leaving it unrecognised — + * before, it was a foreign tier and `foreignCallerTiers: "verbatim"` forwarded it. Every + * suite still passed, because none of them asserted the decision. + */ +describe("an unmapped canonical tier is forwarded, not dropped", () => { + const policy = { + capability: true, + eligibility: "eligible", + fastWire: { + kind: "service-tier", + canonicalToWire: { priority: "priority" }, + foreignCallerTiers: "verbatim", + }, + forwardCallerTier: true, + } as unknown as Parameters[0]; + + test("ultrafast reaches the provider instead of being stripped", () => { + expect(decideTier(policy, undefined, "ultrafast")).toEqual({ kind: "forward-caller" }); + }); + + test("mapped Fast spellings still resolve to the wire value", () => { + expect(decideTier(policy, undefined, "priority")).toEqual({ kind: "set", value: "priority" }); + expect(decideTier(policy, undefined, "fast")).toEqual({ kind: "set", value: "priority" }); + }); + + test("unrelated and absent tiers are unchanged", () => { + expect(decideTier(policy, undefined, "auto")).toEqual({ kind: "forward-caller" }); + expect(decideTier(policy, undefined, undefined)).toEqual({ kind: "forward-caller" }); + }); +}); + +describe("the Fast toggle does not claim to have suppressed a different tier", () => { + const observe = (callerTier: string, fastMode: boolean | undefined) => { + const policy = { + capability: true, + eligibility: "eligible", + fastWire: { + kind: "service-tier", + canonicalToWire: { priority: "priority" }, + foreignCallerTiers: "verbatim", + }, + forwardCallerTier: true, + } as unknown as Parameters[0]; + const context = tierObservationContext(policy, fastMode, callerTier); + const decision = decideTier(policy, fastMode, callerTier); + const wireValue = decision.kind === "set" ? decision.value : decision.kind === "drop" ? null : callerTier; + return createAdapterTierMetadata(context, decision, "service-tier", wireValue)?.outcome; + }; + + test("force-default suppressing a real Fast request is recorded as suppression", () => { + const outcome = observe("priority", false); + expect(outcome?.callerFastSuppressedByConfig).toBe(true); + }); + + test("force-default turning away ultrafast is a dropped tier, not a suppressed Fast", () => { + // The Fast toggle did not suppress a 1.5x Fast request; it turned away a different one. + const outcome = observe("ultrafast", false); + expect(outcome?.callerFastSuppressedByConfig).toBeUndefined(); + expect(outcome?.callerTierDropped).toBe(true); + }); +});