package ratio_setting import ( "fmt" "math" "regexp" "sort" "strconv" "strings" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/types" ) const ModelPricingRulesOptionKey = "ModelPricingRules" var ( pricingConfigMap = types.NewRWMap[string, *types.PricingConfig]() dimensionKeyRE = regexp.MustCompile(`^[a-z][a-z0-9_]{0,31}$`) requestSourceRE = regexp.MustCompile(`^request\.[a-zA-Z_][a-zA-Z0-9_.]*$`) derivedSourceRE = regexp.MustCompile(`^derived\.(megapixels|aspect_ratio|input_mode|video_input)$`) reservedDimKeys = map[string]bool{ "price": true, "source": true, "__fallback__": true, "price_formula": true, "pricing_mode": true, "fallback_strategy": true, "schema_version": true, "scope": true, "billing_unit": true, "preconsume_strategy": true, } ) func GetPricingConfig(model string) *types.PricingConfig { cfg, ok := pricingConfigMap.Get(model) if !ok { return nil } return types.ClonePricingConfig(cfg) } func GetPricingConfigCopy() map[string]*types.PricingConfig { src := pricingConfigMap.ReadAll() dst := make(map[string]*types.PricingConfig, len(src)) for k, v := range src { dst[k] = types.ClonePricingConfig(v) } return dst } func SetPricingConfig(model string, cfg *types.PricingConfig) error { if strings.TrimSpace(model) == "" { return fmt.Errorf("model name is required") } clone := types.ClonePricingConfig(cfg) if err := ValidatePricingConfig(model, clone); err != nil { return err } pricingConfigMap.Set(model, clone) InvalidateExposedDataCache() return nil } func DeletePricingConfig(model string) error { all := pricingConfigMap.ReadAll() delete(all, model) pricingConfigMap.Clear() pricingConfigMap.AddAll(all) InvalidateExposedDataCache() return nil } func ModelPricingRules2JSONString() string { bytes, err := common.Marshal(GetPricingConfigCopy()) if err != nil { return "{}" } return string(bytes) } func UpdateModelPricingRulesByJSONString(jsonStr string) error { if strings.TrimSpace(jsonStr) == "" { jsonStr = "{}" } raw := map[string]*types.PricingConfig{} if err := common.UnmarshalJsonStr(jsonStr, &raw); err != nil { return err } next := make(map[string]*types.PricingConfig, len(raw)) for model, cfg := range raw { clone := types.ClonePricingConfig(cfg) if err := ValidatePricingConfig(model, clone); err != nil { return fmt.Errorf("model %s: %w", model, err) } next[model] = clone } pricingConfigMap.Clear() pricingConfigMap.AddAll(next) InvalidateExposedDataCache() return nil } func ValidatePricingConfig(model string, cfg *types.PricingConfig) error { if cfg == nil { return fmt.Errorf("pricing config is required") } if cfg.SchemaVersion != types.PricingSchemaVersion { return fmt.Errorf("schema_version must be %d", types.PricingSchemaVersion) } if cfg.Scope != types.PricingScopeModel { return fmt.Errorf("scope must be %q", types.PricingScopeModel) } types.NormalizePricingConfigDefaults(cfg) if !isAllowedBillingUnit(cfg.BillingUnit) { return fmt.Errorf("billing_unit must be per_call or per_1m_tokens") } if !isAllowedPreconsumeStrategy(cfg.PreconsumeStrategy) { return fmt.Errorf("preconsume_strategy must be exact or minimum") } if cfg.BillingUnit == types.BillingUnitPerCall && cfg.PreconsumeStrategy != types.PreconsumeStrategyExact { return fmt.Errorf("billing_unit per_call requires preconsume_strategy exact") } if cfg.BillingUnit == types.BillingUnitPer1MTokens { if cfg.PreconsumeStrategy != types.PreconsumeStrategyMinimum { return fmt.Errorf("billing_unit per_1m_tokens requires preconsume_strategy minimum") } if !SupportsAnyMatrixUsageBillingModel(model) { return fmt.Errorf("model %s does not support matrix usage billing", model) } } if len(cfg.Dimensions) == 0 { return fmt.Errorf("dimensions must not be empty") } if len(cfg.Table) == 0 { return fmt.Errorf("table must not be empty") } if err := validateFallback(cfg.Fallback); err != nil { return err } if err := validateDimensions(cfg.Dimensions); err != nil { return err } if err := validateRows(cfg.Dimensions, cfg.Table); err != nil { return err } return nil } func SupportsAnyMatrixUsageBillingModel(modelName string) bool { // Keep in sync with matrixUsageCapabilities in relay/helper/matrix_usage_capability.go switch modelName { case "seedance-2", "doubao-video", "doubao-video-seedance", "doubao-seedance-2-0-260128", "doubao-seedance-2-0-fast-260128", "Doubao-Seedance-2.0", "Doubao-Seedance-2.0-fast", "kling-v1", "kling-v1-6", "kling-v2-6", "kling-v3", "kling-video-o1", "kling-v3-omni": return true default: return false } } func isAllowedBillingUnit(unit string) bool { return unit == types.BillingUnitPerCall || unit == types.BillingUnitPer1MTokens } func isAllowedPreconsumeStrategy(strategy string) bool { return strategy == types.PreconsumeStrategyExact || strategy == types.PreconsumeStrategyMinimum } func validateFallback(fallback types.PricingFallback) error { switch fallback.Strategy { case types.PricingFallbackReject, types.PricingFallbackMax: return nil case types.PricingFallbackDefault: if fallback.DefaultPrice < 0 { return fmt.Errorf("fallback.default_price must be >= 0") } return nil default: return fmt.Errorf("fallback.strategy must be reject, max, or default") } } func validateDimensions(dimensions []types.PricingDimension) error { seen := map[string]bool{} for _, dim := range dimensions { if !dimensionKeyRE.MatchString(dim.Key) { return fmt.Errorf("dimension key %q is invalid", dim.Key) } if reservedDimKeys[dim.Key] { return fmt.Errorf("dimension key %q is reserved", dim.Key) } if seen[dim.Key] { return fmt.Errorf("dimension key %q is duplicated", dim.Key) } seen[dim.Key] = true if dim.Type != "string" && dim.Type != "number" && dim.Type != "boolean" { return fmt.Errorf("dimension %q type must be string, number, or boolean", dim.Key) } if !requestSourceRE.MatchString(dim.Source) && !derivedSourceRE.MatchString(dim.Source) { return fmt.Errorf("dimension %q source is not allowed", dim.Key) } if dim.Optional { if dim.Default == nil { return fmt.Errorf("dimension %q default required when optional=true", dim.Key) } if !valueMatchesType(dim.Default, dim.Type) { return fmt.Errorf("dimension %q default type must match %s", dim.Key, dim.Type) } } else if dim.Default != nil { return fmt.Errorf("dimension %q default must be null when optional=false", dim.Key) } } return nil } func validateRows(dimensions []types.PricingDimension, rows []types.PricingRow) error { for i, row := range rows { if err := validateRowFields(dimensions, row); err != nil { return fmt.Errorf("row %d: %w", i, err) } } if err := validateUniqueRows(dimensions, rows); err != nil { return err } if err := validateAmbiguousRows(dimensions, rows); err != nil { return err } return nil } func validateRowFields(dimensions []types.PricingDimension, row types.PricingRow) error { allowed := map[string]bool{"price": true, "source": true} for _, dim := range dimensions { allowed[dim.Key] = true } for key := range row { if !allowed[key] { return fmt.Errorf("unexpected field %q", key) } } for _, dim := range dimensions { v, ok := row[dim.Key] if !ok { return fmt.Errorf("missing dimension %q", dim.Key) } if s, ok := v.(string); ok && s == "*" { continue } if !valueMatchesType(v, dim.Type) { return fmt.Errorf("dimension %q value type must match %s or wildcard", dim.Key, dim.Type) } } price, ok := numberValue(row["price"]) if !ok || price < 0 { return fmt.Errorf("price must be >= 0") } source, ok := row["source"].(string) if !ok || (source != types.PricingRowSourceManual && source != types.PricingRowSourceGenerated) { return fmt.Errorf("source must be manual or generated") } return nil } func validateUniqueRows(dimensions []types.PricingDimension, rows []types.PricingRow) error { seen := map[string]bool{} for _, row := range rows { key := rowSignature(dimensions, row) if seen[key] { return fmt.Errorf("duplicate pricing row %s", key) } seen[key] = true } return nil } func validateAmbiguousRows(dimensions []types.PricingDimension, rows []types.PricingRow) error { for i := 0; i < len(rows); i++ { for j := i + 1; j < len(rows); j++ { if wildcardCount(dimensions, rows[i]) != wildcardCount(dimensions, rows[j]) { continue } if rowsIntersect(dimensions, rows[i], rows[j]) { return fmt.Errorf("ambiguous rows can both match with same wildcard count") } } } return nil } func rowSignature(dimensions []types.PricingDimension, row types.PricingRow) string { parts := make([]string, 0, len(dimensions)) for _, dim := range dimensions { parts = append(parts, dim.Key+"="+literalValue(row[dim.Key])) } sort.Strings(parts) return strings.Join(parts, "|") } func wildcardCount(dimensions []types.PricingDimension, row types.PricingRow) int { count := 0 for _, dim := range dimensions { if s, ok := row[dim.Key].(string); ok && s == "*" { count++ } } return count } func rowsIntersect(dimensions []types.PricingDimension, a, b types.PricingRow) bool { for _, dim := range dimensions { av := a[dim.Key] bv := b[dim.Key] as, awild := av.(string) bs, bwild := bv.(string) if awild && as == "*" || bwild && bs == "*" { continue } if !sameLiteralValue(av, bv) { return false } } return true } func valueMatchesType(v any, typ string) bool { switch typ { case "string": _, ok := v.(string) return ok case "number": _, ok := numberValue(v) return ok case "boolean": _, ok := v.(bool) return ok default: return false } } func numberValue(v any) (float64, bool) { switch n := v.(type) { case float64: return n, !math.IsNaN(n) && !math.IsInf(n, 0) case float32: f := float64(n) return f, !math.IsNaN(f) && !math.IsInf(f, 0) case int: return float64(n), true case int64: return float64(n), true case int32: return float64(n), true case jsonNumber: f, err := strconv.ParseFloat(string(n), 64) return f, err == nil && !math.IsNaN(f) && !math.IsInf(f, 0) default: return 0, false } } type jsonNumber string func literalValue(v any) string { if f, ok := numberValue(v); ok { return strconv.FormatFloat(f, 'f', -1, 64) } return fmt.Sprintf("%v", v) } func sameLiteralValue(a, b any) bool { af, aok := numberValue(a) bf, bok := numberValue(b) if aok && bok { return af == bf } return fmt.Sprintf("%v", a) == fmt.Sprintf("%v", b) }