Keep current Anthropic budgets enforceable

Price Sonnet 5 at its current first-party rate and recognize Fable 5 and Mythos 5 so current models cannot prematurely stop Patrol or make budget enforcement unknown. Refresh the surfaced pricing review date and cover the budget behavior.

Refs #1789
This commit is contained in:
pulse-triage[bot]
2026-08-29 08:18:10 +01:00
parent f58ff1addf
commit ea3f6388f3
5 changed files with 57 additions and 16 deletions
@@ -6637,12 +6637,16 @@ First-party Anthropic cost estimation is version-sensitive even within one
model family. Standard API calls for Opus 4.5, 4.6, 4.7, 4.8, and 5 use the
reviewed $5 input / $25 output per-million-token rates, while Opus 4, 4.1, and
3 retain the legacy $15 / $75 rates. Haiku 4.5 similarly uses $1 / $5 rather
than the Haiku 3 fallback. These version-specific rows must precede broad
family fallbacks because pricing lookup is first-match prefix-based, and
dated model aliases must resolve to the same row as their base version. The
30-day runtime budget recalculates retained usage through this table, so a
stale higher fallback must not disable Assistant or Patrol before estimated
first-party spend reaches the configured limit.
than the Haiku 3 fallback. Sonnet 5 uses $2 / $10 rather than the prior
Sonnet-family $3 / $15 rate, while Fable 5 and Mythos 5 use $10 / $50 and must
remain priced rather than making the complete cost summary unknown. These
version-specific rows must precede broad family fallbacks because pricing
lookup is first-match prefix-based, and dated model aliases must resolve to the
same row as their base version. The 30-day runtime budget recalculates retained
usage through this table, so a stale higher fallback must not disable Assistant
or Patrol before estimated first-party spend reaches the configured limit, and
a current priced model must not bypass enforcement by falling through as
unknown.
After a non-interactive Patrol finding lifecycle write succeeds, the structured
tool result is authoritative and investigation remains closed. Watch first
offers a bounded lifecycle-completion request containing only report and
@@ -32,7 +32,7 @@ func TestBranchcov0724pmEmptySummaryTable(t *testing.T) {
// pricingAsOf is the build-embedded pricing-table date (pricing.go:39).
// Asserted as a concrete observable value, not via PricingAsOf(), so the
// test would still catch a regression if both moved together.
const wantPricingAsOf = "2026-06-04"
const wantPricingAsOf = "2026-08-29"
cases := []struct {
name string
+5 -2
View File
@@ -36,7 +36,7 @@ type priceTier struct {
OutputUSDPerMTok float64
}
const pricingAsOf = "2026-06-04"
const pricingAsOf = "2026-08-29"
// PricingAsOf indicates the effective date of the pricing table used for estimation.
func PricingAsOf() string {
@@ -52,15 +52,18 @@ var providerPrices = map[string][]modelPrice{
},
"anthropic": {
// Anthropic first-party standard API prices, checked from
// https://platform.claude.com/docs/en/about-claude/pricing on 2026-08-28.
// https://platform.claude.com/docs/en/about-claude/pricing on 2026-08-29.
// Keep version-specific rows before the legacy family fallbacks because
// lookupPrice returns the first matching prefix.
flatPriceAsOf("claude-fable-5*", 10.00, 50.00, "2026-08-29"),
flatPriceAsOf("claude-mythos-5*", 10.00, 50.00, "2026-08-29"),
flatPriceAsOf("claude-opus-5*", 5.00, 25.00, "2026-08-28"),
flatPriceAsOf("claude-opus-4-8*", 5.00, 25.00, "2026-08-28"),
flatPriceAsOf("claude-opus-4-7*", 5.00, 25.00, "2026-08-28"),
flatPriceAsOf("claude-opus-4-6*", 5.00, 25.00, "2026-08-28"),
flatPriceAsOf("claude-opus-4-5*", 5.00, 25.00, "2026-08-28"),
flatPrice("claude-opus*", 15.00, 75.00),
flatPriceAsOf("claude-sonnet-5*", 2.00, 10.00, "2026-08-29"),
flatPrice("claude-sonnet*", 3.00, 15.00),
flatPriceAsOf("claude-haiku-4-5*", 1.00, 5.00, "2026-08-28"),
flatPrice("claude-haiku*", 0.25, 1.25),
@@ -48,3 +48,33 @@ func TestLookupPriceUsesCurrentAnthropicHaiku45Pricing(t *testing.T) {
t.Fatalf("Anthropic Haiku 4.5 pricing date = %q, want 2026-08-28", price.AsOf)
}
}
func TestLookupPriceUsesCurrentAnthropicGeneration5Pricing(t *testing.T) {
tests := []struct {
model string
input float64
output float64
}{
{model: "claude-fable-5", input: 10.00, output: 50.00},
{model: "claude-fable-5-20260609", input: 10.00, output: 50.00},
{model: "claude-mythos-5", input: 10.00, output: 50.00},
{model: "claude-mythos-5-20260609", input: 10.00, output: 50.00},
{model: "claude-sonnet-5", input: 2.00, output: 10.00},
{model: "claude-sonnet-5-20260630", input: 2.00, output: 10.00},
}
for _, test := range tests {
t.Run(test.model, func(t *testing.T) {
price, ok := lookupPrice("anthropic", test.model, 50_000)
if !ok {
t.Fatal("expected current Anthropic generation 5 pricing to resolve")
}
if price.InputUSDPerMTok != test.input || price.OutputUSDPerMTok != test.output {
t.Fatalf("unexpected Anthropic generation 5 price: %+v", price)
}
if price.AsOf != "2026-08-29" {
t.Fatalf("Anthropic generation 5 pricing date = %q, want 2026-08-29", price.AsOf)
}
})
}
}
+11 -7
View File
@@ -9,13 +9,17 @@ import (
func TestServiceCheckBudgetUsesCurrentAnthropicRates(t *testing.T) {
tests := []struct {
name string
model string
wantErr bool
name string
model string
budgetUSD float64
wantErr bool
}{
{name: "current Opus remains below budget", model: "claude-opus-5"},
{name: "dated current Opus remains below budget", model: "claude-opus-4-5-20251101"},
{name: "legacy Opus still exceeds budget", model: "claude-opus-4-1-20250805", wantErr: true},
{name: "current Opus remains below budget", model: "claude-opus-5", budgetUSD: 20},
{name: "dated current Opus remains below budget", model: "claude-opus-4-5-20251101", budgetUSD: 20},
{name: "legacy Opus still exceeds budget", model: "claude-opus-4-1-20250805", budgetUSD: 20, wantErr: true},
{name: "Sonnet 5 uses lower current rate", model: "claude-sonnet-5", budgetUSD: 8},
{name: "Fable 5 remains budget enforceable", model: "claude-fable-5", budgetUSD: 20, wantErr: true},
{name: "Mythos 5 remains budget enforceable", model: "claude-mythos-5", budgetUSD: 20, wantErr: true},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -27,7 +31,7 @@ func TestServiceCheckBudgetUsesCurrentAnthropicRates(t *testing.T) {
OutputTokens: 500_000,
UseCase: "patrol",
})
svc := &Service{cfg: &config.AIConfig{CostBudgetUSD30d: 20}, costStore: store}
svc := &Service{cfg: &config.AIConfig{CostBudgetUSD30d: test.budgetUSD}, costStore: store}
err := svc.CheckBudget("patrol")
if (err != nil) != test.wantErr {
t.Fatalf("CheckBudget() error = %v, wantErr %v; summary = %+v", err, test.wantErr, store.GetSummary(30).Totals)