fix(pricing): price Claude Opus 5, correct Sonnet 5, drop retired models
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.
This commit is contained in:
+10
-4
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+47
-15
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user