From ea3f6388f3eb00381b02f33a78bca403dafa6dd7 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 08:18:10 +0100 Subject: [PATCH] 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 --- .../v6/internal/subsystems/ai-runtime.md | 16 ++++++---- .../cost/emptysummary_branchcov0724pm_test.go | 2 +- internal/ai/cost/pricing.go | 7 +++-- internal/ai/cost/pricing_anthropic_test.go | 30 +++++++++++++++++++ internal/ai/service_anthropic_budget_test.go | 18 ++++++----- 5 files changed, 57 insertions(+), 16 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index fd462b68d..86c45812f 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -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 diff --git a/internal/ai/cost/emptysummary_branchcov0724pm_test.go b/internal/ai/cost/emptysummary_branchcov0724pm_test.go index 31301f975..8729a025a 100644 --- a/internal/ai/cost/emptysummary_branchcov0724pm_test.go +++ b/internal/ai/cost/emptysummary_branchcov0724pm_test.go @@ -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 diff --git a/internal/ai/cost/pricing.go b/internal/ai/cost/pricing.go index a6c28d06b..7ac56bbe2 100644 --- a/internal/ai/cost/pricing.go +++ b/internal/ai/cost/pricing.go @@ -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), diff --git a/internal/ai/cost/pricing_anthropic_test.go b/internal/ai/cost/pricing_anthropic_test.go index c53444f5c..89bd9a48b 100644 --- a/internal/ai/cost/pricing_anthropic_test.go +++ b/internal/ai/cost/pricing_anthropic_test.go @@ -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) + } + }) + } +} diff --git a/internal/ai/service_anthropic_budget_test.go b/internal/ai/service_anthropic_budget_test.go index eb40d8824..2e2cd6ca8 100644 --- a/internal/ai/service_anthropic_budget_test.go +++ b/internal/ai/service_anthropic_budget_test.go @@ -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)