package billing import ( "context" "fmt" "math" "strings" ) const ( DefaultBillingMarkupMultiplier = 1.2 BillingCatalogObservedAt = "2026-08-21" ) // 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-seedream-5-0-pro", Provider: "seedream", Capability: "image.generate", ReqKey: Seedream50ProModel, Unit: UnitImage, StandardUnitPriceFen: 30, Note: "基础生图:1K/1.5K ¥0.30,2K ¥0.60;首张输入图免费,额外输入图 ¥0.02/张。", Source: catalogSource("https://docs.volcengine.com/docs/82379/1544106?lang=zh"), Dimensions: seedreamDimensions()}, {ID: "base-seedream-5-0-pro-layers", Provider: "seedream", Capability: "image.generate", ReqKey: Seedream50ProModel, Unit: UnitImage, StandardUnitPriceFen: 15, Conditions: Conditions{"layerDecomposition": true}, Priority: 10, Note: "图层拆分:≤261万像素 ¥0.15/张,>261万像素 ¥0.30/张;最多预留底图加16个图层,完成后按每张实际像素结算。", Source: catalogSource("https://docs.volcengine.com/docs/82379/1544106?lang=zh"), Dimensions: seedreamDimensions()}, {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")}, {ID: "base-seedance-2-5-480p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-5-260628", VariantKey: "resolution=480p", Unit: UnitVideoSecond, StandardUnitPriceFen: 67, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh#sd25_price")}, {ID: "base-seedance-2-5-720p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-5-260628", VariantKey: "resolution=720p", Unit: UnitVideoSecond, StandardUnitPriceFen: 151, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh#sd25_price")}, {ID: "base-seedance-2-5-1080p", Provider: "seedance", Capability: "video.generate", ReqKey: "doubao-seedance-2-5-260628", VariantKey: "resolution=1080p", Unit: UnitVideoSecond, StandardUnitPriceFen: 374, Source: catalogSource("https://www.volcengine.com/docs/82379/1544106?lang=zh#sd25_price")}, } for i := range rules { rules[i].Enabled = true rules[i].MarkupMultiplier = DefaultBillingMarkupMultiplier } return rules } func seedreamDimensions() []ParameterDimension { return []ParameterDimension{{ Key: "size", Label: "分辨率", BaselineValue: "1.5K", DefaultValue: "1.5K", Tiers: []ParameterTier{ {Value: "auto", Label: "自动(图层拆分)", StandardFactor: 2, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, {Value: "1K", Label: "1K", StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, {Value: "1.5K", Label: "1.5K", StandardFactor: 1, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, {Value: "2K", Label: "2K", StandardFactor: 2, MarkupMultiplier: DefaultBillingMarkupMultiplier, Enabled: true}, }, }} } 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(settings["imageCount"], providerPayload["n"], providerParameters["n"], input["n"], input["imageCount"]), true) setNumber(out, "scale", first(input["scale"], settings["scale"]), false) setText(out, "outputFormat", first(settings["outputFormat"], providerPayload["output_format"], inputSettings["outputFormat"], input["outputFormat"])) setText(out, "optimizeMode", first(settings["optimizeMode"], record(providerPayload["optimize_prompt_options"])["mode"], inputSettings["optimizeMode"], input["optimizeMode"])) setText(out, "creationMode", first(settings["creationMode"], inputSettings["creationMode"], input["creationMode"])) if value, ok := boolean(first(settings["layerDecomposition"], providerPayload["layer_decomposition"], inputSettings["layerDecomposition"], input["layerDecomposition"])); ok { out["layerDecomposition"] = value } if strings.EqualFold(fmt.Sprint(out["creationMode"]), "layers") { out["layerDecomposition"] = true } if value, _ := out["layerDecomposition"].(bool); value { // Seedream can return one base image plus at most sixteen layers. Reserve // the documented maximum, then settle each returned image by pixel tier. out["imageCount"] = float64(17) } width, widthOK := finiteNumber(first(providerPayload["width"], input["width"])) height, heightOK := finiteNumber(first(providerPayload["height"], input["height"])) if size := normalizedText(first(settings["size"], 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 { for _, value := range []any{payload["imageUrls"], payload["inputUrls"], input["imageUrls"], providerPayload["image_urls"], providerPayload["imageUrls"]} { switch items := value.(type) { case []any: if len(items) > 0 { return len(items) } case []string: if len(items) > 0 { return len(items) } } } count := 0 for _, material := range billingMaterials(payload) { materialType := strings.ToLower(strings.TrimSpace(fmt.Sprint(material["type"]))) if materialType == "" || materialType == "image" { count++ } } return count }