Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Phase 1: Token cost data correctness (FEA-1431/1432/1433) - #253

Closed
aeyeCEO wants to merge 11 commits into
mainfrom
FEA-1431-litellm-pricing
Closed

aeyeCEO wants to merge 11 commits into
mainfrom
FEA-1431-litellm-pricing

Conversation

@aeyeCEO

@aeyeCEO aeyeCEO commented May 29, 2026

Copy link
Copy Markdown
Contributor

Parent PRD: PRD-414 — Accurate token cost calculation & reconciliation across multi-vendor agents.

Summary

Phase 1 of the multi-vendor token cost work. Three stacked features that make the per-session cost numbers we already compute (a) match Anthropic/OpenAI list prices, (b) account for vendor-specific cache semantics, and (c) actually surface in the Sessions UI instead of being computed and thrown away.

  • FEA-1431 — Adopt LiteLLM's model_prices_and_context_window.json as the canonical pricing source. Vendor at build time, refresh via just desktop-refresh-pricing. Fixes ~3× under-pricing on Opus 4.5/4.6/4.7 inherited from the prior hand-curated seed; the Anthropic upstream rate in LiteLLM is also wrong here so HOST_ONLY_OVERRIDES pin them at $15/$75 with a documented deviation. Adds build-time invariants (Opus floor, OpenAI cache discount, Anthropic 1h floor) that throw if upstream regresses.
  • FEA-1432 — Vendor-specific cache pricing math. Adds cache_write_1h_per_mtok column (Anthropic 1h tier = input × 2.0); zeros cache_write for all OpenAI rows including gpt-codex% fallback (OpenAI publishes no cache-write surcharge); clamps OpenAI cache_read to input × 0.5 only when LiteLLM reports a non-zero rate (preserves o1-pro no-caching semantics). Bumps AGENT_SESSION_SYNC_SCHEMA_VERSION 1→2.
  • FEA-1433 — Renders a Cost column in the Sessions list with a em-dash + tooltip for unpriced models. Adds a Settings → Pricing surface listing models seen recently with no rule, with an inline "Add pricing rule" form. Surfaces partial-cost sessions with an amber asterisk + tooltip (Claude review fix). Sidecar exposes a cost_breakdown block on session detail for FEA-1446 to wire the per-token-category UI.

Review chain:

  • FEA-1431: codex review caught 2 P2 in scope (legacy 4.2 aliases, existing-DB Opus migration); both fixed before this stack.
  • FEA-1432: Claude review caught 2 (CLAUDE.md stale + o1-/o3- invariant gap); both fixed.
  • FEA-1433: Claude review caught 4 (mixed-session display + comment accuracy + negative limit + missing FEA-1446 ref); all fixed.

Schema-version coordination

FEA-1432 bumps AGENT_SESSION_SYNC_SCHEMA_VERSION from 1 → 2. The sibling FEA-1434 PR (#PR-2) also bumps 1 → 2. Whichever PR merges first wins; the second needs to rebase to 2 → 3 before merging.

Follow-up tickets filed during the work

  • FEA-1440 — Recover Anthropic 5min/1h cache-write tier split from request correlation (referenced in code comments at token-usage.ts and agent-session-sync-service.ts)
  • FEA-1446 — Session detail per-token-category cost breakdown UI (data already on the wire from this PR)

Test plan

  • CI pipeline green (test + typecheck + lint + build:agent-monitor)
  • Smoke: `just desktop-dev` and verify Cost column populates for a recent Claude session
  • Smoke: launch with an unknown model in token_usage; verify "—" + tooltip appears
  • Smoke: Settings → Pricing lists the unknown model; add-rule form persists
  • DB upgrade: open a profile last touched on 0.15.93; verify Opus 4.5/4.6/4.7 rows are migrated from $5/$25 to $15/$75 on first boot

🤖 Generated with Claude Code

Andrew Eye and others added 7 commits May 28, 2026 20:13
- Vendor model_prices_and_context_window.json + meta SHA at build time
- Fix Opus 4.5/4.6/4.7 ~3x under-pricing inherited from prior seed
- Add Opus floor + OpenAI cache-discount invariants at build time
- Add `just desktop-refresh-pricing` recipe for weekly upstream sync
- Preserve host-only overrides for fallback rows (big-pickle, *-default)
  and 3.5 Sonnet/3.5 Haiku/3.7 Sonnet/3 Opus/3 Haiku aliases LiteLLM omits
- Document inline why Opus 4.x rows are pinned over LiteLLM (deviation)
- Bump apps/desktop version 0.15.93 -> 0.15.94

Testing:
- new fetch + invariant tests (17 cases): pass
- existing agent-session-sync-service tests (26 cases): pass
- full apps/desktop suite: 1938 tests pass
- `pnpm build:agent-monitor --force` regenerates db.js with corrected
  Opus rows (15/75 input/output) and new fallback rows
- `pnpm typecheck && pnpm lint`: clean

Risks:
- Vendored JSON growth (~50KB) — filtered to 5 vendor prefixes; sentinel
  + 20-row-floor checks block accidental near-empty refreshes
- LiteLLM upstream regression caught by build-time invariants; refresh
  recipe re-runs the loader so a bad fetch never silently lands
- HOST_ONLY_OVERRIDES intentionally diverge from LiteLLM for Opus 4.5-4.7
  until upstream is corrected; comment includes follow-up trigger

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review of 5b8546d surfaced two P2 findings in scope for FEA-1431
(both about correctness of the pricing-source switchover for existing
deployments):

- HOST_ONLY_OVERRIDES now carries `claude-opus-4-2%` and `claude-sonnet-4-2%`.
  These shipped as host-curated defaults in 0.15.93 but are absent from
  LiteLLM's catalog; without overrides, sessions tagged with the legacy
  4.2 model ids would fall through to the OpenCode `$0` fallback on a
  fresh DB.

- One-shot legacy-row migration patched into the generated db.js. The
  startup top-up uses INSERT OR IGNORE, so existing users on 0.15.93
  with mis-priced claude-opus-4-5/6/7 rows ($5/$25/0.5/6.25 — Sonnet
  rates) would keep the wrong price after upgrade. The new UPDATE
  rewrites only rows still matching the exact legacy bad tuple so
  user-edited rows are preserved.

Third codex finding (GPT cache_write rate inconsistency between
concrete `gpt-5-codex` and fallback `gpt-codex%`) intentionally
deferred to FEA-1432, which owns OpenAI cache math. Codex's preferred
fix (keep input-rate cache_write) is wrong: OpenAI publishes no
cache-write surcharge — 0 is the correct value. FEA-1432 will zero the
fallback to match.

Testing:
- 1834 + 104 = 1938 tests passing
- typecheck + lint clean
- pnpm build:agent-monitor regenerates db.js with both 4.2 aliases
  and the legacy Opus migration block

Risks:
- The legacy migration runs on every startup; it is a no-op once rows
  are corrected (or once a user has deliberately edited them away from
  the exact bad tuple). No perf concern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add `cache_write_1h_per_mtok` 5th rate column to `model_pricing` so
  Anthropic's 5-minute and 1-hour ephemeral cache write tiers are
  priced separately. CREATE TABLE picks up the column for fresh DBs;
  an additive ALTER TABLE migration backfills existing DBs to input ×
  2.0 for every claude-* row currently at 0 (so upgraders are not
  stuck at $0 for 1h writes once parsers learn the split).
- LiteLLM transformer (`fetch-litellm-pricing.mjs`) derives the new
  column: Anthropic = input × 2.0 (per Anthropic's rate card); OpenAI
  and Gemini = 0 (no analogous tier). For OpenAI rows the transformer
  also (a) clamps cache_read to input × 0.5 when LiteLLM's positive
  ratio falls outside [40%, 60%] (GPT-5 series ships at 10% upstream)
  and (b) zeroes cache_write — OpenAI has no cache-write surcharge.
  Zero cache_read values are preserved (they signal "no caching").
- Promote the FEA-1431 soft-warn OpenAI invariant to a hard build-time
  throw: gpt-* rows must have cache_read ≤ 55% of input AND
  cache_write = cache_write_1h = 0. Add Anthropic invariant: every
  priced claude-* row must have cache_write_1h ≥ input × 1.5.
- HOST_ONLY_OVERRIDES regrown to 7-tuples. The gpt-codex% fallback
  pre-FEA-1432 carried cache_read=0.125 (10%) and cache_write=1.25 (a
  surcharge OpenAI does not bill). Now: input=1.25, output=10,
  cache_read=0.625 (50%), cache_write=0, cache_write_1h=0. Cursor,
  Copilot, and Anthropic fallbacks gain a 0 or input × 2.0 7th column.
- Bump AGENT_SESSION_SYNC_SCHEMA_VERSION 1 → 2 with
  `cacheWrite1hTokens?: number | null` on SyncedAgentSessionTokenUsage.
  Cost compute in `estimateTokenUsageCostUsd` now sums all five
  components. v1 receivers tolerate the new field; legacy-shape
  removal tracked by FEA-1439.
- Document the v1 parser limitation in token-usage.ts: Anthropic's
  response usage block does not split the cache-write tier (it is set
  by the request's cache_control.ttl), so v1 attributes every
  cache_creation_input_tokens to the 5-min bucket and synthesizes
  cache_write_1h_tokens = 0 downstream. Recovery of the per-tier
  split is tracked by FEA-1438.

Testing:
- pnpm -C apps/desktop test: 1949 pass / 0 fail (1845 tsx + 104 node)
- pnpm -C apps/desktop typecheck: clean
- pnpm -C apps/desktop lint: clean
- pnpm -C apps/desktop build:agent-monitor: passes SQLite gate
- New: test/openai-cache-math.test.ts (5 cases) — gpt-codex fallback
  shape, OpenAI cache_write contributes $0, Anthropic 1h billed at
  input × 2.0, 5-component cost sum, v1 backward compat.
- Extended: test/fetch-litellm-pricing.test.ts adds Anthropic 1h
  derivation and OpenAI clamp/zeroing cases.
- Extended: test/build-agent-monitor-pricing-invariants.test.ts pins
  the three hard FEA-1432 invariants on the merged steady state.

Risks:
- Upgrade DB migration backfills claude-* cache_write_1h via UPDATE
  conditional on the column currently being 0. Rows users manually
  set to 0 will be rewritten to input × 2.0; the conditional is the
  conservative trade-off (vs leaving every upgrader stuck at $0 for
  1h cache writes once FEA-1438 lands per-tier parsing).
- OpenAI cache_read clamping inflates effective billed cost for GPT-5
  series sessions that currently use cached prompts at the upstream
  10% rate — but that rate disagrees with OpenAI's published 50%
  discount. We treat published rate as truth.
- Schema-version bump (1 → 2) follows the optional-field discipline,
  so a v1 relay receiver continues to accept the payload (the new
  field is ignored). Cloud-side hard removal tracked at FEA-1439.

Deviations from plan:
- Plan assumed FEA-1434 (schema v2) had landed first; it lives on a
  separate branch. We bump 1 → 2 instead of 2 → 3. The FEA-1434 merge
  will need to rebase its own bump to 3.
- Plan step 8 boundary translation refers to a comment FEA-1434
  added; since FEA-1434 is not in this branch, the equivalent
  forward-compat note now lives in this commit's contract file.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FEA-1432's commit body referenced FEA-1438 in code comments as a
placeholder for the follow-up that recovers the 5min/1h cache-write
tier split from request correlation. The follow-up ticket was filed
as FEA-1440. This commit substitutes the correct ticket id in all
four comment sites.

No behavior change. No tests touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ant gap

Claude reviewer flagged two findings on 5e17acf:

Medium: apps/desktop/CLAUDE.md said the OpenAI cache discount check was
"Soft-warn for now (FEA-1432 will tighten to a hard assertion once
downstream cache math is fixed)" — but FEA-1432 IS that commit, and the
invariant already throws. Also said "two invariants" when the build runs
three (Opus floor, OpenAI cache, Anthropic 1h floor). Replaces the section
with current behavior: three hard build-time assertions, explicit per-
invariant bullet list.

Low: the OpenAI invariant guard at build-agent-monitor.mjs:528 was
`if (!pattern.startsWith("gpt-")) continue;` — the fetch transformer
correctly treats o1- and o3- as OpenAI via isOpenAIKey(), but the build-
time invariant would silently pass a future HOST_ONLY_OVERRIDES row for
o1-pro or similar with a non-zero cache_write. Extends the guard to match
isOpenAIKey()'s definition.

Testing:
- 28 pricing-specific tests still pass (build-agent-monitor-pricing-
  invariants + fetch-litellm-pricing + openai-cache-math suites)
- pnpm build:agent-monitor regenerates db.js clean (SQLite gate PASS)
- Pre-existing flaky integration tests (python3 health, shell path,
  symphony-loop wrappers) unrelated to pricing work — not regressions
  from this commit

Risks:
- None. CLAUDE.md is documentation; the invariant guard widening is
  protective and only changes behavior for theoretical future rows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FEA-1431/1432 priced every Anthropic / OpenAI / Gemini model and synced
the per-session cost to the cloud, but the local Sessions UI never
read it back — the column showed "-" for every row. When a model had
no `model_pricing` match, the sidecar silently fabricated $0.00 so
unpriced models (local Ollama, OpenCode-hosted free tiers, custom
vendor IDs) looked identical to genuinely-zero sessions. This commit
surfaces the cost and turns the silent zero into a visible
diagnostic.

Highlights:

- Sidecar `routes/sessions.js` (patched by build-agent-monitor.mjs):
  new `calculateSessionCostFea1433(sessionTokens, rules)` returns
  `{ cost, unpriced_models, priced }`. When every model in a session
  is unpriced, `cost` is `null` (not 0). The price-sort comparator
  now anchors `null` to the end in both directions so diagnostic
  rows stay visible without polluting an ascending sort. The session
  detail handler attaches a `cost_breakdown` block with `priced`
  per row so a future SessionDetail overlay (deferred — upstream
  detail page is 39.7K, too heavy to overlay just for this) can
  show "Estimated cost unavailable" inline.
- New sidecar route `GET /api/pricing/diagnostics/unpriced-models?limit=20`
  returning the most-recently-seen distinct models in `token_usage`
  that don't match any `model_pricing` LIKE pattern. Uses
  `stmts.matchPricing` so a `claude-opus-4-%` rule covers every
  dated variant without listing each.
- Sessions.tsx overlay: `session.cost == null` renders an em-dash
  with a tooltip naming the unpriced model and pointing to
  Settings → Pricing. `cost > 0` renders `fmtCost`; the genuine-zero
  case still renders "-" so priced-but-unused sessions look
  unchanged.
- Renderer Settings → Pricing sub-panel (`apps/desktop/src/renderer/
  index.html`): lists unpriced models with an inline 5-rate form
  (input, output, cache_read, cache_write 5min, cache_write 1h —
  $/Mtok). Submits via the new `cost-pricing:add-rule` IPC.
- Two-file IPC split: `cost-pricing-client.ts` holds the Zod schema
  + HTTP client (no `electron` import → node:test can exercise it
  directly); `cost-pricing-ipc.ts` registers ipcMain handlers.
  fetchUnpricedModels + addPricingRule never throw — sidecar
  unreachable returns null / { ok:false, error } so the renderer
  degrades gracefully.
- Contract update (no schema bump per CLAUDE.md additive-optional
  policy): `SyncedAgentSessionTokenUsage` gains optional
  `priced?: boolean` and widens `estimatedCostUsd` to
  `number | null`. Missing `priced` decodes as `true` for legacy
  v1/v2 payloads. New `estimateTokenUsageCostBreakdown(row, rules)`
  in agent-session-sync-service.ts returns
  `{ priced, costUsd: number | null }`; the legacy
  `estimateTokenUsageCostUsd` is preserved as a thin wrapper so
  existing call sites (and the openai-cache-math regression tests)
  keep working.
- types.ts overlay edits widen `Session.cost` to `number | null`
  and add `unpriced_models?: string[] | null` so the React
  component types match the new sidecar shape under
  `noUncheckedIndexedAccess`.

Testing:
- pnpm -C apps/desktop typecheck: clean
- pnpm -C apps/desktop lint: clean
- pnpm -C apps/desktop build:agent-monitor: SQLite gate PASS,
  patches re-apply on a clean .generated tree
- pnpm -C apps/desktop test: 1856 tests, 1856 pass on the second
  steady-state run (1845 baseline + 11 new). Two pre-existing
  parallel-run flaky tests under test/symphony-launcher.test.ts
  occasionally surface; they exist on FEA-1432 main and are
  unrelated to this change.
- New tests:
  - test/cost-pricing-ipc.test.ts: 7 cases covering Zod rejection
    before network IO, GET proxy + limit query param, PUT proxy
    with defaults applied, sidecar-unreachable degradation, sidecar
    HTTP-error surfacing.
  - test/sidecar-cost-priced-flag.test.ts: 1 case requiring the
    generated `pricing.calculateCost` and pinning the
    `matched_rule: null` contract our session-list wrapper depends
    on. Skips when `.generated/` doesn't exist yet (fresh checkout
    before `pnpm build:agent-monitor`).
  - test/agent-session-sync-service.test.ts: 3 added cases for
    estimateTokenUsageCostBreakdown — priced=false on no match,
    priced=true with correct USD math, and priced=true preserved
    on zero-token rows (must not collapse to null).

Risks:
- `estimatedCostUsd` on the wire is now nullable. Legacy v1/v2
  receivers that treated a missing field as 0 keep working
  unchanged. Cloud-side receivers that strictly enforced
  `number` will accept the optional `null` if their schema treats
  the field as optional (it always was). Hard-typed receivers may
  need a one-line widening — flagged in the contract comment.
- Sidecar `routes/sessions.js` patches anchor on the upstream
  cost-assignment block. Future upstream bump that touches those
  three lines will throw at build time with an actionable error
  rather than silently regressing the diagnostic.
- Settings → Pricing form bypasses no client-side rate validation
  beyond Zod (min 0, finite). A user can type an arbitrarily large
  rate; that is the same trust model as the existing manual
  upstream PUT /api/pricing endpoint.

Deviations from plan:
- Step 3 (Session detail breakdown UI) deferred. The upstream
  SessionDetail.tsx is 39.7K; overlaying it just for a cache_write_1h
  breakdown was disproportionate. The sidecar now emits the
  `cost_breakdown` block on `GET /api/sessions/:id` so a future
  overlay (or a smaller targeted edit) can land it without another
  API change.
- Step 4 unpriced-models data source: planned via IPC bridge or
  new sidecar route — chose the sidecar route per the plan's
  preference ("keep logic close to the data").

Hindsight note on FEA-1432:
- The `openai-cache-math` regression suite still imports
  `estimateTokenUsageCostUsd` directly. We keep that export as a
  thin wrapper around `estimateTokenUsageCostBreakdown`, but a
  follow-up could migrate those tests to the breakdown API and
  drop the legacy wrapper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eeping

Claude reviewer found 2 Medium + 2 Low on commit 66f38df.

Medium #1 — Mixed-session cost was silently understated:
calculateSessionCostFea1433 returns cost=null only when EVERY model is
unpriced; sessions with some priced + some unpriced models returned a
partial dollar total with no diagnostic signal. UI rendered the partial
total via fmtCost(cost) with no asterisk, tooltip, or other indication.
A session mixing claude-opus-4-5 (priced) and a local model (unpriced)
displayed e.g. $0.04 when true cost was $0.13.

Picked Option B from the review (preserve information over hiding it):
Sessions.tsx now distinguishes the mixed case — cost > 0 with non-empty
unpriced_models renders the partial $ + an amber asterisk + a tooltip
explaining the total excludes N unpriced models. The em-dash diagnostic
still fires when every model is unpriced.

Medium #2 — Sessions.tsx comment contradicted implementation:
Said "cost == null → at least one model unpriced", but sidecar only
returns null when ALL models are unpriced. Per CLAUDE.md [mistake] rule
on comment accuracy. Rewrote the block comment to describe the actual
three states (em-dash, mixed-asterisk, full dollars) plus the
no-usage zero case.

Low #1 — Negative-limit bypass in diagnostics route:
parseInt('-1', 10) || 20 → -1 (truthy), Math.min(-1, 200) → -1,
SQLite LIMIT -1 returns all rows. Renderer always passes limit=20 so
no current user-visible bug, but a direct local-sidecar caller could
exfiltrate the full unpriced-models list. Clamped to [1, 200] via
Math.max + Math.min.

Low #2 — Missing FEA-1446 deferral marker:
Step 3 per-token-category UI was deferred to FEA-1446; the data is
emitted in the sidecar's session-detail cost_breakdown block but had no
inline reference. Added a comment near the emit site so FEA-1446 has a
clear linkback.

Testing:
- 36 pricing-related tests pass (build-agent-monitor-pricing-invariants
  + fetch-litellm-pricing + openai-cache-math + cost-pricing-ipc
  + sidecar-cost-priced-flag)
- pnpm build:agent-monitor regenerates db.js with the new clamp
- typecheck + lint clean
- Pre-existing environment-flaky tests (python3 / shell-path /
  symphony-loop) unrelated to this work

Risks:
- The asterisk + tooltip rendering uses inline span with text-amber-400;
  if the sidecar UI later adopts a different visual-warning convention,
  this will need to follow. Low risk; no other UI uses this color today.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aeyeCEO
aeyeCEO requested a review from a team May 29, 2026 04:01
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

// existing `harness` migration style (try a SELECT, ALTER TABLE on miss).
// The INSERT/addMissing patches below still pass the new column so a
// fresh DB and an upgraded DB converge on the same shape.
if (!source.includes("ADD COLUMN cache_write_1h_per_mtok")) {

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.

This ALTER is spliced in after addMissing(DEFAULT_PRICING), but the seed block above now prepares the 7-column INSERT (with cache_write_1h_per_mtok) before this runs. On a fresh DB the CREATE TABLE already has the column so it's fine. On any existing dashboard.db the table was created by an older version without it, and db.prepare() validates column names at compile time, so the INSERT prepare throws 'no column named cache_write_1h_per_mtok' before this migration ever executes. That kills db.js init, and since the column never gets added the next boot fails the exact same way. Every existing user's Agent Dashboard crash-loops on upgrade, which means the feature this PR ships is dead for anyone who isn't a fresh install. The ALTER has to run before the seed block, not after it. Nothing catches this today because every test creates the table with the column already present, so add an upgrade-path test that boots the patched db.js against an old-shape model_pricing table.

// FEA-1432: 1-hour Anthropic ephemeral cache tier. Optional in the form so
// OpenAI/Gemini rules can omit it; we always send a number (default 0) to
// the sidecar to keep the upstream upsert anchored on a stable shape.
cache_write_1h_per_mtok: z.number().finite().min(0).default(0),

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.

You validate and send cache_write_1h_per_mtok here, but the sidecar's PUT /api/pricing handler only destructures 6 fields and upsertPricing only has 6 rate columns. Neither got patched the way the seed INSERT did, so a manually added Claude rule silently stores cache_write_1h as 0. No visible effect today since cacheWrite1hTokens is always 0, but once FEA-1440 starts emitting real 1h tokens these hand-entered rules will under-price and nobody will know why.

/**
* FEA-1433: thin IPC binding for the Settings → Pricing surface.
*
* desktop:list-unpriced-models – GET /api/diagnostics/unpriced-models

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.

Says /api/diagnostics/unpriced-models but the route is mounted under /api/pricing, so the real path is /api/pricing/diagnostics/unpriced-models (which is what the client actually hits).

// Boundary translation: a relay receiver MAY accept v1 payloads (no
// `cacheWrite1hTokens` field) and treat the missing field as 0. The cloud
// removal ticket for the legacy-shape translation lives at FEA-1439.
export const AGENT_SESSION_SYNC_SCHEMA_VERSION = 2 as const;

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.

Bumping this to 2 means desktop starts stamping schemaVersion: 2 on every payload it sends to the cloud. Does the control plane already accept v2, or does it need to land there first so sync doesn't start getting rejected the moment this deploys? The doc comment covers a receiver tolerating v1, but the risk here is the other direction.

@thadeusb thadeusb 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.

The cache_write_1h column migration gets spliced in after the seed INSERT that references it, so db.js init throws on any existing dashboard.db and the sidecar crash-loops for every upgrading user. Tests only ever CREATE TABLE with the column already there, so they stay green and miss it. Reorder the ALTER ahead of the seed block, add a real upgrade-path test, and fix the PUT upsert that drops the same column. Will re-review once that's reworked.

Andrew Eye and others added 4 commits May 29, 2026 08:10
…s right

This branch shipped an Opus 4.5/4.6/4.7 force-override in HOST_ONLY_OVERRIDES
that pinned input=$15/output=$75 based on the *wrong* assumption that Anthropic
priced new Opus models at the same rate as Opus 4.1. Anthropic actually
re-priced Opus DOWN starting at 4.5 — to $5 input / $25 output / $0.50
cache_read / $6.25 5min cache_write / $10 1h cache_write per Mtok. LiteLLM
upstream had it right all along.

Source: https://platform.claude.com/docs/en/about-claude/pricing
(confirmed via WebFetch and cross-checked against vendored
apps/desktop/scripts/litellm-pricing.json which already carries 5/25/0.5/
6.25/10 for Opus 4.5+).

The downstream damage was 3× over-reporting on every Opus 4.5+ session.
Users on the upgraded build saw cost numbers that triple-counted reality.

This commit:

1. Removes the six HOST_ONLY_OVERRIDES rows that force-overrode
   Opus 4.5/4.6/4.7 (both unversioned and date-suffixed snapshots).
   The merged seed now follows LiteLLM's correct rates for these models.

2. Replaces the broken legacy migration in db.js with a REVERSE migration.
   The old migration detected the *correct* $5/$25 values and rewrote them
   to wrong $15/$75 values for upgraders. The new migration detects the
   wrong ($15/$75/$1.50/$18.75) state and resets all five rate columns
   (including cache_write_1h to $10) to the correct ($5/$25/$0.50/$6.25/$10)
   values. The reverse migration anchors AFTER the FEA-1432 cache_write_1h
   column-add+backfill so the column is guaranteed to exist when the
   UPDATE runs.

3. Removes the build-time "Opus 4.x floor" invariant. That invariant
   required input ≥ $10/Mtok for Opus 4.x rows — which now actively
   rejects correct data. The remaining two invariants (OpenAI cache
   discount + zero-write, Anthropic 1h cache floor) still apply.

4. Updates apps/desktop/CLAUDE.md to remove the Opus floor mention and
   document why it was removed.

5. Updates the two existing tests that pinned the old wrong values:
   - "Claude Opus 4.x rows price input ≥ $10/Mtok" → renamed and re-
     scoped to "Opus 4.1/4.2 retain their published $15/$75 list price"
     (Opus 4.1 and the deprecated Opus 4 stayed at $15).
   - "Opus floor invariant catches date-suffixed snapshot keys" → renamed
     to "Opus 4.5+ tracks Anthropic's published list price" with regex
     tightened to `\d{1,2}` so it doesn't trip on date-only snapshot keys
     like `claude-opus-4-20250514%` (deprecated base Opus 4).
   - Updates the openai-cache-math test to expect $10 (input × 2) for
     Opus 4.7 1h cache writes, not the old wrong $30.

Testing:
- 36 pricing-related tests pass (build-agent-monitor-pricing-invariants
  + fetch-litellm-pricing + openai-cache-math + sidecar-cost-priced-flag
  + cost-pricing-ipc)
- pnpm build:agent-monitor regenerates db.js with Opus 4.7 at the correct
  [5, 25, 0.5, 6.25, 10] tuple in DEFAULT_PRICING and the reverse migration
  block in place
- typecheck + lint clean

Risks:
- Reverse migration is conditional on the exact wrong-tuple match. A user
  who deliberately set their Opus 4.5+ rows to $15/$75 (very unlikely) would
  be reset on next launch. Acceptable: Anthropic does not bill at $15/$75
  for these models, so anyone with that value is wrong.
- Users on an existing 0.15.97-99 build of this branch will see their
  reported costs drop by ~3× on Opus 4.5+ sessions after upgrading. That's
  the bug correction, not a regression.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cedence

Architectural cleanup of the host-side pricing data, prompted by the Opus
4.5+ pricing mistake earlier in this PR. The previous design had
HOST_ONLY_OVERRIDES win on collision with LiteLLM, which is exactly the
shape that let a bad override (Opus 4.5+ pinned to $15/$75) ship in
the first place. This commit makes that mistake structurally impossible.

Changes:

1. **Rename** HOST_ONLY_OVERRIDES → HOST_FALLBACKS throughout. The new
   name describes what the rows are *for* (gap-fillers), not what they're
   *against* (overrides). Comment block at the declaration site explains
   the two legitimate categories: SYNTHETIC IDs (cursor-default, gpt-codex,
   etc. that no vendor publishes) and COVERAGE GAPS (bare Claude aliases
   like claude-3-5-sonnet% that LiteLLM only carries under date-suffixed
   keys like claude-3-5-sonnet-20240620).

2. **Flip merge precedence** to LiteLLM-wins. loadHostDefaultPricing()
   now seeds the merge map from LiteLLM first, and HOST_FALLBACKS rows
   are only inserted for patterns the map does NOT already contain.

3. **Anti-override build assertion.** If any HOST_FALLBACKS row has a
   model_pattern that already exists in LiteLLM, the build throws with
   an actionable error message pointing to LiteLLM upstream as the fix
   path. This catches my exact past mistake (force-overriding Opus 4.5+
   to $15/$75) before it can ship.

   The error message:
   "HOST_FALLBACKS contains model_patterns that collide with LiteLLM: …
    Remove these rows — LiteLLM is the source of truth for vendor pricing.
    If a LiteLLM rate is genuinely wrong, fix it upstream at
    https://github.com/BerriAI/litellm/blob/main/model_prices_and_context_window.json"

Current HOST_FALLBACKS contents (all legitimate per the new rule):
  - Synthetic IDs: big-pickle%, opencode/big-pickle%, gpt-codex%,
    cursor-default%, copilot-default%, opencode-default%
  - Coverage gaps: claude-3-5-sonnet%, claude-3-5-haiku%,
    claude-3-7-sonnet%, claude-3-haiku%, claude-3-opus%,
    claude-opus-4-2%, claude-sonnet-4-2%
    (all unversioned aliases that LiteLLM carries only as date-suffixed
    keys — verified via WebFetch against LiteLLM upstream)

Testing:
- 36 pricing-specific tests pass; test file headers updated to describe
  the new contract shape.
- pnpm build:agent-monitor passes the anti-override assertion (no
  collisions in current state).
- typecheck + lint clean.

Risks:
- If LiteLLM upstream starts publishing the bare aliases we currently
  fill (e.g. claude-3-7-sonnet%), the anti-override assertion will fail
  the next build after the daily refresh — that's the expected outcome,
  surfaces the deletion as a TODO, and ensures we follow upstream.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OpenAI re-priced cached input down to 10% of input for the GPT-5 family
(5, 5-codex, 5.4, 5.4-mini, 5.4-nano, 5.5). FEA-1432 was built on the
older "50% discount" assumption — both in the transformer (which CLAMPED
LiteLLM's correct 10% rates UP to 50%) and in the build invariant
(`cache_read ≤ input × 0.55`). Shipped state was 5× overcharging cached
input tokens for every GPT-5.x session.

Verified via the live OpenAI pricing page (gpt-5.5 shows $5 input /
$0.50 cached / $30 output = 10% cache_read ratio) and LiteLLM upstream
(gpt-5.4 / -mini / -nano all carry input × 0.1 cache_read).

Changes:

1. **Transformer clamp removed.** apps/desktop/scripts/fetch-litellm-pricing.mjs
   no longer overrides cache_read; LiteLLM's value passes through verbatim.
   cache_write (5-min) is still forced to 0 because that IS a vendor-stated
   invariant (OpenAI publishes no cache-write surcharge), not a guess at
   a ratio.

2. **Build invariant relaxed.** apps/desktop/scripts/build-agent-monitor.mjs
   no longer asserts `cache_read ≤ input × 0.55`. The new sanity floor is
   `cache_read ≤ input` — catches genuine regressions (a vendor pricing
   cached input HIGHER than uncached would be absurd) without encoding any
   assumption about the specific discount ratio.

3. **gpt-codex synthetic fallback updated** from $0.625 cache_read (old 50%
   guess) to $0.125 (10% — matches the most likely underlying model,
   GPT-5 Codex).

4. **Vendored LiteLLM JSON refreshed** via `pnpm refresh:pricing`. 176
   rows (sha256 cc96ba1f6130…). Notable corrections post-refresh:
   - gpt-5.4: cache_read $1.25 → $0.25 (5× drop, the bug correction)
   - gpt-5.4-mini: $0.375 → $0.075
   - gpt-5.4-nano: $0.1 → $0.02
   - gpt-5.5: new row added at $5/$30/$0.50 cache_read (matches the live page)
   - gpt-5/gpt-5-codex/gpt-5-mini/gpt-5-nano: cache_read also dropped to 10%
   - gpt-4o: stayed at 50% — older tier, LiteLLM correctly reflects the
     mixed state.

5. **Tests updated.** The "FEA-1432: OpenAI rows always carry cache_write
   = 0 and cache_write_1h = 0" test was specifically about clamping
   behavior — rewritten as "FEA-1431-bugfix: OpenAI cache_read passes
   through LiteLLM unchanged; cache_writes always zero" with cases for
   both the 10% (GPT-5 family) and 50% (GPT-4o) tiers, plus the
   no-caching (cache_read=0) preservation case. The gpt-codex fallback
   sanity check was updated to expect 10%.

6. **CLAUDE.md updated** to describe the new sanity floor + the fact that
   LiteLLM is trusted for the actual discount ratio (currently 10% for
   GPT-5 family, 50% for older).

Testing:
- 36 pricing tests pass
- pnpm build:agent-monitor passes the relaxed invariant cleanly
- typecheck + lint clean
- Vendored JSON's gpt-5.4 cache_read now reads $0.25 (was $1.25)

Risks:
- Users with active GPT-5.x sessions and prompt caching will see their
  cached-input cost drop by 5× after this upgrade. That's the bug
  correction, not a regression. No data migration needed — only the
  pricing table changes; future sessions price correctly. Past sessions
  still hold their old (5× too high) estimated_cost_usd in the cloud
  relay; recompute would require re-syncing the session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reported crash on user's existing 0.15.93 DB:

  Error: table model_pricing has no column named cache_write_1h_per_mtok
    at Database.prepare (.../compat-sqlite.js:39:21)
    at .../db.js:355:21 (the top-up's INSERT preparation)

The FEA-1432 ALTER TABLE migration was anchored on
`addMissing(DEFAULT_PRICING);\n}\n` and APPENDED after the call, meaning
the migration block landed BELOW the top-up loop in the generated db.js.
The patched top-up INSERT prepares with the new column name; node:sqlite
validates referenced columns at prepare time, so on an old DB the
sidecar crashed with ERR_SQLITE_ERROR before the ALTER ever ran. The
auto-restart hit its 5-attempt cap and the agent monitor never came up.

Fix: change the anchor to the `// Top-up: insert any default pattern`
comment and PREPEND the migration block before it. The reverse-Opus
migration (FEA-1431-bugfix from earlier in this PR) still anchors after
the FEA-1432 block, so the final order in the generated db.js is now:

  1. CREATE TABLE IF NOT EXISTS (7 cols for fresh DBs)
  2. FEA-1432 try/catch: ALTER TABLE ADD COLUMN + claude-% backfill
  3. FEA-1431-bugfix: reverse Opus 4.5/4.6/4.7 from $15/$75 → $5/$25
  4. Top-up: INSERT OR IGNORE with 7 cols (column now exists)

Inline comments in both migration blocks explain the ordering invariant
so it does not get accidentally re-ordered by a future patch.

Testing:
- 36 pricing tests pass
- pnpm build:agent-monitor regenerates db.js with the migrations now
  positioned at lines 364 (ALTER), 375 (reverse), 428 (top-up) — order
  is verified by grep against the generated output
- typecheck clean

Risks:
- For users who already hit the 5-restart cap on 0.15.101, the
  app-startup state machine may have given up. Quitting Electron and
  re-launching should fire the migration on the first try with this
  build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aeyeCEO

aeyeCEO commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by a clean single-PR rebuild on FEA-1431-genai-prices (off latest main).

Why: the override pipeline these PRs were built on was the root cause of the repeated overcharge bugs (Opus 4.5+ 3x, OpenAI cache-read 5x, phantom 1h-cache derivation). PRD-414 was reset to v2: adopt @pydantic/genai-prices as the single pricing source of truth and delete the override pipeline entirely. The library is trusted with no clamps/asserts/rewrites.

The three-phase split (1431/1432/1433, 1434, 1435/1436) is being consolidated into one PR. FEA-1432 is now OBSOLETE (cache math is library-owned). Head commit archived locally as tag archive/pr-253-FEA-1431.

@aeyeCEO aeyeCEO closed this May 29, 2026
@aeyeCEO
aeyeCEO deleted the FEA-1431-litellm-pricing branch May 29, 2026 15:01
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants