功能增加
This commit is contained in:
1 parent
b340fc7ad7
commit
f1993eb388
69 files changed
+2243
-162
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -115,6 +115,23 @@ func TestServiceQuoteUsesSeedance25CatalogAndTokenEstimate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestServiceQuoteReservesMinimaxH3OfficialRateWithMarkup(t *testing.T) {
|
||||
store := "eStoreStub{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 := "eStoreStub{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" }}
|
||||
|
||||
Reference in new issue
Block a user