From f80d26e5aff12f79d2a2addc5dcfa622a316ccbc Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Wed, 23 Sep 2026 09:29:58 -0700 Subject: [PATCH] feat(models): Opus 5.5 and GPT-6 Sol/Luna pricing and current-generation defaults Pricing (verified 2026-09-23 against first-party pages): - Add claude-opus-5-5 ($4/$20, cache reads 0.05x), gpt-6-sol ($2/$10) and gpt-6-luna ($0.10/$0.50), all released 2026-09-22. - claude-opus-5-5 previously matched claude-opus-5 on the token boundary and was costed at $5/$25 with 0.1x cache reads; real local usage was overstated by 94%. - Provider-namespaced ids (anthropic/claude-opus-4.7) now match on their last path segment instead of falling to the fallback rate. - codex-auto-review stays on the flagged fallback: OpenAI publishes no rate. Defaults: - Claude reasoning routes and the implementation/testing escalation rung move to claude-opus-5-5, Anthropic's recommended starting model. - Codex execution moves to gpt-6-sol and mechanical work to gpt-6-luna. Codex's catalog ranks GPT-6 first and labels GPT-5.6 "Older". - Cross-host tiers pair by role and price: Sol <-> Sonnet 5, Astra <-> Fable 5.1, Luna <-> Haiku. Earlier models stay listed for pins; seeded routes report divergence until `ak x host refresh`. Catalog: Opus 5.5 lifecycle facts; OpenAI doc links for Sol and Luna. Tests derive their "prior catalog" fixtures from DEFAULT_ROUTES. Docs: MODEL-PRICING-AUDIT, USAGE-SCORECARD-METRICS, PROVIDERS, MODELS, ADR-0002. Co-Authored-By: Claude Opus 5.5 --- docs/MODEL-PRICING-AUDIT.md | 155 +++++++++++------- docs/MODELS.md | 4 +- docs/PROVIDERS.md | 13 +- docs/USAGE-SCORECARD-METRICS.md | 63 +++---- ...-vocabulary-defaults-from-ruv-templates.md | 11 +- src/commands/models.mjs | 4 +- src/commands/x/host.mjs | 2 +- .../discovery/anthropic-catalog.mjs | 5 +- src/lib/model-inventory/read-model.mjs | 2 + src/lib/pricing.mjs | 39 +++-- src/lib/routing.mjs | 44 +++-- .../dashboard-integration-identity.test.mjs | 3 +- tests/kit/model-dashboard-read-model.test.mjs | 2 +- .../kit/model-discovery-claude-codex.test.mjs | 13 +- tests/kit/pricing.test.mjs | 43 ++++- tests/kit/routing-divergence.test.mjs | 16 +- tests/kit/status-viability.test.mjs | 2 +- 17 files changed, 282 insertions(+), 139 deletions(-) diff --git a/docs/MODEL-PRICING-AUDIT.md b/docs/MODEL-PRICING-AUDIT.md index 8fcf340e..b5c09f97 100644 --- a/docs/MODEL-PRICING-AUDIT.md +++ b/docs/MODEL-PRICING-AUDIT.md @@ -1,66 +1,107 @@ -# Model listing and pricing audit — 2026-09-08 +# Model listing and pricing audit — 2026-09-23 The maintained OpenAI and Anthropic text-token rate entries were checked against provider-owned public documentation. API-equivalent estimates are not subscription -bills or evidence of account access. Existing routing defaults remain unchanged. +bills or evidence of account access. Routing defaults moved to the current generation (see below). + +## Additions + +- **Claude Opus 5.5** (`claude-opus-5-5`, released 2026-09-22): $4 input, $5 + five-minute writes, $8 one-hour writes, $0.20 cache reads and $20 output per + million tokens. Cache reads are 0.05× input, not the usual 0.1×. 1M context, + 128K output, retirement not sooner than 2027-09-22. + [Pricing](https://platform.claude.com/docs/en/about-claude/pricing), + [models overview](https://platform.claude.com/docs/en/about-claude/models/overview), + [deprecations](https://platform.claude.com/docs/en/about-claude/model-deprecations). +- **GPT-6 Sol** (`gpt-6-sol`): $2 input, $0.20 cached input, $2.50 cache writes, + $10 output. [Model page](https://developers.openai.com/api/docs/models/gpt-6-sol). +- **GPT-6 Luna** (`gpt-6-luna`): $0.10 input, $0.01 cached input, $0.125 cache + writes, $0.50 output. [Model page](https://developers.openai.com/api/docs/models/gpt-6-luna). + +Both GPT-6 models were announced on 2026-09-22 +([SiliconANGLE](https://siliconangle.com/2026/09/22/anthropic-releases-claude-opus-5-5-and-openai-counters-with-two-cheaper-gpt-6-models/)). +Their model pages list April 20 and May 18, 2026 as the "release date", which +match the stated knowledge cutoffs, not the public launch. Both have a 1.05M +context window, 128K output and the same >272K request surcharge as Astra, +which the estimator does not model. + +## Routing defaults + +- Claude reasoning activities (architecture, design, security-analysis, + debugging, and the implementation/testing escalation rung) now default to + `claude-opus-5-5`, Anthropic's recommended starting model + ([models overview](https://platform.claude.com/docs/en/about-claude/models/overview)). + Opus 5 remains listed for pins. +- Codex implementation, testing and security-scan now default to `gpt-6-sol`, + documentation and packaging to `gpt-6-luna`. Codex's own model catalog ranks the + GPT-6 models first and labels GPT-5.6 "Older"; OpenAI's Codex docs make Sol the + default preset ([Codex models](https://learn.chatgpt.com/docs/models)). GPT-5.6 + models remain listed for pins. +- Cross-host tiers pair by role and per-token price: GPT-6 Sol ↔ Sonnet 5 + ($2/$10), GPT-6 Astra ↔ Fable 5.1 ($10/$50), GPT-6 Luna ↔ Haiku 4.5. +- Routes seeded before this change are reported as diverged and keep their model + until `ak x host refresh`. +- Not added as retirements: OpenAI's Codex docs retire GPT-5.4 / 5.4 Mini + (2026-08-31) and GPT-5.5 (2026-10-14) for ChatGPT sign-in, but the API still + serves them, so a read-time substitution would override API-key users' pins. ## Corrections -- Added GPT-6 Astra at $10 input, $1 cached input, $12.50 cache writes and $50 output - per million tokens. [OpenAI model page](https://developers.openai.com/api/docs/models/gpt-6-astra). -- Confirmed Fable 5.1 and Mythos 5.1 already exist with $10 input, $0.25 cache reads, - $12.50 five-minute writes and $50 output. One-hour writes cost $20 and remain - outside the estimator's five-minute assumption. [Anthropic pricing](https://platform.claude.com/docs/en/about-claude/pricing). -- Removed Sonnet 5's canceled September increase: $2/$10 is now standard. - [Anthropic pricing notice](https://platform.claude.com/docs/en/about-claude/pricing). -- Resolved the exact `gpt-5.6` alias to Sol; unknown variants remain unmatched. - [Sol model page](https://developers.openai.com/api/docs/models/gpt-5.6-sol). -- Removed the unsupported cached-input discount on Pro models. GPT-5.5 Pro - explicitly has no discount; GPT-5.4 Pro publishes no cached rate, so the - estimator conservatively uses ordinary input rates for reported cached tokens. - [GPT-5.5 Pro](https://developers.openai.com/api/docs/models/gpt-5.5-pro), - [GPT-5.4 Pro](https://developers.openai.com/api/docs/models/gpt-5.4-pro). -- Applied OpenAI's 1.25× write premium only to GPT-5.6 and later; older OpenAI - entries use ordinary input rates. [Prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching). -- Removed GLM 5's static “cheapest” claim. Current OpenRouter listings put - GLM 5.2 below GLM 5 in per-token price; provider offers can change. - [GLM 5.2](https://openrouter.ai/z-ai/glm-5.2), [GLM 5](https://openrouter.ai/z-ai/glm-5). - -## Model facts and retained rates - -Fable 5.1 and Mythos 5.1 have 1M context and 128K maximum output. Mythos is -invitation-only; listing it as a choice does not assert entitlement. -[Official overview](https://platform.claude.com/docs/en/models/fable-5-1/overview). -The remaining bundled Anthropic lifecycle entries match the -[deprecation history](https://platform.claude.com/docs/en/about-claude/model-deprecations); -Mythos availability is established by the model overview, not the retirement table. - -OpenAI base input/output rates below were verified and retained (USD/MTok): - -| Model | Input | Output | Source | -|---|---|---|---| -| GPT-5.6 Sol | 4 | 20 | [Pricing](https://developers.openai.com/api/docs/pricing) | -| GPT-5.6 Terra | 2 | 12 | [Pricing](https://developers.openai.com/api/docs/pricing) | -| GPT-5.6 Luna | 0.20 | 1.20 | [Pricing](https://developers.openai.com/api/docs/pricing) | -| GPT-5.5 | 5 | 30 | [Model](https://developers.openai.com/api/docs/models/gpt-5.5) | -| GPT-5.5 Pro | 30 | 180 | [Model](https://developers.openai.com/api/docs/models/gpt-5.5-pro) | -| GPT-5.4 | 2.50 | 15 | [Model](https://developers.openai.com/api/docs/models/gpt-5.4) | -| GPT-5.4 Mini | 0.75 | 4.50 | [Model](https://developers.openai.com/api/docs/models/gpt-5.4-mini) | -| GPT-5.4 Nano | 0.20 | 1.25 | [Model](https://developers.openai.com/api/docs/models/gpt-5.4-nano) | -| GPT-5.4 Pro | 30 | 180 | [Model](https://developers.openai.com/api/docs/models/gpt-5.4-pro) | -| GPT-5.3 Codex | 1.75 | 14 | [Pricing](https://developers.openai.com/api/docs/pricing) | -| chat-latest | 5 | 30 | [Pricing](https://developers.openai.com/api/docs/pricing) | -| Realtime 2.1 (text) | 4 | 24 | [Pricing](https://developers.openai.com/api/docs/pricing) | -| Realtime 2.1 Mini (text) | 0.60 | 2.40 | [Pricing](https://developers.openai.com/api/docs/pricing) | - -Sol's promotional price is guaranteed at least through November 21, 2026; -no definite expiry is published. No future price is invented. Retained historical -rates do not imply that a host still offers a model. +- `claude-opus-5-5` had no entry and matched `claude-opus-5` on the token + boundary, so real Opus 5.5 usage was costed at $5/$25 with 0.1× cache reads. + It now resolves to its own row. +- Provider-namespaced ids (`anthropic/claude-opus-4.7`, `openai/gpt-5.5`) fell + through to the fallback rate because matching is by prefix. They now match on + their last path segment. +- The previous audit said one-hour cache writes were outside the estimator. + They are priced at 2× input from the transcript's TTL split. + +## Verified and unchanged + +All other Anthropic entries match the live pricing table, including Fable 5.1 / +Mythos 5.1 (0.025× cache reads) and Sonnet 5 at $2/$10. No Anthropic model was +deprecated since the last audit; Sonnet 4.5 and Haiku 4.5 remain Active. + +OpenAI base rates (USD/MTok), checked against +[OpenAI pricing](https://developers.openai.com/api/docs/pricing): + +| Model | Input | Output | +|---|---|---| +| GPT-6 Astra | 10 | 50 | +| GPT-5.6 Sol | 4 | 20 | +| GPT-5.6 Terra | 2 | 12 | +| GPT-5.6 Luna | 0.20 | 1.20 | +| GPT-5.5 | 5 | 30 | +| GPT-5.5 Pro | 30 | 180 | +| GPT-5.4 | 2.50 | 15 | +| GPT-5.4 Mini | 0.75 | 4.50 | +| GPT-5.4 Nano | 0.20 | 1.25 | +| GPT-5.4 Pro | 30 | 180 | +| GPT-5.3 Codex | 1.75 | 14 | +| chat-latest | 5 | 30 | +| Realtime 2.1 (text) | 4 | 24 | +| Realtime 2.1 Mini (text) | 0.60 | 2.40 | + +GPT-5.6 Sol's promotional price is guaranteed at least through November 21, +2026; no future price is invented. Some third-party summaries list GPT-5.6 Sol +at $5/$30; OpenAI's own pricing page shows $4/$20. + +## Not priced, deliberately + +- `codex-auto-review`, Codex's automatic approval reviewer, appears in local + Codex rollouts with real token counts. OpenAI publishes no per-token price for + it. It is a server-side alias: Codex + [PR #18169](https://github.com/openai/codex/pull/18169) replaced a hardcoded + `gpt-5.4` with it so the backend can choose the model. It stays unmatched and + is costed at the flagged fallback rate rather than at a guessed one. +- `gpt-reserve` (Luna Reserve) is a hidden Codex fallback allowance with no + published API price. ## Estimator limits -Request-level long-context surcharges, service modes, regional uplifts, audio/image -rates and one-hour cache writes are not inferred from daily aggregate token counts. -OpenAI's API cache-write counter is included in total input; native host transcripts -must expose equivalent evidence before their parsers can attribute those writes. -The model list and pricing table do not constitute a complete provider catalog. +Request-level long-context surcharges, service modes (Batch, Flex, Priority, +Fast), regional uplifts and audio/image rates are not inferred from daily +aggregate token counts. OpenAI's API cache-write counter is included in total +input; native host transcripts must expose equivalent evidence before their +parsers can attribute those writes. The model list and pricing table do not +constitute a complete provider catalog. diff --git a/docs/MODELS.md b/docs/MODELS.md index 8fdc36a9..ca723970 100644 --- a/docs/MODELS.md +++ b/docs/MODELS.md @@ -17,8 +17,8 @@ ak models refresh ak models refresh --host claude ak models status ak models diff -ak models explain codex:gpt-5.6-terra -ak models plan --activity testing --from codex:gpt-5.4 --to codex:gpt-5.6-terra +ak models explain codex:gpt-6-sol +ak models plan --activity testing --from codex:gpt-5.6-terra --to codex:gpt-6-sol ``` Only `refresh` contacts model sources or writes a snapshot. `status`, `diff`, `explain`, `plan`, diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 9ad1a44b..bf0adaf0 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -373,7 +373,7 @@ When you want explicit ordering rather than env auto-enable, `ak` manages agenti ```bash ak host pick \ --aqe-provider claude-code \ - --aqe-fallback 'claude-code:claude-opus-5; openai:gpt-5.6; gemini:gemini-3.5-flash' + --aqe-fallback 'claude-code:claude-opus-5-5; openai:gpt-5.6; gemini:gemini-3.5-flash' ``` Each `provider:model,model` becomes an ordered chain entry (first = highest priority). `ak` @@ -434,7 +434,7 @@ provider — add them to the chain and put `OPENROUTER_API_KEY` in your env: ```bash ak host pick \ --aqe-provider claude-code \ - --aqe-fallback 'claude-code:claude-opus-5; openrouter:z-ai/glm-5.2' + --aqe-fallback 'claude-code:claude-opus-5-5; openrouter:z-ai/glm-5.2' ``` Curated picks (verified September 8, 2026): `z-ai/glm-5.2` (flagship — 1M context, strong @@ -507,12 +507,15 @@ Defaults (all overridable; your edits are marked `custom` and never re-seeded): | Activity | Host | Default model | |---|---|---| | specification, review, release | claude | `claude-sonnet-5` | -| architecture, design, debugging, security-analysis | claude | `claude-opus-5` | -| implementation, testing, security-scan | codex | `gpt-5.6-terra` | -| documentation, packaging | codex | `gpt-5.6-luna` | +| architecture, design, debugging, security-analysis | claude | `claude-opus-5-5` | +| implementation, testing, security-scan | codex | `gpt-6-sol` | +| documentation, packaging | codex | `gpt-6-luna` | *(packaging & release are `ak`-added — ruflo ships templates for feature/security/refactor only.)* +Implementation and testing escalate to `claude-opus-5-5`. Routes seeded before a default +changes are reported as diverged and keep their model until you run `ak x host refresh`. + **Retired Codex models.** `ak` has no automatic Codex retirement substitutions as of 2026-08-25. The current [OpenAI API model catalog](https://developers.openai.com/api/docs/models/all) still lists GPT-5.4 and GPT-5.4 mini, and no first-party withdrawal notice supports the former automatic diff --git a/docs/USAGE-SCORECARD-METRICS.md b/docs/USAGE-SCORECARD-METRICS.md index 3baed825..210f67ae 100644 --- a/docs/USAGE-SCORECARD-METRICS.md +++ b/docs/USAGE-SCORECARD-METRICS.md @@ -515,7 +515,7 @@ cost = (inputUnits × rate_in + output × rate_out) / 1,000,000 Mythos 5.1, which resolve to 0.025, and OpenAI Pro, which uses 1 (§13). Summed across every row in the window. -**Source:** `costOf()`, `src/lib/pricing.mjs:304-315`, reproduced verbatim: +**Source:** `costOf()`, `src/lib/pricing.mjs`, reproduced verbatim: ```js export function costOf(usage) { @@ -567,12 +567,12 @@ so the estimator conservatively uses ordinary input rates for reported cached to See [OpenAI prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching). Rate resolution is **longest-prefix match** (`isPrefixOf`, `KEYS_BY_LENGTH`) -on a normalized model id (`pricing.mjs:205-214`), so a dated release +on a normalized model id (`pricing.mjs`), so a dated release (`claude-haiku-4-5-20251001`) resolves to the same entry as its bare alias, and a more specific entry (`gpt-5.6-sol`) is never shadowed by a shorter one (`gpt-5.6`). An id matching nothing gets `FALLBACK_PRICE` — Sonnet-class rate, `$3`/`$15` -(`pricing.mjs:182`) — rather than `$0`, so an unrecognized model can never be +(`pricing.mjs`) — rather than `$0`, so an unrecognized model can never be silently free; `matched: false` travels with the result so a maintainer can find fallback-priced rows if the table needs a new entry. @@ -582,7 +582,7 @@ Each table entry is a **schedule** — an ordered list of periods, each with the day it takes effect. Nearly every entry has exactly one period that has always applied (`anthropic(5, 25)` builds that shape); a confirmed effective rate change can be represented by multiple periods (`schedule`, -`pricing.mjs:45-68`). `periodOn` (`pricing.mjs:229`) picks the last period +`pricing.mjs`). `periodOn` (`pricing.mjs`) picks the last period already in effect on the given day, comparing ISO date strings lexicographically so no `Date` parsing is involved and the module stays clock-free. @@ -608,7 +608,7 @@ Two rules bound the mechanism: Codex promo would be a one-line edit rather than new machinery. Rates that vary by *how* a request was served — regional uplift, large-prompt surcharge, service tiers — are a different axis, are deliberately **not** expressible - here, and remain in `UNMODELLED_PRICING_FACTORS` (`pricing.mjs:182-185`) + here, and remain in `UNMODELLED_PRICING_FACTORS` (`pricing.mjs`) because a transcript does not record the endpoint or tier. A `priceFor` call with no day prices as of `PRICES_AS_OF`, the table's @@ -721,7 +721,7 @@ rates; the subtitle is overgeneralized and needs a UI correction. **Why this number matters more than it looks like it should.** On the reference corpus, 96.3% of tokens were cache reads — pricing them as fresh input (rather than at the 0.1× multiplier) would overstate cost by roughly -10× (`pricing.mjs:7-10`). A cache-read share this high is not an anomaly to be +10× (`pricing.mjs`). A cache-read share this high is not an anomaly to be suspicious of on its own — it is the expected steady state for any long-running agentic session that resends a large, mostly-unchanged system prompt and tool-result history on every turn, which both Claude Code and @@ -1154,13 +1154,13 @@ public documentation; they are maintained data, not live quotes. ### 13.1 Anthropic — primary source, directly verified Rates rechecked against [Anthropic's official pricing](https://platform.claude.com/docs/en/about-claude/pricing) -on 2026-09-08. Fable 5.1 and Mythos 5.1 were already catalogued with the -correct rates; this pass refreshes their verification evidence. +on 2026-09-23, which adds Claude Opus 5.5 (released 2026-09-22). | Model | Base input | 5m cache write | 1h cache write | Cache read (hit) | Output | |---|---|---|---|---|---| | Claude Fable 5.1 / Mythos 5.1 | $10/MTok | $12.50/MTok | $20/MTok | $0.25/MTok¹ | $50/MTok | | Claude Fable 5 / Mythos 5 | $10/MTok | $12.50/MTok | $20/MTok | $1/MTok | $50/MTok | +| Claude Opus 5.5 | $4/MTok | $5/MTok | $8/MTok | $0.20/MTok² | $20/MTok | | Claude Opus 5 | $5/MTok | $6.25/MTok | $10/MTok | $0.50/MTok | $25/MTok | | Claude Opus 4.8 / 4.7 / 4.6 / 4.5 | $5/MTok | $6.25/MTok | $10/MTok | $0.50/MTok | $25/MTok | | Claude Sonnet 5 | $2/MTok | $2.50/MTok | $4/MTok | $0.20/MTok | $10/MTok | @@ -1168,8 +1168,8 @@ correct rates; this pass refreshes their verification evidence. | Claude Haiku 4.5 | $1/MTok | $1.25/MTok | $2/MTok | $0.10/MTok | $5/MTok | ¹ Cache hits on Claude Fable 5.1 and Claude Mythos 5.1 price at **0.025×** base -input — every other current Anthropic model uses 0.1×. Verified 2026-09-08 -against **[C1]**'s live pricing page. +input. ² Cache hits on Claude Opus 5.5 price at **0.05×**. Every other current +Anthropic model uses 0.1×. Verified 2026-09-23 against **[C1]**'s live pricing page. Sonnet 5's previously announced September increase was canceled by Anthropic; $2/$10 is now its standard price. Fable 5.1 is generally available; Mythos 5.1 @@ -1182,15 +1182,15 @@ cache-read *columns* in this table are provider-published absolute rates; the kit's `pricing.mjs` instead stores **multipliers** — 1.25× for a 5-minute cache write and 2× for a 1-hour cache write (both uniform, no published per-model exception) and, for cache reads, 0.1× -for every model *except* Fable 5.1 / Mythos 5.1, which carry their own -0.025× cache-read override on that catalog entry (`pricing.mjs:90-91`) +for every model *except* Fable 5.1 / Mythos 5.1 (0.025×) and Opus 5.5 (0.05×), +which carry their own cache-read override on their `PRICES` entries instead of the module-wide default multiplier. -`priceFor()` (`pricing.mjs:261-280`) is what resolves that: it returns a +`priceFor()` (`pricing.mjs`) is what resolves that: it returns a `cacheReadMultiplier` field taken from the matched entry, falling back to -`CACHE_READ_MULTIPLIER` (`pricing.mjs:186-188`) when the entry carries none. +`CACHE_READ_MULTIPLIER` (`pricing.mjs`) when the entry carries none. -`costOf()` (`pricing.mjs:304-315`) then multiplies cache-read tokens by +`costOf()` (`pricing.mjs`) then multiplies cache-read tokens by that resolved value rather than a hardcoded constant. Claude transcripts record which TTL each cache write used @@ -1201,7 +1201,7 @@ and `costOf` prices it at 2×. A record with no split prices every write at the rate is exactly *$5 × 1.25*, its $10 1-hour rate exactly *$5 × 2*; its published $0.50 cache-read rate is exactly *$5 × 0.1*. Every row in Anthropic's own table satisfies `cache_write_5m = input × 1.25`, `cache_write_1h = input × 2` and, except for the Fable 5.1 / Mythos 5.1 -row noted above, `cache_read = input × 0.1` — confirming the multiplier +and Opus 5.5 rows noted above, `cache_read = input × 0.1` — confirming the multiplier approach is arithmetically identical to using the provider's published absolute cache rates directly. @@ -1210,11 +1210,11 @@ in prose, independent of the pricing table: *"5-minute cache write tokens are 1.25 times the base input tokens price... 1-hour cache write tokens are 2 times the base input tokens price... Cache read tokens are 0.1 times the base input tokens price."* This is the second, independent confirmation of -`CACHE_READ_MULTIPLIER`/`CACHE_WRITE_MULTIPLIER`/`CACHE_WRITE_1H_MULTIPLIER` (`pricing.mjs:185-188`). +`CACHE_READ_MULTIPLIER`/`CACHE_WRITE_MULTIPLIER`/`CACHE_WRITE_1H_MULTIPLIER` (`pricing.mjs`). ### 13.2 OpenAI (Codex) — hand-maintained, no canonical machine-readable source -`pricing.mjs`'s own comment (`pricing.mjs:116-119`) records that +`pricing.mjs`'s own comment (`pricing.mjs`) records that `~/.codex/models_cache.json` was checked directly and contains **zero** price-related keys — Codex CLI does not ship pricing data locally, unlike Anthropic which publishes a fetchable pricing document. OpenAI's rates in @@ -1223,37 +1223,44 @@ documentation and are the most drift-prone entries in the file — this is explicitly why `PRICES_AS_OF` is surfaced in the UI (`u-asof`, `dashboard/client.mjs`) rather than assumed current. -Standard USD rates per million tokens (Astra verified 2026-09-08 against -[OpenAI's model documentation](https://developers.openai.com/api/docs/models/gpt-6-astra); -GPT-5.6 rates reconciled with the implementation and -[OpenAI pricing](https://developers.openai.com/api/docs/pricing)): +Standard USD rates per million tokens, verified 2026-09-23 against +[OpenAI pricing](https://developers.openai.com/api/docs/pricing) and the +individual model pages for [Astra](https://developers.openai.com/api/docs/models/gpt-6-astra), +[GPT-6 Sol](https://developers.openai.com/api/docs/models/gpt-6-sol) and +[GPT-6 Luna](https://developers.openai.com/api/docs/models/gpt-6-luna): | Model (kit key) | Input | Output | Cache read | |---|---|---|---| | `gpt-6-astra` | $10 | $50 | $1 | +| `gpt-6-sol` | $2 | $10 | $0.20 | +| `gpt-6-luna` | $0.10 | $0.50 | $0.01 | | `gpt-5.6-sol` | $4 | $20 | $0.40 | | `gpt-5.6-terra` | $2 | $12 | $0.20 | | `gpt-5.6-luna` | $0.20 | $1.20 | $0.02 | | `gpt-5.5` | $5 | $30 | $0.50 | | `gpt-5.5-pro` | $30 | $180 | Not published | -Astra cache writes cost $12.50/MTok (1.25× input), using the existing -cache-write arithmetic. Sol's promotional rate has no confirmed end date: +GPT-6 and GPT-5.6 cache writes cost 1.25× input (Astra $12.50, GPT-6 Sol +$2.50, GPT-6 Luna $0.125/MTok), using the existing cache-write arithmetic. +Do not confuse GPT-6 Sol ($2/$10) with GPT-5.6 Sol ($4/$20); both are current. Sol's promotional rate has no confirmed end date: OpenAI says at least through 2026-11-21, so no future reversion is invented. These are API list-price equivalents, not subscription charges or access guarantees. -The full maintained rate table was rechecked on 2026-09-08; `PRICES_AS_OF` +The full maintained rate table was rechecked on 2026-09-23; `PRICES_AS_OF` and the Usage summary now show that date. Individual entries can still override verification dates. The official `gpt-5.6` alias resolves exactly to Sol; -unknown suffixed variants do not inherit its price. Realtime entries describe +unknown suffixed variants do not inherit its price. A provider-namespaced id +(`anthropic/claude-opus-4.7`) is matched on its last path segment. Codex's +`codex-auto-review` (its auto-approval reviewer) has no published per-token +price, so it stays unmatched and is costed at the flagged fallback rate. Realtime entries describe text tokens only; audio and image charges are outside this estimator. See [the pricing audit](MODEL-PRICING-AUDIT.md) for source links and scope. ### 13.3 What the pricing table deliberately does not model -Recorded verbatim from `pricing.mjs:153-181` (`UNMODELLED_PRICING_FACTORS`, -`pricing.mjs:182-185`) because listing known gaps is what makes the +Recorded verbatim from `pricing.mjs` (`UNMODELLED_PRICING_FACTORS`, +`pricing.mjs`) because listing known gaps is what makes the *modelled* factors credible: - **Regional-processing uplift.** OpenAI charges +10% on data-residency diff --git a/docs/adr/0002-activity-vocabulary-defaults-from-ruv-templates.md b/docs/adr/0002-activity-vocabulary-defaults-from-ruv-templates.md index db9251e2..82c149d3 100644 --- a/docs/adr/0002-activity-vocabulary-defaults-from-ruv-templates.md +++ b/docs/adr/0002-activity-vocabulary-defaults-from-ruv-templates.md @@ -2,7 +2,8 @@ - **Status:** Amended by [ADR-0020](0020-ga-stable-surfaces.md) - **Date:** 2026-07-23 -- **Updated:** 2026-09-09 — reconciled against repository source and tests for issue #211 +- **Updated:** 2026-09-23 — default models moved to the current generation: `claude-opus-5-5`, `gpt-6-sol`, `gpt-6-luna` +- **Previous update:** 2026-09-09 — reconciled against repository source and tests for issue #211 - **Earlier update:** 2026-07-30 - **Update note:** Preserved the activity vocabulary and moved its runtime ownership to the in-repository host-neutral runner. @@ -49,9 +50,11 @@ model for execution) and are treated as **soft defaults** — see the "open ques Tier is the pairing key, not the model id: `MODEL_CATALOG` spells `flagship`/`balanced`/`fast` identically on both hosts so primary-host mirroring can map a route to its counterpart's equivalent. -As of 2026-08-07 execution routes to `gpt-5.6-terra` (balanced) and mechanical work to `gpt-5.6-luna` -(fast), as agentic-kit default preferences, not as evidence that `gpt-5.4` or -`gpt-5.4-mini` were withdrawn; deep reasoning routes to `claude-opus-5`. Withdrawn ids are handled by the retirement +As of 2026-09-23 execution routes to `gpt-6-sol` (balanced) and mechanical work to `gpt-6-luna` +(fast), and deep reasoning routes to `claude-opus-5-5`. These are agentic-kit default preferences, +following each vendor's recommended starting model, not evidence that earlier models were withdrawn. +Cross-host tiers pair by role and per-token price: `gpt-6-sol` ↔ `claude-sonnet-5`, +`gpt-6-astra` ↔ `claude-fable-5-1`, `gpt-6-luna` ↔ Haiku. Withdrawn ids are handled by the retirement mechanism in [ADR-0003](0003-auto-seed-dual-host-provenance.md), not by editing this table alone. ## Consequences diff --git a/src/commands/models.mjs b/src/commands/models.mjs index 25369a25..89026435 100644 --- a/src/commands/models.mjs +++ b/src/commands/models.mjs @@ -40,8 +40,8 @@ Examples: ak models refresh --all ak models status --host codex --json ak models diff models:before models:after - ak models explain codex:gpt-5.6-terra - ak models plan --activity testing --to codex:gpt-5.6-terra`; + ak models explain codex:gpt-6-sol + ak models plan --activity testing --to codex:gpt-6-sol`; const ALL_OWNERS = Object.freeze(['claude', 'codex', 'opencode', 'ollama']); diff --git a/src/commands/x/host.mjs b/src/commands/x/host.mjs index b0a50f26..dfe3f464 100644 --- a/src/commands/x/host.mjs +++ b/src/commands/x/host.mjs @@ -127,7 +127,7 @@ Options (pick, all optional — omit for interactive): needs OPENROUTER_API_KEY in the env) --provider register ruflo providers (e.g. ollama:qwen3.6:27b) --route 'act:host[:model]' override one activity's routing (repeatable), e.g. - --route 'implementation:claude:claude-opus-5' + --route 'implementation:claude:claude-opus-5-5' activities: specification, architecture, design, implementation, testing, review, security-scan, security-analysis, documentation, debugging, diff --git a/src/lib/model-inventory/discovery/anthropic-catalog.mjs b/src/lib/model-inventory/discovery/anthropic-catalog.mjs index ffdde46b..d9d60c18 100644 --- a/src/lib/model-inventory/discovery/anthropic-catalog.mjs +++ b/src/lib/model-inventory/discovery/anthropic-catalog.mjs @@ -1,7 +1,7 @@ import { PRICES_AS_OF, priceFor } from '../../pricing.mjs'; import { modelRecord, sourceRecord } from './index.mjs'; -export const ANTHROPIC_PUBLIC_CATALOG_AS_OF = '2026-09-02'; +export const ANTHROPIC_PUBLIC_CATALOG_AS_OF = '2026-09-23'; export const ANTHROPIC_MODELS_URL = 'https://platform.claude.com/docs/en/about-claude/models/overview'; export const ANTHROPIC_LIFECYCLE_URL = @@ -67,6 +67,9 @@ export const ANTHROPIC_PUBLIC_MODELS = Object.freeze([ availability: 'limited', replacement: 'claude-mythos-5', contextLimit: 1_000_000, outputLimit: 128_000, }, + active('claude-opus-5-5', 'Claude Opus 5.5', { + contextLimit: 1_000_000, outputLimit: 128_000, retirementNotBefore: '2027-09-22', + }), active('claude-opus-5', 'Claude Opus 5', { contextLimit: 1_000_000, outputLimit: 128_000, retirementNotBefore: '2027-07-24', }), diff --git a/src/lib/model-inventory/read-model.mjs b/src/lib/model-inventory/read-model.mjs index 50c61a30..3ea7d338 100644 --- a/src/lib/model-inventory/read-model.mjs +++ b/src/lib/model-inventory/read-model.mjs @@ -182,6 +182,8 @@ const OFFICIAL_CLAUDE_IDS = new Set(ANTHROPIC_OFFICIAL_MODEL_IDS); const OPENAI_MODEL_DOCUMENTATION = new Map([ ['gpt-5.6', 'https://developers.openai.com/api/docs/models/gpt-5.6-sol'], ['gpt-6-astra', 'https://developers.openai.com/api/docs/models/gpt-6-astra'], + ['gpt-6-sol', 'https://developers.openai.com/api/docs/models/gpt-6-sol'], + ['gpt-6-luna', 'https://developers.openai.com/api/docs/models/gpt-6-luna'], ['gpt-5.6-sol', 'https://developers.openai.com/api/docs/models/gpt-5.6-sol'], ['gpt-5.6-terra', 'https://developers.openai.com/api/docs/models/gpt-5.6-terra'], ['gpt-5.6-luna', 'https://developers.openai.com/api/docs/models/gpt-5.6-luna'], diff --git a/src/lib/pricing.mjs b/src/lib/pricing.mjs index 873df4ff..1311e1bc 100644 --- a/src/lib/pricing.mjs +++ b/src/lib/pricing.mjs @@ -6,9 +6,9 @@ // // The cache multipliers are the whole point of this module. On a real corpus // ~96% of tokens are cache reads, which bill at 0.1× input for nearly every -// model (0.025× on Fable 5.1 / Mythos 5.1 — a per-entry override, see below); -// pricing them as fresh input overstates cost by roughly 10×, or 40× on those -// two. Cache-write premiums apply to Anthropic writes (1.25× for the 5-minute +// model (0.025× on Fable 5.1 / Mythos 5.1, 0.05× on Opus 5.5 — per-entry +// overrides, see below); pricing them as fresh input overstates cost by +// roughly 10×, or 20–40× on those three. Cache-write premiums apply to Anthropic writes (1.25× for the 5-minute // tier, 2× for the 1-hour tier) and OpenAI GPT-5.6 and later; older OpenAI // models use the ordinary input rate. // @@ -19,7 +19,7 @@ // ── Table ──────────────────────────────────────────────────────────────────── /** The baseline verification date; newer entries may carry their own asOf — date-stamped in the UI. */ -export const PRICES_AS_OF = '2026-09-08'; +export const PRICES_AS_OF = '2026-09-23'; // ── Rate constructors ──────────────────────────────────────────────────────── // A rate entry is always a SCHEDULE — an ordered list of periods, each with the @@ -87,7 +87,13 @@ export const PRICES = { 'claude-mythos-5-1': anthropic(10, 50, { cacheReadMultiplier: 0.025, asOf: '2026-09-08' }), 'claude-fable-5': anthropic(10, 50), 'claude-mythos-5': anthropic(10, 50), - // Anthropic — Opus line (5 and the prior generations share a price) + // Anthropic — Opus line. Opus 5.5 (released 2026-09-22) is CHEAPER than the + // generations before it, $4/$20, and reads cache at 0.05x base input ($0.20), + // not 0.1x (platform.claude.com/docs/en/about-claude/pricing, verified + // 2026-09-23). It needs its own key: without one, `claude-opus-5-5` matches + // `claude-opus-5` on the token boundary and is priced as Opus 5. + 'claude-opus-5-5': anthropic(4, 20, { cacheReadMultiplier: 0.05, asOf: '2026-09-23' }), + // Opus 5 and the prior generations share a price. 'claude-opus-5': anthropic(5, 25), 'claude-opus-4-8': anthropic(5, 25), 'claude-opus-4-7': anthropic(5, 25), @@ -102,7 +108,7 @@ export const PRICES = { 'claude-haiku-4-5': anthropic(1, 5), // OpenAI (Codex). Verified against the individual OpenAI API model pages on - // 2026-09-08. There is NO machine-readable pricing in + // 2026-09-23. There is NO machine-readable pricing in // ~/.codex/models_cache.json (checked: zero price/pricing/usd keys), so this // table is maintained by hand and is the most drift-prone thing in this file. // @@ -125,6 +131,12 @@ export const PRICES = { // https://developers.openai.com/api/docs/models/gpt-6-astra (2026-09-08). // Standard rates: input $10, cached input $1, writes $12.50, output $50 / MTok. 'gpt-6-astra': openai(10, 50, { cacheWriteMultiplier: 1.25 }), + // GPT-6 Sol and Luna (released 2026-09-22; the model pages' "release date" + // field shows the knowledge cutoff instead). Standard rates, cached input 0.1x, + // writes 1.25x, same >272K surcharge as Astra (unmodelled): + // https://developers.openai.com/api/docs/models/gpt-6-sol and .../gpt-6-luna. + 'gpt-6-sol': openai(2, 10, { cacheWriteMultiplier: 1.25, asOf: '2026-09-23' }), + 'gpt-6-luna': openai(0.1, 0.5, { cacheWriteMultiplier: 1.25, asOf: '2026-09-23' }), 'gpt-5.6-sol': openai(4, 20, { cacheWriteMultiplier: 1.25 }), 'gpt-5.6-terra': openai(2, 12, { cacheWriteMultiplier: 1.25 }), 'gpt-5.6-luna': openai(0.2, 1.2, { cacheWriteMultiplier: 1.25 }), @@ -191,10 +203,14 @@ export const CACHE_WRITE_1H_MULTIPLIER = 2; // Model ids arrive with inconsistent separators and case (`claude-opus-4-8` vs // `claude-opus-4.8`, Codex's `gpt-5.6`), so both key and id are normalised the -// same way before comparison. Underscores and slashes fold too, so a namespaced -// id (`anthropic/claude-opus-5`) still ends on the same tokens. +// same way before comparison. const normalize = (s) => String(s).toLowerCase().replace(/[._/]/g, '-'); +// A provider-namespaced id (`anthropic/claude-opus-4.7`, `openai/gpt-5.5`, as +// OpenCode and custom Codex providers record them) is matched on its last path +// segment. Matching is by prefix, so the namespace would otherwise hide the model. +const bareModel = (s) => String(s).slice(String(s).lastIndexOf('/') + 1); + // A prefix only counts on a TOKEN boundary: `claude-opus-5` must match // `claude-opus-5-20260401` but not `claude-opus-50`, which is a different model. const isPrefixOf = (key, id) => id === key || (id.startsWith(key) && id[key.length] === '-'); @@ -239,7 +255,7 @@ function periodOn(periods, day) { * never throws. * * `cacheReadMultiplier` defaults to the module-wide `CACHE_READ_MULTIPLIER` - * unless the matched entry carries its own (Claude 5.1 or OpenAI Pro) — + * unless the matched entry carries its own (Claude 5.1, Opus 5.5 or OpenAI Pro) — * see the `schedule()` comment in the table above. * * `day` (ISO `YYYY-MM-DD`) selects the rate IN EFFECT ON THAT DAY. Cost @@ -250,7 +266,7 @@ function periodOn(periods, day) { * Omitting `day` prices as of `PRICES_AS_OF` (see `periodOn`). */ export function priceFor(model, provider, day) { - const normalized = typeof model === 'string' ? normalize(model) : ''; + const normalized = typeof model === 'string' ? normalize(bareModel(model)) : ''; // Official exact alias; unknown gpt-5.6-* variants must still remain unknown. const id = normalized === 'gpt-5-6' ? 'gpt-5-6-sol' : normalized; if (id) { @@ -289,7 +305,8 @@ const tokens = (v) => (Number.isFinite(v) && v > 0 ? v : 0); * every write at the 5-minute rate, exactly as before the split was retained. * * `cacheReadMultiplier` is 0.1 for nearly every model but is resolved per-model - * via `priceFor` (Fable 5.1 / Mythos 5.1 price cache reads at 0.025x — see the + * via `priceFor` (Fable 5.1 / Mythos 5.1 price cache reads at 0.025x, Opus 5.5 + * at 0.05x — see the * `PRICES` table comment), so this is never hardcoded here. * * All counters are optional and default to 0, so all-zero usage returns exactly diff --git a/src/lib/routing.mjs b/src/lib/routing.mjs index 56fc7af8..3b3a8b9a 100644 --- a/src/lib/routing.mjs +++ b/src/lib/routing.mjs @@ -77,7 +77,7 @@ export const SUBSCRIPTION_PROVIDERS = new Set(['claude-code', 'codex', 'ollama', // WITHOUT saying "per-token" gets read as cost-per-task, which is the axis users // actually pay on. No workload benchmark is embedded in this catalog; notes // describe curated roles, not measured superiority. -export const MODEL_CATALOG_VERIFIED = '2026-09-08'; +export const MODEL_CATALOG_VERIFIED = '2026-09-23'; export const COST_AXIS_NOTE = 'per-token price ≠ per-task cost — compare total tokens, cache use and agentic turns on representative tasks'; // Tier names are the pairing key for swapHostModel(): a codex tier only mirrors // to a claude model (and back) when BOTH catalogs use the same tier string. @@ -85,7 +85,10 @@ export const COST_AXIS_NOTE = 'per-token price ≠ per-task cost — compare tot // renaming one side silently degrades every mirrored route to cat[0]. export const MODEL_CATALOG = { claude: [ - { id: 'claude-opus-5', tier: 'reasoning', note: 'reasoning-oriented preset; compare agentic turns and per-task cost on representative work' }, + // Opus 5.5 (released 2026-09-22) is the reasoning preset: Anthropic's + // recommended default for most workloads, at a lower per-token rate than + // Opus 5 and 0.05x cache reads. Opus 5 stays listed for user pins. + { id: 'claude-opus-5-5', tier: 'reasoning', note: 'reasoning-oriented preset; compare agentic turns and per-task cost on representative work' }, { id: 'claude-sonnet-5', tier: 'balanced', note: 'balanced preset for review, specification and release work' }, { id: 'claude-fable-5-1', tier: 'flagship', note: 'flagship preset for demanding tasks; availability depends on the host and account' }, { id: 'claude-haiku-4-5-20251001', tier: 'fast', note: 'fast-tier preset for high-volume mechanical work' }, @@ -93,14 +96,25 @@ export const MODEL_CATALOG = { // longer what ak routes to by default. Kept listed so divergedRoutes can name // its cost-per-task trade when a policy is still pointing at it. { id: 'claude-opus-4-8', tier: 'prior', note: 'prior-generation option retained for user pins; compare measured per-task results before switching' }, + { id: 'claude-opus-5', tier: 'prior', note: 'prior reasoning preset retained for user pins; compare measured per-task results before switching' }, { id: 'claude-mythos-5-1', tier: 'restricted', note: 'restricted-access option; entitlement must be established independently' }, { id: 'claude-fable-5', tier: 'prior', note: 'prior flagship option retained for explicit selection' }, ], codex: [ - { id: 'gpt-5.6-sol', tier: 'flagship', note: 'flagship preset for complex coding workflows' }, - { id: 'gpt-5.6-terra', tier: 'balanced', note: 'balanced preset for everyday implementation and testing' }, - { id: 'gpt-5.6-luna', tier: 'fast', note: 'fast-tier preset for mechanical work, documentation and packaging' }, - { id: 'gpt-6-astra', tier: 'frontier', note: 'frontier preset; inspect current model evidence and prices before selection' }, + // GPT-6 class (Sol/Luna released 2026-09-22; Astra 2026-09-03). Codex's own + // catalog ranks these first and labels every GPT-5.6 model "Older", and + // OpenAI's Codex docs make Sol the default preset + // (learn.chatgpt.com/docs/models, verified 2026-09-23). Tier pairing with the + // claude catalog follows role and per-token price: Sol ↔ Sonnet 5 ($2/$10 + // both), Astra ↔ Fable 5.1 ($10/$50 both), Luna ↔ Haiku. Sol is first, so a + // claude tier with no codex twin (reasoning) mirrors to the workhorse. + { id: 'gpt-6-sol', tier: 'balanced', note: 'workhorse preset for coding, testing and everyday work' }, + { id: 'gpt-6-luna', tier: 'fast', note: 'fast-tier preset for mechanical work, documentation and packaging' }, + { id: 'gpt-6-astra', tier: 'flagship', note: 'frontier preset for the hardest end-to-end work; compare per-task cost before selection' }, + // Still served, retained for user pins. Codex labels them "Older". + { id: 'gpt-5.6-sol', tier: 'prior', note: 'prior coding preset retained for user pins; compare measured per-task results before switching' }, + { id: 'gpt-5.6-terra', tier: 'prior', note: 'prior balanced preset retained for user pins' }, + { id: 'gpt-5.6-luna', tier: 'prior', note: 'prior fast preset retained for user pins' }, ], }; @@ -239,16 +253,16 @@ export function swapRoute(route) { const R = (host, model, escalation) => ({ host, model, ...(escalation ? { escalation } : {}) }); export const DEFAULT_ROUTES = { specification: R('claude', 'claude-sonnet-5'), - architecture: R('claude', 'claude-opus-5'), - design: R('claude', 'claude-opus-5'), - implementation: R('codex', 'gpt-5.6-terra', [{ host: 'claude', model: 'claude-opus-5' }]), - testing: R('codex', 'gpt-5.6-terra', [{ host: 'claude', model: 'claude-opus-5' }]), + architecture: R('claude', 'claude-opus-5-5'), + design: R('claude', 'claude-opus-5-5'), + implementation: R('codex', 'gpt-6-sol', [{ host: 'claude', model: 'claude-opus-5-5' }]), + testing: R('codex', 'gpt-6-sol', [{ host: 'claude', model: 'claude-opus-5-5' }]), review: R('claude', 'claude-sonnet-5'), - 'security-scan': R('codex', 'gpt-5.6-terra'), - 'security-analysis': R('claude', 'claude-opus-5'), - documentation: R('codex', 'gpt-5.6-luna'), - debugging: R('claude', 'claude-opus-5'), - packaging: R('codex', 'gpt-5.6-luna'), + 'security-scan': R('codex', 'gpt-6-sol'), + 'security-analysis': R('claude', 'claude-opus-5-5'), + documentation: R('codex', 'gpt-6-luna'), + debugging: R('claude', 'claude-opus-5-5'), + packaging: R('codex', 'gpt-6-luna'), release: R('claude', 'claude-sonnet-5'), }; diff --git a/tests/kit/dashboard-integration-identity.test.mjs b/tests/kit/dashboard-integration-identity.test.mjs index b414c8c1..c033ca9a 100644 --- a/tests/kit/dashboard-integration-identity.test.mjs +++ b/tests/kit/dashboard-integration-identity.test.mjs @@ -1,6 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { routingPayload } from '../../src/lib/dashboard-server.mjs'; +import { DEFAULT_ROUTES } from '../../src/lib/routing.mjs'; test('dashboard routing describes host assignment without manufacturing a provider', () => { const payload = routingPayload({ @@ -58,7 +59,7 @@ test('retirement and divergence are separate signals on the wire', () => { const route = payload.routes.find((entry) => entry.activity === 'architecture'); assert.equal(route.model, 'claude-opus-4-8', 'a superseded-but-live model is left alone'); assert.equal(Object.hasOwn(route, 'retiredFrom'), false, 'not a retirement'); - assert.equal(route.diverged.defaultModel, 'claude-opus-5'); + assert.equal(route.diverged.defaultModel, DEFAULT_ROUTES.architecture.model); assert.ok(route.diverged.currentNote && route.diverged.defaultNote, 'both sides must carry their cost-per-task note so the trade is legible'); }); diff --git a/tests/kit/model-dashboard-read-model.test.mjs b/tests/kit/model-dashboard-read-model.test.mjs index f991c540..042c891e 100644 --- a/tests/kit/model-dashboard-read-model.test.mjs +++ b/tests/kit/model-dashboard-read-model.test.mjs @@ -656,7 +656,7 @@ test('Astra inventory and unlinked routes show first-party pricing and its verif const projected = createDashboardModelPayload(input, { key: KEY }).snapshot.models[0]; assert.equal(projected.pricing.input, 10); assert.equal(projected.pricing.output, 50); - assert.equal(projected.pricing.asOf, '2026-09-08'); + assert.equal(projected.pricing.asOf, '2026-09-23'); assert.equal(projected.pricing.sourceUrl, 'https://developers.openai.com/api/docs/models/gpt-6-astra'); assert.equal(projected.dimensions.entitled.value, null); input.snapshot.models = []; diff --git a/tests/kit/model-discovery-claude-codex.test.mjs b/tests/kit/model-discovery-claude-codex.test.mjs index 2aae6c8b..433053b9 100644 --- a/tests/kit/model-discovery-claude-codex.test.mjs +++ b/tests/kit/model-discovery-claude-codex.test.mjs @@ -54,11 +54,11 @@ test('Claude keeps the 1M selector as a variant instead of a duplicate base mode test('Claude public facts retain first-party lifecycle, discovery, limits, and scope', () => { const result = discoverAnthropicPublicCatalog({ - capturedAt: '2026-09-02T13:00:00.000Z', scope: { profile: 'default' }, scopeKey: SCOPE_KEY, + capturedAt: '2026-09-23T13:00:00.000Z', scope: { profile: 'default' }, scopeKey: SCOPE_KEY, }); assert.equal(result.source.id, 'anthropic-docs'); assert.equal(result.source.ownerType, 'provider'); - assert.equal(result.source.sourceVersion, '2026-09-02'); + assert.equal(result.source.sourceVersion, '2026-09-23'); assert.equal(result.models.length, ANTHROPIC_PUBLIC_MODELS.length); const fable = result.models.find((model) => model.identity.modelId === 'claude-fable-5'); @@ -123,7 +123,7 @@ test('Claude public facts include Fable 5.1 and Mythos 5.1 as active, priced ent test('Claude bundled public facts become explicitly stale instead of silently aging', () => { const result = discoverAnthropicPublicCatalog({ - capturedAt: '2026-12-09T00:00:00.000Z', scopeKey: SCOPE_KEY, + capturedAt: '2026-12-23T00:00:01.000Z', scopeKey: SCOPE_KEY, // just past 90 days after 2026-09-23 }); assert.equal(result.source.status, 'stale'); assert.equal(result.models[0].evidence.every(({ freshness }) => freshness === 'stale'), true); @@ -207,3 +207,10 @@ test('Codex inventory derives an effective override from a valid per-model clamp configRaw: 'model_context_window = 872000\n', scopeKey: SCOPE_KEY }); assert.equal(result.models[0].variant.effectiveContextWindow, 828400); }); + +test('Claude public facts include Opus 5.5 as an active 1M-context entry at its own rate', () => { + const opus55 = ANTHROPIC_PUBLIC_MODELS.find((model) => model.id === 'claude-opus-5-5'); + assert.ok(opus55, 'Opus 5.5 is catalogued'); + assert.deepEqual([opus55.lifecycle, opus55.contextLimit, opus55.outputLimit, opus55.retirementNotBefore], + ['active', 1_000_000, 128_000, '2027-09-22']); +}); diff --git a/tests/kit/pricing.test.mjs b/tests/kit/pricing.test.mjs index 6b2dc660..e35496c8 100644 --- a/tests/kit/pricing.test.mjs +++ b/tests/kit/pricing.test.mjs @@ -10,7 +10,7 @@ const M = 1_000_000; // ── Table shape ────────────────────────────────────────────────────────────── test('PRICES_AS_OF is the ISO date the table was last verified', () => { - assert.equal(PRICES_AS_OF, '2026-09-08'); + assert.equal(PRICES_AS_OF, '2026-09-23'); }); test('every PRICES entry carries finite in/out rates, a provider, and asOf', () => { @@ -220,7 +220,7 @@ test('Astra resolves its published rates and independent verification date', () const p = priceFor(id, 'openai'); assert.equal(p.matched, true); assert.equal(p.key, 'gpt-6-astra'); - assert.deepEqual([p.in, p.out, p.cacheReadMultiplier, p.asOf], [10, 50, 0.1, '2026-09-08']); + assert.deepEqual([p.in, p.out, p.cacheReadMultiplier, p.asOf], [10, 50, 0.1, PRICES_AS_OF]); } assert.equal(priceFor('gpt-6-astral').matched, false); assert.equal(priceFor('gpt-6').matched, false); @@ -259,3 +259,42 @@ test('OpenAI cache premiums follow the published model generation', () => { assert.equal(costOf({ model: 'gpt-5.6', cacheWrite: M }), 5); assert.equal(costOf({ model: 'gpt-6-astra', cacheWrite: M }), 12.5); }); + + +// Opus 5.5 (2026-09-22) is priced BELOW the Opus line before it and reads cache +// at 0.05x. Before its own key existed, `claude-opus-5-5` matched `claude-opus-5` +// on the token boundary and was costed at $5/$25 with 0.1x cache reads. +test('Opus 5.5 resolves its own row: $4/$20, 0.05x cache reads, 1.25x/2x writes', () => { + const p = priceFor('claude-opus-5-5'); + assert.equal(p.key, 'claude-opus-5-5', 'must not fall through to claude-opus-5'); + assert.deepEqual([p.in, p.out, p.cacheReadMultiplier, p.asOf], [4, 20, 0.05, '2026-09-23']); + for (const [counter, expected] of Object.entries({ input: 4, output: 20, cacheRead: 0.2, cacheWrite: 5 })) { + assert.equal(costOf({ model: 'claude-opus-5-5', [counter]: M }), expected, counter); + } + assert.equal(costOf({ model: 'claude-opus-5-5', cacheWrite: M, cacheWrite1h: M }), 8, '1-hour writes at 2x'); + assert.equal(priceFor('claude-opus-5').key, 'claude-opus-5', 'Opus 5 keeps its own rate'); + assert.equal(priceFor('claude-opus-5-20260401').key, 'claude-opus-5', 'a dated Opus 5 id is not Opus 5.5'); +}); + +test('GPT-6 Sol and Luna carry their published standard rates and write premium', () => { + for (const [model, rates] of Object.entries({ + 'gpt-6-sol': { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 }, + 'gpt-6-luna': { input: 0.1, output: 0.5, cacheRead: 0.01, cacheWrite: 0.125 }, + })) { + assert.equal(priceFor(model).key, model); + for (const [counter, expected] of Object.entries(rates)) { + assert.ok(Math.abs(costOf({ model, [counter]: M }) - expected) < 1e-12, `${model} ${counter}`); + } + } + assert.equal(priceFor('gpt-5.6-sol').key, 'gpt-5.6-sol', 'the GPT-5.6 namesake keeps its own rate'); +}); + +// OpenCode and custom Codex providers record provider-namespaced ids. Matching +// is by prefix, so before this the namespace sent them to the fallback rate. +test('a provider-namespaced id is matched on its model segment', () => { + assert.equal(priceFor('anthropic/claude-opus-4.7').key, 'claude-opus-4-7'); + assert.equal(priceFor('openai/gpt-5.5').key, 'gpt-5.5'); + assert.equal(priceFor('openrouter/anthropic/claude-sonnet-5').key, 'claude-sonnet-5'); + assert.equal(priceFor('anthropic/unknown-model').matched, false); + assert.equal(priceFor('anthropic/').matched, false); +}); diff --git a/tests/kit/routing-divergence.test.mjs b/tests/kit/routing-divergence.test.mjs index 04948a04..31c1d3bd 100644 --- a/tests/kit/routing-divergence.test.mjs +++ b/tests/kit/routing-divergence.test.mjs @@ -66,7 +66,7 @@ test('divergedRoutes returns [] for an empty policy (nothing persisted, nothing test('divergedRoutes reports a seeded entry whose model no longer matches the default', () => { // The exact #55 reproduction: a machine seeded pre-alpha.22 still pinned to - // the prior Opus generation while DEFAULT_ROUTES moved to claude-opus-5. + // the prior Opus generation while DEFAULT_ROUTES moved on. const policy = { architecture: { host: 'claude', model: 'claude-opus-4-8', provenance: 'seeded' } }; const out = divergedRoutes(policy); assert.equal(out.length, 1); @@ -121,12 +121,13 @@ test('the codex-primary clean-seed assertion actually covers a LADDER-bearing ac test('a corrupted rung on a MIRRORED activity is still reported (mirroring is not amnesty)', () => { const seed = seedActivityRoutes({ primary: 'codex' }); const [act] = Object.entries(seed).find(([, r]) => r.escalation?.length); + const mirroredRung = seed[act].escalation[0].model; seed[act].escalation[0].model = 'gpt-5.6-luna'; const [d] = divergedRoutes(seed); assert.equal(d.activity, act); assert.equal(d.modelDiverged, false, 'only the rung moved — the primary model is untouched'); assert.equal(d.escalation[0].model, 'gpt-5.6-luna'); - assert.equal(d.escalation[0].defaultModel, 'gpt-5.6-sol', 'compared against the MIRRORED default rung'); + assert.equal(d.escalation[0].defaultModel, mirroredRung, 'compared against the MIRRORED default rung'); }); test('divergedRoutes ignores a seeded entry that still matches the default', () => { @@ -138,21 +139,26 @@ test('divergedRoutes ignores a seeded entry that still matches the default', () }); /** The pre-alpha.22 seed: a REAL seeded policy (escalation ladders included, - * exactly as seedActivityRoutes writes them) with every claude-opus-5 pin rewound + * exactly as seedActivityRoutes writes them) with every default-Opus pin rewound * to claude-opus-4-8 — primary models and escalation rungs alike. Building this * from seedActivityRoutes rather than by hand matters: a fixture that silently * dropped `escalation` would under-report divergence and hide the escalation-only * case entirely. */ function priorCatalogSeed({ primary = 'claude' } = {}) { // Two rewind rules, one per primary. A claude-primary seed carries - // claude-opus-5; a codex-primary seed mirrors every claude model away, so it + // the default Opus; a codex-primary seed mirrors every claude model away, so it // carries none — rewinding only that id would make the mirrored fixture // silently vacuous (it tested nothing at all once the catalog's tier pairing // became complete). gpt-5.6-sol appears ONLY in the mirrored seed, so the // second rule bites exactly where the first cannot. Both targets are current, // non-retired models: this fixture is about divergence, and letting a RETIRED // id in would conflate it with the substitution mechanism. - const PRIOR = { 'claude-opus-5': 'claude-opus-4-8', 'gpt-5.6-sol': 'gpt-5.6-terra' }; + // Keyed on the CURRENT defaults so the fixture follows a future default change + // instead of silently rewinding nothing. The claude default appears only in a + // claude-primary seed, the codex workhorse is the rung a mirrored seed carries. + const PRIOR = primary === 'codex' + ? { [DEFAULT_ROUTES.implementation.model]: 'gpt-5.6-sol' } + : { [DEFAULT_ROUTES.architecture.model]: 'claude-opus-4-8' }; const rewind = (m) => PRIOR[m] ?? m; const policy = seedActivityRoutes({ hosts: ['claude', 'codex'], primary }); const out = {}; diff --git a/tests/kit/status-viability.test.mjs b/tests/kit/status-viability.test.mjs index db528ce0..c40cd83f 100644 --- a/tests/kit/status-viability.test.mjs +++ b/tests/kit/status-viability.test.mjs @@ -91,7 +91,7 @@ const cfgWithChain = (aqeFallback) => ({ * writes them: two of the six diverge ONLY on their escalation rung, and a * hand-built fixture that dropped `escalation` would silently under-report. */ function divergedPolicy() { - const rewind = (m) => (m === 'claude-opus-5' ? 'claude-opus-4-8' : m); + const rewind = (m) => (m === DEFAULT_ROUTES.architecture.model ? 'claude-opus-4-8' : m); const seed = seedActivityRoutes({ hosts: ['claude', 'codex'] }); const activityRoutes = {}; for (const [act, r] of Object.entries(seed)) {