Files
NianAIGC/backend/internal/billing/defaults.go

178 lines
8.9 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 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
}