From 9128352d1b2cffbff9c12ca7567e6f342802453e Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Tue, 15 Sep 2026 15:17:55 +0700 Subject: [PATCH] fix(pricing): price Claude Opus 5, correct Sonnet 5, drop retired models MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Opus 5 had no pricing row at all, so every token it burned was costed at $0 — the pattern list stopped at the 4.x Opus family and nothing matched `claude-opus-5`. Add it (5/25, fast mode 10/50, same shape as 4.8). Sonnet 5's launch discount expired 2026-08-31 and the discounted rate became the published standard, so the row's 3/15 standard rate started overcharging the day after. Fold 2/10 into DEFAULT_PRICING and empty DEFAULT_INTRO_PRICING; no model ships a promo today. Fable/Mythos 5.1 fell through to the `claude-fable-5%` row, which caches at $1/MTok against 5.1's $0.25 — give them their own rows. That overlap is only resolvable if the lookup prefers the more specific pattern, and `matchPricing` took `LIMIT 1` with no ORDER BY, i.e. whichever row was inserted first. Order by pattern length descending, which is what docs/DATABASE.md already claimed. (The nested REPLACE(model_pattern, '%', '%') it dropped was a no-op.) Retire Claude 3.x, Opus 4/4.1 and Sonnet 4 from the defaults. The startup top-up only ever ADDS missing rows, so removing them from DEFAULT_PRICING would leave every existing install untouched — prune them once, guarded by PRAGMA user_version (0 -> 1) so a model deliberately re-added in Settings survives later restarts. The same one-shot migration corrects the existing Sonnet 5 row. Only rates are deleted; token_usage is never touched, and Reset Defaults still restores the full current set. --- docs/DATABASE.md | 14 +++++++---- server/db.js | 62 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/docs/DATABASE.md b/docs/DATABASE.md index ed0740cf..7a4d8b76 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -430,11 +430,17 @@ CREATE TABLE model_pricing ( Standard rates and intro rates are edited independently: the pricing update path writes intro columns only when the caller sends intro fields, so a standard-rate edit never disturbs a promo (and vice versa). Clearing `intro_until` also zeroes the intro rates. -**Example default rule (Claude Sonnet 5, with its launch promo):** +**Example default rules:** -| Pattern | Input | Output | Intro Input | Intro Output | Intro Until | -|---------|-------|--------|-------------|--------------|-------------| -| `claude-sonnet-5%` | $3.00 | $15.00 | $2.00 | $10.00 | `2026-08-31` | +| Pattern | Input | Output | Cache read | Fast in/out | +|---------|-------|--------|------------|-------------| +| `claude-opus-5%` | $5.00 | $25.00 | $0.50 | $10 / $50 | +| `claude-sonnet-5%` | $2.00 | $10.00 | $0.20 | — | +| `claude-fable-5-1%` | $10.00 | $50.00 | $0.25 | — | + +No rule ships an intro rate today: Sonnet 5's launch promo ran through `2026-08-31` and its discounted rate became the standard one, so `DEFAULT_INTRO_PRICING` is empty. `claude-fable-5-1%` exists alongside `claude-fable-5%` only because 5.1 caches cheaper — that overlap is what the longest-pattern-first match rule above resolves. + +**Retired models.** Claude 3.x, Opus 4/4.1 and Sonnet 4 were dropped from the defaults. Because the startup top-up only *adds* missing rows, those rows are also deleted once from existing databases, guarded by `PRAGMA user_version` (0 → 1) so a model you deliberately re-add in Settings survives later restarts. The same one-shot migration corrects the pre-existing Sonnet 5 row to its post-promo $2 / $10 standard rate. Deleting a rule never touches `token_usage` — only the rates used to price it. --- diff --git a/server/db.js b/server/db.js index a74d29ff..77f11e28 100644 --- a/server/db.js +++ b/server/db.js @@ -828,29 +828,39 @@ try { // Each model gets its own explicit row — no catch-all grouping. // Rate shape mirrors Anthropic's published table: 5m write = 1.25× input, 1h write = 2× input. const DEFAULT_PRICING = [ - // Next-gen flagship + // Next-gen flagship (5.1 caches cheaper than 5 — needs its own row) + ["claude-fable-5-1%", "Claude Fable 5.1", 10, 50, 0.25, 12.5, 20, 0, 0], + ["claude-mythos-5-1%", "Claude Mythos 5.1", 10, 50, 0.25, 12.5, 20, 0, 0], ["claude-fable-5%", "Claude Fable 5", 10, 50, 1, 12.5, 20, 0, 0], ["claude-mythos-5%", "Claude Mythos 5", 10, 50, 1, 12.5, 20, 0, 0], - // Opus family (fast mode available on 4.6 / 4.7 / 4.8) + // Opus family (fast mode available on 5 / 4.6 / 4.7 / 4.8) + ["claude-opus-5%", "Claude Opus 5", 5, 25, 0.5, 6.25, 10, 10, 50], ["claude-opus-4-8%", "Claude Opus 4.8", 5, 25, 0.5, 6.25, 10, 10, 50], ["claude-opus-4-7%", "Claude Opus 4.7", 5, 25, 0.5, 6.25, 10, 30, 150], ["claude-opus-4-6%", "Claude Opus 4.6", 5, 25, 0.5, 6.25, 10, 30, 150], ["claude-opus-4-5%", "Claude Opus 4.5", 5, 25, 0.5, 6.25, 10, 0, 0], - ["claude-opus-4-1%", "Claude Opus 4.1", 15, 75, 1.5, 18.75, 30, 0, 0], - ["claude-opus-4-2%", "Claude Opus 4", 15, 75, 1.5, 18.75, 30, 0, 0], // Sonnet family - ["claude-sonnet-5%", "Claude Sonnet 5", 3, 15, 0.3, 3.75, 6, 0, 0], + ["claude-sonnet-5%", "Claude Sonnet 5", 2, 10, 0.2, 2.5, 4, 0, 0], ["claude-sonnet-4-6%", "Claude Sonnet 4.6", 3, 15, 0.3, 3.75, 6, 0, 0], ["claude-sonnet-4-5%", "Claude Sonnet 4.5", 3, 15, 0.3, 3.75, 6, 0, 0], - ["claude-sonnet-4-2%", "Claude Sonnet 4", 3, 15, 0.3, 3.75, 6, 0, 0], - ["claude-3-7-sonnet%", "Claude Sonnet 3.7", 3, 15, 0.3, 3.75, 6, 0, 0], - ["claude-3-5-sonnet%", "Claude Sonnet 3.5", 3, 15, 0.3, 3.75, 6, 0, 0], // Haiku family ["claude-haiku-4-5%", "Claude Haiku 4.5", 1, 5, 0.1, 1.25, 2, 0, 0], - ["claude-3-5-haiku%", "Claude Haiku 3.5", 0.8, 4, 0.08, 1, 1.6, 0, 0], - ["claude-3-haiku%", "Claude Haiku 3", 0.25, 1.25, 0.03, 0.3, 0.5, 0, 0], - // Legacy - ["claude-3-opus%", "Claude Opus 3", 15, 75, 1.5, 18.75, 30, 0, 0], +]; + +// Retired models (Claude 3.x, Opus 4/4.1, Sonnet 4): removed from the defaults +// AND pruned ONCE from existing DBs, since the top-up below only ever adds rows. +// Once-only (guarded by `user_version`) so a user who deliberately re-adds one +// in Settings keeps it. Pruning is safe — these rows only hold rates; historical +// token_usage is untouched. +const RETIRED_PRICING_PATTERNS = [ + "claude-opus-4-1%", + "claude-opus-4-2%", + "claude-sonnet-4-2%", + "claude-3-7-sonnet%", + "claude-3-5-sonnet%", + "claude-3-5-haiku%", + "claude-3-haiku%", + "claude-3-opus%", ]; // Top-up: insert any default pattern that isn't already present. Preserves @@ -858,6 +868,24 @@ const DEFAULT_PRICING = [ // This runs every startup so new default models (e.g. Opus 4.8) appear in the // Settings UI automatically without requiring a manual "Reset Defaults". { + if (db.pragma("user_version", { simple: true }) < 1) { + const drop = db.prepare("DELETE FROM model_pricing WHERE model_pattern = ?"); + db.transaction(() => { + for (const p of RETIRED_PRICING_PATTERNS) drop.run(p); + // Sonnet 5's launch discount expired 2026-08-31 and became the published + // standard rate. The top-up below only inserts missing rows, so correct + // the existing row here or every pre-existing install keeps billing 3/15. + db.prepare( + `UPDATE model_pricing SET + input_per_mtok = 2, output_per_mtok = 10, cache_read_per_mtok = 0.2, + cache_write_per_mtok = 2.5, cache_write_1h_per_mtok = 4, + intro_until = NULL + WHERE model_pattern = 'claude-sonnet-5%'` + ).run(); + })(); + db.pragma("user_version = 1"); + } + const existing = new Set( db .prepare("SELECT model_pattern FROM model_pricing") @@ -877,8 +905,9 @@ const DEFAULT_PRICING = [ // Known introductory promo rates: [pattern, in, out, cacheRead, cw5m, cw1h, until]. // Standard rates live in the DEFAULT_PRICING row; these add the time-limited -// discount on top. Claude Sonnet 5: 2/3-off launch pricing through 2026-08-31. -const DEFAULT_INTRO_PRICING = [["claude-sonnet-5%", 2, 10, 0.2, 2.5, 4, "2026-08-31"]]; +// discount on top. Empty today — Sonnet 5's launch discount (through 2026-08-31) +// became its standard rate, so it lives in DEFAULT_PRICING now. +const DEFAULT_INTRO_PRICING = []; // Backfill the known intro rates. Only fills rows whose intro_until is still // NULL, so a user who edits or clears an intro rate in Settings is never @@ -1594,8 +1623,11 @@ const stmts = { WHERE model_pattern = ? `), deletePricing: db.prepare("DELETE FROM model_pricing WHERE model_pattern = ?"), + // Longest pattern wins: `claude-fable-5-1%` and `claude-fable-5%` both match + // `claude-fable-5-1`, and without an explicit order the winner is whichever + // row happened to be inserted first. Most specific pattern is the right one. matchPricing: db.prepare( - "SELECT * FROM model_pricing WHERE ? LIKE REPLACE(model_pattern, '%', '%') LIMIT 1" + "SELECT * FROM model_pricing WHERE ? LIKE model_pattern ORDER BY LENGTH(model_pattern) DESC LIMIT 1" ), toolUsageCounts: db.prepare(` SELECT tool_name, COUNT(*) as count