diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bbd7c4..47c33e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,6 +91,15 @@ surface is governed by [`COMPATIBILITY.md`](COMPATIBILITY.md). Go/Python setup. The daemon pulls fresh on each run so a stale cached image can't skew the demo; a short README shows how to swap the mock for a real provider. +### Changed +- **Refreshed the built-in pricing table** to current published list prices + (Anthropic and OpenAI pricing pages, 2026-06-19). Adds Claude Fable 5 and the + OpenAI `gpt-5.5` / `gpt-5.4` family, and **fixes two now-incorrect prefixes**: the + `claude-opus-4` prefix was mispricing Opus 4.5–4.8 (now $5/$25 via longer-prefix + entries; Opus 4 / 4.1 stay $15/$75), and the stale `gpt-5` prefix was mispricing + `gpt-5.4`/`gpt-5.5` (bare `gpt-5` is now unpriced rather than wrong). Prices remain + fully overridable via `RISKKERNEL_PRICING_FILE`. + ## [0.7.0] - 2026-06-14 Reach and scale. RiskKernel now plugs into the whole Python agent ecosystem — diff --git a/internal/pricing/pricing.go b/internal/pricing/pricing.go index b415519..203e0c7 100644 --- a/internal/pricing/pricing.go +++ b/internal/pricing/pricing.go @@ -19,20 +19,36 @@ type Rate struct { OutputPerM float64 `json:"outputPerM"` } -// defaultRates holds built-in list prices. Matching is by case-insensitive prefix -// so dated model snapshots (e.g. "claude-sonnet-4-5-20250101") resolve to the -// family rate. Users override or extend these via config; unknown models price to -// zero and report ok=false so the caller can decide how to treat them. +// defaultRates holds built-in list prices in USD per 1M tokens, as published on the +// providers' pricing pages. Refreshed 2026-06-19 from: +// +// Anthropic: https://platform.claude.com/docs/en/about-claude/pricing +// OpenAI: https://developers.openai.com/api/docs/pricing +// +// Matching is case-insensitive LONGEST-prefix, so dated snapshots (e.g. +// "claude-opus-4-8-20260101") and minor versions resolve to the right rate, and a +// more specific key wins over a shorter one. Provider prices drift — override or +// extend via config (RISKKERNEL_PRICING_FILE); unknown models price to zero and +// report ok=false so the caller can decide how to treat them. var defaultRates = map[string]Rate{ - // Anthropic (native provider in v0.1). - "claude-opus-4": {InputPerM: 15.0, OutputPerM: 75.0}, - "claude-sonnet-4": {InputPerM: 3.0, OutputPerM: 15.0}, - "claude-haiku-4": {InputPerM: 1.0, OutputPerM: 5.0}, + // Anthropic (native). Opus 4.5+ dropped to $5/$25; Opus 4 / 4.1 stay $15/$75, + // so the newer versions get explicit (longer-prefix) entries. + "claude-fable-5": {InputPerM: 10.0, OutputPerM: 50.0}, + "claude-opus-4-8": {InputPerM: 5.0, OutputPerM: 25.0}, + "claude-opus-4-7": {InputPerM: 5.0, OutputPerM: 25.0}, + "claude-opus-4-6": {InputPerM: 5.0, OutputPerM: 25.0}, + "claude-opus-4-5": {InputPerM: 5.0, OutputPerM: 25.0}, + "claude-opus-4": {InputPerM: 15.0, OutputPerM: 75.0}, // Opus 4 / 4.1 + "claude-sonnet-4": {InputPerM: 3.0, OutputPerM: 15.0}, // Sonnet 4 / 4.5 / 4.6 + "claude-haiku-4": {InputPerM: 1.0, OutputPerM: 5.0}, // Haiku 4.5 "claude-3-5-haiku": {InputPerM: 0.8, OutputPerM: 4.0}, - // OpenAI (stub provider; prices kept for ledger/ingress accounting). - "gpt-5": {InputPerM: 1.25, OutputPerM: 10.0}, - "gpt-4o": {InputPerM: 2.5, OutputPerM: 10.0}, - "gpt-4o-mini": {InputPerM: 0.15, OutputPerM: 0.6}, + // OpenAI (native). The gpt-5.5 / 5.4 family is current; 4o kept as legacy. + "gpt-5.5": {InputPerM: 5.0, OutputPerM: 30.0}, + "gpt-5.4-mini": {InputPerM: 0.75, OutputPerM: 4.5}, + "gpt-5.4-nano": {InputPerM: 0.2, OutputPerM: 1.25}, + "gpt-5.4": {InputPerM: 2.5, OutputPerM: 15.0}, + "gpt-4o-mini": {InputPerM: 0.15, OutputPerM: 0.6}, + "gpt-4o": {InputPerM: 2.5, OutputPerM: 10.0}, } // Table prices models. The zero value is usable (built-in rates only); use diff --git a/internal/pricing/pricing_test.go b/internal/pricing/pricing_test.go index a5bbfae..392f266 100644 --- a/internal/pricing/pricing_test.go +++ b/internal/pricing/pricing_test.go @@ -30,6 +30,60 @@ func TestCost_KnownModel(t *testing.T) { } } +func TestCost_CurrentModels(t *testing.T) { + tbl := NewTable(nil) + cases := []struct { + model string + in, out int64 + want float64 + }{ + // Anthropic (per platform.claude.com pricing, 2026-06-19). + {"claude-fable-5", 1_000_000, 1_000_000, 60.0}, // 10 + 50 + {"claude-opus-4-8-20260101", 1_000_000, 1_000_000, 30.0}, // 5 + 25 (current Opus) + {"claude-opus-4-5", 1_000_000, 0, 5.0}, // 5 (current Opus) + {"claude-opus-4-1-20250805", 1_000_000, 1_000_000, 90.0}, // 15 + 75 (deprecated 4.1) + {"claude-opus-4", 1_000_000, 0, 15.0}, // retired 4.0 + {"claude-haiku-4-5", 1_000_000, 1_000_000, 6.0}, // 1 + 5 + // OpenAI (per developers.openai.com pricing, 2026-06-19). + {"gpt-5.5", 1_000_000, 1_000_000, 35.0}, // 5 + 30 + {"gpt-5.4", 1_000_000, 1_000_000, 17.5}, // 2.5 + 15 + {"gpt-5.4-mini", 1_000_000, 1_000_000, 5.25}, // 0.75 + 4.5 + {"gpt-5.4-nano", 1_000_000, 0, 0.2}, // 0.2 + } + for _, c := range cases { + usd, ok := tbl.Cost(c.model, c.in, c.out) + if !ok || !approx(usd, c.want) { + t.Errorf("Cost(%q) = %v ok=%v, want %v", c.model, usd, ok, c.want) + } + } +} + +func TestCost_OpusVersionPrefixSplit(t *testing.T) { + // Longest-prefix must price current Opus (4.5+) at $5/$25 while deprecated + // 4.0/4.1 stay $15/$75 — a regression guard, since the shorter "claude-opus-4" + // prefix would otherwise misprice the newer (cheaper) models. + tbl := NewTable(nil) + if r, _ := tbl.Rate("claude-opus-4-8"); !approx(r.InputPerM, 5.0) || !approx(r.OutputPerM, 25.0) { + t.Errorf("opus-4-8 rate = %+v, want 5/25", r) + } + if r, _ := tbl.Rate("claude-opus-4-1-20250805"); !approx(r.InputPerM, 15.0) || !approx(r.OutputPerM, 75.0) { + t.Errorf("opus-4-1 rate = %+v, want 15/75", r) + } +} + +func TestCost_SupersededGpt5Unpriced(t *testing.T) { + // Bare "gpt-5" (5.0) is superseded by 5.4/5.5 and no longer published, so it's + // unpriced (the user adds an override) rather than carrying a stale rate — while + // the 5.4/5.5 family that shares the stem is priced. + tbl := NewTable(nil) + if _, ok := tbl.Rate("gpt-5"); ok { + t.Error("bare gpt-5 should be unpriced after the refresh") + } + if _, ok := tbl.Rate("gpt-5.5"); !ok { + t.Error("gpt-5.5 should be priced") + } +} + func TestCost_DatedSnapshotResolvesToFamily(t *testing.T) { tbl := NewTable(nil) usd, ok := tbl.Cost("claude-sonnet-4-5-20250101", 1_000_000, 0)