功能增加
This commit is contained in:
1 parent
b340fc7ad7
commit
f1993eb388
69 files changed
+2243
-162
No files matched your search
@@ -306,7 +306,7 @@ func billingSQLState(err error) string {
|
||||
|
||||
func retryEngine(provider, capability string) string {
|
||||
if capability == "video.generate" {
|
||||
if provider == "seedance" || provider == "bailian" {
|
||||
if provider == "seedance" || provider == "bailian" || provider == "minimax" {
|
||||
return provider
|
||||
}
|
||||
return ""
|
||||
|
||||
@@ -30,6 +30,12 @@ func TestCreationCoordinatorDoesNotFailCreationWhenAtomicCommitOutcomeIsUnknown(
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryEnginePreservesMinimaxVideoProvider(t *testing.T) {
|
||||
if got := retryEngine("minimax", "video.generate"); got != "minimax" {
|
||||
t.Fatalf("retry engine = %q, want minimax", got)
|
||||
}
|
||||
}
|
||||
|
||||
type commitUnknownCreationState struct {
|
||||
failCalls int
|
||||
}
|
||||
|
||||
@@ -24,9 +24,9 @@ type FencedSettlementStateWriter interface {
|
||||
WriteBillingFenced(context.Context, string, json.RawMessage, jobs.Status, string) error
|
||||
}
|
||||
|
||||
// SettlementProcessor adds actual-usage settlement for Seedance videos and
|
||||
// Seedream layer decomposition. Other providers and non-successful advances
|
||||
// pass through.
|
||||
// SettlementProcessor adds actual-usage settlement for Seedance and MiniMax
|
||||
// videos plus Seedream layer decomposition. Other providers and unsuccessful
|
||||
// advances pass through.
|
||||
type SettlementProcessor struct {
|
||||
inner jobs.Processor
|
||||
ledger SettlementLedger
|
||||
@@ -52,6 +52,9 @@ func (p *SettlementProcessor) Advance(ctx context.Context, job jobs.Job) (jobs.J
|
||||
if advanced.Provider == "seedream" && seedreamLayerRequest(advanced.RequestPayload) {
|
||||
return p.settleSeedreamLayers(ctx, advanced)
|
||||
}
|
||||
if advanced.Provider == "minimax" && billing.IsMinimaxH3BillingModel(advanced.ReqKey) {
|
||||
return p.settleMinimaxH3(ctx, advanced)
|
||||
}
|
||||
if advanced.Provider != "seedance" {
|
||||
return advanced, nil
|
||||
}
|
||||
@@ -139,6 +142,85 @@ func (p *SettlementProcessor) Advance(ctx context.Context, job jobs.Job) (jobs.J
|
||||
return p.write(ctx, advanced, charge.raw)
|
||||
}
|
||||
|
||||
func (p *SettlementProcessor) settleMinimaxH3(ctx context.Context, job jobs.Job) (jobs.Job, error) {
|
||||
var charge billingState
|
||||
if json.Unmarshal(job.Billing, &charge) != nil {
|
||||
return jobs.Job{}, errors.New("settle MiniMax H3 generation charge")
|
||||
}
|
||||
status, _ := charge.raw["settlementStatus"].(string)
|
||||
if status == "settled" || status == "estimated" {
|
||||
return job, nil
|
||||
}
|
||||
var use usageContext
|
||||
if len(job.UsageContext) != 0 && json.Unmarshal(job.UsageContext, &use) != nil {
|
||||
return jobs.Job{}, errors.New("settle MiniMax H3 generation charge")
|
||||
}
|
||||
quotaExempt := charge.QuotaExempt || use.Source == "platform" && use.Role == "super_admin"
|
||||
chargeReady := charge.Status == "charged" || quotaExempt && charge.Status == "not_charged"
|
||||
if !chargeReady || !quotaExempt && use.OrganizationID == "" {
|
||||
return job, nil
|
||||
}
|
||||
|
||||
usage := minimaxH3Usage(job.ResponsePayload)
|
||||
if usage.outputSeconds <= 0 {
|
||||
charge.raw["settlementStatus"] = "estimated"
|
||||
charge.raw["settlementReason"] = "provider_usage_unavailable"
|
||||
charge.raw["settledAt"] = p.now().UTC().Format(time.RFC3339Nano)
|
||||
return p.write(ctx, job, charge.raw)
|
||||
}
|
||||
resolution := minimaxResolution(charge.raw)
|
||||
actualAmount, err := billing.CalculateMinimaxH3ActualAmountFen(billing.MinimaxH3ActualAmountInput{
|
||||
Resolution: resolution, OutputSeconds: usage.outputSeconds, InputVideoSeconds: usage.inputSeconds,
|
||||
InputImageCount: usage.inputImageCount, MarkupMultiplier: numberOrZero(charge.raw["markupMultiplier"]),
|
||||
})
|
||||
if err != nil {
|
||||
return jobs.Job{}, errors.New("settle MiniMax H3 generation charge")
|
||||
}
|
||||
delta := actualAmount - charge.AmountFen
|
||||
settledAt := p.now().UTC()
|
||||
var settlementLedgerID string
|
||||
if delta != 0 && !quotaExempt {
|
||||
if p.ledger == nil {
|
||||
return jobs.Job{}, errors.New("settle MiniMax H3 generation charge")
|
||||
}
|
||||
description := capabilityLabel(job.Capability) + "实际用量差额退回"
|
||||
if delta > 0 {
|
||||
description = capabilityLabel(job.Capability) + "实际用量补扣"
|
||||
}
|
||||
posting, postErr := p.ledger.Settle(ctx, billing.SettlementRequest{
|
||||
OrganizationID: use.OrganizationID, AccountID: use.AccountID, JobID: job.ID,
|
||||
DeltaFen: delta, Description: description,
|
||||
Metadata: map[string]any{
|
||||
"operation": "minimax_h3_actual_settlement", "reservedAmountFen": charge.AmountFen,
|
||||
"actualAmountFen": actualAmount, "outputSeconds": usage.outputSeconds,
|
||||
"inputVideoSeconds": usage.inputSeconds, "inputImageCount": usage.inputImageCount,
|
||||
"resolution": resolution, "model": job.ReqKey,
|
||||
},
|
||||
})
|
||||
if postErr != nil {
|
||||
return jobs.Job{}, errors.New("settle MiniMax H3 generation charge")
|
||||
}
|
||||
settlementLedgerID = posting.LedgerID
|
||||
if !posting.CreatedAt.IsZero() {
|
||||
settledAt = posting.CreatedAt.UTC()
|
||||
}
|
||||
}
|
||||
charge.raw["amountFen"] = actualAmount
|
||||
charge.raw["settlementStatus"] = "settled"
|
||||
charge.raw["settledAt"] = settledAt.Format(time.RFC3339Nano)
|
||||
if settlementLedgerID != "" {
|
||||
charge.raw["settlementLedgerEntryId"] = settlementLedgerID
|
||||
} else {
|
||||
delete(charge.raw, "settlementLedgerEntryId")
|
||||
}
|
||||
charge.raw["providerUsage"] = map[string]any{
|
||||
"model": job.ReqKey, "resolution": resolution, "outputSeconds": usage.outputSeconds,
|
||||
"inputVideoSeconds": usage.inputSeconds, "inputImageCount": usage.inputImageCount,
|
||||
"videoPriceFenPerSecond": billing.MinimaxH3VideoPriceFenPerSecond(resolution),
|
||||
}
|
||||
return p.write(ctx, job, charge.raw)
|
||||
}
|
||||
|
||||
func (p *SettlementProcessor) settleSeedreamLayers(ctx context.Context, job jobs.Job) (jobs.Job, error) {
|
||||
var charge billingState
|
||||
if json.Unmarshal(job.Billing, &charge) != nil {
|
||||
@@ -242,6 +324,34 @@ func seedanceCompletionTokens(payload json.RawMessage) int64 {
|
||||
return value
|
||||
}
|
||||
|
||||
type minimaxUsage struct {
|
||||
outputSeconds int64
|
||||
inputSeconds int64
|
||||
inputImageCount int64
|
||||
}
|
||||
|
||||
func minimaxH3Usage(payload json.RawMessage) minimaxUsage {
|
||||
var response struct {
|
||||
Usage map[string]any `json:"usage"`
|
||||
}
|
||||
if json.Unmarshal(payload, &response) != nil {
|
||||
return minimaxUsage{}
|
||||
}
|
||||
outputSeconds, _ := nonNegativeInteger(response.Usage["outputSeconds"])
|
||||
inputSeconds, _ := nonNegativeInteger(response.Usage["inputSeconds"])
|
||||
inputImageCount, _ := nonNegativeInteger(response.Usage["inputImageCount"])
|
||||
return minimaxUsage{outputSeconds: outputSeconds, inputSeconds: inputSeconds, inputImageCount: inputImageCount}
|
||||
}
|
||||
|
||||
func minimaxResolution(snapshot map[string]any) string {
|
||||
parameters, _ := snapshot["parameters"].(map[string]any)
|
||||
resolution, _ := parameters["resolution"].(string)
|
||||
if strings.EqualFold(strings.TrimSpace(resolution), "2K") {
|
||||
return "2K"
|
||||
}
|
||||
return "768P"
|
||||
}
|
||||
|
||||
func seedreamLayerRequest(payload json.RawMessage) bool {
|
||||
var request struct {
|
||||
Settings map[string]any `json:"settings"`
|
||||
@@ -341,4 +451,12 @@ func integer(value any) (int64, bool) {
|
||||
return int64(number), true
|
||||
}
|
||||
|
||||
func nonNegativeInteger(value any) (int64, bool) {
|
||||
number, ok := value.(float64)
|
||||
if !ok || number < 0 || number != float64(int64(number)) {
|
||||
return 0, false
|
||||
}
|
||||
return int64(number), true
|
||||
}
|
||||
|
||||
var _ jobs.Processor = (*SettlementProcessor)(nil)
|
||||
@@ -102,6 +102,28 @@ func TestSettlementProcessorLeavesNonSeedanceSuccessAlone(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettlementProcessorSettlesSuccessfulMinimaxH3Usage(t *testing.T) {
|
||||
state := &settlementStateStub{}
|
||||
ledger := &settlementLedgerStub{posting: billing.WalletPosting{LedgerID: "minimax-settlement", CreatedAt: time.Date(2026, 9, 4, 8, 0, 0, 0, time.UTC)}}
|
||||
advanced := jobs.Job{
|
||||
ID: "job-h3", Provider: "minimax", ReqKey: billing.MinimaxH3Model, Capability: "video.generate", Status: jobs.StatusSucceeded,
|
||||
ResponsePayload: json.RawMessage(`{"usage":{"outputSeconds":6,"inputSeconds":0,"inputImageCount":1}}`),
|
||||
Billing: json.RawMessage(`{"status":"charged","amountFen":300,"reservedAmountFen":300,"markupMultiplier":1.2,"settlementStatus":"pending","parameters":{"resolution":"768p"}}`),
|
||||
UsageContext: json.RawMessage(`{"organizationId":"org-1","accountId":"account-1"}`),
|
||||
}
|
||||
processor := NewSettlementProcessor(settlementProcessorStub{job: advanced}, ledger, state, time.Now)
|
||||
got, err := processor.Advance(context.Background(), jobs.Job{ID: "job-h3", Provider: "minimax", Status: jobs.StatusRunning})
|
||||
if err != nil || ledger.calls != 1 || ledger.request.DeltaFen != 60 || ledger.request.Metadata["operation"] != "minimax_h3_actual_settlement" {
|
||||
t.Fatalf("job=%#v request=%#v calls=%d err=%v", got, ledger.request, ledger.calls, err)
|
||||
}
|
||||
var snapshot map[string]any
|
||||
_ = json.Unmarshal(got.Billing, &snapshot)
|
||||
usage := snapshot["providerUsage"].(map[string]any)
|
||||
if snapshot["amountFen"] != float64(360) || snapshot["settlementStatus"] != "settled" || usage["outputSeconds"] != float64(6) || usage["videoPriceFenPerSecond"] != float64(50) {
|
||||
t.Fatalf("billing snapshot=%#v", snapshot)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSettlementProcessorRefundsUnusedSeedreamLayerReservation(t *testing.T) {
|
||||
state := &settlementStateStub{}
|
||||
ledger := &settlementLedgerStub{posting: billing.WalletPosting{LedgerID: "layer-settlement", CreatedAt: time.Date(2026, 8, 21, 8, 0, 0, 0, time.UTC)}}
|
||||
|
||||
Reference in new issue
Block a user