Files
sub2api/backend/internal/service/video_billing.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

156 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}