diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 89ea28fc6..d7ba40227 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -6627,6 +6627,16 @@ uses the provider resolved by Patrol readiness, even when the configured OpenRouter model is an unprefixed slash route such as `anthropic/claude-sonnet-5`; scorer replay persists and reuses that resolved provider so the same captured report prices deterministically. +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. 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/pricing.go b/internal/ai/cost/pricing.go index a6ba85602..a6c28d06b 100644 --- a/internal/ai/cost/pricing.go +++ b/internal/ai/cost/pricing.go @@ -51,8 +51,18 @@ var providerPrices = map[string][]modelPrice{ flatPrice("gpt-4o*", 5.00, 15.00), }, "anthropic": { + // Anthropic first-party standard API prices, checked from + // https://platform.claude.com/docs/en/about-claude/pricing on 2026-08-28. + // Keep version-specific rows before the legacy family fallbacks because + // lookupPrice returns the first matching prefix. + 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), 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), }, "deepseek": { diff --git a/internal/ai/cost/pricing_anthropic_test.go b/internal/ai/cost/pricing_anthropic_test.go new file mode 100644 index 000000000..c53444f5c --- /dev/null +++ b/internal/ai/cost/pricing_anthropic_test.go @@ -0,0 +1,50 @@ +package cost + +import "testing" + +func TestLookupPriceUsesCurrentAnthropicOpusPricing(t *testing.T) { + tests := []struct { + model string + input float64 + output float64 + asOf string + }{ + {model: "claude-opus-5", input: 5.00, output: 25.00, asOf: "2026-08-28"}, + {model: "claude-opus-5-20260801", input: 5.00, output: 25.00, asOf: "2026-08-28"}, + {model: "claude-opus-4-8", input: 5.00, output: 25.00, asOf: "2026-08-28"}, + {model: "claude-opus-4-7", input: 5.00, output: 25.00, asOf: "2026-08-28"}, + {model: "claude-opus-4-6", input: 5.00, output: 25.00, asOf: "2026-08-28"}, + {model: "claude-opus-4-5-20251101", input: 5.00, output: 25.00, asOf: "2026-08-28"}, + {model: "claude-opus-4-1-20250805", input: 15.00, output: 75.00, asOf: pricingAsOf}, + {model: "claude-opus-4-20250514", input: 15.00, output: 75.00, asOf: pricingAsOf}, + {model: "claude-opus-20240229", input: 15.00, output: 75.00, asOf: pricingAsOf}, + } + + 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 Anthropic Opus pricing to resolve") + } + if price.InputUSDPerMTok != test.input || price.OutputUSDPerMTok != test.output { + t.Fatalf("unexpected Anthropic Opus price: %+v", price) + } + if price.AsOf != test.asOf { + t.Fatalf("Anthropic Opus pricing date = %q, want %q", price.AsOf, test.asOf) + } + }) + } +} + +func TestLookupPriceUsesCurrentAnthropicHaiku45Pricing(t *testing.T) { + price, ok := lookupPrice("anthropic", "claude-haiku-4-5-20251001", 50_000) + if !ok { + t.Fatal("expected Anthropic Haiku 4.5 pricing to resolve") + } + if price.InputUSDPerMTok != 1.00 || price.OutputUSDPerMTok != 5.00 { + t.Fatalf("unexpected Anthropic Haiku 4.5 price: %+v", price) + } + if price.AsOf != "2026-08-28" { + t.Fatalf("Anthropic Haiku 4.5 pricing date = %q, want 2026-08-28", price.AsOf) + } +}