mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user