功能增加

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

View File

@@ -9,6 +9,7 @@ import { rootDir } from "@/lib/server/runtime";
import { getVisualClientConfig } from "@/lib/volcengine/visual-client";
import type { EnabledImageCapability } from "@/lib/types";
import { getBailianConfig } from "@/lib/bailian/client";
import { getMinimaxConfig, MINIMAX_H3_MODEL } from "@/lib/minimax/client";
type SettingsImageCreationEngine = ImageCreationEngine | "seedream";
const SEEDREAM_50_PRO_MODEL = "doubao-seedream-5-0-pro-260628";
@@ -132,6 +133,15 @@ const settingDefinitions: Array<{
{ key: "BAILIAN_VIDEO_MODEL", label: "视频模型", defaultValue: "wan2.7-i2v-2026-04-25" }
]
},
{
id: "minimax",
title: "MiniMax API",
description: "用于 MiniMax H3 文生视频与单首帧图生视频。",
fields: [
{ key: "MINIMAX_API_KEY", label: "MiniMax API Key", secret: true, type: "password" },
{ key: "MINIMAX_BASE_URL", label: "Base URL", defaultValue: "https://api.minimax.cn" }
]
},
{
id: "oss",
title: "OSS 资产存储",
@@ -166,6 +176,7 @@ const engineFieldDefinitions: FieldDefinition[] = [
defaultValue: "bailian",
options: [
{ label: "Seedance", value: "seedance" },
{ label: "MiniMax H3", value: "minimax" },
{ label: "阿里云百炼 Wan 2.7", value: "bailian" }
]
},
@@ -199,6 +210,7 @@ export async function getApiSettings() {
seedance: Boolean(seedance.apiKey),
seedream: Boolean(seedance.apiKey),
bailian: Boolean(getBailianConfig().apiKey),
minimax: Boolean(getMinimaxConfig().apiKey),
auth: authConfigSummary(auth) === "configured",
organization: platformAccountStoreConfigured()
},
@@ -254,9 +266,12 @@ function buildEngineAssignments(fileEnv: Map<string, string>): EngineAssignment[
const evolink = getEvolinkImageSettings();
const seedance = getSeedanceConfig();
const bailian = getBailianConfig();
const minimax = getMinimaxConfig();
const generateEngine = currentEngineValue(engineFieldDefinitions[0], fileEnv);
const videoEngine = currentVideoEngineValue(engineFieldDefinitions[1], fileEnv);
const videoConnected = videoEngine === "bailian" ? Boolean(bailian.apiKey) : Boolean(seedance.apiKey);
const videoConnected = videoEngine === "bailian"
? Boolean(bailian.apiKey)
: videoEngine === "minimax" ? Boolean(minimax.apiKey) : Boolean(seedance.apiKey);
return [
imageEngineAssignment({
@@ -271,10 +286,10 @@ function buildEngineAssignments(fileEnv: Map<string, string>): EngineAssignment[
id: "video.generate",
label: "视频生成",
engine: videoEngine,
engineLabel: videoEngine === "bailian" ? "阿里云百炼" : "Seedance",
engineLabel: videoEngine === "bailian" ? "阿里云百炼" : videoEngine === "minimax" ? "MiniMax H3" : "Seedance",
connected: videoConnected,
connectionLabel: videoConnected ? "已连接" : "待配置",
reqKey: videoEngine === "bailian" ? bailian.videoModel : seedance.model,
reqKey: videoEngine === "bailian" ? bailian.videoModel : videoEngine === "minimax" ? MINIMAX_H3_MODEL : seedance.model,
configurable: true,
field: fieldWithValue(engineFieldDefinitions[1], videoEngine, isConfigured(engineFieldDefinitions[1].key, fileEnv))
}
@@ -339,7 +354,9 @@ function imageEngineConnected(engine: SettingsImageCreationEngine) {
}
function currentVideoEngineValue(field: FieldDefinition, fileEnv: Map<string, string>) {
return (process.env[field.key] ?? fileEnv.get(field.key) ?? field.defaultValue) === "bailian" ? "bailian" : "seedance";
const value = (process.env[field.key] ?? fileEnv.get(field.key) ?? field.defaultValue)?.trim().toLowerCase();
if (value === "bailian" || value === "minimax") return value;
return "seedance";
}
function envFilePath() {

View File

@@ -16,6 +16,7 @@ const EVOLINK_IMAGE_URL = "https://evolink.ai/zh/gpt-image-2";
const VOLC_ARK_PRICING_URL = "https://www.volcengine.com/docs/82379/1544106?lang=zh";
const JIMENG_PRICING_URL = "https://www.volcengine.com/activity/jimeng";
const SEEDREAM_PRICING_URL = "https://docs.volcengine.com/docs/82379/1544106?lang=zh";
const MINIMAX_PRICING_URL = "https://platform.minimaxi.com/docs/guides/pricing-paygo#视频";
const SEEDREAM_50_PRO_DIMENSIONS = [
{
@@ -65,6 +66,8 @@ const EVOLINK_GPT_IMAGE_DIMENSIONS = [
{ value: "3:4", label: "3:4", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
{ value: "16:9", label: "16:9", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
{ value: "9:16", label: "9:16", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
{ value: "70:99", label: "A4 竖版", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
{ value: "99:70", label: "A4 横版", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
{ value: "other", label: "其他比例", match: {}, standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true, note: "同一分辨率档位下按相同比例预算估算。" }
]
},
@@ -328,6 +331,44 @@ export const DEFAULT_BILLING_PRICE_RULES: BillingPriceRuleInput[] = [
basis: "doubao-seedance-2.5,1080P,无输入视频、16:9、5 秒示例折算",
observedAt: BILLING_CATALOG_OBSERVED_AT
}
},
{
id: "base-minimax-h3-768p",
provider: "minimax",
capability: "video.generate",
reqKey: "MiniMax-H3",
variantKey: "resolution=768p",
unit: "video_second",
standardUnitPriceFen: 50,
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
enabled: true,
note: "MiniMax H3 768P 输出 ¥0.50/秒;首五张输入图免费。",
source: {
url: MINIMAX_PRICING_URL,
currency: "CNY",
unitPrice: 0.5,
basis: "MiniMax H3 768P 输出视频,按成功视频秒数计费",
observedAt: "2026-09-04"
}
},
{
id: "base-minimax-h3-2k",
provider: "minimax",
capability: "video.generate",
reqKey: "MiniMax-H3",
variantKey: "resolution=2k",
unit: "video_second",
standardUnitPriceFen: 80,
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
enabled: true,
note: "MiniMax H3 2K 输出 ¥0.80/秒;首五张输入图免费。",
source: {
url: MINIMAX_PRICING_URL,
currency: "CNY",
unitPrice: 0.8,
basis: "MiniMax H3 2K 输出视频,按成功视频秒数计费",
observedAt: "2026-09-04"
}
}
];

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(/\.$/, "");
}

View File

@@ -759,18 +759,7 @@ function billingJobChargeFromValue(value: unknown): GenerationJob["billing"] {
settlementLedgerEntryId: optionalString(value.settlementLedgerEntryId),
settledAt: optionalString(value.settledAt),
settlementReason: optionalString(value.settlementReason),
providerUsage: isRecord(value.providerUsage)
&& Number.isFinite(Number(value.providerUsage.completionTokens))
&& typeof value.providerUsage.resolution === "string"
&& typeof value.providerUsage.inputVideo === "boolean"
&& Number.isFinite(Number(value.providerUsage.tokenPriceFenPerMillion))
? {
completionTokens: Number(value.providerUsage.completionTokens),
resolution: value.providerUsage.resolution,
inputVideo: value.providerUsage.inputVideo,
tokenPriceFenPerMillion: Number(value.providerUsage.tokenPriceFenPerMillion)
}
: undefined,
providerUsage: billingProviderUsageFromValue(value.providerUsage),
ledgerEntryId: optionalString(value.ledgerEntryId),
refundLedgerEntryId: optionalString(value.refundLedgerEntryId),
chargedAt: optionalString(value.chargedAt),
@@ -779,6 +768,30 @@ function billingJobChargeFromValue(value: unknown): GenerationJob["billing"] {
};
}
function billingProviderUsageFromValue(value: unknown): NonNullable<GenerationJob["billing"]>["providerUsage"] {
if (!isRecord(value) || typeof value.resolution !== "string") return undefined;
const completionTokens = optionalNumber(value.completionTokens);
const tokenPriceFenPerMillion = optionalNumber(value.tokenPriceFenPerMillion);
const outputSeconds = optionalNumber(value.outputSeconds);
const inputVideoSeconds = optionalNumber(value.inputVideoSeconds);
const inputImageCount = optionalNumber(value.inputImageCount);
const videoPriceFenPerSecond = optionalNumber(value.videoPriceFenPerSecond);
const inputVideo = typeof value.inputVideo === "boolean" ? value.inputVideo : undefined;
const model = optionalString(value.model);
if (completionTokens === undefined && outputSeconds === undefined) return undefined;
return {
resolution: value.resolution,
model,
completionTokens,
inputVideo,
tokenPriceFenPerMillion,
outputSeconds,
inputVideoSeconds,
inputImageCount,
videoPriceFenPerSecond
};
}
function imageTemplateToRow(template: Partial<ImageTemplate>) {
const row: Record<string, unknown> = {};
if (template.id !== undefined) row.id = template.id;

View File

@@ -65,6 +65,17 @@ export async function checkDatabaseReadiness(): Promise<void> {
WHERE to_regclass('public.' || table_name) IS NULL
OR NOT has_table_privilege(current_user, 'public.' || table_name, privilege_name)
)
AND EXISTS (
SELECT 1
FROM pg_constraint AS constraint_record
JOIN pg_class AS constrained_table ON constrained_table.oid = constraint_record.conrelid
JOIN pg_namespace AS constrained_schema ON constrained_schema.oid = constrained_table.relnamespace
WHERE constrained_schema.nspname = 'public'
AND constrained_table.relname = 'billing_price_rules'
AND constraint_record.conname = 'billing_price_rules_provider_check'
AND position('seedream' IN pg_get_constraintdef(constraint_record.oid)) > 0
AND position('minimax' IN pg_get_constraintdef(constraint_record.oid)) > 0
)
AND has_function_privilege(
current_user,
'public.claim_generation_jobs(text,integer,integer)',

View File

@@ -0,0 +1,31 @@
export const MINIMAX_H3_MODEL = "MiniMax-H3";
export function minimaxH3VideoPriceFenPerSecond(resolution: unknown) {
return String(resolution || "").trim().toUpperCase() === "2K" ? 80 : 50;
}
export function calculateMinimaxH3ActualAmountFen(input: {
resolution?: unknown;
outputSeconds: number;
inputVideoSeconds?: number;
inputImageCount?: number;
markupMultiplier: number;
}) {
const outputSeconds = integer(input.outputSeconds);
const inputVideoSeconds = nonNegativeInteger(input.inputVideoSeconds);
const inputImageCount = nonNegativeInteger(input.inputImageCount);
if (outputSeconds <= 0) throw new Error("MiniMax H3 实际输出时长必须大于 0。");
if (!Number.isFinite(input.markupMultiplier) || input.markupMultiplier < 1) throw new Error("MiniMax H3 计费倍率不能低于 1.00。");
const standardAmountFen = (outputSeconds + inputVideoSeconds) * minimaxH3VideoPriceFenPerSecond(input.resolution)
+ Math.max(0, inputImageCount - 5) * 20;
return Math.ceil(standardAmountFen * input.markupMultiplier);
}
function integer(value: unknown) {
const number = Number(value);
return Number.isInteger(number) ? number : 0;
}
function nonNegativeInteger(value: unknown) {
return Math.max(0, integer(value));
}

View File

@@ -5,15 +5,16 @@ import {
recordUsageForJob,
updateGenerationJob
} from "@/lib/server/data-store";
import { chargeGenerationJob, quoteGenerationCharge, settleSeedanceGenerationCharge } from "@/lib/server/billing-service";
import { chargeGenerationJob, quoteGenerationCharge, settleMinimaxGenerationCharge, settleSeedanceGenerationCharge } from "@/lib/server/billing-service";
import { DEFAULT_OWNER_ID } from "@/lib/server/runtime";
import { importRemoteAssetAsAsset } from "@/lib/server/storage";
import { createSeedanceTask, getSeedanceConfig, querySeedanceTask, type SeedanceSettings } from "@/lib/seedance/client";
import type { BillingJobCharge, GenerationJob, UsageContext } from "@/lib/types";
import { VIDEO_DURATION_DEFAULT, clampVideoDuration, isSupportedSeedanceModel, normalizeSeedanceModel, normalizeVideoDuration, normalizeVideoRatio, normalizeVideoResolution } from "@/lib/video-settings";
import { bailianResultUrls, bailianStatus, bailianTaskId, buildBailianVideoPayload, getBailianConfig, queryBailianTask, submitBailianTask } from "@/lib/bailian/client";
import { buildMinimaxH3Payload, createMinimaxH3Task, getMinimaxConfig, MINIMAX_H3_MODEL, queryMinimaxH3Task } from "@/lib/minimax/client";
export type VideoCreationEngine = "seedance" | "bailian";
export type VideoCreationEngine = "seedance" | "bailian" | "minimax";
export type SubmitVideoJobInput = PromptAssemblyInput & {
model?: string;
@@ -40,7 +41,7 @@ type PreparedVideoGeneration = {
finalPrompt: string;
settings: SeedanceSettings;
missingProviderCredentials: boolean;
provider: "bailian" | "seedance";
provider: "bailian" | "seedance" | "minimax";
reqKey: string;
requestPayload: Record<string, unknown>;
};
@@ -110,8 +111,10 @@ export async function submitVideoJob(input: SubmitVideoJobInput, origin: string)
function prepareVideoGeneration(input: SubmitVideoJobInput, origin: string): PreparedVideoGeneration {
const ownerId = input.ownerId || DEFAULT_OWNER_ID;
const configuredEngine = process.env.VIDEO_GENERATE_ENGINE === "seedance" ? "seedance" : "bailian";
const engine: VideoCreationEngine = input.engine === "seedance" || input.engine === "bailian"
const configuredEngine: VideoCreationEngine = process.env.VIDEO_GENERATE_ENGINE === "seedance"
? "seedance"
: process.env.VIDEO_GENERATE_ENGINE === "minimax" ? "minimax" : "bailian";
const engine: VideoCreationEngine = input.engine === "seedance" || input.engine === "bailian" || input.engine === "minimax"
? input.engine
: configuredEngine;
const config = getSeedanceConfig();
@@ -130,13 +133,20 @@ function prepareVideoGeneration(input: SubmitVideoJobInput, origin: string): Pre
settings.duration = normalizeVideoDuration(settings.duration) ?? 10;
settings.resolution = String(settings.resolution || "720P").toUpperCase();
buildBailianVideoPayload({ prompt: finalPrompt, materials: assembled.materials, origin, settings });
} else if (engine === "minimax") {
const payload = buildMinimaxH3Payload({ prompt: finalPrompt, materials: assembled.materials, settings, origin });
settings.duration = payload.duration;
settings.resolution = payload.resolution;
settings.ratio = payload.ratio;
} else {
settings.ratio = normalizeVideoRatio(settings.ratio, config.ratio);
settings.duration = normalizeVideoDuration(settings.duration, model)
?? clampVideoDuration(config.duration, VIDEO_DURATION_DEFAULT, { model });
settings.resolution = normalizeVideoResolution(settings.resolution, model, config.resolution);
}
const missingProviderCredentials = engine === "bailian" ? !getBailianConfig().apiKey : !config.apiKey;
const missingProviderCredentials = engine === "bailian"
? !getBailianConfig().apiKey
: engine === "minimax" ? !getMinimaxConfig().apiKey : !config.apiKey;
const requestPayload = {
input,
assembled,
@@ -153,12 +163,13 @@ function prepareVideoGeneration(input: SubmitVideoJobInput, origin: string): Pre
settings,
missingProviderCredentials,
provider: engine,
reqKey: engine === "bailian" ? getBailianConfig().videoModel : model,
reqKey: engine === "bailian" ? getBailianConfig().videoModel : engine === "minimax" ? MINIMAX_H3_MODEL : model,
requestPayload
};
}
function missingProviderMessage(engine: VideoCreationEngine) {
if (engine === "minimax") return "缺少 MINIMAX_API_KEY,请先配置 MiniMax 凭据。";
return engine === "bailian"
? "缺少 BAILIAN_API_KEY 或 DASHSCOPE_API_KEY,请先配置阿里云百炼凭据。"
: "缺少 SEEDANCE_API_KEY,请先配置 Seedance 凭据。";
@@ -187,6 +198,11 @@ async function dispatchVideoJob(job: GenerationJob, origin: string): Promise<Gen
if (!providerTaskId) throw new Error("视频任务响应缺少 task_id。");
return updateGenerationJob(job.id, { status: "running", providerTaskId, responsePayload: response });
}
if (job.provider === "minimax") {
const payload = buildMinimaxH3Payload({ prompt: job.prompt || "", settings, materials, origin });
const response = await createMinimaxH3Task(payload);
return updateGenerationJob(job.id, { status: "running", providerTaskId: response.providerTaskId, responsePayload: response.raw });
}
const response = await createSeedanceTask({ model: job.reqKey, prompt: job.prompt || "", settings, materials, origin });
return updateGenerationJob(job.id, { status: "running", providerTaskId: response.providerTaskId, responsePayload: response.raw });
} catch (error) {
@@ -217,6 +233,17 @@ export async function syncVideoJob(jobId: string, origin: string): Promise<Gener
}
return completeRemoteVideo(job, origin, resultUrl, response);
}
if (job.provider === "minimax") {
const result = await queryMinimaxH3Task(job.providerTaskId);
if (result.status !== "succeeded" || !result.resultUrl) {
return updateGenerationJob(job.id, {
status: result.status,
responsePayload: result.raw,
error: result.errorMessage ? { message: result.errorMessage, retryable: false } : undefined
});
}
return completeRemoteVideo(job, origin, result.resultUrl, result.raw, undefined, result.usage);
}
const result = await querySeedanceTask(job.providerTaskId);
if (result.status !== "succeeded" || !result.resultUrl) {
return updateGenerationJob(job.id, {
@@ -242,11 +269,14 @@ async function completeRemoteVideo(
origin: string,
resultUrl: string,
responsePayload: Record<string, unknown>,
completionTokens?: number
completionTokens?: number,
minimaxUsage?: { outputSeconds: number; inputSeconds: number; inputImageCount: number }
) {
const settledJob = job.provider === "seedance"
? await settleSeedanceGenerationCharge(job, completionTokens) || job
: job;
: job.provider === "minimax"
? await settleMinimaxGenerationCharge(job, minimaxUsage) || job
: job;
const asset = await importRemoteAssetAsAsset({
ownerId: settledJob.ownerId,
url: resultUrl,