From 5f2c2404805c666d03c75add907be49953b6798c Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 12 Dec 2025 13:19:03 +0000 Subject: [PATCH] Clarify AI cost estimates with pricing coverage --- .../src/components/AI/AICostDashboard.tsx | 25 +++++++++++++++++++ frontend-modern/src/types/ai.ts | 1 + internal/ai/cost/pricing.go | 5 ++++ internal/ai/cost/store.go | 3 +++ internal/ai/service.go | 1 + internal/api/ai_handlers.go | 1 + 6 files changed, 36 insertions(+) diff --git a/frontend-modern/src/components/AI/AICostDashboard.tsx b/frontend-modern/src/components/AI/AICostDashboard.tsx index b290e4b..3a1a28b 100644 --- a/frontend-modern/src/components/AI/AICostDashboard.tsx +++ b/frontend-modern/src/components/AI/AICostDashboard.tsx @@ -65,6 +65,12 @@ export const AICostDashboard: Component = () => { return data.provider_models.some((pm) => pm.pricing_known); }); + const unpricedProviderModels = createMemo(() => { + const data = summary(); + if (!data) return []; + return (data.provider_models ?? []).filter((pm) => !pm.pricing_known && (pm.total_tokens ?? 0) > 0); + }); + const estimatedTotalUSD = createMemo(() => { const data = summary(); if (!data || !anyPricingKnown()) return null; @@ -412,6 +418,25 @@ export const AICostDashboard: Component = () => {
USD is an estimate based on public list prices. It may differ from billing. + 0}> + + Estimated spend is partial. Pricing is unknown for{' '} + {unpricedProviderModels() + .slice(0, 6) + .map( + (pm) => + `${PROVIDER_NAMES[pm.provider as keyof typeof PROVIDER_NAMES] || pm.provider}/${pm.model}`, + ) + .join(', ')} + 6}> + (+{unpricedProviderModels().length - 6} more) + + . + + + + Prices as of {summary()?.pricing_as_of}. +
diff --git a/frontend-modern/src/types/ai.ts b/frontend-modern/src/types/ai.ts index 82d3aa7..d73cde2 100644 --- a/frontend-modern/src/types/ai.ts +++ b/frontend-modern/src/types/ai.ts @@ -220,6 +220,7 @@ export interface AICostSummary { retention_days: number; effective_days: number; truncated: boolean; + pricing_as_of?: string; provider_models: AICostProviderModelSummary[]; use_cases: AICostUseCaseSummary[]; targets: AICostTargetSummary[]; diff --git a/internal/ai/cost/pricing.go b/internal/ai/cost/pricing.go index 95da791..de2585c 100644 --- a/internal/ai/cost/pricing.go +++ b/internal/ai/cost/pricing.go @@ -31,6 +31,11 @@ type modelPrice struct { const pricingAsOf = "2025-12" +// PricingAsOf indicates the effective date of the pricing table used for estimation. +func PricingAsOf() string { + return pricingAsOf +} + // NOTE: Keep this table small and conservative. // The goal is quick estimation and relative comparisons, not exact billing. var providerPrices = map[string][]modelPrice{ diff --git a/internal/ai/cost/store.go b/internal/ai/cost/store.go index 0fd6e17..c7c81ce 100644 --- a/internal/ai/cost/store.go +++ b/internal/ai/cost/store.go @@ -237,6 +237,7 @@ func (s *Store) GetSummary(days int) Summary { RetentionDays: retentionDays, EffectiveDays: effectiveDays, Truncated: truncated, + PricingAsOf: PricingAsOf(), ProviderModels: providerModels, UseCases: summarizeUseCases(events), Targets: summarizeTargets(events), @@ -356,6 +357,8 @@ type Summary struct { EffectiveDays int `json:"effective_days"` Truncated bool `json:"truncated"` + PricingAsOf string `json:"pricing_as_of,omitempty"` + ProviderModels []ProviderModelSummary `json:"provider_models"` UseCases []UseCaseSummary `json:"use_cases"` Targets []TargetSummary `json:"targets"` diff --git a/internal/ai/service.go b/internal/ai/service.go index a8670be..8f2fde9 100644 --- a/internal/ai/service.go +++ b/internal/ai/service.go @@ -138,6 +138,7 @@ func (s *Service) GetCostSummary(days int) cost.Summary { RetentionDays: cost.DefaultMaxDays, EffectiveDays: effectiveDays, Truncated: truncated, + PricingAsOf: cost.PricingAsOf(), ProviderModels: []cost.ProviderModelSummary{}, UseCases: []cost.UseCaseSummary{}, DailyTotals: []cost.DailySummary{}, diff --git a/internal/api/ai_handlers.go b/internal/api/ai_handlers.go index def16ee..93761dd 100644 --- a/internal/api/ai_handlers.go +++ b/internal/api/ai_handlers.go @@ -2441,6 +2441,7 @@ func (h *AISettingsHandler) HandleGetAICostSummary(w http.ResponseWriter, r *htt } else { summary = cost.Summary{ Days: days, + PricingAsOf: cost.PricingAsOf(), ProviderModels: []cost.ProviderModelSummary{}, DailyTotals: []cost.DailySummary{}, Totals: cost.ProviderModelSummary{Provider: "all"},