vojo/apps/ai-bot/pricing.go

48 lines
2.3 KiB
Go

package main
// pricing.go centralises model pricing as a per-model table (the LiteLLM pattern)
// instead of three hardcoded Grok fields. The spend ledger prices each call by the
// model it actually used, so when a second model (Gemini) starts answering some
// routes, its cost books correctly against the same global ceiling.
// ModelPrice is the per-1M-token USD price for one model, applied to the API's
// returned usage so the wallet ceiling tracks real cost even as prices change.
type ModelPrice struct {
InputPerM float64 // non-cached prompt tokens
CachedPerM float64 // prompt tokens served from the provider cache (cheaper)
OutputPerM float64 // completion tokens
}
// CostBreakdown is the per-component USD cost of answering one request. A plain
// grok_direct call has only Token; a cascade adds Router (the cheap classifier),
// Grounding (Gemini Google-search) and/or WebTool (Grok web search) on top. Settle
// books each column separately so the ledger and request_log can attribute spend,
// and so a half-finished cascade can book only what it actually spent (§8.1).
type CostBreakdown struct {
Token float64
Grounding float64 // Gemini grounded-prompt TOKEN cost
WebTool float64
Router float64
// GroundingFee is the per-grounded-prompt FEE (the $35/1k overage on a paid Gemini
// tier, GEMINI_GROUNDING_PER_PROMPT_USD) — kept separate from Grounding (the token
// cost) for clean analytics. Booked the moment the grounded prompt is admitted, even
// on the error return (§7 SG1). Settle folds it into the grounding_usd spend column,
// so the $10 ceiling finally sees it without a spend-table migration.
GroundingFee float64
}
// Total is the grand total across all components (the number the wallet ceiling and
// request_log.total_usd care about). Computed, never stored, so it can't drift.
func (c CostBreakdown) Total() float64 {
return c.Token + c.Grounding + c.WebTool + c.Router + c.GroundingFee
}
// priceFor returns the configured price for a model. An unknown model falls back to
// the default (final-voice) model's price rather than $0 — a $0 price would silently
// blind the global ceiling to that call, the one failure mode we never want.
func (c *Config) priceFor(model string) ModelPrice {
if p, ok := c.Prices[model]; ok {
return p
}
return c.Prices[c.XAIModel]
}