diff --git a/internal/ai/cost/pricing.go b/internal/ai/cost/pricing.go index cc86ebb..95da791 100644 --- a/internal/ai/cost/pricing.go +++ b/internal/ai/cost/pricing.go @@ -43,10 +43,13 @@ var providerPrices = map[string][]modelPrice{ {Pattern: "claude-sonnet*", InputUSDPerMTok: 3.00, OutputUSDPerMTok: 15.00}, {Pattern: "claude-haiku*", InputUSDPerMTok: 0.25, OutputUSDPerMTok: 1.25}, }, + "deepseek": { + // DeepSeek docs include an "input cache hit" discount; this uses cache-miss rates for conservative estimates. + {Pattern: "deepseek-*", InputUSDPerMTok: 0.28, OutputUSDPerMTok: 0.42}, + }, "ollama": { {Pattern: "*", InputUSDPerMTok: 0, OutputUSDPerMTok: 0}, }, - // deepseek pricing intentionally omitted until stable public pricing is confirmed. } func lookupPrice(provider, model string) (TokenPrice, bool) { diff --git a/internal/ai/cost/store.go b/internal/ai/cost/store.go index 9797bb9..1e6981e 100644 --- a/internal/ai/cost/store.go +++ b/internal/ai/cost/store.go @@ -122,8 +122,9 @@ func (s *Store) GetSummary(days int) Summary { var totalInput, totalOutput int64 for _, e := range events { - provider := e.Provider + provider := strings.ToLower(strings.TrimSpace(e.Provider)) model := normalizeModel(provider, e.RequestModel, e.ResponseModel) + provider, model = inferProviderAndModel(provider, model) k := pmKey{provider: provider, model: model} pm := pmTotals[k] @@ -278,6 +279,19 @@ func normalizeModel(provider, requestModel, responseModel string) string { return "" } +func inferProviderAndModel(provider, model string) (string, string) { + if provider == "openai" { + parts := strings.SplitN(strings.TrimSpace(model), ":", 2) + if len(parts) == 2 && strings.ToLower(parts[0]) == "deepseek" { + return "deepseek", parts[1] + } + if strings.HasPrefix(strings.ToLower(strings.TrimSpace(model)), "deepseek") { + return "deepseek", model + } + } + return provider, model +} + // ProviderModelSummary is a rollup for a provider/model pair. type ProviderModelSummary struct { Provider string `json:"provider"` diff --git a/internal/ai/cost/store_test.go b/internal/ai/cost/store_test.go index 1455e64..9f671f4 100644 --- a/internal/ai/cost/store_test.go +++ b/internal/ai/cost/store_test.go @@ -130,7 +130,11 @@ func TestEstimateUSDKnownAndUnknownModels(t *testing.T) { } usd, ok, _ = EstimateUSD("deepseek", "deepseek-reasoner", 1_000_000, 1_000_000) - if ok || usd != 0 { - t.Fatalf("expected deepseek pricing to be unknown, got ok=%v usd=%.4f", ok, usd) + if !ok { + t.Fatalf("expected deepseek pricing to be known for deepseek-reasoner") + } + expected = 0.70 // 1M input * $0.28 + 1M output * $0.42 + if math.Abs(usd-expected) > 0.0001 { + t.Fatalf("expected usd %.4f, got %.4f", expected, usd) } } diff --git a/internal/ai/service.go b/internal/ai/service.go index 840184c..d143175 100644 --- a/internal/ai/service.go +++ b/internal/ai/service.go @@ -1002,9 +1002,13 @@ Always execute the commands rather than telling the user how to do it.` } if costStore != nil { + providerName, _ := config.ParseModelString(modelString) + if providerName == "" { + providerName = provider.Name() + } costStore.Record(cost.UsageEvent{ Timestamp: time.Now(), - Provider: provider.Name(), + Provider: providerName, RequestModel: modelString, ResponseModel: resp.Model, UseCase: req.UseCase, @@ -1189,9 +1193,13 @@ Always execute the commands rather than telling the user how to do it.` } if costStore != nil { + providerName, _ := config.ParseModelString(modelString) + if providerName == "" { + providerName = provider.Name() + } costStore.Record(cost.UsageEvent{ Timestamp: time.Now(), - Provider: provider.Name(), + Provider: providerName, RequestModel: modelString, ResponseModel: resp.Model, UseCase: req.UseCase,