Repository navigation
fix(usage): price GPT-6 Astra, and declare when the Codex cost total omits a model - #22073
Conversation
gpt-6-astra was missing from MODEL_PRICING, so normalizeModelForPricing returned null and estimateCostUsd dropped every event on that model from the total. Stats & Usage showed ~$0 for hundreds of millions of tokens with no unpriced indicator, since hasInferredPricing only covers a missing model name, not a missing table entry. Rates are the published ones: $10 input / $1 cached input / $50 output per 1M, with the >272K long-context tier at 2x input and cache and 1.5x output, which the existing tier fields already express. Fixes #22005 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A daily row whose model has no `MODEL_PRICING` entry gets a null cost, and
`buildSummary` simply skips it. As long as one other row is priced,
`hasAnyBillableCost` is true, so the Codex card prints a confident dollar
figure that silently leaves those tokens out. That is how GPT-6 Astra usage
read as near-$0 before the entry landed, and it is how the next unpriced
model will read too.
`hasInferredPricing` does not cover this: it only fires when a rollout has no
model name at all, and its label ("inferred pricing") describes a guess, not
an omission.
So the summary now carries `hasUnpricedModels`, set when a row has a model
name and no price, and the estimated-cost card appends
"• excludes unpriced models" — the same bullet-suffix idiom the breakdown
rows already use for "• inferred pricing". The number stays; it stops
claiming to be the whole bill.
|
Understand this PR’s impact Explore downstream dependencies and potential security impact with Blast Radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (1)
Included review availability: Your plan provides up to 10 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughThe change adds GPT-6 Astra pricing and normalization for suffixed model names. Codex usage summaries now flag named models without pricing entries while excluding their tokens from estimated cost totals. The partial-cost state propagates to provider overviews. The Codex cost card displays an exclusion notice when a cost exists. Tests, translations, and summary fixtures cover the updated behavior. Priority: ➖ Normal Severity of issue fixed: Medium 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 17f727ab-6962-4511-a703-ea474997a57f
📒 Files selected for processing (9)
src/main/codex-usage/codex-model-pricing.tssrc/main/codex-usage/codex-usage-rollup-projections.tssrc/main/codex-usage/store-model-pricing.test.tssrc/renderer/src/components/stats/CodexUsagePane.test.tsxsrc/renderer/src/components/stats/CodexUsagePane.tsxsrc/renderer/src/components/stats/usage-overview-model.test.tssrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/store/slices/usage-snapshot-refresh.benchmark.test.tssrc/shared/codex-usage-types.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
| inputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 20 }], | ||
| cachedInputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 2 }], | ||
| outputTiers: [{ threshold: LONG_CONTEXT_THRESHOLD_TOKENS, price: 75 }] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,220p' src/main/codex-usage/codex-model-pricing.ts
sed -n '1,120p' src/main/codex-usage/codex-usage-cost-estimate.ts
sed -n '1,110p' src/main/codex-usage/codex-usage-rollup-projections.ts
sed -n '200,280p' src/main/codex-usage/store-model-pricing.test.ts
rg -n 'LONG_CONTEXT_THRESHOLD|inputTiers|cachedInputTiers|outputTiers|estimateCostUsd' src/main/codex-usageRepository: stablyai/orca
Length of output: 18977
🌐 Web query:
OpenAI GPT-6 Astra pricing long context over 272K input tokens full request rates
💡 Result:
<source_evidence>
Citations:
- 1: https://developers.openai.com/api/docs/models/gpt-6-astra
- 2: https://developers.openai.com/api/docs/pricing
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- candidate files ---'
git ls-files 'src/main/codex-usage/*' | sed -n '1,120p'
printf '%s\n' '--- persisted types and aggregation symbols ---'
rg -n -C 4 'dailyAggregates|CodexUsageDaily|eventCount|inputTokens|cachedInputTokens|outputTokens|usage' src/main/codex-usage \
-g '*.ts' | sed -n '1,260p'
printf '%s\n' '--- rollup projection cost paths ---'
sed -n '130,215p' src/main/codex-usage/codex-usage-rollup-projections.ts
printf '%s\n' '--- automation attribution cost paths ---'
sed -n '90,155p' src/main/codex-usage/codex-automation-run-attribution.tsRepository: stablyai/orca
Length of output: 27403
Apply the long-context rate to each complete request.
GPT-6 Astra applies the higher rates to the full request when input exceeds 272K tokens. The current progressive tiers calculate $87.208 for the 2M-input, 1M-cached, 1M-output fixture, but one qualifying request should cost $97.
The rollup and automation paths pass aggregated token counts to estimateCostUsd, so they cannot determine which individual requests exceeded the threshold. Preserve per-request threshold data and sum request costs. If that data is unavailable, do not apply the long-context surcharge to the aggregate.
…exists Review of #22073 found the Codex caveat stopped at the Codex tab. The Overview tab prints a combined total across providers and already has a "- some model prices are unavailable" line, but `hasPartialCost` only noticed a provider whose whole cost was null. A Codex range with one unpriced model among priced ones kept a real number, so the line stayed hidden and the Overview repeated the same confident, incomplete figure. `UsageProviderOverview` now carries `hasPartialCost` — set from `hasUnpricedModels` for Codex, false for the providers that cannot yet report it — and the reduction ORs it in. No new string. Second, the Codex card could read "n/a • excludes unpriced models" when nothing at all was priced. "Excludes" promises a remainder, and there was none. The suffix now also requires a non-null total; that case is still declared, on the Overview, through the null-cost path. `unpricedCostLabel` becomes `costCardLabel`, since it holds the plain label whenever there is nothing to qualify.
There was a problem hiding this comment.
ℹ️ No critical issues — the Astra pricing and the
hasUnpricedModelsflag are correct. One incomplete-fix gap: the same partial total still renders unqualified on the Overview and share surfaces.
Reviewed changes
- GPT-6 Astra pricing — adds
gpt-6-astratoMODEL_PRICINGat $10/$1/$50 per M with 2×/2×/1.5× long-context tiers above 272K, plus anormalizeModelForPricingbranch so bare, reasoning-suffixed (-high,(high)) and dated-snapshot IDs resolve to it. The tiered arithmetic and the two new expected values (87.208,1.8) check out, and no other model's normalization changes. - Unpriced-model caveat —
buildSummarysetshasUnpricedModelswhen a daily row carries a model name butestimateCostUsdreturnsnull, andCodexUsagePaneappends• excludes unpriced modelsto the cost card label. Rows with no model name are deliberately excluded, keeping the flag disjoint fromhasInferredPricing.
ℹ️ Partial Codex totals stay unqualified on the Overview and share surfaces
The new caveat is read only by CodexUsagePane. The Usage Overview's combined Est. cost and per-provider cost, and the share card/share text, still present a mixed priced/unpriced Codex total as if it were complete. The overview's existing hasPartialCost caveat cannot cover this: it fires only when a provider has no priced model, not when one named model among priced ones was dropped.
Technical details
# Propagate `hasUnpricedModels` to the remaining cost surfaces
## Affected sites
- `src/renderer/src/components/stats/usage-provider-normalization.ts:75` — `createCodexProvider` copies `estimatedCostUsd` but never reads `hasUnpricedModels`; `UsageProviderOverview` (`usage-overview-types.ts:19-39`) has no field for it.
- `src/renderer/src/components/stats/usage-overview-model.ts:36-38` — `hasPartialCost` is `provider.hasData && provider.estimatedCostUsd === null`, so a non-null partial cost never trips it.
- `src/renderer/src/components/stats/UsageOverviewPane.tsx:206` — combined total; caveat at `:111-116` is driven by `hasPartialCost`.
- `src/renderer/src/components/stats/usage-overview-sections.tsx:253` — per-provider row renders `formatUsageCost(provider.estimatedCostUsd)` with no qualifier.
- `src/renderer/src/components/stats/ShareUsageCard.tsx:149` and `src/renderer/src/components/stats/ShareUsageButton.tsx:94-111` — receive the full `CodexUsageSummary` (`share-card-utils.tsx:16`) but do not read the flag.
## Required outcome
A Codex total that omits a named-but-unpriced model is disclosed wherever it is shown, not only on the Codex pane. The overview already has the wording (`UsageOverviewPane` "some model prices are unavailable"); it needs the mixed case to set it.
## Suggested approach (optional)
Add `hasUnpricedModels` (or a provider-level `hasPartialCost`) to `UsageProviderOverview`, set it from the Codex summary in `createCodexProvider`, and OR it into the overview's `hasPartialCost` at `usage-overview-model.ts:36`. For the share surfaces, gate the `Est. cost` value on the same flag.
## Open questions for the human (optional)
Is the Overview/share disclosure in scope for this PR, or tracked as a deliberate follow-up? If follow-up, a comment on the new field noting the untouched surfaces would keep the gap discoverable.ℹ️ Nitpicks
- Confirm whether a bare
gpt-6alias exists.gpt-5.6is mapped to Sol atcodex-model-pricing.ts:185-190; if OpenAI routesgpt-6to Astra the same way, it currently normalizes tonulland is flagged unpriced rather than priced.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ The Overview fixup is correct. One surface from the earlier review remains: the share card and share text still render a partial Codex total unqualified.
Reviewed changes (since 1d85f1c)
- Provider-level partial-cost signal —
UsageProviderOverviewgainshasPartialCost, set fromsummary.hasUnpricedModelsincreateCodexProviderand hardcodedfalsefor Claude/OpenCode; the aggregatehasPartialCostORs it in (usage-overview-model.ts:36-39), so the Overview's existing "some model prices are unavailable" line now fires when Codex priced some models but not all. A test asserts both directions against a real non-null cost. - All-unpriced guard — the pane's
• excludes unpriced modelssuffix now also requiresestimatedCostUsd !== null, so an all-unpriced range shows the plain label withn/a; a third pane test pins that state.
ℹ️ Nitpicks
- The share surfaces still present a partial Codex cost as complete:
ShareUsageCard.tsx:149(StatsGridreceives onlyestimatedCostUsd) andShareUsageButton.tsx:94-111both hold the fullCodexUsageSummarybut never readhasUnpricedModels. If share disclosure is a deliberate follow-up, a note on the new field would keep the gap discoverable.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
The stage the staged plan existed for. Measured from the post-stage-2 main this cost 78 commits and 14 conflicts (13 in the PTY delivery subsystem) instead of 881 commits and 32 conflicts from the original base. **The PTY wall was not what the 월드맵 feared.** It recorded `pty-project-window-stream-routing` as a ~1,500-line feature-scale re-implantation and deferred it twice. In fact all 14 conflicts come from three upstream commits: - `9700030689` refactor(pty): make renderer delivery optional for headless runtimes (stablyai#23117) — `session.mainWindow` becomes optional and every `isDestroyed()` guard gains a null check. - `82412dab8b` Persist profile state in SQLite with background writes (stablyai#22612). - `d443320af2` refactor(native-chat): remove the unused terminal handoff (stablyai#22783). On the same lines the fork had already replaced the main-window guard with owner-window routing (`resolvePtyOwnerWindow`, `sendToPtyOwner`, `ownerFlowStateForPty`). The fork's guard is strictly finer: in headless mode no window is registered with the router, the owner resolves to null, and the fork takes the same early-return upstream's null check adds — while upstream's coarser guard would drop or misroute project-window delivery when the main window is gone. So most of the cluster resolves to ours, and nothing had to be reimplanted. Conflicts resolved (14) - `pty/delivery/{accept,accounting,exit,flush}.ts`, `pty/provider/bind-listeners.ts`: ours. Verified mechanically — resolving every hunk to ours produced a file byte-identical to main, so upstream has no other change in them. - `pty/delivery/payload.ts`, `pty/runtime/operations.ts`: per hunk, never whole-file. A whole-file ours on operations.ts would have restored imports of two modules `d443320af2` deleted and a 3-arg `writePtyFromRuntimeController` while the merged `runtime/controller.ts` calls the 2-arg form. - `pty/ipc/{resize-visibility,serialize-buffer,spawn-commit-persist,write-input,write}.ts`, `pty/register-handlers.ts`: combined, preserving every per-window router injection. A previous attempt at this merge failed its gates by dropping them. - `persistence/loading-store/store.ts`: combined for `82412dab8b`'s SQLite authority. Edits outside the 14 conflicted files (6) Each one is a defect the conflict list could not show, because the file merged cleanly while the meaning did not. 1. `pty/session.ts` + `pty/delivery/window-flow-state.ts`: `session.ts` merged cleanly into a crash. `mainWindow` is now optional, upstream's `register-headless-runtime.ts` passes `undefined` where it used to pass a fake destroyed window, and the fork's `createWindowFlowState(session, args.mainWindow.webContents)` therefore threw on every `orca serve` and orcad registration. Fixed by narrowing a flow state's `webContents` to `Pick<WebContents, 'id' | 'isDestroyed' | 'send'>` and passing a destroyed stand-in, with no cast. `window-flow-state.ts` stays at 297 of its 300 permitted code lines. 2. `persistence/loading-store/store-domain-composition.ts`: added `SatelliteWindowSessionPersistence &` to `StoreDomainOperations`, without which four satellite-session methods disappear from the `Store` type. 3. `pty/register-without-renderer.test.ts`: two `registerPtyHandlers(renderer as never, …)` casts, matching the shape 274 existing calls in this repo use. This is an upstream-owned file, so the next sync touching it will conflict here. 4. Three usage test fixtures (`usage-custom-range-filters.test.ts`, `UsageNumberFormatToggle.test.tsx`, `usage-pane-test-state.ts`): added the fields upstream `297cfe0cf3` (stablyai#22073) made required. **These were already broken on main**: a fresh worktree at `e0686816e8` reports exactly these 4 type errors. Stage 2's gate missed them because a warm `composite` `.tsbuildinfo` cache under-reported. Typecheck cold before trusting a pass. 5. `pty/register-handlers.ts` + the window-affinity router: `setMainWindowForRouting` widened to `BrowserWindow | null` and called unconditionally as `setMainWindowForRouting(mainWindow ?? null)`. Skipping it when `mainWindow` is undefined left a previously routed live window routed, so a desktop → headless re-registration would send `pty:exit`, `pty:spawned`, `pty:writeUnavailable`, `pty:clearBuffer:request`, `pty:serializeBuffer:request` and `pty:modelRestoreNeeded` to a stale window. Current call order prevents it in production; that is not a guarantee worth depending on. Restores parity with main. Gates (each captured from the command's own exit code) - typecheck, run cold after deleting `config/*.tsbuildinfo`: 0 errors. The same command on a main worktree reports its 4 pre-existing errors. - oxlint: exit 0 on the full tree and on the touched files. - verify-orcinus-identity: OK, upstreamRefs=0. - electron-builder smoke: unsigned, update signature checks disabled. - fork contract tests: 3 files / 24 tests. - `register-without-renderer.test.ts` (upstream's new 231-line headless test): 4/4. - router-referencing tests: 3 files / 70 tests. - Broad conflict-area suite: 122 failures, none attributable. 121 fail identically on a main worktree; the remaining one, upstream's own `profile-state-crash-recovery.test.ts`, times out identically on a `da6d483ab9` worktree. Per-test JSON diff: 0 failures unique to main. Reviewed by an independent critic: APPROVE, no blocking defects, every gate re-measured, every fork-only and upstream-only file confirmed equal to its parent apart from the six edits above, no IPC channel dropped. Non-blocking and deliberately not addressed: `hasUnpricedModels: false` does not match its own fixture's data (behaviour-neutral); the upstream-owned test file will re-conflict; plan-B's satellite-session durability question is still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g unpriced models (#30421) normalizeModelForPricing was an ordered chain of regexes in which a new point release fell through to its major: claude-sonnet-5-5 was billed as Sonnet 5 (cache reads at $0.20 instead of $0.10), and any future opus-5-x / fable-5-x would be too. claude-haiku-5-5 matched nothing and silently added $0 to a total that showed no caveat. The resolver now parses family + major + optional minor (family-first and legacy version-first IDs) and only accepts an exact MODEL_PRICING key, so an unknown version is unpriced rather than borrowing another model's rates. The deliberate opus-4.x -> 4.8 and sonnet-4.x -> 4.6 fallbacks go with it. Adds Sonnet 5.5 and Mythos 5/5.1 rows. Haiku 5.5 stays unpriced: it reprices a whole request above 100K prompt tokens, which daily totals cannot express. The Claude summary now carries hasUnpricedModels (as Codex has since #22073), and both panes share one unpricedCostLabel helper for the caveat. Fixes #30338

ELI5
Orca's Stats & Usage page shows roughly what your Codex tokens would have cost at API rates. It works from a table of per-model prices. GPT-6 Astra was not in that table, so all of its tokens were quietly thrown away before the total was added up — hundreds of millions of tokens could show as a few dollars, and nothing on screen said anything was missing.
This adds Astra's prices, and makes the card admit when it has dropped something.
What Changed
Before. A user on GPT-6 Astra opened Stats & Usage, saw "Est. API-equivalent cost" with a small, confident dollar figure, and had no way to tell it was wrong.
After. Astra usage is priced at the published rates. And if any model in the range still has no price entry, the Codex card's label reads "Est. API-equivalent cost • excludes unpriced models", so the number no longer claims to be the whole bill. The Overview tab, which prints a combined total across providers, now raises its existing "- some model prices are unavailable" line for the same reason.
Two commits:
Pricing entry (authored by @Alfred212121, cherry-picked from feat(usage): price GPT-6 Astra token usage #22006). Adds
gpt-6-astratoMODEL_PRICINGand a matching branch innormalizeModelForPricingso reasoning suffixes and dated snapshot IDs (gpt-6-astra-high,gpt-6-astra-2026-09-01) price correctly. Rates are $10 input / $1 cached input / $50 output per million, with the existing above-272K-token tier fields carrying the 2x input, 2x cache, 1.5x output long-context prices.The caveat. The mechanism behind the silent zero is in
buildSummary: for each day-row it asksestimateCostUsdfor a cost, and when that returnsnullit just skips the row. A single flag,hasAnyBillableCost, decides whether the total is shown at all — so one priced model anywhere in the range is enough to print a number that omits every unpriced one. The summary now also carrieshasUnpricedModels, set when a row has a model name but no price, and the pane appends the caveat to the cost card's label.Review fixups. The Overview tab had the same blind spot from the other side: its
hasPartialCostonly noticed a provider whose entire cost wasnull, so a Codex range with one unpriced model among priced ones kept a real number and the warning line stayed hidden. Each provider's overview entry now carries its ownhasPartialCost, and the reduction ORs it in — no new string. Also, the Codex card could read "n/a • excludes unpriced models" when nothing at all was priced; "excludes" promises a remainder, so the suffix now requires a non-null total. That all-unpriced case is still declared on the Overview, through the null-cost path.Why
Adding the Astra entry alone fixes today's wrong number and leaves the failure class intact: the next model OpenAI ships lands in exactly the same hole, and again nobody finds out from the UI. The first commit is the fix; the second is the reason it won't need to be found by a user twice.
Alternatives considered:
hasInferredPricing. Rejected. It fires only when a rollout has no model name at all, and "inferred pricing" describes a guess, not an omission — it would mislabel what happened.• excludes unpriced models) is the one the breakdown rows already use for• inferred pricing, in the same muted style, so the caveat reads as part of the existing vocabulary rather than a new alarm.Scope stayed narrow on purpose: one boolean on
CodexUsageSummary, one place that sets it, one place that reads it. Claude, Grok and OpenCode have their own summary types and are untouched.Linked Issue
Fixes #22005
Visual Proof
The only visual change is the text of one stat card's label, and it is asserted directly rather than screenshotted.
src/renderer/src/components/stats/CodexUsagePane.test.tsxrenders the real pane against a summary and checks both states:Est. API-equivalent cost • excludes unpriced models, with the value$12.50still shownEst. API-equivalent cost, and no "excludes unpriced models" text is in the documentn/aunder the plain label, with no suffixThe first case fails if the caveat is removed from the pane; the third fails if the non-null-total gate is removed. The second is the control and passes either way. No screenshot was captured, because doing so would have meant launching a visible app window.
Testing
Run from the worktree with the repo's own binaries:
New tests, each confirmed to fail against the unfixed code:
store-model-pricing.test.ts— a named model with no price entry setshasUnpricedModelsand its ten million tokens are absent from the total beside it; a fully priced summary, and one containing a row with no model name at all, both leave the flag off. (The no-model-name case is the boundary that separates this flag fromhasInferredPricing.)store-model-pricing.test.ts(from feat(usage): price GPT-6 Astra token usage #22006) — Astra long-context pricing, and normalization of reasoning suffixes and snapshot IDs.CodexUsagePane.test.tsx— the rendered caveat, in all three states described above.usage-overview-model.test.ts— a Codex provider with a real cost andhasUnpricedModelstrue makes the Overview'shasPartialCosttrue, and false leaves it false. Confirmed to fail before the Overview fix.Platform: macOS. The change is pure TypeScript with no platform-dependent behavior, no process spawning, and no filesystem or path handling. The Codex usage summary does not cross the remote or mobile wire — it travels only over local Electron IPC between the main process and the renderer, which ship together — so there is no mixed-version concern.
AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Security: none — no new inputs, no I/O. Cross-platform: no platform-dependent code. Remote SSH and mobile: neither consumes
CodexUsageSummary; it crosses local Electron IPC only, so no wire contract changes. Backwards compatibility: adding a model to the pricing table changes only estimates for that model, which were previously being dropped entirely. Performance: one boolean assignment inside a loop that already computes the cost.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)