增加2.5模型调用
This commit is contained in:
1 parent
3f29ba716a
commit
be655b6569
32 files changed
+1075
-56
No files matched your search
@@ -30,6 +30,61 @@ describe("EvoLink image client helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["gpt-image-2.5-flare", "gpt-image-2.5-sunburst"])("selects %s for a single 1K image", (model) => {
|
||||
const payload = buildEvolinkImagePayload("image.generate", {
|
||||
prompt: "商品海报",
|
||||
model,
|
||||
quality: "high"
|
||||
}, { baseUrl: "https://api.evolink.ai", model: "gpt-image-2" });
|
||||
|
||||
expect(payload).toMatchObject({ model, quality: "high", n: 1, resolution: "1K", size: "1:1" });
|
||||
expect(buildEvolinkImagePayload("image.generate", {
|
||||
prompt: "商品海报",
|
||||
model
|
||||
}, { baseUrl: "https://api.evolink.ai", model: "gpt-image-2" }).quality).toBe("medium");
|
||||
});
|
||||
|
||||
it("rejects arbitrary models supplied by a caller but preserves a configured default model", () => {
|
||||
const settings = { baseUrl: "https://api.evolink.ai", model: "custom-image-model" };
|
||||
expect(() => buildEvolinkImagePayload("image.generate", { prompt: "海报", model: "arbitrary-model" }, settings))
|
||||
.toThrow("Unsupported EvoLink image model.");
|
||||
expect(buildEvolinkImagePayload("image.generate", { prompt: "海报" }, settings).model).toBe("custom-image-model");
|
||||
expect(buildEvolinkImagePayload("image.generate", { prompt: "海报", model: "custom-image-model" }, settings).model).toBe("custom-image-model");
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ quality: 1 }, "quality"],
|
||||
[{ quality: "ultra" }, "quality"],
|
||||
[{ resolution: "2K" }, "resolution"],
|
||||
[{ n: 2 }, "n"],
|
||||
[{ imageCount: 2 }, "imageCount"],
|
||||
[{ size: "2048x2048" }, "size"],
|
||||
[{ width: 3504, height: 2400 }, "dimensions"],
|
||||
[{ width: 1000, height: 992 }, "dimensions"],
|
||||
[{ width: 1200 }, "both width and height"],
|
||||
[{ force_single: false }, "force_single"],
|
||||
[{ settings: { resolution: "2K" } }, "resolution"],
|
||||
[{ settings: { n: 2 } }, "n"],
|
||||
[{ imageUrls: Array(17).fill("https://example.com/ref.png") }, "16 reference images"]
|
||||
])("rejects invalid Image 2.5 input %j", (invalid, reason) => {
|
||||
expect(() => buildEvolinkImagePayload("image.generate", { prompt: "海报", model: "gpt-image-2.5-flare", ...invalid }, {
|
||||
baseUrl: "https://api.evolink.ai", model: "gpt-image-2"
|
||||
})).toThrow(reason);
|
||||
});
|
||||
|
||||
it("rejects an invalid configured quality for Image 2.5", () => {
|
||||
expect(() => buildEvolinkImagePayload("image.generate", { prompt: "海报", model: "gpt-image-2.5-flare" }, {
|
||||
baseUrl: "https://api.evolink.ai", model: "gpt-image-2", quality: "ultra"
|
||||
})).toThrow("quality");
|
||||
});
|
||||
|
||||
it("accepts a bounded 16px custom size and A4 presets", () => {
|
||||
const settings = { baseUrl: "https://api.evolink.ai", model: "gpt-image-2.5-flare" };
|
||||
expect(buildEvolinkImagePayload("image.generate", { prompt: "海报", width: 1024, height: 768 }, settings).size).toBe("4:3");
|
||||
expect(buildEvolinkImagePayload("image.generate", { prompt: "海报", width: 848, height: 1200 }, settings).size).toBe("848x1200");
|
||||
expect(buildEvolinkImagePayload("image.generate", { prompt: "海报", width: 1280, height: 736 }, settings).size).toBe("1280x736");
|
||||
});
|
||||
|
||||
it("normalizes task ids, statuses, and result URLs", () => {
|
||||
const response = {
|
||||
data: {
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { quoteFromPriceRule } from "@/lib/billing";
|
||||
import { DEFAULT_BILLING_PRICE_RULES } from "@/lib/server/billing-catalog";
|
||||
import type { BillingParameterSnapshot, BillingPriceRule } from "@/lib/types";
|
||||
|
||||
describe("EvoLink Image 2.5 prices", () => {
|
||||
it.each(["gpt-image-2.5-flare", "gpt-image-2.5-sunburst"])("quotes independent quality tiers for %s", (model) => {
|
||||
const rule = DEFAULT_BILLING_PRICE_RULES.find((rule) => rule.reqKey === model) as BillingPriceRule;
|
||||
for (const [quality, cost, amount] of [["low", 4, 5], ["medium", 9, 11], ["high", 35, 42]] as const) {
|
||||
for (const aspectRatio of ["1:1", "3:4", "70:99", "99:70"]) {
|
||||
const quote = quoteFromPriceRule({ rule, provider: "evolink", capability: "image.generate", reqKey: model, quantity: 1, parameters: { quality, aspectRatio } });
|
||||
expect(quote).toMatchObject({ priceRuleId: `base-evolink-${model}`, standardUnitPriceFen: cost, amountFen: amount });
|
||||
}
|
||||
}
|
||||
const unsupported: BillingParameterSnapshot[] = [{ quality: "xhigh" }, { resolution: "2K" }, { resolution: "4K" }];
|
||||
for (const parameters of unsupported) {
|
||||
expect(() => quoteFromPriceRule({ rule, provider: "evolink", capability: "image.generate", reqKey: model, quantity: 1, parameters })).toThrow("当前生成参数没有对应的平台标准价格");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { IMAGE_MODEL_OPTIONS, evolinkModelCapability, imageModelAfterHealth, imageModelFromJob, templateImageModel } from "@/lib/image-models";
|
||||
import type { GenerationJob } from "@/lib/types";
|
||||
|
||||
function imageJob(overrides: Partial<GenerationJob>): GenerationJob {
|
||||
return {
|
||||
id: "job-1",
|
||||
ownerId: "owner-1",
|
||||
capability: "image.generate",
|
||||
provider: "evolink",
|
||||
reqKey: "",
|
||||
status: "succeeded",
|
||||
inputAssetIds: [],
|
||||
inputUrls: [],
|
||||
outputAssetIds: [],
|
||||
requestPayload: {},
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe("EvoLink image model selection", () => {
|
||||
it("uses the configured model when health loads and preserves a selection made while loading", () => {
|
||||
expect(imageModelAfterHealth(IMAGE_MODEL_OPTIONS, "gpt-image-2.5-flare", "gpt-image-2", false)).toBe("gpt-image-2.5-flare");
|
||||
expect(imageModelAfterHealth(IMAGE_MODEL_OPTIONS, "gpt-image-2.5-flare", "gpt-image-2.5-sunburst", true)).toBe("gpt-image-2.5-sunburst");
|
||||
expect(imageModelAfterHealth([{ id: "custom-image" }], "custom-image", "gpt-image-2", false)).toBe("custom-image");
|
||||
});
|
||||
|
||||
it("reads EvoLink defaults even when another image engine is primary", () => {
|
||||
const capability = evolinkModelCapability({
|
||||
engine: "jimeng",
|
||||
reqKey: "jimeng-model",
|
||||
evolink: { reqKey: "gpt-image-2.5-flare", models: [{ id: "gpt-image-2.5-flare", label: "Image 2.5 Flare" }] }
|
||||
});
|
||||
expect(capability?.reqKey).toBe("gpt-image-2.5-flare");
|
||||
expect(imageModelAfterHealth(capability!.models!, capability?.reqKey, "gpt-image-2", false)).toBe("gpt-image-2.5-flare");
|
||||
expect(evolinkModelCapability({ engine: "evolink", reqKey: "gpt-image-2.5-sunburst" })?.reqKey).toBe("gpt-image-2.5-sunburst");
|
||||
});
|
||||
|
||||
it("uses the configured runtime default for older templates without a saved model", () => {
|
||||
expect(templateImageModel(undefined, "gpt-image-2.5-flare")).toBe("gpt-image-2.5-flare");
|
||||
expect(templateImageModel("gpt-image-2.5-sunburst", "gpt-image-2.5-flare")).toBe("gpt-image-2.5-sunburst");
|
||||
});
|
||||
|
||||
it("shows the persisted Go model before request fallbacks", () => {
|
||||
expect(imageModelFromJob(imageJob({
|
||||
reqKey: "gpt-image-2.5-sunburst",
|
||||
requestPayload: { model: "gpt-image-2.5-flare", input: { model: "gpt-image-2" } }
|
||||
}))).toBe("gpt-image-2.5-sunburst");
|
||||
expect(imageModelFromJob(imageJob({ requestPayload: { model: "gpt-image-2.5-flare" } }))).toBe("gpt-image-2.5-flare");
|
||||
expect(imageModelFromJob(imageJob({ requestPayload: { input: { model: "gpt-image-2.5-flare" } } }))).toBe("gpt-image-2.5-flare");
|
||||
expect(imageModelFromJob(imageJob({}))).toBe("gpt-image-2");
|
||||
expect(imageModelFromJob(imageJob({ provider: "bailian", reqKey: "gpt-image-2.5-flare" }))).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -104,6 +104,22 @@ describe("image templates", () => {
|
||||
}).settings).toEqual({ engine: "seedream", size: "1.5K", outputFormat: "jpeg", optimizeMode: "fast" });
|
||||
});
|
||||
|
||||
it("keeps an EvoLink model in a template without leaking it to another engine", () => {
|
||||
const base = { name: "模型模板", prompt: "生成商品图" };
|
||||
expect(normalizeImageTemplateCreate({
|
||||
...base,
|
||||
settings: { engine: "evolink", model: "gpt-image-2.5-sunburst", quality: "medium" }
|
||||
}).settings).toEqual({ engine: "evolink", model: "gpt-image-2.5-sunburst", quality: "medium" });
|
||||
expect(normalizeImageTemplateCreate({
|
||||
...base,
|
||||
settings: { engine: "jimeng", model: "gpt-image-2.5-sunburst" }
|
||||
}).settings).toEqual({ engine: "jimeng" });
|
||||
expect(() => normalizeImageTemplateCreate({
|
||||
...base,
|
||||
settings: { engine: "evolink", model: "../invalid" }
|
||||
})).toThrow("模型 ID 格式无效");
|
||||
});
|
||||
|
||||
it("extracts prompt material placeholders for template upload slots", () => {
|
||||
expect(extractMaterialPlaceholders("以 @图片1 为主体,参考 @图2 的色调,再参考 @视频1 的运动感。")).toEqual([
|
||||
{ token: "@图片1", type: "image", index: 1 },
|
||||
|
||||
@@ -169,6 +169,37 @@ describe("task management and public API helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each(["gpt-image-2.5-flare", "gpt-image-2.5-sunburst"])("passes %s through public jobs and billing identity", async (model) => {
|
||||
process.env.IMAGE_GENERATE_ENGINE = "evolink";
|
||||
process.env.EVOLINK_API_KEY = "test-key";
|
||||
const result = await createPublicGenerationJob({
|
||||
client: { id: "agent-a", key: "secret-a" },
|
||||
request: new Request("http://local.test/api/v1/jobs"),
|
||||
origin: "http://local.test",
|
||||
body: { capability: "image.generate", prompt: "商品海报", model }
|
||||
});
|
||||
expect(result.job.provider).toBe("evolink");
|
||||
expect(result.job.reqKey).toBe(model);
|
||||
expect(result.job.requestPayload.providerPayload).toMatchObject({ model, quality: "medium", n: 1, resolution: "1K" });
|
||||
});
|
||||
|
||||
it("rejects a model for a non-EvoLink engine and invalid Image 2.5 quality through the public API", async () => {
|
||||
process.env.EVOLINK_API_KEY = "test-key";
|
||||
const common = {
|
||||
client: { id: "agent-a", key: "secret-a" },
|
||||
request: new Request("http://local.test/api/v1/jobs"),
|
||||
origin: "http://local.test"
|
||||
};
|
||||
await expect(createPublicGenerationJob({
|
||||
...common,
|
||||
body: { capability: "image.generate", engine: "jimeng", prompt: "海报", model: "gpt-image-2.5-flare" }
|
||||
})).rejects.toThrow("Unsupported image model for the selected engine.");
|
||||
await expect(createPublicGenerationJob({
|
||||
...common,
|
||||
body: { capability: "image.generate", engine: "evolink", prompt: "海报", model: "gpt-image-2.5-flare", quality: 3 }
|
||||
})).rejects.toThrow("Unsupported EvoLink image quality.");
|
||||
});
|
||||
|
||||
it("allows image jobs to override the default generation engine", async () => {
|
||||
process.env.IMAGE_GENERATE_ENGINE = "evolink";
|
||||
process.env.EVOLINK_API_KEY = "test-key";
|
||||
|
||||
Reference in new issue
Block a user