功能增加

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

+60 -5
View File
@@ -17,6 +17,7 @@ import {
import { getGenerationJob, updateGenerationJob } from "@/lib/server/data-store";
import { SEEDANCE_20_MODEL, SEEDANCE_25_MODEL } from "@/lib/video-settings";
import { calculateSeedreamAmountFen, SEEDREAM_50_PRO_MODEL } from "@/lib/server/seedream-billing";
import { calculateMinimaxH3ActualAmountFen, MINIMAX_H3_MODEL, minimaxH3VideoPriceFenPerSecond } from "@/lib/server/minimax-billing";
import type {
BillingAccountConfig,
BillingConditionValue,
@@ -99,6 +100,9 @@ export async function quoteGenerationCharge(input: BillingGenerationInput): Prom
}
};
}
if (input.provider === "minimax" && input.reqKey === MINIMAX_H3_MODEL) {
return { ...quote, reservedAmountFen: quote.amountFen, settlementStatus: "pending" };
}
if (input.provider !== "seedance" || !isSeedanceBillingModel(input.reqKey)) {
return quote;
}
@@ -131,7 +135,7 @@ export async function chargeGenerationJob(job: GenerationJob): Promise<Generatio
...billing,
quotaExempt: true,
status: "not_charged",
settlementStatus: job.provider === "seedance" ? "pending" : billing.settlementStatus
settlementStatus: job.provider === "seedance" || job.provider === "minimax" ? "pending" : billing.settlementStatus
}
});
}
@@ -156,7 +160,7 @@ export async function chargeGenerationJob(job: GenerationJob): Promise<Generatio
...billing,
reservedAmountFen,
status: "charged",
settlementStatus: job.provider === "seedance" ? "pending" : billing.settlementStatus,
settlementStatus: job.provider === "seedance" || job.provider === "minimax" ? "pending" : billing.settlementStatus,
ledgerEntryId: result.entry.id,
chargedAt: result.entry.createdAt
}
@@ -272,6 +276,57 @@ export async function settleSeedanceGenerationCharge(
});
}
export async function settleMinimaxGenerationCharge(
jobOrId: GenerationJob | string,
usage?: { outputSeconds: number; inputSeconds: number; inputImageCount: number }
): Promise<GenerationJob | null> {
const job = typeof jobOrId === "string" ? await getGenerationJob(jobOrId) : jobOrId;
if (!job) return null;
const billing = job.billing;
const quotaExempt = isQuotaExemptBilling(job);
const organizationId = job.usageContext?.organizationId;
const chargeReady = billing?.status === "charged" || quotaExempt && billing?.status === "not_charged";
if (job.provider !== "minimax" || job.reqKey !== MINIMAX_H3_MODEL || !billing || !chargeReady || !quotaExempt && !organizationId) return job;
if (billing.settlementStatus === "settled" || billing.settlementStatus === "estimated") return job;
if (!usage || !Number.isFinite(usage.outputSeconds) || usage.outputSeconds <= 0) {
return updateGenerationJob(job.id, { billing: { ...billing, settlementStatus: "estimated", settlementReason: "provider_usage_unavailable", settledAt: new Date().toISOString() } });
}
const resolution = billing.parameters?.resolution;
const actualAmountFen = calculateMinimaxH3ActualAmountFen({
resolution, outputSeconds: usage.outputSeconds, inputVideoSeconds: usage.inputSeconds,
inputImageCount: usage.inputImageCount, markupMultiplier: billing.markupMultiplier
});
const deltaFen = actualAmountFen - billing.amountFen;
let settlementLedgerEntryId: string | undefined;
let settledAt = new Date().toISOString();
if (deltaFen !== 0 && !quotaExempt && organizationId) {
const settlement = await postWalletEntry({
organizationId, accountId: job.usageContext?.accountId, jobId: job.id,
kind: deltaFen > 0 ? "charge" : "refund", deltaFen: -deltaFen,
idempotencyKey: `job-settlement:${job.id}`,
description: deltaFen > 0 ? `${capabilityLabel(job.capability)}实际用量补扣` : `${capabilityLabel(job.capability)}实际用量差额退回`,
metadata: {
operation: "minimax_h3_actual_settlement", reservedAmountFen: billing.reservedAmountFen ?? billing.amountFen,
actualAmountFen, outputSeconds: usage.outputSeconds, inputVideoSeconds: usage.inputSeconds,
inputImageCount: usage.inputImageCount, resolution: String(resolution || "768P"), model: job.reqKey
}
});
settlementLedgerEntryId = settlement.entry.id;
settledAt = settlement.entry.createdAt;
}
return updateGenerationJob(job.id, {
billing: {
...billing, amountFen: actualAmountFen, settlementStatus: "settled", settlementLedgerEntryId, settledAt,
providerUsage: {
model: job.reqKey, resolution: String(resolution || "768P"), outputSeconds: usage.outputSeconds,
inputVideoSeconds: usage.inputSeconds, inputImageCount: usage.inputImageCount,
videoPriceFenPerSecond: minimaxH3VideoPriceFenPerSecond(resolution)
}
}
});
}
function isSeedanceBillingModel(model: string) {
return model === SEEDANCE_20_MODEL || model === SEEDANCE_25_MODEL;
}
@@ -418,8 +473,8 @@ export function normalizeBillingParameters(requestPayload: Record<string, unknow
input?.size
), normalizeSizeParameter);
const width = numberParameter(firstValue(providerPayload?.width, input?.width));
const height = numberParameter(firstValue(providerPayload?.height, input?.height));
const width = numberParameter(firstValue(providerPayload?.width, input?.width, settings?.width, requestPayload.width));
const height = numberParameter(firstValue(providerPayload?.height, input?.height, settings?.height, requestPayload.height));
if (parameters.size === undefined && width !== undefined && height !== undefined) {
parameters.size = `${width}*${height}`;
}
@@ -624,7 +679,7 @@ function normalizeAspectRatioParameter(value: unknown, width?: number, height?:
if (normalized) return normalized;
if (width === undefined || height === undefined || height <= 0) return undefined;
const ratio = width / height;
const common = [[1, 1], [4, 3], [3, 2], [16, 9], [9, 16], [21, 9], [9, 21], [2, 3], [3, 4]] as const;
const common = [[1, 1], [4, 3], [3, 2], [16, 9], [9, 16], [21, 9], [9, 21], [2, 3], [3, 4], [70, 99], [99, 70]] as const;
const match = common.find(([numerator, denominator]) => Math.abs(ratio - numerator / denominator) < 0.02);
return match ? `${match[0]}:${match[1]}` : ratio.toFixed(3).replace(/0+$/, "").replace(/\.$/, "");
}