mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 10:35:51 +00:00
Fix Gemini cost estimation tiers (#1360)
This commit is contained in:
@@ -49,17 +49,17 @@ func TestNormalizeModelForProvider(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLookupPriceAndPatterns(t *testing.T) {
|
||||
if _, ok := lookupPrice("", "gpt-4o"); ok {
|
||||
if _, ok := lookupPrice("", "gpt-4o", 0); ok {
|
||||
t.Fatal("expected empty provider to be unknown")
|
||||
}
|
||||
if _, ok := lookupPrice("openai", ""); ok {
|
||||
if _, ok := lookupPrice("openai", "", 0); ok {
|
||||
t.Fatal("expected empty model to be unknown")
|
||||
}
|
||||
if _, ok := lookupPrice("unknown", "model"); ok {
|
||||
if _, ok := lookupPrice("unknown", "model", 0); ok {
|
||||
t.Fatal("expected unknown provider to be unknown")
|
||||
}
|
||||
|
||||
price, ok := lookupPrice("openai", "gpt-4o-mini")
|
||||
price, ok := lookupPrice("openai", "gpt-4o-mini", 0)
|
||||
if !ok || price.InputUSDPerMTok == 0 {
|
||||
t.Fatalf("expected openai pricing match, got ok=%v price=%+v", ok, price)
|
||||
}
|
||||
@@ -78,6 +78,32 @@ func TestLookupPriceAndPatterns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupPrice_UsesTieredGeminiPricing(t *testing.T) {
|
||||
under200k, ok := lookupPrice("gemini", "gemini-2.5-pro", 150_000)
|
||||
if !ok {
|
||||
t.Fatal("expected Gemini 2.5 Pro pricing to resolve")
|
||||
}
|
||||
if under200k.InputUSDPerMTok != 1.25 || under200k.OutputUSDPerMTok != 10.00 {
|
||||
t.Fatalf("unexpected <=200k tier: %+v", under200k)
|
||||
}
|
||||
|
||||
over200k, ok := lookupPrice("gemini", "gemini-2.5-pro", 250_000)
|
||||
if !ok {
|
||||
t.Fatal("expected Gemini 2.5 Pro high-tier pricing to resolve")
|
||||
}
|
||||
if over200k.InputUSDPerMTok != 2.50 || over200k.OutputUSDPerMTok != 15.00 {
|
||||
t.Fatalf("unexpected >200k tier: %+v", over200k)
|
||||
}
|
||||
|
||||
flash, ok := lookupPrice("gemini", "gemini-3-flash-preview", 50_000)
|
||||
if !ok {
|
||||
t.Fatal("expected Gemini 3 Flash Preview pricing to resolve")
|
||||
}
|
||||
if flash.InputUSDPerMTok != 0.50 || flash.OutputUSDPerMTok != 3.00 {
|
||||
t.Fatalf("unexpected Gemini 3 Flash Preview pricing: %+v", flash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetPersistence_LoadError(t *testing.T) {
|
||||
store := NewStore(30)
|
||||
mock := &savePersistence{loadErr: errors.New("load failed")}
|
||||
|
||||
+73
-22
@@ -13,7 +13,7 @@ type TokenPrice struct {
|
||||
// EstimateUSD returns an estimated USD cost for the given provider/model and token counts.
|
||||
// If the model pricing is unknown, ok is false and usd is 0.
|
||||
func EstimateUSD(provider, model string, inputTokens, outputTokens int64) (usd float64, ok bool, price TokenPrice) {
|
||||
price, ok = lookupPrice(provider, model)
|
||||
price, ok = lookupPrice(provider, model, inputTokens)
|
||||
if !ok {
|
||||
return 0, false, TokenPrice{}
|
||||
}
|
||||
@@ -24,12 +24,18 @@ func EstimateUSD(provider, model string, inputTokens, outputTokens int64) (usd f
|
||||
}
|
||||
|
||||
type modelPrice struct {
|
||||
Pattern string
|
||||
Pattern string
|
||||
Tiers []priceTier
|
||||
}
|
||||
|
||||
type priceTier struct {
|
||||
// MaxInputTokens is inclusive. Zero means "no upper bound".
|
||||
MaxInputTokens int64
|
||||
InputUSDPerMTok float64
|
||||
OutputUSDPerMTok float64
|
||||
}
|
||||
|
||||
const pricingAsOf = "2025-12"
|
||||
const pricingAsOf = "2026-03-25"
|
||||
|
||||
// PricingAsOf indicates the effective date of the pricing table used for estimation.
|
||||
func PricingAsOf() string {
|
||||
@@ -40,35 +46,61 @@ func PricingAsOf() string {
|
||||
// The goal is quick estimation and relative comparisons, not exact billing.
|
||||
var providerPrices = map[string][]modelPrice{
|
||||
"openai": {
|
||||
{Pattern: "gpt-4o*", InputUSDPerMTok: 5.00, OutputUSDPerMTok: 15.00},
|
||||
{Pattern: "gpt-4o-mini*", InputUSDPerMTok: 0.15, OutputUSDPerMTok: 0.60},
|
||||
flatPrice("gpt-4o*", 5.00, 15.00),
|
||||
flatPrice("gpt-4o-mini*", 0.15, 0.60),
|
||||
},
|
||||
"anthropic": {
|
||||
{Pattern: "claude-opus*", InputUSDPerMTok: 15.00, OutputUSDPerMTok: 75.00},
|
||||
{Pattern: "claude-sonnet*", InputUSDPerMTok: 3.00, OutputUSDPerMTok: 15.00},
|
||||
{Pattern: "claude-haiku*", InputUSDPerMTok: 0.25, OutputUSDPerMTok: 1.25},
|
||||
flatPrice("claude-opus*", 15.00, 75.00),
|
||||
flatPrice("claude-sonnet*", 3.00, 15.00),
|
||||
flatPrice("claude-haiku*", 0.25, 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},
|
||||
flatPrice("deepseek-*", 0.28, 0.42),
|
||||
},
|
||||
"gemini": {
|
||||
// Gemini pricing (as of December 2025)
|
||||
// Gemini 3 models are in preview, pricing may change
|
||||
{Pattern: "gemini-3-pro*", InputUSDPerMTok: 1.25, OutputUSDPerMTok: 5.00},
|
||||
{Pattern: "gemini-3-flash*", InputUSDPerMTok: 0.075, OutputUSDPerMTok: 0.30},
|
||||
{Pattern: "gemini-2.5-pro*", InputUSDPerMTok: 1.25, OutputUSDPerMTok: 5.00},
|
||||
{Pattern: "gemini-2.5-flash*", InputUSDPerMTok: 0.075, OutputUSDPerMTok: 0.30},
|
||||
{Pattern: "gemini-1.5-pro*", InputUSDPerMTok: 1.25, OutputUSDPerMTok: 5.00},
|
||||
{Pattern: "gemini-1.5-flash*", InputUSDPerMTok: 0.075, OutputUSDPerMTok: 0.30},
|
||||
{Pattern: "gemini-*", InputUSDPerMTok: 0.075, OutputUSDPerMTok: 0.30}, // Default to flash pricing
|
||||
// Google Gemini Developer API pricing sourced from
|
||||
// https://ai.google.dev/gemini-api/docs/pricing on 2026-03-25.
|
||||
tieredPrice("gemini-3.1-pro-preview*", priceTier{MaxInputTokens: 200_000, InputUSDPerMTok: 2.00, OutputUSDPerMTok: 12.00}, priceTier{InputUSDPerMTok: 4.00, OutputUSDPerMTok: 18.00}),
|
||||
tieredPrice("gemini-3-pro-image-preview*", priceTier{MaxInputTokens: 200_000, InputUSDPerMTok: 2.00, OutputUSDPerMTok: 12.00}, priceTier{InputUSDPerMTok: 4.00, OutputUSDPerMTok: 18.00}),
|
||||
flatPrice("gemini-3-flash-preview*", 0.50, 3.00),
|
||||
flatPrice("gemini-3.1-flash-image-preview*", 0.50, 3.00),
|
||||
flatPrice("gemini-3.1-flash-lite-preview*", 0.25, 1.50),
|
||||
tieredPrice("gemini-2.5-pro*", priceTier{MaxInputTokens: 200_000, InputUSDPerMTok: 1.25, OutputUSDPerMTok: 10.00}, priceTier{InputUSDPerMTok: 2.50, OutputUSDPerMTok: 15.00}),
|
||||
flatPrice("gemini-2.5-flash*", 0.30, 2.50),
|
||||
flatPrice("gemini-2.5-flash-lite-preview*", 0.10, 0.40),
|
||||
flatPrice("gemini-2.5-flash-lite*", 0.10, 0.40),
|
||||
flatPrice("gemini-2.0-flash*", 0.10, 0.40),
|
||||
flatPrice("gemini-2.0-flash-lite*", 0.075, 0.30),
|
||||
flatPrice("gemini-1.5-pro*", 1.25, 5.00),
|
||||
flatPrice("gemini-1.5-flash*", 0.075, 0.30),
|
||||
flatPrice("gemini-*", 0.30, 2.50), // Default to current non-lite Flash pricing.
|
||||
},
|
||||
"ollama": {
|
||||
{Pattern: "*", InputUSDPerMTok: 0, OutputUSDPerMTok: 0},
|
||||
flatPrice("*", 0, 0),
|
||||
},
|
||||
}
|
||||
|
||||
func lookupPrice(provider, model string) (TokenPrice, bool) {
|
||||
func flatPrice(pattern string, inputUSDPerMTok, outputUSDPerMTok float64) modelPrice {
|
||||
return modelPrice{
|
||||
Pattern: pattern,
|
||||
Tiers: []priceTier{
|
||||
{
|
||||
InputUSDPerMTok: inputUSDPerMTok,
|
||||
OutputUSDPerMTok: outputUSDPerMTok,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func tieredPrice(pattern string, tiers ...priceTier) modelPrice {
|
||||
return modelPrice{
|
||||
Pattern: pattern,
|
||||
Tiers: tiers,
|
||||
}
|
||||
}
|
||||
|
||||
func lookupPrice(provider, model string, inputTokens int64) (TokenPrice, bool) {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
model = strings.ToLower(strings.TrimSpace(model))
|
||||
if provider == "" || model == "" {
|
||||
@@ -82,9 +114,13 @@ func lookupPrice(provider, model string) (TokenPrice, bool) {
|
||||
|
||||
for _, p := range prices {
|
||||
if matchPattern(model, strings.ToLower(p.Pattern)) {
|
||||
tier, ok := selectPriceTier(p.Tiers, inputTokens)
|
||||
if !ok {
|
||||
return TokenPrice{}, false
|
||||
}
|
||||
return TokenPrice{
|
||||
InputUSDPerMTok: p.InputUSDPerMTok,
|
||||
OutputUSDPerMTok: p.OutputUSDPerMTok,
|
||||
InputUSDPerMTok: tier.InputUSDPerMTok,
|
||||
OutputUSDPerMTok: tier.OutputUSDPerMTok,
|
||||
AsOf: pricingAsOf,
|
||||
}, true
|
||||
}
|
||||
@@ -92,6 +128,21 @@ func lookupPrice(provider, model string) (TokenPrice, bool) {
|
||||
return TokenPrice{}, false
|
||||
}
|
||||
|
||||
func selectPriceTier(tiers []priceTier, inputTokens int64) (priceTier, bool) {
|
||||
if len(tiers) == 0 {
|
||||
return priceTier{}, false
|
||||
}
|
||||
if inputTokens < 0 {
|
||||
inputTokens = 0
|
||||
}
|
||||
for _, tier := range tiers {
|
||||
if tier.MaxInputTokens == 0 || inputTokens <= tier.MaxInputTokens {
|
||||
return tier, true
|
||||
}
|
||||
}
|
||||
return tiers[len(tiers)-1], true
|
||||
}
|
||||
|
||||
func matchPattern(model, pattern string) bool {
|
||||
if pattern == "*" {
|
||||
return true
|
||||
|
||||
Reference in New Issue
Block a user