package billing import ( "context" "fmt" "math" "strings" ) const ( DefaultBillingMarkupMultiplier = 1.2 BillingCatalogObservedAt = "2026-08-11" ) // PriceRuleSeeder is an optional store capability. Existing Store // implementations remain source-compatible and seed-capable stores can make // the official base catalog durable before quotes are read. type PriceRuleSeeder interface { SeedBillingPriceRules(context.Context, []PriceRule) error } func DefaultBillingPriceRules() []PriceRule { rules := []PriceRule{ {ID: "base-volcengine-jimeng-seedream46", Provider: "volcengine-visual", Capability: "image.generate", ReqKey: "jimeng_seedream46_cvtob", Unit: UnitImage, StandardUnitPriceFen: 20, Note: "即梦4.6公开资源包基准。", Source: catalogSource("https://www.volcengine.com/activity/jimeng")}, {ID: "base-evolink-gpt-image-2", Provider: "evolink", Capability: "image.generate", ReqKey: "gpt-image-2", Unit: UnitImage, StandardUnitPriceFen: 34, Note: "medium / 1K / 1:1 / 无参考图基准。", Source: catalogSource("https://evolink.ai/zh/gpt-image-2"), Dimensions: evolinkDimensions()}, {ID: "base-bailian-wan27-image-pro", Provider: "bailian", Capability: "image.generate", ReqKey: "wan2.7-image-pro", Unit: UnitImage, StandardUnitPriceFen: 50, Source: catalogSource("https://help.aliyun.com/zh/model-studio/wan2-7-image-pro")}, {ID: "base-bailian-wan27-i2v-720p", Provider: "bailian", Capability: "video.generate", ReqKey: "wan2.7-i2v-2026-04-25", VariantKey: "resolution=720p", Unit: UnitVideoSecond, StandardUnitPriceFen: 60, Source: catalogSource("https://help.aliyun.com/zh/model-studio/wan2-7-i2v")}, {ID: "base-bailian-wan27-i2v-1080p", Provider: "bailian", Capability: "video.generate", ReqKey: "wan2.7-i2v-2026-04-25", VariantKey: "resolution=1080p", Unit: UnitVideoSecond, StandardUnitPriceFen: 100, Source: catalogSource("https://help.aliyun.com/zh/model-studio/wan2-7-i2v")}, {ID: "base-seedance-2-0-480p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=480p", Unit: UnitVideoSecond, StandardUnitPriceFen: 46, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")}, {ID: "base-seedance-2-0-720p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=720p", Unit: UnitVideoSecond, StandardUnitPriceFen: 99, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")}, {ID: "base-seedance-2-0-1080p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=1080p", Unit: UnitVideoSecond, StandardUnitPriceFen: 248, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")}, {ID: "base-seedance-2-0-4k", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-0-260128", VariantKey: "resolution=4k", Unit: UnitVideoSecond, StandardUnitPriceFen: 505, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh")}, } for i := range rules { rules[i].Enabled = true rules[i].MarkupMultiplier = DefaultBillingMarkupMultiplier } return rules } func catalogSource(url string) map[string]any { return map[string]any{"url": url, "observedAt": BillingCatalogObservedAt} } func evolinkDimensions() []ParameterDimension { tiers := func(values ...any) []ParameterTier { out := make([]ParameterTier, 0, len(values)) for index := 0; index < len(values); index += 2 { out = append(out, ParameterTier{Value: values[index], StandardFactor: values[index+1].(float64), MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}) } return out } return []ParameterDimension{ {Key: "quality", BaselineValue: "medium", DefaultValue: "medium", Tiers: tiers("low", .11, "medium", 1.0, "high", 4.0)}, {Key: "resolution", BaselineValue: "1K", DefaultValue: "1K", Tiers: tiers("1K", 1.0, "2K", 4.0, "4K", 8.0)}, {Key: "aspectRatio", BaselineValue: "1:1", DefaultValue: "1:1", Tiers: tiers("1:1", 1.0, "4:3", 1.0, "16:9", 1.0, "9:16", 1.0)}, {Key: "referenceImageCount", BaselineValue: 0, DefaultValue: 0, Tiers: []ParameterTier{ {Value: 0, StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, {Value: "1–4", Match: map[string]any{"min": 1, "max": 4}, StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, {Value: "5–8", Match: map[string]any{"min": 5, "max": 8}, StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, {Value: "9–16", Match: map[string]any{"min": 9, "max": 16}, StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, }}, } } // NormalizeBillingParameters derives quote inputs from the persisted generation // payload. Explicit parameters are retained only as a fallback for values the // payload does not carry. func NormalizeBillingParameters(payload map[string]any, fallback Parameters) Parameters { out := Parameters{} for key, value := range fallback { out[key] = value } settings := record(payload["settings"]) providerPayload := record(payload["providerPayload"]) providerParameters := record(providerPayload["parameters"]) input := record(payload["input"]) inputSettings := record(input["settings"]) setText(out, "model", first(providerPayload["model"], input["model"], settings["model"])) setText(out, "resolution", first(settings["resolution"], providerParameters["resolution"], providerPayload["resolution"], inputSettings["resolution"], input["resolution"])) setText(out, "quality", first(input["quality"], settings["quality"], providerPayload["quality"], providerParameters["quality"])) setNumber(out, "duration", first(settings["duration"], providerParameters["duration"], providerPayload["duration"], inputSettings["duration"], input["duration"]), false) setNumber(out, "imageCount", first(providerPayload["n"], providerParameters["n"], input["n"], input["imageCount"]), true) setNumber(out, "scale", first(input["scale"], settings["scale"]), false) width, widthOK := finiteNumber(first(providerPayload["width"], input["width"])) height, heightOK := finiteNumber(first(providerPayload["height"], input["height"])) if size := normalizedText(first(providerPayload["size"], providerParameters["size"], inputSettings["size"], input["size"])); size != "" { out["size"] = strings.ReplaceAll(size, "×", "*") } else if widthOK && heightOK { out["size"] = fmt.Sprintf("%g*%g", width, height) } if ratio := normalizedText(first(settings["ratio"], settings["aspectRatio"], providerPayload["ratio"], providerParameters["ratio"], inputSettings["ratio"], input["ratio"], input["aspectRatio"])); ratio != "" { out["aspectRatio"] = ratio } else if widthOK && heightOK && height > 0 { out["aspectRatio"] = aspectRatio(width, height) } if count := referenceCount(payload, input, providerPayload); count > 0 { out["referenceImageCount"] = float64(count) } if value, ok := boolean(first(settings["generate_audio"], settings["generateAudio"], providerParameters["generate_audio"], providerParameters["generateAudio"], input["generate_audio"], input["generateAudio"])); ok { out["generateAudio"] = value } return out } func record(value any) map[string]any { result, _ := value.(map[string]any); return result } func first(values ...any) any { for _, value := range values { if value != nil { return value } } return nil } func normalizedText(value any) string { if value == nil { return "" } return strings.ToLower(strings.TrimSpace(fmt.Sprint(value))) } func setText(out Parameters, key string, value any) { if value != nil { if text := normalizedText(value); text != "" { out[key] = text } } } func setNumber(out Parameters, key string, value any, ceil bool) { if number, ok := finiteNumber(value); ok && (!ceil || number > 0) { if ceil { number = math.Ceil(number) } out[key] = number } } func finiteNumber(value any) (float64, bool) { return number(value) } func boolean(value any) (bool, bool) { switch v := value.(type) { case bool: return v, true case string: if strings.EqualFold(strings.TrimSpace(v), "true") { return true, true } if strings.EqualFold(strings.TrimSpace(v), "false") { return false, true } } return false, false } func aspectRatio(width, height float64) string { ratio := width / height for _, item := range []struct { value float64 label string }{{1, "1:1"}, {4.0 / 3, "4:3"}, {16.0 / 9, "16:9"}, {3.0 / 4, "3:4"}, {9.0 / 16, "9:16"}, {21.0 / 9, "21:9"}} { if math.Abs(ratio-item.value) < .02 { return item.label } } return fmt.Sprintf("%g:%g", width, height) } func referenceCount(payload, input, providerPayload map[string]any) int { count := 0 for _, value := range []any{payload["imageUrls"], payload["inputUrls"], input["imageUrls"], providerPayload["image_urls"], providerPayload["imageUrls"]} { switch items := value.(type) { case []any: count += len(items) case []string: count += len(items) } } return count }