增加即梦图片5.0和视频2.5的能力
This commit is contained in:
1 parent
542246e4ce
commit
88a4268d40
55 files changed
+1412
-200
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
@@ -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 })
|
||||
]));
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user