refactor(cost): CLI stops storing cost; platform owns it (supersedes #1702)#1763
Open
entire[bot] wants to merge 35 commits into
Open
refactor(cost): CLI stops storing cost; platform owns it (supersedes #1702)#1763entire[bot] wants to merge 35 commits into
entire[bot] wants to merge 35 commits into
Conversation
Versioned per-provider rate tables (USD per MTok) embedded via go:embed, with exact-id then alias-glob lookup and no implicit family fallback: an unknown model yields no estimate rather than a guessed price. Cache rates default to the Anthropic convention (read 0.1x, write 1.25x of input) unless a model sets explicit rates. Users can replace or extend entries and disable estimation entirely through a new settings.pricing block.
…oint metadata TokenUsage gains optional cost_usd/cost_source (reported|estimated|mixed) carried through every accumulation path including subagent totals; nil means unknown, never $0. A new ModelUsageCalculator capability lets agents attribute usage to the model that produced it (model can change mid-session, so session-level attribution is not enough); Claude Code implements it with streaming dedup keeping the model on the kept row. A centralized dispatcher prices each per-model bucket once, adds a remainder bucket when flat totals exceed bucket coverage, and folds honest provenance (mixed when any tokens stay unpriced). Buckets persist as model_usage[] on session metadata and aggregate onto the checkpoint summary, so downstream consumers can ingest per-model cost without re-deriving it.
…mmands Renders cost_usd with its provenance suffix, e.g. "Cost: $0.23 (estimated)"; absent cost renders nothing rather than $0.00. checkpoint tokens --compare adds a cost delta only when both sides carry cost, and tokens profile reports coverage as "across N of M checkpoints". Display never computes cost; it renders what the lifecycle stored.
Extend the embedded Anthropic pricing table so any Claude model id resolves to a rate, regardless of how the caller spells it. Models: add the current lineup and the legacy tiers that older transcripts still reference — mythos-5, opus 4.5/4.1/4.0, opus 3 (claude-3-opus), sonnet 4.5/4.0, 3.7/3.5/3 sonnet, and 3.5/3 haiku — alongside the existing entries, and correct the opus 4.7/4.6 effective dates. Rates and cache convention are unchanged (cache read/write are derived at 0.1x/1.25x of input). Aliases: every entry now carries the id spellings seen in the wild — Anthropic dated ids (<id>-2*), slash-prefixed ids (anthropic/<id>), Bedrock ids and regional inference profiles (anthropic.<id>[-*], us|eu|apac.anthropic.<id>*, which also cover legacy dated -vN:0 forms), and Vertex ids (<id>@*). Long-context [1m] fix: Claude Code emits ids like "claude-fable-5[1m]", but path.Match reads a literal "[1m]" alias as a character class, so it would never match. Rather than add a broken alias, Lookup now strips a trailing "[...]" suffix from the query before matching, so the id resolves to its base model and bills at the base rate (1M-context requests carry no long-context premium on current-generation models). doc.go records that behavior plus two known under-estimates: 1-hour-TTL cache writes (bill 2x input vs the 1.25x 5-minute default, pending per-TTL bucket parsing) and fast-mode turns (usage.speed="fast", a premium not published in-table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ex legacy ids A resolution sweep over real-world model-id spellings surfaced three forms the alias set missed: Bedrock global inference profiles (global.anthropic.<id>-v1:0), slash-prefixed dated ids (anthropic/<id>-<date>), and Vertex legacy versioned ids (<id>-v2@<date>). Each entry now carries alias globs for all three; the slash alias became a glob so dated variants resolve too.
model_usage entries persist full TokenUsage values, so the remainder bucket must account for api_call_count too — otherwise per-model buckets don't sum to the flat total on that field whenever a shortfall bucket is created.
…nsation Both extractSessionData and the Copilot session-wide backfill passed an empty fallbackModel to tokenUsageWithCost even though state.ModelName is in scope at their call sites (the live-transcript sibling already threads it). That left the Copilot CLI backfill permanently unpriced and degraded Claude Code checkpoints to a mixed cost source whenever subagent usage produced a remainder bucket. Both paths now receive the session model.
…pic cache rates Table.add now canonicalizes the index key (trimmed, lower-cased) and stores the trimmed id, so an override whose id differs only in case or whitespace replaces the builtin entry instead of appending a phantom duplicate. An alias-less override inherits the replaced entry's aliases, keeping the dated and provider-prefixed spellings resolvable. Lookup's fast path probes the index with the same canonicalization. Estimate's nil-cache-rate fallbacks are now provider-aware: the Anthropic 0.1x/1.25x multipliers apply only to anthropic-provider rates; any other provider missing an explicit cache rate falls back to the full input rate (1.0x) rather than silently undercharging. The openai and google tables now set cache_write_per_mtok explicitly. doc.go documents the Anthropic-only scope. validateRate rejects empty/whitespace aliases and malformed glob aliases (path.Match ErrBadPattern) so a mistyped override alias fails loudly at load instead of silently never matching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merging a priced usage with an unpriced-but-token-bearing usage now folds to CostSourceMixed, mirroring foldBucketCost's partial-coverage rule so an aggregate that combines a costed session with an uncosted token-bearing one no longer masquerades as fully priced. Adds one exported helper, types.MergeCostSourceUsages(a, b), which behaves like MergeCostSource over the two usages' (source, cost) pairs but additionally returns mixed when exactly one side is priced and the other carries nonzero billable tokens with no cost. Applied at the three scalar merge sites: accumulateTokenUsage (strategy), aggregateTokenUsage (checkpoint), and addCheckpointTokenUsage (cli). The in-place accumulate keeps computing the source before the cost and token fields are summed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
remainderBucket now flattens the flat total's SubagentTokens subtree (at arbitrary depth) before computing the per-field shortfall against the per-model buckets. Real extractors report main-transcript totals in the scalar fields and subagent totals in a SubagentTokens subtree that CalculateModelUsage never sees, so previously the subagent tokens went entirely unpriced — a Claude Code session with 100k main + 500k subagent tokens priced only the 100k. The subagent shortfall now lands in a remainder bucket priced under the fallback model. bucketTokens drops the SubagentTokens subtree so a fallback bucket carries main-scoped scalar counts only: the remainder bucket already covers the subtree, and keeping it on the fallback bucket would both double-count downstream and alias the caller's subtree. PriceUsage builds its single bucket the same way (nil subtree, cost preserved) so a reported-cost usage is untouched while its subagent pointer is no longer shared. accumulateTokenUsage's existing==nil branch deep-copies SubagentTokens so two aggregates that fold the same step cannot cross-contaminate each other's subagent counts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… them
A minimal price override ({id, input, output}) inherited aliases but not
the provider, so the provider-gated cache multipliers stopped applying and
cache-read tokens billed at the full input rate (10x overcharge on
Anthropic models). Overrides now inherit provider and explicit cache-rate
pointers from the replaced entry, mirroring alias inheritance.
Two condensation-time cost-attribution fixes.
Fix 1 (reprice-after-backfill): CondenseSession priced the extracted
session data before backfilling the model from the transcript. For agents
whose model only comes from the transcript (Pi implements ModelExtractor;
state.ModelName can be "" at condensation, e.g. a mid-turn commit before
agent_end), the pricing pass ran with an empty fallbackModel, so the flat
usage came out unpriced under an empty-model bucket while the persisted
checkpoint recorded Model="gpt-5.5" with a nil cost and ModelUsage=[{Model:"",…}].
After the model is recovered, re-price the SAME extracted transcript slice
with the recovered model (both extraction paths priced from
state.CheckpointTranscriptStart with an empty subagents dir, so the reprice
is equivalent) and swap in the priced usage and real-model buckets. Gated by
sessionDataUnpricedFromTranscript so the reprice only fires when the
transcript actually yielded unpriced usage, leaving the state.ModelUsage
mirror fallback intact.
Fix 2 (skip zero-token buckets): priceBuckets estimated buckets with no
billable tokens, producing a $0.00 "estimated" cost. Add a bucketHasTokens
guard so zero-usage buckets stay unpriced (nil cost, empty source),
consistent with the ModelUsageCalculator path and preventing aggregated
provenance from flipping reported to mixed. Reported costs on zero-token
buckets still win via the existing CostUSD!=nil early-continue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tate total The reprice-after-backfill swap accepted any non-nil re-extraction, so a zero-data window (assistant activity before CheckpointTranscriptStart, or a Pi fork abandoning the token-consuming branch) clobbered usage restored from checkpoint state with zeros. The swap now requires real token data. When the session-state total aliases the pre-reprice usage it is repointed to the priced copy, keeping the state diagnostic consistent with the persisted checkpoint.
Released 2026-07-09: sol $5/$30, terra $2.50/$15, luna $1/$6 per MTok, cache reads at 10% of input and — new in this family — cache writes at 1.25x input, all set explicitly. Alias forms cover dated, slash-prefixed, and vertex-style spellings; a guard test pins gpt-5.6-sol-pro (a distinct, differently priced model) to a lookup miss rather than Sol rates. Known limitation documented: >272K-token prompts bill 2x input / 1.5x output, which the flat-rate table cannot express (undercount only).
Keeps CondenseSession under the maintainability threshold and drops the always-empty subagentsDir parameter from tokenUsageWithCost (condensation never passes one; the call-site TODOs document the open question).
…variants
The OpenAI table carried the legacy gpt-5 rate (1.25/10) for gpt-5.5 and
gpt-5.4, which understates their real cost. Correct them to the current
published rates:
- gpt-5.5: 5 / 30 (cache 0.5 / 6.25)
- gpt-5.4: 2.5 / 15 (cache 0.25 / 3.125)
Both carry effective_date 2026-07-10 (the correction date; the old rate was
never actually in effect, it was a copy-paste of gpt-5). gpt-5 itself stays
1.25/10 — that is a genuine legacy rate, not the mistake. Historical persisted
estimates that already billed gpt-5.5/5.4 at the old rate are left as-is: they
are estimate-grade, not invoices, and are not retroactively repriced.
New entries (all effective_date 2026-07-10):
- gpt-5.3-codex: 1.75 / 14 (cache 0.175 / 2.1875).
- gpt-5.5-priority: 12.50 / 75. The cache rates (1.25 / 15.625) are a
2.5x-scaled assumption off the standard gpt-5.5 cache rates — flagged for
verification against the published priority-tier cache pricing.
- claude-opus-4-8-fast: 10 / 50, no explicit cache rates. Omitting them is
deliberate: the Anthropic multipliers derive cache-read 1.0 (0.1x) and
cache-write 12.5 (1.25x) from the fast input base, which is the intended
fast-mode cache economics. Ordered BEFORE claude-opus-4-8 so the dated
"-fast" glob wins over the base "claude-opus-4-8-2*" glob (which would
otherwise capture claude-opus-4-8-<date>-fast).
- cursor.json (new provider file): composer-2 (0.5/2.5), composer-2-fast
(1.5/7.5), composer-2.5-fast (3.0/15.0). Cache rates are Anthropic-ratio
assumptions — verify against cursor.com/docs/models-and-pricing. The
models/*.json embed glob picks the new file up automatically.
The base gpt-5.5 alias is tightened from the broad "gpt-5.5-*" to the
dated-only "gpt-5.5-2*" (matching the gpt-5.6 family convention) so a dated
priority spelling (gpt-5.5-priority-<date>) resolves to gpt-5.5-priority
instead of being glob-captured by gpt-5.5. Both bare ids resolve via the exact
index regardless; the tightening only affects dated/suffixed glob resolution.
Deliberate omissions:
- No opus-4-7-fast: 4-7 is deprecated and its fast-mode rate is inconsistent
with the 4-8 fast base, so it is left out rather than guessed.
- No composer-2.5 standard entry: the non-fast 2.5 rate is unconfirmed.
- No gpt-5.6 priority entries: the priority tier for 5.6 is unpublished.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSS42VM7hpJrPkVQCxCsWb
Claude Code transcripts tag fast-mode turns with usage.speed == "fast" (vs
"standard" or ""). Those turns bill at a premium the standard model rate does
not capture, so per-model attribution must route them to the model's "-fast"
pricing variant (added in the prior commit).
- messageUsage gains a Speed field (json:"speed").
- CalculateModelUsage buckets each kept row under modelKeyWithSpeed(model,
speed): a "fast" turn on claude-opus-4-8 lands in a "claude-opus-4-8-fast"
bucket, everything else stays on the base id. Speed rides the SAME
dedup-kept row as the model (the max-output streaming row), so attribution
stays consistent with the flat token loop and buckets still sum to the flat
total. The flat CalculateTokenUsage path is untouched.
modelKeyWithSpeed is a no-op for standard/empty speed, an empty model, or an id
that already ends in "-fast", so buckets never double-suffix.
Tests:
- claudecode (no pricing import): a new fixture stream_session_fast.jsonl
with a standard + fast turn on claude-opus-4-8 (the fast turn a streaming
duplicate pair proving speed rides the kept max-output row) and a fast turn
on a second model. Asserts the mixed model splits into "claude-opus-4-8"
and "claude-opus-4-8-fast" buckets, the token split, and buckets == flat.
The existing no-speed fixtures/tests prove absent speed keeps the base id
(back-compat). modelKeyWithSpeed has a direct table test.
- agent package (the priced half, respecting the claudecode-must-not-import-
pricing rule): a "claude-opus-4-8-fast" bucket prices at 10/50 ($60 for
1M+1M) while "claude-opus-4-8" prices at 5/25 ($30) via the real embedded
table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSS42VM7hpJrPkVQCxCsWb
Codex can run on OpenAI's priority service tier, which bills at a premium the
standard model rate does not capture. Add an opt-in knob that prices those
turns under the model's "-priority" variant (added earlier).
- PricingSettings gains CodexServiceTier (json:"codex_service_tier"),
accepted by the strict unknown-field-rejecting decoder and applied by
mergePricing. A nil-safe EntireSettings.CodexServiceTier() accessor
defaults to "".
- handleLifecycleTurnEnd computes the pricing model via pricingModelForTier:
for a Codex turn with CodexServiceTier == "priority" (EqualFold) and a
non-empty model, it prices under model+"-priority"; every other agent/tier
is unchanged. The resolved model feeds BOTH the PriceUsage (hook-provided
counts) and CalculateUsageWithCost (transcript) paths. Codex is
transcript-based, so in practice the fallbackModel of CalculateUsageWithCost
is what carries the tier. Settings are loaded only for Codex, so other
agents' turn-end skips the extra read. An empty model or an already
"-priority" id is returned as-is (no double-suffix).
Tests:
- settings: JSON omitempty round-trip, the nil/empty-safe accessor default
"", and a Load() merge of pricing.codex_service_tier through the strict
decoder.
- cli (respecting import boundaries: cli imports agent + pricing): a table
test of pricingModelForTier (codex/priority, case-insensitivity, non-codex
ignored, empty model, already-priority) and an end-to-end priced assertion
that "priority" prices gpt-5.5 under gpt-5.5-priority (12.5/75 -> $87.5 for
1M+1M) while no knob prices under gpt-5.5 (5/30 -> $35) via the real table.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSS42VM7hpJrPkVQCxCsWb
Add an opt-in remote pricing layer that reads a locally cached table and merges it beneath user overrides and above the embedded defaults. This is the read/gating half; a later change adds the background refresh that populates the cache. pricing/remote.go: RemoteCache wraps the shared fileSchema (so validateRate and the schema_version==1 contract are reused, not duplicated) with fetch bookkeeping. The cache lives beside the discovery caches under userdirs.Cache(). LoadRemoteEntries is read-only, does no network I/O, and never errors: a missing/corrupt cache, absent Doc, unsupported schema_version, or individually invalid entries all degrade to fewer (or zero) merged entries. settings: pricing.remote opt-in (nil/false default, nil-safe IsRemoteEnabled accessor + ctx-level package func). LoadPricingTable, when enabled, layers remote entries first then user pricing.models so a user override stays authoritative; embedded LoadTable signature is unchanged. Debug-logs the merged entry count, fetched_at, and staleness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSS42VM7hpJrPkVQCxCsWb
Populate the remote pricing cache with a background daily refresh, so the opt-in read path added earlier has something to merge. pricing/remote.go: RefreshRemoteCache does a versioncheck-style conditional fetch (2s timeout, 1 MiB cap, If-None-Match, entire-cli UA) under a cross-process flock for herd control — a burst of spawned workers collapses to one request via an under-lock FetchedAt re-check. Any failure (network, timeout, non-200, oversized/garbage, or a doc failing the schema/sanity gate) keeps the last good Doc and only bumps FetchedAt, so a broken endpoint is retried at most once per 24h and never corrupts the served table. Writes are atomic (temp + rename). ShouldRefresh gates the 24h backoff; RefreshResult + RefreshRemoteCacheForce drive the manual command. telemetry: generalize the detached spawner into SpawnDetached(exe, args...) (behavior identical: Setpgid/detached, Dir "/", discarded stdio, Start+Release), reused by analytics and the pricing refresh. Triggers (never inline, never blocking): the hidden __refresh_pricing worker runs the fetch; turn-end and root PersistentPostRun spawn it detached only when remote is enabled and the cache is stale. The worker is Hidden, so the post-run parent-chain guard skips it — no fork bomb, no telemetry/version check. spawnDetachedRefresh is a package var so tests prove the triggers only spawn, never fetch inline. Manual surface: `entire tokens pricing-refresh` force-refreshes and reports source, outcome, entry count, fetched_at, staleness, and whether merging is enabled; documented in labs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSS42VM7hpJrPkVQCxCsWb
The Codex priority-tier knob (pricing.codex_service_tier="priority") was applied at turn-end but discarded at condensation: the committed per-model buckets were re-derived from the raw state.ModelName with no "-priority" suffix, so the persisted checkpoint carried standard rates. A live gpt-5.5 Codex session with the knob set persisted $0.154513 (standard $5/$30) instead of the expected $0.3862825 (priority $12.50/$75, = 2.5x standard). Move the tier-suffix logic into a shared seam both call sites use: settings.PricingModelForAgent (a method plus a ctx-loading package func). It appends "-priority" only for a Codex session on the priority tier, never double-suffixes, and leaves the stored model name raw — the suffix is a pricing-lookup concern applied at the point of pricing. lifecycle.go delegates to it (deleting the duplicate pricingModelForTier); condensation applies it in the tokenUsageWithCost choke point every extraction/repricing path funnels through, so turn-end and condensation now price the same tier.
Real Cursor CLI sessions report the model id "composer-2.5" (the current default), captured live with real token counts, but cursor.json only carried composer-2, composer-2-fast, and composer-2.5-fast, so the id resolved to no rate and cost stayed nil. Add a composer-2.5 entry mirroring composer-2's structure and standard $0.50/$2.50 per-M rate (published rate, same as composer-2; fast tier $3/$15 already present), with cache rates and aliases following the family convention. Exact-match lookup keeps composer-2.5 and composer-2.5-fast distinct with no glob cross-match.
Cursor CLI and Gemini CLI report gemini-3.1-pro and gemini-3.5-flash today, but google.json carried only the 3.0 generation (gemini-3-pro, gemini-3-flash), so those ids resolved to no rate and cost stayed nil. Add both entries mirroring the existing structure, cache convention, and alias style: gemini-3.1-pro at $2/$12 per M (cache read $0.20) and gemini-3.5-flash at $1.50/$9 per M (cache read $0.15). Exact-match lookup keeps the dotted 3.1/3.5 ids from cross-matching the 3.0 "gemini-3-pro-*"/"gemini-3-flash-*" globs, and vice versa.
telemetry.SpawnDetached hardcoded cmd.Dir="/", so the detached
__refresh_pricing worker ran with cwd "/" and its own IsRemoteEnabled check
loaded settings relative to "/". A project-level .entire/settings.json enabling
pricing.remote was therefore invisible and the auto-refresh silently no-oped
forever (manual `entire tokens pricing-refresh` worked because it runs in the
project).
Generalize SpawnDetached to take a leading working-directory argument: analytics
passes "" to keep the platform default ("/" on Unix, temp dir on Windows), while
the pricing refresh passes the process working directory so the worker resolves
project settings via the normal cwd-based loader. A dir that no longer exists
falls back to the platform default (resolveDetachedDir) so a deleted project
directory never fails the spawn. The Windows and no-op build-tagged variants
take the same signature.
A tier-suffixed fallback model (e.g. gpt-5.5-priority from the Codex service-tier knob) only reached the no-calculator fallback bucket. An agent with a ModelUsageCalculator emits buckets under raw transcript ids, which would price at standard rates and silently re-introduce the tier-loss defect on that path. applyTierVariant retargets buckets whose id equals the fallback's base onto the variant id before pricing; other models stay raw since the table has no variant entry for them. No agent on this branch implements a calculator for Codex, so behavior here is unchanged; the seam is exercised by the tier fixture now carrying the turn_context line every real rollout has before its token_counts.
The service-tier knob suffixes whatever model the agent reports, but only gpt-5.5 ships a -priority rate. A gpt-5.3-codex session with the knob set priced under the nonexistent gpt-5.3-codex-priority id: entirely unpriced (nil) instead of the standard-rate estimate, under a bucket id claiming a premium that was never applied. resolveTierFallback keeps a suffixed fallback only when the table prices it, otherwise reverting to the base id — an honest undercount, mirroring the fast-rate rule.
claudecode's modelKeyWithSpeed appends "-fast" to any model reporting usage.speed=="fast", but only claude-opus-4-8-fast has a published fast rate. A fast turn on any other model (e.g. claude-fable-5) buckets as claude-fable-5-fast, which priceBuckets can't resolve -> CostUSD nil: worse than the base-rate estimate, under an id claiming a premium never applied. This is the same failure class resolveTierFallback fixed for the Codex -priority tier, but it bites ModelUsageCalculator buckets, which the fallback-model-only resolveTierFallback never touches. Add a downgradeUnpriceableVariants pass in CalculateUsageWithCost (after the remainder, before pricing) that reverts any bucket whose id carries a known variant suffix (-fast, -priority) to its base id when the table can't price the variant but can price the base; a nil table downgrades too, and a variant whose base is also unpriceable keeps its truthful id. Detection stays truthful at the source; priceability is only known here at pricing time. Rekeying is in place, not merging: same-model buckets are already emitted today (calculator bucket + remainder) and folded by model key downstream and in foldBucketCost, so leaving the duplicate conserves tokens and cost. Priceable variants (claude-opus-4-8-fast) are unchanged.
# Conflicts: # cmd/entire/cli/agent/types/token_usage_test.go
A shortfall that was only an APICallCount delta (all four token fields fully covered by per-model buckets) still emitted a spurious zero-token remainder bucket. Also defensively normalize a costed-but-unlabeled bucket (non-nil CostUSD with empty CostSource) to CostSourceEstimated in foldBucketCost, so a latent mislabel can never render with no reported/estimated/mixed suffix.
…covery backfillModelAndReprice's identity check (state.TokenUsage == sessionData.TokenUsage) never fires for the Copilot CLI full-session backfill, since sessionStateBackfillTokenUsage assigns state.TokenUsage a distinct object from sessionData.TokenUsage's checkpoint-scoped slice. That left the session-state diagnostic total permanently unpriced once the model was later recovered, even though the persisted checkpoint was priced correctly. Reprice it too, guarded so a zero-data recovery window can't clobber a good value.
doc.go was stale: it said pricing was embedded-plus-settings-overrides only with no runtime fetch. Describe the opt-in remote-pricing refresh (remote.go, gated by pricing.remote) and clarify there is no inline fetch on the hook/condensation path — the refresh is a detached background worker.
The CLI no longer treats persisted checkpoint cost as authoritative. The
token commands (entire session tokens, entire checkpoint tokens incl.
--compare, entire tokens profile) now recompute cost on the fly from the
persisted token breakdown + per-model buckets via the pricing package and
label it as a local estimate ("estimated locally"), with a note that the
authoritative cost is computed on the Entire platform.
- agent.EstimateCost prices the persisted per-model buckets (or the flat
usage, subagents flattened in, under a fallback model) at current rates,
never trusting any pre-existing cost. Unpriceable input yields no cost
(never $0).
- buildSessionTokensUsage no longer copies cost from usage; callers apply the
local estimate via applyLocalCostEstimate with the loaded pricing table.
- costSourceSuffix labels estimated cost as (estimated locally) and partial
coverage as (estimated locally, partial).
The platform (entire-api) computes cost server-side from the token breakdown, so the CLI must no longer store cost in checkpoint metadata. Every checkpoint-metadata write now goes through Metadata.WithoutCost / CheckpointSummary.WithoutCost, which clear cost_usd/cost_source at every level (flat, nested subagent, and per-model bucket) while preserving the four token fields, APICallCount, subagent token counts, and per-model model ids the platform prices from. This covers the fresh session/summary writes plus the attribution/summary/transcript backfill and session-metadata update paths in the git store, and the test-only fsstore backend. The compute/reprice/accumulate pipeline is intentionally left intact: it still computes cost in-memory (driving condensation's model-recovery rebucketing and local session-state diagnostics), so model_usage stays byte-identical to before. Only what gets written to disk drops cost. The cost struct fields remain (omitempty) so older/committed checkpoints still deserialize; they are simply never populated on write.
suhaanthayyil
marked this pull request as ready for review
July 15, 2026 13:52
Contributor
There was a problem hiding this comment.
Pull request overview
This PR changes the CLI’s cost ownership model: checkpoint/session metadata no longer persists cost_usd/cost_source, while token-related commands still show a local, on-the-fly estimate computed from persisted token facts (flat + per-model buckets) and a pricing table (embedded + optional remote cache).
Changes:
- Add
WithoutCost()choke points to ensure no checkpoint backend persists cost, while preserving token facts (including newmodel_usage). - Update token reporting (
session tokens,checkpoint tokens,tokens profile) to recompute and label cost as a local estimate, plus addtokens pricing-refreshand a detached daily refresh worker. - Thread and persist per-model token usage (
model_usage), including Claude Code per-message attribution (with-fastbucketing) and Codex priority-tier pricing seams.
Reviewed changes
Copilot reviewed 72 out of 72 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| e2e/testutil/metadata.go | Extends E2E metadata shapes with optional cost fields and model_usage. |
| cmd/entire/cli/tokens_profile.go | Recomputes aggregate cost locally for tokens profile; adds pricing-refresh subcommand. |
| cmd/entire/cli/tokens_profile_test.go | Adds coverage for locally-estimated total cost display + JSON behavior. |
| cmd/entire/cli/tokens_pricing_refresh.go | Implements entire tokens pricing-refresh (force refresh remote cache). |
| cmd/entire/cli/telemetry/detached.go | Adds shared detached spawn helpers and cwd resolution fallback. |
| cmd/entire/cli/telemetry/detached_windows.go | Refactors Windows detached spawning into SpawnDetached with dir handling. |
| cmd/entire/cli/telemetry/detached_unix.go | Refactors Unix detached spawning into SpawnDetached with dir handling. |
| cmd/entire/cli/telemetry/detached_other.go | Defines no-op SpawnDetached on unsupported platforms. |
| cmd/entire/cli/telemetry/detached_dir_test.go | Tests detached working-directory fallback behavior. |
| cmd/entire/cli/strategy/strategy.go | Adds StepContext.ModelUsage so strategies can accumulate per-model usage. |
| cmd/entire/cli/strategy/manual_commit_types.go | Carries extracted per-model usage through condensation pipeline. |
| cmd/entire/cli/strategy/manual_commit_hooks.go | Resets checkpoint-scoped model usage on condensation boundary. |
| cmd/entire/cli/strategy/manual_commit_git.go | Accumulates per-model usage; fixes token-usage deep-copying; adds deterministic serialization helpers. |
| cmd/entire/cli/strategy/condensation_codex_tier_test.go | Regression tests for Codex tier (priority) pricing during condensation. |
| cmd/entire/cli/strategy/accumulate_token_cost_test.go | Tests cost/cost-source folding and subagent-pointer alias prevention. |
| cmd/entire/cli/strategy/accumulate_model_usage_test.go | Tests per-model accumulation, mixed provenance, and deterministic ordering. |
| cmd/entire/cli/settings/settings_test.go | Adds Codex service tier settings tests. |
| cmd/entire/cli/settings/settings_pricing_test.go | Adds pricing override + disable-estimation accessor/load tests. |
| cmd/entire/cli/settings/settings_pricing_remote_test.go | Tests embedded vs remote vs user-override precedence and remote-enabled accessor. |
| cmd/entire/cli/settings/pricing_model_test.go | Tests tier-suffix model selection seam (Codex priority). |
| cmd/entire/cli/sessions_test.go | Updates helper callsites for new pricing-table parameter threading. |
| cmd/entire/cli/session/state.go | Persists checkpoint-scoped per-model usage map in session state. |
| cmd/entire/cli/session_tokens.go | Displays locally estimated cost; adds formatting helpers; estimates from model buckets or fallback model. |
| cmd/entire/cli/session_tokens_cost_test.go | Tests formatting matrix + local estimation behavior and omission rules. |
| cmd/entire/cli/session_adopt.go | Resets model-usage state when adopting sessions. |
| cmd/entire/cli/root.go | Spawns detached daily pricing refresh after real (non-hidden) commands. |
| cmd/entire/cli/pricing/remote_test.go | Tests reading/validating cached remote pricing entries. |
| cmd/entire/cli/pricing/remote_refresh_test.go | Tests refresh outcomes (200/304/404/timeout/throttle) and cache persistence. |
| cmd/entire/cli/pricing/models/openai.json | Adds/updates OpenAI model rate entries (incl. priority variant). |
| cmd/entire/cli/pricing/models/google.json | Adds Gemini 3.1 Pro / 3.5 Flash entries and aliases. |
| cmd/entire/cli/pricing/models/cursor.json | Adds Cursor composer-2.5 rate entries and aliases. |
| cmd/entire/cli/pricing/google_gemini_test.go | Validates Gemini dotted IDs + alias/glob boundary behavior. |
| cmd/entire/cli/pricing/embed.go | Embeds pricing model JSON tables via go:embed. |
| cmd/entire/cli/pricing/doc.go | Documents pricing table behavior and known approximation limits. |
| cmd/entire/cli/pricing/cursor_composer25_test.go | Validates composer-2.5 resolution and non-cross-matching. |
| cmd/entire/cli/pricing_refresh.go | Adds detached __refresh_pricing worker + spawn trigger logic. |
| cmd/entire/cli/pricing_refresh_test.go | Ensures worker spawns with project cwd and never fetches inline. |
| cmd/entire/cli/lifecycle.go | Prices turn-end usage locally; captures ModelUsage into StepContext. |
| cmd/entire/cli/lifecycle_pricing_test.go | Tests Codex priority tier affects priced outcomes through the seam. |
| cmd/entire/cli/labs.go | Advertises tokens pricing-refresh in experimental/labs list. |
| cmd/entire/cli/config.go | Adds cli-level LoadPricingTable wrapper around settings pricing loader. |
| cmd/entire/cli/checkpoint/persistent.go | Strips cost on all marshal paths; aggregates/persists root model_usage. |
| cmd/entire/cli/checkpoint/merge_model_usage_test.go | Tests root-level per-model merge semantics + pointer non-aliasing. |
| cmd/entire/cli/checkpoint/fsstore/fsstore.go | Ensures FS backend also strips cost on write. |
| cmd/entire/cli/checkpoint/cost_not_persisted_test.go | Asserts the “cost not persisted anywhere” invariant end-to-end. |
| cmd/entire/cli/checkpoint/aggregate_token_cost_test.go | Tests cost/cost-source folding in checkpoint aggregation utilities. |
| cmd/entire/cli/checkpoint_tokens.go | Recomputes local cost; adds cost delta comparison + caveats. |
| cmd/entire/cli/agent/types/token_usage.go | Adds cost fields, per-model usage types, and provenance merge helpers. |
| cmd/entire/cli/agent/types/token_usage_test.go | Adds extensive tests for cost helpers and JSON encoding behavior. |
| cmd/entire/cli/agent/types.go | Re-exports ModelUsage for agent package consumers. |
| cmd/entire/cli/agent/token_usage_variant_downgrade_test.go | Tests downgrading unpriceable -fast/-priority variants for estimation. |
| cmd/entire/cli/agent/token_usage_tier_test.go | Tests tier retargeting and fallback downgrade behavior. |
| cmd/entire/cli/agent/estimate_cost_test.go | Tests display-side EstimateCost semantics and partial coverage. |
| cmd/entire/cli/agent/claudecode/types.go | Adds per-message model and usage.speed parsing support. |
| cmd/entire/cli/agent/claudecode/transcript.go | Implements per-model usage attribution (incl. -fast keying) for Claude Code. |
| cmd/entire/cli/agent/claudecode/testdata/stream_session_fast.jsonl | Adds fixture transcript including fast/standard turns and multiple models. |
| cmd/entire/cli/agent/claudecode/lifecycle.go | Declares Claude Code implements ModelUsageCalculator. |
| cmd/entire/cli/agent/capabilities.go | Adds ModelUsageCalculator to declared capabilities + helper accessor. |
| cmd/entire/cli/agent/agent.go | Defines ModelUsageCalculator interface for per-model attribution. |
| api/checkpoint/metadata.go | Adds ModelUsage to metadata/summary + WithoutCost() deep-strip helpers. |
| api/checkpoint/metadata_test.go | Tests WithoutCost, model_usage wire shape, and legacy decoding behavior. |
Comment on lines
+118
to
122
| // usageHasBillableTokens reports whether u carries any nonzero billable token | ||
| // count (input, cache-creation, cache-read, or output). It is nil-safe. | ||
| func usageHasBillableTokens(u *TokenUsage) bool { | ||
| return u != nil && (u.InputTokens != 0 || u.CacheCreationTokens != 0 || u.CacheReadTokens != 0 || u.OutputTokens != 0) | ||
| } |
Reconcile the cost/tokens-only work with main's subagent-token accounting overhaul (SubagentTokensBaseline) and the execx.SpawnDetached refactor: - session.State keeps both new fields: ModelUsage (checkpoint-scoped per-model breakdown) and SubagentTokensBaseline (main). session adopt resets both. - Centralize the checkpoint-window reset in resetCheckpointWindow (main) and fold ModelUsage=nil into it, so all three condensation reset sites clear the per-model mirror alongside CheckpointTokenUsage and re-baseline subagents. - accumulateTokenUsage: keep cost folding (this branch) on top of main's "latest snapshot wins" subagent replace, and deep-copy the replaced subtree so the two aggregates folding a step never share it. Update the branch's cost tests to the replace semantics. - Condensation backfill: adopt main's applyBackfilledSessionTokenUsage (preserves the cumulative subagent total for the baseline) and thread state.ModelName into sessionStateBackfillTokenUsage so Copilot full-session pricing still works; keep this branch's model recovery/reprice and per-model fallback. - Port the detached pricing refresh onto execx.SpawnDetached(dir, args...), dropping the telemetry-local SpawnDetached platform files main removed.
…subagent tokens Address three trail-review findings on the cost/tokens-only display path: - session tokens: estimate cost from the session-wide flat TokenUsage (the same scope as the displayed token total) instead of the checkpoint-scoped ModelUsage, which is reset at each condensation. Pricing the per-model map while showing the session-wide total silently understated cost after any condensation. Session state carries no session-cumulative per-model breakdown, so the flat total is the only scope-consistent basis; drop the now-unused modelUsageSliceFromMap. - pricing overrides: validate .entire/settings.json pricing.models per-entry and drop only the invalid ones (new pricing.ValidOverrides, mirroring LoadRemoteEntries) instead of letting one bad entry hard-error LoadTable and disable cost estimation for the whole table. - cost source: usageHasBillableTokens now recurses into SubagentTokens, so a step whose tokens live only in a subagent subtree triggers MergeCostSourceUsages's mixed-coverage rule (cost source "mixed", not "estimated"). Adds a mutation-verified test for each.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Trail: https://entire.io/gh/entireio/cli/trails/865
What
The CLI stops persisting cost into checkpoint metadata. It keeps persisting the token facts the platform prices from (
model_usagewith all four token fields per model + model id, flat token counts, model, api-call counts). Cost is now shown only as a clearly-labeled local estimate in the token commands.Why
Per team discussion (Dipree): don't store cost in the CLI — the platform can pull fresh pricing far faster (no CLI release to fix a rate or add a model). The CLI's job narrows to capturing the facts only it can see; the platform (entire-api) owns pricing and the authoritative cost.
How
Metadata.WithoutCost()/CheckpointSummary.WithoutCost()— applied at all six checkpoint-metadata marshal sites (branch + refs backends). It clearscost_usd/cost_sourceat every level (flat, subagent subtree, each per-model bucket) while preserving the four token fields, model ids, and counts.CostUSD == nil, so removing it would change whenmodel_usageis rebucketed. Only what's written to disk drops cost.entire session tokens,entire checkpoint tokens [--compare],entire tokens profile) recompute a local estimate on the fly viaagent.EstimateCostfrom the persisted tokens +model_usage, labeled$X (estimated locally)with a note that the authoritative cost lives on the platform. Unknown model → no cost line (never$0). Thepricing/package is retained for this local display.Supersedes
Supersedes #1702 (which computed and stored cost in the CLI). This branch is #1702 plus the two reduction commits on top.
Testing
gofmt/mise run lintclean;go test ./cmd/entire/cli/... ./api/checkpoint/...green.TestWriteCommitted_DoesNotPersistCost: a checkpoint with cost + subagents + per-model buckets round-trips with token counts / model ids / subagent counts intact and cost nil/empty at every level.model_usageis bucketed under the recovered model with token counts, but no cost.Note for review
Backfill/summary rewrites now also strip cost from any legacy checkpoint they rewrite (cost written by an older CLI is cleared on the next attribution/summary backfill). Consistent with "the CLI does not persist cost" — flagging to confirm it's intended.
Follow-up
If the team decides the CLI should drop cost display entirely too, the
pricing/package + the cost fields/machinery could be removed (larger blast radius — condensation's model-recovery currently rides on the cost path; would need re-expressing as a pure model-signal). Not done here.