增加2.5模型调用

This commit is contained in:
andy committed 2026-09-23 15:10:13 +08:00
1 parent 3f29ba716a
commit be655b6569
32 files changed
+1075 -56

No files matched your search

+55
View File
@@ -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: {
+20
View File
@@ -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("当前生成参数没有对应的平台标准价格");
}
});
});
+56
View File
@@ -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();
});
});
+16
View File
@@ -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 },
+31
View File
@@ -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";