Skip to content

refactor(cost): CLI stops storing cost; platform owns it (supersedes #1702)#1763

Open
entire[bot] wants to merge 35 commits into
mainfrom
feat/cli-cost-tokens-only
Open

refactor(cost): CLI stops storing cost; platform owns it (supersedes #1702)#1763
entire[bot] wants to merge 35 commits into
mainfrom
feat/cli-cost-tokens-only

Conversation

@entire

@entire entire Bot commented Jul 15, 2026

Copy link
Copy Markdown

Trail: https://entire.io/gh/entireio/cli/trails/865

Draft — direction change pending team sign-off. Reverses the round-2 decision that "the CLI is the single source of truth" for cost. Opening as a draft so it can be reviewed alongside #1702 before committing to the reversal. Pairs with entirehq/entire-api (platform computes cost).

What

The CLI stops persisting cost into checkpoint metadata. It keeps persisting the token facts the platform prices from (model_usage with 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

  • A single choke point — Metadata.WithoutCost() / CheckpointSummary.WithoutCost() — applied at all six checkpoint-metadata marshal sites (branch + refs backends). It clears cost_usd/cost_source at every level (flat, subagent subtree, each per-model bucket) while preserving the four token fields, model ids, and counts.
  • The in-memory compute path is intentionally kept — condensation's model-recovery rebucketing keys on CostUSD == nil, so removing it would change when model_usage is rebucketed. Only what's written to disk drops cost.
  • Token commands (entire session tokens, entire checkpoint tokens [--compare], entire tokens profile) recompute a local estimate on the fly via agent.EstimateCost from 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). The pricing/ 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 lint clean; 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.
  • Updated condensation test proves persisted model_usage is bucketed under the recovered model with token counts, but no cost.
  • Display tests: token commands still render a cost line, computed live, labeled as a local estimate.

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.

suhaanthayyil and others added 30 commits July 9, 2026 16:49
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 suhaanthayyil changed the title refactor(cost): CLI stops storing cost; platform owns it refactor(cost): CLI stops storing cost; platform owns it (supersedes #1702) Jul 15, 2026
@suhaanthayyil
suhaanthayyil marked this pull request as ready for review July 15, 2026 13:52
@suhaanthayyil
suhaanthayyil requested a review from a team as a code owner July 15, 2026 13:52
Copilot AI review requested due to automatic review settings July 15, 2026 13:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 new model_usage).
  • Update token reporting (session tokens, checkpoint tokens, tokens profile) to recompute and label cost as a local estimate, plus add tokens pricing-refresh and a detached daily refresh worker.
  • Thread and persist per-model token usage (model_usage), including Claude Code per-message attribution (with -fast bucketing) 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants