Files
David Gageot 5b38f1d517 fix: price OpenAI requests by actual service tier
Remove the model allowlist and instead derive the Fast tier premium
from matching catalogue entries, falling back to the documented
multiplier when metadata is missing or inconsistent. Endpoint and
cost-override guards, tests, and docs are preserved.

Signed-off-by: David Gageot <david.gageot@docker.com>
2026-10-17 13:09:15 +02:00

107 lines
2.9 KiB
Go

package modelsdev
import (
"math"
"slices"
"time"
)
// ForServiceTier estimates tier pricing from the actual response tier without mutating c.
// Other providers and unrecognized tiers retain their catalogue pricing.
func (c *Cost) ForServiceTier(id ID, tier string) *Cost {
if c == nil || id.Provider != "openai" {
return c
}
// Current tier premiums; models with different rates need a cost override.
// https://developers.openai.com/api/docs/pricing
var factor float64
switch tier {
case "fast", "priority":
factor = c.fastMultiplier
if factor == 0 {
factor = 2
}
case "ultrafast":
factor = 6
default:
return c
}
out := *c
out.Input *= factor
out.Output *= factor
out.CacheRead *= factor
out.CacheWrite *= factor
out.Tiers = slices.Clone(c.Tiers)
for i := range out.Tiers {
out.Tiers[i].Input *= factor
out.Tiers[i].Output *= factor
out.Tiers[i].CacheRead *= factor
out.Tiers[i].CacheWrite *= factor
}
return &out
}
// withFastPricing derives a uniform premium only when gateway and vendor base rates agree.
func (c *Cost) withFastPricing(db *Database, id ID) *Cost {
if c == nil || id.Provider != "openai" {
return c
}
models := db.Providers["vercel"].Models
model := id.Model
for {
standard := models["openai/"+model].Cost
fast := models["openai/"+model+"-fast"].Cost
if standard != nil && fast != nil && standard.RatesFor(0) == c.RatesFor(0) {
if factor := uniformPremium(standard, fast); factor > 0 {
out := *c
out.fastMultiplier = factor
return &out
}
}
// Dated models may share the alias's pricing, but never assume its base rates match.
if len(model) <= len("-2006-01-02") {
return c
}
cut := len(model) - len("2006-01-02")
if model[cut-1] != '-' {
return c
}
if _, err := time.Parse(time.DateOnly, model[cut:]); err != nil {
return c
}
model = model[:cut-1]
}
}
func uniformPremium(standard, fast *Cost) float64 {
factor := fast.Input / standard.Input
if factor < 1 || math.IsNaN(factor) || math.IsInf(factor, 0) {
return 0
}
matches := func(base, premium Rates) bool {
return closeRate(base.Input*factor, premium.Input) &&
closeRate(base.Output*factor, premium.Output) &&
closeRate(base.CacheRead*factor, premium.CacheRead) &&
closeRate(base.CacheWrite*factor, premium.CacheWrite)
}
if !matches(standard.RatesFor(0), fast.RatesFor(0)) {
return 0
}
for _, tier := range fast.Tiers {
if tier.Tier.Size < 0 || tier.Tier.Size == math.MaxInt64 || (tier.Tier.Type != "" && tier.Tier.Type != "context") {
return 0
}
// Gateway thresholds may name the first long-context token, rather than the last short one.
if !matches(standard.RatesFor(tier.Tier.Size+1), tier.Rates) {
return 0
}
}
return factor
}
func closeRate(a, b float64) bool {
return !math.IsNaN(a) && !math.IsNaN(b) && !math.IsInf(a, 0) && !math.IsInf(b, 0) &&
math.Abs(a-b) <= 1e-9*math.Max(1, math.Max(math.Abs(a), math.Abs(b)))
}