diff --git a/src/app/api/generate/providers/__tests__/newapiwg.test.ts b/src/app/api/generate/providers/__tests__/newapiwg.test.ts index d14458d5..d777f287 100644 --- a/src/app/api/generate/providers/__tests__/newapiwg.test.ts +++ b/src/app/api/generate/providers/__tests__/newapiwg.test.ts @@ -375,6 +375,29 @@ describe("NewApiWG generation payloads", () => { expect(capturedBody!.image).toBe("data:image/png;base64,input"); }); + it("maps Seedream image resolution and aspect ratio to provider pixel size", async () => { + const result = await generateWithNewApiWG("test-key", "https://newapi.example/v1", makeInput({ + model: { + id: "apiyi_seedream5.0", + name: "APIYI Seedream 5.0", + description: null, + provider: "newapiwg", + capabilities: ["text-to-image", "image-to-image"], + }, + prompt: "draw a landscape", + parameters: { + resolution: "4K", + imageSize: "4K", + aspectRatio: "16:9", + aspect_ratio: "16:9", + }, + })); + + expect(result.success).toBe(true); + expect(capturedBody!.model).toBe("apiyi_seedream5.0"); + expect(capturedBody!.size).toBe("3840x2160"); + }); + it("blocks cached Doubao chat models before calling media generation endpoints", async () => { const result = await generateWithNewApiWG("test-key", "https://newapi.example/v1", makeInput({ model: { diff --git a/src/app/api/generate/providers/newapiwg.ts b/src/app/api/generate/providers/newapiwg.ts index cb7c5396..b7495736 100644 --- a/src/app/api/generate/providers/newapiwg.ts +++ b/src/app/api/generate/providers/newapiwg.ts @@ -421,6 +421,13 @@ const NEWAPIWG_VIDEO_AUDIO_INPUTS = new Set([ "reference_audio_urls", ]); +const NEWAPIWG_IMAGE_RESOLUTION_PIXELS: Record = { + "512": 512 * 512, + "1K": 1024 * 1024, + "2K": 2560 * 1440, + "4K": 3840 * 2160, +}; + function asStringArray(value: string | string[] | undefined): string[] { if (typeof value === "string") return value.length > 0 ? [value] : []; if (Array.isArray(value)) return value.filter((item) => typeof item === "string" && item.length > 0); @@ -471,6 +478,48 @@ function buildNewApiWGImageInputs(input: GenerationInput): Record | undefined, key: string): string | null { + const value = parameters?.[key]; + return typeof value === "string" && value.length > 0 ? value : null; +} + +function parseAspectRatio(value: string | null): { width: number; height: number } { + const match = value?.match(/^(\d+):(\d+)$/); + if (!match) return { width: 1, height: 1 }; + const width = Number(match[1]); + const height = Number(match[2]); + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return { width: 1, height: 1 }; + } + return { width, height }; +} + +function ceilToMultiple(value: number, multiple: number): number { + return Math.ceil(value / multiple) * multiple; +} + +function resolveNewApiWGImageSize(parameters: Record | undefined): string { + const explicitSize = getStringParameter(parameters, "size"); + if (explicitSize) return explicitSize; + + const requestedSize = + getStringParameter(parameters, "imageSize") ?? + getStringParameter(parameters, "resolution"); + if (!requestedSize) return "1024x1024"; + if (/^\d+x\d+$/i.test(requestedSize)) return requestedSize; + + const targetPixels = NEWAPIWG_IMAGE_RESOLUTION_PIXELS[requestedSize]; + if (!targetPixels) return "1024x1024"; + + const ratio = parseAspectRatio( + getStringParameter(parameters, "aspectRatio") ?? + getStringParameter(parameters, "aspect_ratio") + ); + const width = ceilToMultiple(Math.sqrt(targetPixels * ratio.width / ratio.height), 8); + const height = ceilToMultiple(Math.sqrt(targetPixels * ratio.height / ratio.width), 8); + return `${width}x${height}`; +} + function isImagenGenerationModel(modelId: string): boolean { return /(^|[\/_\-.])imagen(\d|[\/_\-.]|$)/i.test(modelId); } @@ -800,7 +849,7 @@ export async function generateWithNewApiWG( } const endpoint = `${normalizedBaseUrl}/${isVideo ? "video" : "images"}/generations`; - const imageSize = typeof input.parameters?.size === "string" ? input.parameters.size : "1024x1024"; + const imageSize = resolveNewApiWGImageSize(input.parameters); const rawDynamicInputs = dynamicInputsWithoutPrompt(input); const normalizedVideoInputs = isVideo ? normalizeNewApiWGVideoInputs(input, rawDynamicInputs)