Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand Down
40 changes: 28 additions & 12 deletions internal/pricing/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions internal/pricing/pricing_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down