增加即梦图片5.0和视频2.5的能力

This commit is contained in:
andy committed 2026-08-21 13:44:56 +08:00
1 parent 542246e4ce
commit 88a4268d40
55 files changed
+1412 -200

No files matched your search

+23
View File
@@ -195,6 +195,29 @@ describe("organization billing", () => {
});
});
it("seeds and quotes Seedance 2.5 independently at the default 1.2 markup", async () => {
const quote = await quoteGenerationCharge({
provider: "seedance",
capability: "video.generate",
reqKey: "doubao-seedance-2-5-260628",
requestPayload: { settings: { duration: 5, resolution: "720p", ratio: "16:9" } },
usageContext: {
source: "platform",
accountId: "user-1",
displayName: "测试用户",
organizationId: "org-1"
}
});
expect(quote).toMatchObject({
variantKey: "resolution=720p",
standardUnitPriceFen: 151,
quantity: 5,
amountFen: 908,
markupMultiplier: 1.2,
settlementStatus: "pending"
});
});
it("matches EvoLink quality tiers from the platform catalog", async () => {
const quote = await quoteGenerationCharge({
provider: "evolink",
+5
View File
@@ -97,6 +97,11 @@ describe("image templates", () => {
});
expect(() => normalizeImageTemplateCreate({ name: "", prompt: "x" })).toThrow("模板名称不能为空");
expect(() => normalizeImageTemplateCreate({ name: "x", prompt: "x", previewImageUrl: "javascript:alert(1)" })).toThrow("效果预览图地址");
expect(normalizeImageTemplateCreate({
name: "Seedream",
prompt: "基础生图",
settings: { engine: "seedream", size: "1.5k", outputFormat: "JPEG", optimizeMode: "fast" }
}).settings).toEqual({ engine: "seedream", size: "1.5K", outputFormat: "jpeg", optimizeMode: "fast" });
});
it("extracts prompt material placeholders for template upload slots", () => {
+20 -2
View File
@@ -4,7 +4,7 @@ import { describe, expect, it } from "vitest";
describe("Go provider adapter contract", () => {
it("freezes bounded adapters and safe errors", async () => {
const contract = JSON.parse(await readFile(new URL("../contracts/providers/http-v1.json", import.meta.url), "utf8"));
expect(Object.keys(contract.providers)).toEqual(["volcengine-visual", "evolink", "bailian", "seedance"]);
expect(Object.keys(contract.providers)).toEqual(["volcengine-visual", "evolink", "bailian", "seedance", "seedream"]);
expect(contract.providers["volcengine-visual"]).toMatchObject({
model: "jimeng_seedream46_cvtob",
submitAction: "JimengSeedream46CVToBSubmitTask",
@@ -13,11 +13,29 @@ describe("Go provider adapter contract", () => {
});
expect(contract.providers.seedance).toMatchObject({
model: "doubao-seedance-2-0-260128",
models: ["doubao-seedance-2-0-260128", "doubao-seedance-2-5-260628"],
submit: "/contents/generations/tasks",
query: "/contents/generations/tasks/{id}",
maxMaterials: 4,
maxMaterials: {
"doubao-seedance-2-0-260128": 4,
"doubao-seedance-2-5-260628": 50
},
maxDurationSeconds: {
"doubao-seedance-2-0-260128": 15,
"doubao-seedance-2-5-260628": 30
},
resolutions: ["480p", "720p", "1080p"]
});
expect(contract.providers.seedream).toMatchObject({
model: "doubao-seedream-5-0-pro-260628",
submit: "/images/generations",
auth: "bearer",
synchronous: true,
maxReferenceImages: 10,
sizes: ["1K", "1.5K", "2K"],
outputFormats: ["png", "jpeg"],
optimizeModes: ["standard", "fast"]
});
expect(contract.errors).toEqual({ generic: true, secretSafe: true });
expect(contract.liveCallsInTests).toBe(false);
});
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it } from "vitest";
import {
calculateSeedanceActualAmountFen,
estimateSeedanceAmountFen,
normalizeInputVideoDuration,
seedanceTokenPriceFenPerMillion
} from "@/lib/server/seedance-billing";
import { SEEDANCE_20_MODEL, SEEDANCE_25_MODEL } from "@/lib/video-settings";
describe("Seedance model-aware billing", () => {
it("keeps Seedance 2.0 rates and adds the official Seedance 2.5 rates", () => {
expect(seedanceTokenPriceFenPerMillion(SEEDANCE_20_MODEL, "720p", false)).toBe(4_600);
expect(seedanceTokenPriceFenPerMillion(SEEDANCE_20_MODEL, "1080p", true)).toBe(3_100);
expect(seedanceTokenPriceFenPerMillion(SEEDANCE_25_MODEL, "720p", false)).toBe(7_000);
expect(seedanceTokenPriceFenPerMillion(SEEDANCE_25_MODEL, "720p", true)).toBe(4_200);
expect(seedanceTokenPriceFenPerMillion(SEEDANCE_25_MODEL, "1080p", false)).toBe(7_700);
expect(seedanceTokenPriceFenPerMillion(SEEDANCE_25_MODEL, "1080p", true)).toBe(4_600);
});
it("quotes Seedance 2.5 with a 1.2 markup and its 4-to-30-second input billing range", () => {
expect(estimateSeedanceAmountFen({
model: SEEDANCE_25_MODEL,
resolution: "720p",
aspectRatio: "16:9",
outputDurationSeconds: 5,
markupMultiplier: 1.2
})).toBe(908);
expect(normalizeInputVideoDuration(2, SEEDANCE_25_MODEL)).toBe(4);
expect(normalizeInputVideoDuration(undefined, SEEDANCE_25_MODEL)).toBe(30);
expect(estimateSeedanceAmountFen({
model: SEEDANCE_25_MODEL,
resolution: "720p",
aspectRatio: "16:9",
outputDurationSeconds: 5,
inputVideo: true,
inputVideoDurationSeconds: 2,
markupMultiplier: 1.2
})).toBe(980);
});
it("settles Seedance 2.5 provider tokens with the selected model rate", () => {
expect(calculateSeedanceActualAmountFen({
model: SEEDANCE_25_MODEL,
resolution: "720p",
completionTokens: 100_000,
markupMultiplier: 1.2
})).toBe(840);
});
});
+28
View File
@@ -60,6 +60,34 @@ describe("Seedance usage extraction", () => {
await expect(querySeedanceTask("task-1")).resolves.toMatchObject({ status: "expired" });
});
it("sends the selected Seedance 2.5 model and accepts its extended limits", async () => {
process.env.SEEDANCE_API_KEY = "test-key";
let body: Record<string, unknown> | undefined;
vi.stubGlobal("fetch", vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
body = JSON.parse(String(init?.body));
return new Response(JSON.stringify({ id: "task-25" }), { status: 200, headers: { "Content-Type": "application/json" } });
}));
await createSeedanceTask({
model: "doubao-seedance-2-5-260628",
prompt: "combine",
settings: { ratio: "16:9", duration: 30, resolution: "720p" },
materials: Array.from({ length: 5 }, (_, index) => ({
type: "image" as const,
url: `/image-${index + 1}.png`,
label: `@图片${index + 1}`
})),
origin: "https://app.test"
});
expect(body).toMatchObject({
model: "doubao-seedance-2-5-260628",
duration: 30,
omni_reference_task_type: "auto"
});
expect(body?.content).toHaveLength(6);
});
it("rejects more materials than the Seedance 2.0 content limit before calling the provider", async () => {
process.env.SEEDANCE_API_KEY = "test-key";
const fetchMock = vi.fn();
+19
View File
@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import { calculateSeedreamAmountFen, SEEDREAM_50_PRO_MODEL } from "@/lib/server/seedream-billing";
import { DEFAULT_BILLING_PRICE_RULES } from "@/lib/server/billing-catalog";
describe("Seedream 5.0 Pro billing", () => {
it("keeps official output and reference image rates under the 1.2x markup", () => {
expect(calculateSeedreamAmountFen({ outputPriceFen: 30, referenceImageCount: 0, markupMultiplier: 1.2 })).toBe(36);
expect(calculateSeedreamAmountFen({ outputPriceFen: 30, referenceImageCount: 3, markupMultiplier: 1.2 })).toBe(41);
expect(calculateSeedreamAmountFen({ outputPriceFen: 60, referenceImageCount: 3, markupMultiplier: 1.2 })).toBe(77);
expect(calculateSeedreamAmountFen({ outputPriceFen: 60, referenceImageCount: 10, markupMultiplier: 1.2 })).toBe(94);
});
it("seeds an independent 5.0 Pro price rule without replacing Jimeng 4.6", () => {
expect(DEFAULT_BILLING_PRICE_RULES).toEqual(expect.arrayContaining([
expect.objectContaining({ id: "base-volcengine-jimeng-seedream46", reqKey: "jimeng_seedream46_cvtob" }),
expect.objectContaining({ id: "base-seedream-5-0-pro", provider: "seedream", reqKey: SEEDREAM_50_PRO_MODEL, standardUnitPriceFen: 30 })
]));
});
});
+9 -1
View File
@@ -4,10 +4,12 @@ import {
VIDEO_DURATION_DEFAULT,
VIDEO_DURATION_MAX,
VIDEO_DURATION_MIN,
SEEDANCE_25_MODEL,
clampVideoDuration,
normalizeVideoDuration,
normalizeVideoRatio,
normalizeVideoResolution
normalizeVideoResolution,
seedanceDurationOptions
} from "@/lib/video-settings";
describe("video settings", () => {
@@ -22,6 +24,12 @@ describe("video settings", () => {
expect(clampVideoDuration(VIDEO_DURATION_AUTO, VIDEO_DURATION_DEFAULT, { allowAuto: false })).toBe(VIDEO_DURATION_MIN);
});
it("allows Seedance 2.5 durations through 30 seconds without changing 2.0", () => {
expect(clampVideoDuration(30, VIDEO_DURATION_DEFAULT, { model: SEEDANCE_25_MODEL })).toBe(30);
expect(clampVideoDuration(30)).toBe(VIDEO_DURATION_MAX);
expect(seedanceDurationOptions(SEEDANCE_25_MODEL).at(-1)).toBe(30);
});
it("keeps empty duration optional and falls back for invalid values", () => {
expect(normalizeVideoDuration(undefined)).toBeUndefined();
expect(normalizeVideoDuration("")).toBeUndefined();