Files
new-api/relay/channel/task/gemini/billing.go
T
CaIon d0bd8aac74 fix(billing): validate quantity parameters and harden quota calculations
Bound user-supplied count/duration parameters at request validation,
route ratio multipliers through guarded setters, and use saturating
int conversions in all quota math paths.
2026-07-07 00:21:06 +08:00

143 lines
3.6 KiB
Go

package gemini
import (
"strconv"
"strings"
relaycommon "github.com/QuantumNous/new-api/relay/common"
)
// ParseVeoDurationSeconds extracts durationSeconds from metadata.
// Returns 8 (Veo default) when not specified or invalid.
func ParseVeoDurationSeconds(metadata map[string]any) int {
if metadata == nil {
return 8
}
v, ok := metadata["durationSeconds"]
if !ok {
return 8
}
switch n := v.(type) {
case float64:
if int(n) > 0 {
return int(n)
}
case int:
if n > 0 {
return n
}
}
return 8
}
// ParseVeoResolution extracts resolution from metadata.
// Returns "720p" when not specified.
func ParseVeoResolution(metadata map[string]any) string {
if metadata == nil {
return "720p"
}
v, ok := metadata["resolution"]
if !ok {
return "720p"
}
if s, ok := v.(string); ok && s != "" {
return strings.ToLower(s)
}
return "720p"
}
// ResolveVeoDuration returns the effective duration in seconds.
// Priority: metadata["durationSeconds"] > stdDuration > stdSeconds > default (8).
// The result is capped because it is used as a billing multiplier and the
// metadata path bypasses standard request validation.
func ResolveVeoDuration(metadata map[string]any, stdDuration int, stdSeconds string) int {
if metadata != nil {
if _, exists := metadata["durationSeconds"]; exists {
if d := ParseVeoDurationSeconds(metadata); d > 0 {
return min(d, relaycommon.MaxTaskDurationSeconds)
}
}
}
if stdDuration > 0 {
return min(stdDuration, relaycommon.MaxTaskDurationSeconds)
}
if s, err := strconv.Atoi(stdSeconds); err == nil && s > 0 {
return min(s, relaycommon.MaxTaskDurationSeconds)
}
return 8
}
// ResolveVeoResolution returns the effective resolution string (lowercase).
// Priority: metadata["resolution"] > SizeToVeoResolution(stdSize) > default ("720p").
func ResolveVeoResolution(metadata map[string]any, stdSize string) string {
if metadata != nil {
if _, exists := metadata["resolution"]; exists {
if r := ParseVeoResolution(metadata); r != "" {
return r
}
}
}
if stdSize != "" {
return SizeToVeoResolution(stdSize)
}
return "720p"
}
// SizeToVeoResolution converts a "WxH" size string to a Veo resolution label.
func SizeToVeoResolution(size string) string {
parts := strings.SplitN(strings.ToLower(size), "x", 2)
if len(parts) != 2 {
return "720p"
}
w, _ := strconv.Atoi(parts[0])
h, _ := strconv.Atoi(parts[1])
maxDim := w
if h > maxDim {
maxDim = h
}
if maxDim >= 3840 {
return "4k"
}
if maxDim >= 1920 {
return "1080p"
}
return "720p"
}
// SizeToVeoAspectRatio converts a "WxH" size string to a Veo aspect ratio.
func SizeToVeoAspectRatio(size string) string {
parts := strings.SplitN(strings.ToLower(size), "x", 2)
if len(parts) != 2 {
return "16:9"
}
w, _ := strconv.Atoi(parts[0])
h, _ := strconv.Atoi(parts[1])
if w <= 0 || h <= 0 {
return "16:9"
}
if h > w {
return "9:16"
}
return "16:9"
}
// VeoResolutionRatio returns the pricing multiplier for the given resolution.
// Standard resolutions (720p, 1080p) return 1.0.
// 4K returns a model-specific multiplier based on Google's official pricing.
func VeoResolutionRatio(modelName, resolution string) float64 {
if resolution != "4k" {
return 1.0
}
// 4K multipliers derived from Vertex AI official pricing (video+audio base):
// veo-3.1-generate: $0.60 / $0.40 = 1.5
// veo-3.1-fast-generate: $0.35 / $0.15 ≈ 2.333
// Veo 3.0 models do not support 4K; return 1.0 as fallback.
if strings.Contains(modelName, "3.1-fast-generate") {
return 2.333333
}
if strings.Contains(modelName, "3.1-generate") || strings.Contains(modelName, "3.1") {
return 1.5
}
return 1.0
}