156 lines
4.4 KiB
Go
156 lines
4.4 KiB
Go
package service
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"log/slog"
|
|||
|
|
"sort"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/xai"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// Canonical video price family keys used in groups.video_model_prices JSONB.
|
|||
|
|
const (
|
|||
|
|
VideoPriceFamilyGrokImagineVideo = "grok-imagine-video"
|
|||
|
|
VideoPriceFamilyGrokImagineVideo15 = "grok-imagine-video-1.5"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// CanonicalGrokImagineVideoPriceFamily normalizes model aliases / preview / legacy
|
|||
|
|
// IDs onto the price-family keys stored in video_model_prices.
|
|||
|
|
func CanonicalGrokImagineVideoPriceFamily(model string) string {
|
|||
|
|
if model == "" {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
// Prefer shared xAI helper for known aliases. Keep future native Imagine
|
|||
|
|
// models distinct so operators can assign them independent prices.
|
|||
|
|
if c := xai.CanonicalImagineVideoModel(model); c != "" {
|
|||
|
|
switch c {
|
|||
|
|
case xai.DefaultImagineVideo15Model:
|
|||
|
|
return VideoPriceFamilyGrokImagineVideo15
|
|||
|
|
case xai.DefaultImagineVideoModel:
|
|||
|
|
return VideoPriceFamilyGrokImagineVideo
|
|||
|
|
}
|
|||
|
|
if strings.HasPrefix(c, "grok-imagine-video-") {
|
|||
|
|
return c
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
m := strings.ToLower(strings.TrimSpace(model))
|
|||
|
|
for _, prefix := range []string{"xai/", "x-ai/", "grok/"} {
|
|||
|
|
if strings.HasPrefix(m, prefix) {
|
|||
|
|
m = strings.TrimPrefix(m, prefix)
|
|||
|
|
break
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
switch {
|
|||
|
|
case m == "grok-imagine-video-1.5" || m == "grok-imagine-video-1.5-preview" ||
|
|||
|
|
m == "grok-video-1.5" || strings.Contains(m, "video-1.5"):
|
|||
|
|
return VideoPriceFamilyGrokImagineVideo15
|
|||
|
|
case m == "grok-imagine-video" || m == "grok-imagine-video-preview" ||
|
|||
|
|
m == "grok-video" || m == "grok-video-latest":
|
|||
|
|
return VideoPriceFamilyGrokImagineVideo
|
|||
|
|
default:
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// NormalizeVideoModelPrices cleans and canonicalizes a per-model resolution map.
|
|||
|
|
// Keys become price families; tiers use 480p/720p/1080p. Negative prices dropped.
|
|||
|
|
//
|
|||
|
|
// Model keys are walked in sorted order rather than in Go map order: several
|
|||
|
|
// aliases can canonicalize onto the same family, and an unordered walk would
|
|||
|
|
// make the winning price for a conflicting tier vary between processes.
|
|||
|
|
// Unrecognized tiers are dropped with a warning instead of silently collapsing
|
|||
|
|
// into the 480p bucket.
|
|||
|
|
func NormalizeVideoModelPrices(in map[string]map[string]float64) map[string]map[string]float64 {
|
|||
|
|
if len(in) == 0 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
modelKeys := make([]string, 0, len(in))
|
|||
|
|
for modelKey := range in {
|
|||
|
|
modelKeys = append(modelKeys, modelKey)
|
|||
|
|
}
|
|||
|
|
sort.Strings(modelKeys)
|
|||
|
|
out := make(map[string]map[string]float64)
|
|||
|
|
for _, modelKey := range modelKeys {
|
|||
|
|
tierPrices := in[modelKey]
|
|||
|
|
if len(tierPrices) == 0 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
family := CanonicalGrokImagineVideoPriceFamily(modelKey)
|
|||
|
|
if family == "" {
|
|||
|
|
key := strings.ToLower(strings.TrimSpace(modelKey))
|
|||
|
|
switch key {
|
|||
|
|
case VideoPriceFamilyGrokImagineVideo, VideoPriceFamilyGrokImagineVideo15:
|
|||
|
|
family = key
|
|||
|
|
default:
|
|||
|
|
if key == "" {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
family = key
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
normalizedTiers := out[family]
|
|||
|
|
if normalizedTiers == nil {
|
|||
|
|
normalizedTiers = make(map[string]float64)
|
|||
|
|
}
|
|||
|
|
tierKeys := make([]string, 0, len(tierPrices))
|
|||
|
|
for tierKey := range tierPrices {
|
|||
|
|
tierKeys = append(tierKeys, tierKey)
|
|||
|
|
}
|
|||
|
|
sort.Strings(tierKeys)
|
|||
|
|
for _, tierKey := range tierKeys {
|
|||
|
|
price := tierPrices[tierKey]
|
|||
|
|
if price < 0 {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
tier, ok := LookupVideoBillingResolution(tierKey)
|
|||
|
|
if !ok {
|
|||
|
|
slog.Warn("video_model_prices_unknown_resolution_dropped",
|
|||
|
|
"model_key", modelKey,
|
|||
|
|
"family", family,
|
|||
|
|
"resolution", tierKey)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
if existing, exists := normalizedTiers[tier]; exists && existing != price {
|
|||
|
|
slog.Warn("video_model_prices_conflicting_tier_price",
|
|||
|
|
"model_key", modelKey,
|
|||
|
|
"family", family,
|
|||
|
|
"resolution", tier,
|
|||
|
|
"previous_price", existing,
|
|||
|
|
"price", price)
|
|||
|
|
}
|
|||
|
|
normalizedTiers[tier] = price
|
|||
|
|
}
|
|||
|
|
if len(normalizedTiers) > 0 {
|
|||
|
|
out[family] = normalizedTiers
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if len(out) == 0 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
return out
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// LookupVideoModelPrice returns a per-second price from a model×resolution map, or nil.
|
|||
|
|
func LookupVideoModelPrice(prices map[string]map[string]float64, model, resolution string) *float64 {
|
|||
|
|
if len(prices) == 0 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
family := CanonicalGrokImagineVideoPriceFamily(model)
|
|||
|
|
if family == "" {
|
|||
|
|
family = strings.ToLower(strings.TrimSpace(model))
|
|||
|
|
}
|
|||
|
|
if family == "" {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
tierPrices, ok := prices[family]
|
|||
|
|
if !ok || len(tierPrices) == 0 {
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
tier := NormalizeVideoBillingResolutionOrDefault(resolution)
|
|||
|
|
if price, ok := tierPrices[tier]; ok {
|
|||
|
|
p := price
|
|||
|
|
return &p
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|