增加2.5模型调用
This commit is contained in:
1 parent
3f29ba716a
commit
be655b6569
32 files changed
+1075
-56
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import type { EnabledImageCapability, GenerationStatus } from "@/lib/types";
|
||||
import { imageDimensionsForEngine } from "@/lib/image-size-presets";
|
||||
import { isImageModel } from "@/lib/image-models";
|
||||
|
||||
export type ImageCreationEngine = "jimeng" | "evolink" | "bailian";
|
||||
|
||||
@@ -79,17 +80,36 @@ export function buildEvolinkImagePayload(
|
||||
): Record<string, unknown> {
|
||||
const prompt = String(input.prompt || "").trim();
|
||||
const imageUrls = asStringArray(input.imageUrls);
|
||||
const nested = asRecord(input.settings) || {};
|
||||
const requestedModel = input.model;
|
||||
if (requestedModel !== undefined && !isImageModel(requestedModel) && requestedModel !== settings.model) {
|
||||
throw new Error("Unsupported EvoLink image model.");
|
||||
}
|
||||
const model = requestedModel || settings.model;
|
||||
const image25 = model === "gpt-image-2.5-flare" || model === "gpt-image-2.5-sunburst";
|
||||
if (image25) validateImage25Input(input, nested, imageUrls);
|
||||
const payload: Record<string, unknown> = {
|
||||
model: settings.model,
|
||||
model,
|
||||
prompt,
|
||||
n: 1
|
||||
};
|
||||
|
||||
if (!payload.prompt) throw new Error("Prompt is required for image generation.");
|
||||
const quality = cleanOptional(typeof input.quality === "string" ? input.quality : undefined) || settings.quality;
|
||||
const rawQuality = input.quality ?? nested.quality;
|
||||
const requestedQuality = cleanOptional(typeof rawQuality === "string" ? rawQuality : undefined)?.toLowerCase();
|
||||
if (image25 && rawQuality !== undefined && (!requestedQuality || !validQuality(requestedQuality))) {
|
||||
throw new Error("Unsupported EvoLink image quality.");
|
||||
}
|
||||
const quality = requestedQuality || cleanOptional(settings.quality)?.toLowerCase() || (image25 ? "medium" : undefined);
|
||||
if (image25 && !validQuality(quality)) throw new Error("Unsupported EvoLink image quality.");
|
||||
if (quality) payload.quality = quality;
|
||||
payload.resolution = EVOLINK_PLATFORM_RESOLUTION;
|
||||
assignSize(payload, input);
|
||||
assignSize(payload, {
|
||||
size: input.size ?? nested.size,
|
||||
width: input.width ?? nested.width,
|
||||
height: input.height ?? nested.height
|
||||
});
|
||||
if (image25 && payload.size === undefined) payload.size = "1:1";
|
||||
|
||||
if (imageUrls.length) payload.image_urls = imageUrls;
|
||||
return payload;
|
||||
@@ -152,6 +172,49 @@ function cleanOptional(value: string | undefined): string | undefined {
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function validQuality(value: unknown): value is "low" | "medium" | "high" {
|
||||
return value === "low" || value === "medium" || value === "high";
|
||||
}
|
||||
|
||||
function validateImage25Input(input: Record<string, unknown>, nested: Record<string, unknown>, imageUrls: string[]) {
|
||||
if (imageUrls.length > 16) throw new Error("EvoLink Image 2.5 supports at most 16 reference images.");
|
||||
for (const resolution of [input.resolution, nested.resolution]) {
|
||||
if (resolution !== undefined && (typeof resolution !== "string" || resolution.trim().toUpperCase() !== "1K")) {
|
||||
throw new Error("EvoLink Image 2.5 resolution must be 1K.");
|
||||
}
|
||||
}
|
||||
for (const key of ["n", "imageCount"]) {
|
||||
for (const value of [input[key], nested[key]]) {
|
||||
if (value !== undefined && !isOne(value)) throw new Error(`EvoLink Image 2.5 ${key} must be 1.`);
|
||||
}
|
||||
}
|
||||
if (input.force_single === false || nested.force_single === false) throw new Error("EvoLink Image 2.5 requires force_single.");
|
||||
for (const size of [input.size, nested.size]) {
|
||||
if (size !== undefined && (typeof size !== "string" || !supportedImage25Sizes.has(size.trim()))) {
|
||||
throw new Error("EvoLink Image 2.5 size must be a supported 1K aspect ratio or A4 size.");
|
||||
}
|
||||
}
|
||||
const width = input.width ?? nested.width;
|
||||
const height = input.height ?? nested.height;
|
||||
if ((width === undefined) !== (height === undefined)) throw new Error("EvoLink Image 2.5 requires both width and height.");
|
||||
if (width !== undefined) {
|
||||
const w = asPositiveInteger(width);
|
||||
const h = asPositiveInteger(height);
|
||||
if (!w || !h || w > 8192 || h > 8192) throw new Error("Invalid EvoLink Image 2.5 dimensions.");
|
||||
const mapped = imageDimensionsForEngine({ width: w, height: h }, "evolink");
|
||||
const ratio = ratioFromDimensions(mapped.width, mapped.height);
|
||||
const a4 = (mapped.width === 848 && mapped.height === 1200) || (mapped.width === 1200 && mapped.height === 848);
|
||||
const pixels = w * h;
|
||||
const custom = w % 16 === 0 && h % 16 === 0 && pixels >= 655360 && pixels <= 1048576 && Math.max(w, h) / Math.min(w, h) <= 3;
|
||||
if (!ratio && !a4 && !custom) throw new Error("EvoLink Image 2.5 custom dimensions exceed the supported 1K size.");
|
||||
}
|
||||
}
|
||||
|
||||
function isOne(value: unknown): boolean {
|
||||
return (typeof value === "number" && Number.isFinite(value) || typeof value === "string" && value.trim() !== "")
|
||||
&& Number(value) === 1;
|
||||
}
|
||||
|
||||
function assignSize(payload: Record<string, unknown>, input: Record<string, unknown>) {
|
||||
if (typeof input.size === "string" && input.size.trim()) {
|
||||
payload.size = input.size.trim();
|
||||
@@ -170,6 +233,7 @@ function asStringArray(value: unknown): string[] {
|
||||
}
|
||||
|
||||
function asPositiveInteger(value: unknown): number | undefined {
|
||||
if (typeof value !== "number" && (typeof value !== "string" || value.trim() === "")) return undefined;
|
||||
const parsed = Number(value);
|
||||
if (!Number.isInteger(parsed) || parsed <= 0) return undefined;
|
||||
return parsed;
|
||||
@@ -209,6 +273,7 @@ const supportedRatios = new Set([
|
||||
"9:21",
|
||||
"21:9"
|
||||
]);
|
||||
const supportedImage25Sizes = new Set([...supportedRatios, "848x1200", "1200x848"]);
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value)
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { GenerationJob } from "@/lib/types";
|
||||
|
||||
export const IMAGE_MODELS = ["gpt-image-2", "gpt-image-2.5-flare", "gpt-image-2.5-sunburst"] as const;
|
||||
|
||||
export type ImageModel = (typeof IMAGE_MODELS)[number];
|
||||
|
||||
export const DEFAULT_IMAGE_MODEL: ImageModel = IMAGE_MODELS[0];
|
||||
|
||||
export const IMAGE_MODEL_OPTIONS: ReadonlyArray<{ id: ImageModel; label: string }> = [
|
||||
{ id: "gpt-image-2", label: "Image 2" },
|
||||
{ id: "gpt-image-2.5-flare", label: "Image 2.5 Flare" },
|
||||
{ id: "gpt-image-2.5-sunburst", label: "Image 2.5 Sunburst" }
|
||||
];
|
||||
|
||||
export type EvolinkModelCapability = {
|
||||
reqKey?: string;
|
||||
models?: Array<{ id: string; label: string }>;
|
||||
};
|
||||
|
||||
export function evolinkModelCapability(capability?: EvolinkModelCapability & { engine?: string; evolink?: EvolinkModelCapability }): EvolinkModelCapability | undefined {
|
||||
return capability?.evolink || (capability?.engine === "evolink" ? capability : undefined);
|
||||
}
|
||||
|
||||
export function isImageModel(value: unknown): value is ImageModel {
|
||||
return typeof value === "string" && IMAGE_MODELS.some((model) => model === value);
|
||||
}
|
||||
|
||||
export function imageModelLabel(model: string): string {
|
||||
return IMAGE_MODEL_OPTIONS.find((option) => option.id === model)?.label || model;
|
||||
}
|
||||
|
||||
export function imageModelAfterHealth(
|
||||
models: ReadonlyArray<{ id: string }>,
|
||||
configuredModel: string | undefined,
|
||||
currentModel: string,
|
||||
userSelected: boolean
|
||||
): string {
|
||||
if (userSelected) return currentModel;
|
||||
return configuredImageModel(models, configuredModel);
|
||||
}
|
||||
|
||||
export function configuredImageModel(models: ReadonlyArray<{ id: string }>, configuredModel?: string): string {
|
||||
return models.find((option) => option.id === configuredModel)?.id || models[0]?.id || DEFAULT_IMAGE_MODEL;
|
||||
}
|
||||
|
||||
export function templateImageModel(savedModel: string | undefined, runtimeDefaultModel: string): string {
|
||||
return savedModel || runtimeDefaultModel;
|
||||
}
|
||||
|
||||
export function imageModelFromJob(job: GenerationJob): string | undefined {
|
||||
if (job.capability !== "image.generate") return undefined;
|
||||
const payload = record(job.requestPayload);
|
||||
const input = record(payload.input);
|
||||
const engine = nonemptyString(input.engine) || nonemptyString(payload.engine);
|
||||
if (engine !== "evolink" && job.provider !== "evolink") return undefined;
|
||||
return nonemptyString(job.reqKey)
|
||||
|| nonemptyString(payload.model)
|
||||
|| nonemptyString(input.model)
|
||||
|| nonemptyString(record(input.settings).model)
|
||||
|| nonemptyString(record(payload.settings).model)
|
||||
|| nonemptyString(record(payload.providerPayload).model)
|
||||
|| DEFAULT_IMAGE_MODEL;
|
||||
}
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function nonemptyString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
@@ -106,7 +106,7 @@ const settingDefinitions: Array<{
|
||||
{
|
||||
id: "evolink",
|
||||
title: "EvoLink 图片 API",
|
||||
description: "用于 GPT Image 2 图片生成;Base URL 和模型使用系统默认值即可。",
|
||||
description: "支持 GPT Image 2 和 Image 2.5 Flare / Sunburst 图片生成;Base URL 和默认模型可保持系统默认值。",
|
||||
fields: [
|
||||
{ key: "EVOLINK_API_KEY", label: "EvoLink API Key", secret: true, type: "password" },
|
||||
{ key: "EVOLINK_BASE_URL", label: "Base URL", defaultValue: "https://api.evolink.ai" },
|
||||
@@ -165,7 +165,7 @@ const engineFieldDefinitions: FieldDefinition[] = [
|
||||
options: [
|
||||
{ label: "即梦 / 火山视觉", value: "jimeng" },
|
||||
{ label: "Seedream 5.0 Pro / 火山方舟", value: "seedream" },
|
||||
{ label: "EvoLink GPT Image 2", value: "evolink" },
|
||||
{ label: "EvoLink GPT Image 2 / 2.5", value: "evolink" },
|
||||
{ label: "阿里云百炼 Wan 2.7", value: "bailian" }
|
||||
]
|
||||
},
|
||||
|
||||
@@ -85,6 +85,36 @@ const EVOLINK_GPT_IMAGE_DIMENSIONS = [
|
||||
}
|
||||
] satisfies NonNullable<BillingPriceRuleInput["parameterDimensions"]>;
|
||||
|
||||
function evolink25PriceRule(model: string, sourceUrl: string): BillingPriceRuleInput {
|
||||
const parameterDimensions = structuredClone(EVOLINK_GPT_IMAGE_DIMENSIONS);
|
||||
parameterDimensions[0].tiers = [
|
||||
{ value: "low", label: "快速", standardFactor: 4 / 9, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||||
{ value: "medium", label: "标准", standardFactor: 1, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true },
|
||||
{ value: "high", label: "精细", standardFactor: 35 / 9, markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER, enabled: true }
|
||||
];
|
||||
parameterDimensions[1].tiers = parameterDimensions[1].tiers.slice(0, 1);
|
||||
return {
|
||||
id: `base-evolink-${model}`,
|
||||
provider: "evolink",
|
||||
capability: "image.generate",
|
||||
reqKey: model,
|
||||
unit: "image",
|
||||
standardUnitPriceFen: 9,
|
||||
markupMultiplier: DEFAULT_BILLING_MARKUP_MULTIPLIER,
|
||||
enabled: true,
|
||||
note: "1K 基础输出估算:low/medium/high 成本为 ¥0.04/¥0.09/¥0.35;输入费用未单独加收,非上游实际 token 账单。",
|
||||
parameterDimensions,
|
||||
source: {
|
||||
url: sourceUrl,
|
||||
currency: "USD",
|
||||
unitPrice: 0.012,
|
||||
fxRate: BILLING_USD_CNY_RATE,
|
||||
basis: "1024×1024、无参考图输出费用估算:low 约 $0.0053,medium 约 $0.012,high 约 $0.0474;人民币标准成本向上取整到分。参考图和提示词会影响上游实扣。",
|
||||
observedAt: "2026-09-23"
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_BILLING_PRICE_RULES: BillingPriceRuleInput[] = [
|
||||
{
|
||||
id: "base-volcengine-jimeng-seedream46",
|
||||
@@ -124,6 +154,8 @@ export const DEFAULT_BILLING_PRICE_RULES: BillingPriceRuleInput[] = [
|
||||
observedAt: BILLING_CATALOG_OBSERVED_AT
|
||||
}
|
||||
},
|
||||
evolink25PriceRule("gpt-image-2.5-flare", "https://evolink.ai/gpt-image-2-5"),
|
||||
evolink25PriceRule("gpt-image-2.5-sunburst", "https://evolink.ai/zh/gpt-image-2-5-sunburst"),
|
||||
{
|
||||
id: "base-bailian-wan27-image-pro",
|
||||
provider: "bailian",
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
extractEvolinkResultUrls,
|
||||
getEffectiveImageEngine,
|
||||
getEvolinkImageConfig,
|
||||
getEvolinkImageSettings,
|
||||
getEvolinkTaskId,
|
||||
mapEvolinkStatus,
|
||||
queryEvolinkTask,
|
||||
@@ -52,7 +51,13 @@ export type SubmitImageJobInput = {
|
||||
min_ratio?: number;
|
||||
max_ratio?: number;
|
||||
force_single?: boolean;
|
||||
quality?: string;
|
||||
quality?: unknown;
|
||||
model?: string;
|
||||
size?: string;
|
||||
resolution?: unknown;
|
||||
n?: unknown;
|
||||
imageCount?: unknown;
|
||||
settings?: unknown;
|
||||
retryOf?: string;
|
||||
idempotencyKey?: string;
|
||||
idempotencyFingerprint?: string;
|
||||
@@ -141,6 +146,7 @@ function prepareImageGeneration(input: SubmitImageJobInput, origin: string): Pre
|
||||
const capability = getEnabledImageCapability(input.capability);
|
||||
const normalizedUrls = (input.imageUrls || []).map((url) => toAbsoluteUrl(url, origin));
|
||||
const engine = getEffectiveImageEngine(input.capability, input.engine);
|
||||
if (input.model !== undefined && engine !== "evolink") throw new Error("Unsupported image model for the selected engine.");
|
||||
const providerPayload = engine === "bailian"
|
||||
? buildBailianImagePayload(input.capability, { ...input, imageUrls: normalizedUrls })
|
||||
: engine === "evolink"
|
||||
@@ -152,7 +158,7 @@ function prepareImageGeneration(input: SubmitImageJobInput, origin: string): Pre
|
||||
: engine === "evolink"
|
||||
? !getEvolinkImageConfig()
|
||||
: !getVisualClientConfig();
|
||||
const reqKey = engine === "bailian" ? bailianConfig.imageModel : engine === "evolink" ? getEvolinkImageSettings().model : capability.reqKey;
|
||||
const reqKey = engine === "bailian" ? bailianConfig.imageModel : engine === "evolink" ? String(providerPayload.model) : capability.reqKey;
|
||||
const provider = engine === "bailian" ? "bailian" : engine === "evolink" ? "evolink" : "volcengine-visual";
|
||||
const requestPayload = {
|
||||
engine,
|
||||
|
||||
@@ -58,6 +58,7 @@ function normalizeTemplateSettings(value: unknown): ImageTemplate["settings"] {
|
||||
const width = optionalPositiveInteger(value.width);
|
||||
const height = optionalPositiveInteger(value.height);
|
||||
const engine = normalizeImageEngine(value.engine);
|
||||
const model = engine === "evolink" ? normalizeEvolinkModel(value.model) : undefined;
|
||||
const scale = optionalScale(value.scale);
|
||||
const quality = normalizeQuality(value.quality);
|
||||
const size = normalizeSeedreamSize(value.size);
|
||||
@@ -65,6 +66,7 @@ function normalizeTemplateSettings(value: unknown): ImageTemplate["settings"] {
|
||||
const optimizeMode = normalizeOptimizeMode(value.optimizeMode);
|
||||
return {
|
||||
...(engine ? { engine } : {}),
|
||||
...(model ? { model } : {}),
|
||||
...(width && height ? { width, height } : {}),
|
||||
...(typeof value.forceSingle === "boolean" ? { forceSingle: value.forceSingle } : {}),
|
||||
...(scale !== undefined ? { scale } : {}),
|
||||
@@ -99,6 +101,13 @@ function normalizeImageEngine(value: unknown): ImageTemplate["settings"]["engine
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function normalizeEvolinkModel(value: unknown): string | undefined {
|
||||
const text = optionalText(value, 100);
|
||||
if (!text) return undefined;
|
||||
if (!/^[a-z0-9][a-z0-9._-]*$/.test(text)) throw new Error("EvoLink 模型 ID 格式无效");
|
||||
return text;
|
||||
}
|
||||
|
||||
function normalizeSeedreamSize(value: unknown): ImageTemplate["settings"]["size"] | undefined {
|
||||
const text = optionalText(value, 10)?.toUpperCase();
|
||||
if (text === "1K" || text === "1.5K" || text === "2K") return text;
|
||||
|
||||
@@ -35,7 +35,12 @@ export type PublicJobCreateBody = {
|
||||
min_ratio?: number;
|
||||
max_ratio?: number;
|
||||
force_single?: boolean;
|
||||
quality?: string;
|
||||
quality?: unknown;
|
||||
model?: string;
|
||||
size?: string;
|
||||
resolution?: unknown;
|
||||
n?: unknown;
|
||||
imageCount?: unknown;
|
||||
engine?: ImageCreationEngine | VideoCreationEngine;
|
||||
};
|
||||
|
||||
@@ -102,8 +107,14 @@ export async function createPublicGenerationJob(input: {
|
||||
height: asNumber(input.body.height),
|
||||
min_ratio: asNumber(input.body.min_ratio),
|
||||
max_ratio: asNumber(input.body.max_ratio),
|
||||
force_single: Boolean(input.body.force_single),
|
||||
quality: normalizeQuality(input.body.quality)
|
||||
force_single: input.body.force_single,
|
||||
quality: input.body.quality,
|
||||
model: input.body.model,
|
||||
size: input.body.size,
|
||||
resolution: input.body.resolution,
|
||||
n: input.body.n,
|
||||
imageCount: input.body.imageCount,
|
||||
settings: input.body.settings
|
||||
} satisfies SubmitImageJobInput, input.origin);
|
||||
return { job, reused: false };
|
||||
}
|
||||
@@ -134,12 +145,6 @@ function asNumber(value: unknown): number | undefined {
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
function normalizeQuality(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") return undefined;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return ["low", "medium", "high"].includes(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function fingerprintBody(body: PublicJobCreateBody): string {
|
||||
const { idempotencyKey: _idempotencyKey, ...fingerprintSource } = body;
|
||||
return createHash("sha256").update(stableStringify(fingerprintSource)).digest("hex");
|
||||
|
||||
@@ -327,6 +327,7 @@ export type Project = {
|
||||
|
||||
export type ImageTemplateSettings = {
|
||||
engine?: "jimeng" | "evolink" | "bailian" | "seedream";
|
||||
model?: string;
|
||||
width?: number;
|
||||
height?: number;
|
||||
forceSingle?: boolean;
|
||||
|
||||
Reference in new issue
Block a user