功能增加

This commit is contained in:
andy committed 2026-09-11 15:33:18 +08:00
1 parent b340fc7ad7
commit f1993eb388
69 files changed
+2243 -162

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
package billing
import "testing"
func TestA4QuoteUsesExisting1KCostAndMarkup(t *testing.T) {
for _, size := range []struct {
width, height int
ratio string
}{{1680, 2376, "70:99"}, {2376, 1680, "99:70"}, {848, 1200, "70:99"}, {1200, 848, "99:70"}} {
dimensions := map[string]any{"width": size.width, "height": size.height}
for _, payload := range []map[string]any{dimensions, {"settings": dimensions}, {"input": dimensions}, {"providerPayload": dimensions}} {
parameters := NormalizeBillingParameters(payload, Parameters{"resolution": "1K", "quality": "medium"})
if parameters["aspectRatio"] != size.ratio {
t.Fatalf("parameters=%#v", parameters)
}
quote, err := (Catalog{Rules: DefaultBillingPriceRules()}).Quote(QuoteInput{Provider: "evolink", Capability: "image.generate", ReqKey: "gpt-image-2", Source: "platform", Parameters: parameters})
if err != nil || quote == nil || quote.StandardUnitPriceFen != 34 || quote.MarkupMultiplier != 1.2 || quote.AmountFen != 41 {
t.Fatalf("A4 quote=%#v err=%v", quote, err)
}
}
}
}
+10 -4
View File
@@ -35,6 +35,8 @@ func DefaultBillingPriceRules() []PriceRule {
{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")},
{ID: "base-minimax-h3-768p", Provider: "minimax", Capability: "video.generate", ReqKey: MinimaxH3Model, VariantKey: "resolution=768p", Unit: UnitVideoSecond, StandardUnitPriceFen: 50, Note: "MiniMax H3 768P 输出 ¥0.50/秒;首五张输入图免费。", Source: minimaxCatalogSource()},
{ID: "base-minimax-h3-2k", Provider: "minimax", Capability: "video.generate", ReqKey: MinimaxH3Model, VariantKey: "resolution=2k", Unit: UnitVideoSecond, StandardUnitPriceFen: 80, Note: "MiniMax H3 2K 输出 ¥0.80/秒;首五张输入图免费。", Source: minimaxCatalogSource()},
}
for i := range rules {
rules[i].Enabled = true
@@ -43,6 +45,10 @@ func DefaultBillingPriceRules() []PriceRule {
return rules
}
func minimaxCatalogSource() map[string]any {
return map[string]any{"url": "https://platform.minimaxi.com/docs/guides/pricing-paygo#视频", "observedAt": "2026-09-04"}
}
func seedreamDimensions() []ParameterDimension {
return []ParameterDimension{{
Key: "size", Label: "分辨率", BaselineValue: "1.5K", DefaultValue: "1.5K",
@@ -70,7 +76,7 @@ func evolinkDimensions() []ParameterDimension {
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, "3:4", 1.0, "16:9", 1.0, "9:16", 1.0)},
{Key: "aspectRatio", BaselineValue: "1:1", DefaultValue: "1:1", Tiers: tiers("1:1", 1.0, "4:3", 1.0, "3:4", 1.0, "16:9", 1.0, "9:16", 1.0, "70:99", 1.0, "99:70", 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},
@@ -115,8 +121,8 @@ func NormalizeBillingParameters(payload map[string]any, fallback Parameters) Par
out["imageCount"] = float64(17)
}
width, widthOK := finiteNumber(first(providerPayload["width"], input["width"]))
height, heightOK := finiteNumber(first(providerPayload["height"], input["height"]))
width, widthOK := finiteNumber(first(providerPayload["width"], input["width"], settings["width"], payload["width"], out["width"]))
height, heightOK := finiteNumber(first(providerPayload["height"], input["height"], settings["height"], payload["height"], out["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 {
@@ -187,7 +193,7 @@ func aspectRatio(width, height float64) string {
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"}} {
}{{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"}, {70.0 / 99, "70:99"}, {99.0 / 70, "99:70"}} {
if math.Abs(ratio-item.value) < .02 {
return item.label
}
@@ -19,6 +19,8 @@ func TestDefaultBillingPriceRulesFreezeOfficialCatalogMatchKeys(t *testing.T) {
"base-seedance-2-5-480p": false,
"base-seedance-2-5-720p": false,
"base-seedance-2-5-1080p": false,
"base-minimax-h3-768p": false,
"base-minimax-h3-2k": false,
}
for _, rule := range rules {
if _, ok := want[rule.ID]; !ok {
+49
View File
@@ -0,0 +1,49 @@
package billing
import (
"errors"
"math"
"strings"
)
const MinimaxH3Model = "MiniMax-H3"
type MinimaxH3ActualAmountInput struct {
Resolution string
OutputSeconds int64
InputVideoSeconds int64
InputImageCount int64
MarkupMultiplier float64
}
func MinimaxH3VideoPriceFenPerSecond(resolution string) int64 {
if strings.EqualFold(strings.TrimSpace(resolution), "2K") {
return 80
}
return 50
}
// CalculateMinimaxH3ActualAmountFen follows the official pay-as-you-go H3
// formula. The first five input images are free; this first product release
// accepts at most one, while keeping the calculator correct for future modes.
func CalculateMinimaxH3ActualAmountFen(input MinimaxH3ActualAmountInput) (int64, error) {
if input.OutputSeconds <= 0 {
return 0, errors.New("MiniMax H3 output seconds must be positive")
}
if input.InputVideoSeconds < 0 || input.InputImageCount < 0 {
return 0, errors.New("MiniMax H3 input usage must not be negative")
}
if input.MarkupMultiplier < 1 {
return 0, errors.New("MiniMax H3 markup multiplier must be at least 1")
}
videoSeconds := input.OutputSeconds + input.InputVideoSeconds
standardAmountFen := videoSeconds * MinimaxH3VideoPriceFenPerSecond(input.Resolution)
if input.InputImageCount > 5 {
standardAmountFen += (input.InputImageCount - 5) * 20
}
return int64(math.Ceil(float64(standardAmountFen) * input.MarkupMultiplier)), nil
}
func IsMinimaxH3BillingModel(model string) bool {
return strings.EqualFold(strings.TrimSpace(model), MinimaxH3Model)
}
+5
View File
@@ -109,6 +109,11 @@ func (s *Service) Quote(ctx context.Context, command QuoteCommand) (*Quote, erro
quote.Parameters["inputReferencePriceFen"] = float64(max(0, referenceImageCount-1) * 2)
return quote, nil
}
if command.Provider == "minimax" && IsMinimaxH3BillingModel(command.ReqKey) {
quote.ReservedAmountFen = quote.AmountFen
quote.SettlementStatus = "pending"
return quote, nil
}
if command.Provider != "seedance" || !IsSeedanceBillingModel(command.ReqKey) {
return quote, nil
}
+17
View File
@@ -115,6 +115,23 @@ func TestServiceQuoteUsesSeedance25CatalogAndTokenEstimate(t *testing.T) {
}
}
func TestServiceQuoteReservesMinimaxH3OfficialRateWithMarkup(t *testing.T) {
store := &quoteStoreStub{rules: []PriceRule{{
ID: "minimax-h3-2k", Provider: "minimax", Capability: "video.generate", ReqKey: MinimaxH3Model,
VariantKey: "resolution=2k", Unit: UnitVideoSecond, StandardUnitPriceFen: 80, MarkupMultiplier: 1.2, Enabled: true,
}}}
quote, err := NewService(store, nil).Quote(context.Background(), QuoteCommand{
Provider: "minimax", Capability: "video.generate", ReqKey: MinimaxH3Model,
Payload: map[string]any{"settings": map[string]any{"duration": 5.0, "resolution": "2K", "ratio": "16:9"}},
})
if err != nil {
t.Fatal(err)
}
if quote.AmountFen != 480 || quote.ReservedAmountFen != 480 || quote.SettlementStatus != "pending" || quote.Parameters["resolution"] != "2k" {
t.Fatalf("quote=%#v", quote)
}
}
func TestServiceQuoteAppliesSeedreamSizeAndReferenceInputPricing(t *testing.T) {
store := &quoteStoreStub{rules: []PriceRule{{
ID: "seedream-5", Provider: "seedream", Capability: "image.generate", ReqKey: Seedream50ProModel,
@@ -31,6 +31,29 @@ func TestSeedanceActualAmountUsesFrozenTokenPricingAndRoundsUp(t *testing.T) {
}
}
func TestMinimaxH3ActualAmountUsesOfficialPerSecondPricingAndMarkup(t *testing.T) {
tests := []struct {
resolution string
outputSeconds int64
inputVideoSeconds int64
inputImageCount int64
want int64
}{
{resolution: "768P", outputSeconds: 5, inputImageCount: 1, want: 300},
{resolution: "2K", outputSeconds: 5, inputImageCount: 1, want: 480},
{resolution: "2k", outputSeconds: 4, inputVideoSeconds: 2, inputImageCount: 7, want: 624},
}
for _, test := range tests {
got, err := CalculateMinimaxH3ActualAmountFen(MinimaxH3ActualAmountInput{
Resolution: test.resolution, OutputSeconds: test.outputSeconds, InputVideoSeconds: test.inputVideoSeconds,
InputImageCount: test.inputImageCount, MarkupMultiplier: 1.2,
})
if err != nil || got != test.want {
t.Fatalf("resolution=%s amount=%d err=%v want=%d", test.resolution, got, err, test.want)
}
}
}
func TestLedgerSettlesSeedanceDeltaWithOneFrozenIdempotencyKey(t *testing.T) {
poster := &recordingPoster{result: WalletPosting{LedgerID: "settlement-ledger", CreatedAt: time.Unix(7, 0)}}
ledger := Ledger{Poster: poster, NewID: func() string { return "new-ledger" }}