From 27c76b502fa027d44f67f1282ba9511b1062d517 Mon Sep 17 00:00:00 2001 From: jiajia Date: Wed, 20 May 2026 19:59:32 +0800 Subject: [PATCH] Prevent Seedream size requests below provider minimum Seedream rejects image generation requests whose size resolves below its minimum pixel floor. The NewApiWG adapter now clamps Seedream pixel dimensions to the existing 2K area floor while preserving the requested aspect ratio and leaving non-Seedream image models unchanged. Constraint: Provider error requires image size to be at least 3686400 pixels Rejected: Change the composer default resolution | would affect unrelated image models and UI behavior Confidence: high Scope-risk: narrow Directive: Keep Seedream size normalization in the provider adapter so UI controls remain provider-agnostic Tested: npm run test:run -- src/app/api/generate/providers/__tests__/newapiwg.test.ts Tested: npm run test:run -- src/app/api/generate/__tests__/route.test.ts -t NewApiWG provider Not-tested: npm run lint currently fails because next lint is parsed as a missing lint directory Not-tested: npx tsc --noEmit currently fails on pre-existing test type errors --- .../providers/__tests__/newapiwg.test.ts | 23 +++++++++ src/app/api/generate/providers/newapiwg.ts | 47 +++++++++++++++---- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/src/app/api/generate/providers/__tests__/newapiwg.test.ts b/src/app/api/generate/providers/__tests__/newapiwg.test.ts index 8bcd9079..787907fc 100644 --- a/src/app/api/generate/providers/__tests__/newapiwg.test.ts +++ b/src/app/api/generate/providers/__tests__/newapiwg.test.ts @@ -528,6 +528,29 @@ describe("NewApiWG generation payloads", () => { expect(capturedBody!.aspect_ratio).toBeUndefined(); }); + it("raises Seedream 1K requests to the provider minimum pixel size", async () => { + const result = await generateWithNewApiWG("test-key", "https://newapi.example/v1", makeInput({ + model: { + id: "doubao-seedream-4-5-251128", + name: "Doubao Seedream 4.5", + description: null, + provider: "newapiwg", + capabilities: ["text-to-image", "image-to-image"], + }, + prompt: "draw a wide maze", + parameters: { + resolution: "1K", + imageSize: "1K", + aspectRatio: "21:9", + aspect_ratio: "21:9", + }, + })); + + expect(result.success).toBe(true); + expect(capturedBody!.model).toBe("doubao-seedream-4-5-251128"); + expect(capturedBody!.size).toBe("2936x1264"); + }); + 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 72828c43..e633c850 100644 --- a/src/app/api/generate/providers/newapiwg.ts +++ b/src/app/api/generate/providers/newapiwg.ts @@ -420,6 +420,7 @@ const NEWAPIWG_IMAGE_RESOLUTION_PIXELS: Record = { "2K": 2560 * 1440, "4K": 3840 * 2160, }; +const NEWAPIWG_SEEDREAM_MIN_IMAGE_PIXELS = NEWAPIWG_IMAGE_RESOLUTION_PIXELS["2K"]; const REFERENCE_IMAGE_FETCH_TIMEOUT_MS = 30_000; const MAX_REFERENCE_IMAGE_BYTES = 25 * 1024 * 1024; @@ -493,12 +494,44 @@ function ceilToMultiple(value: number, multiple: number): number { return Math.ceil(value / multiple) * multiple; } -function resolveNewApiWGImageSize(parameters: Record | undefined): string { +function isSeedreamImageModel(modelId: string): boolean { + return /seedream/i.test(modelId); +} + +function parsePixelSize(value: string): { width: number; height: number } | null { + const match = value.match(/^(\d+)x(\d+)$/i); + if (!match) return null; + const width = Number(match[1]); + const height = Number(match[2]); + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) { + return null; + } + return { width, height }; +} + +function pixelSizeForAspectRatio(targetPixels: number, ratio: { width: number; height: number }): string { + 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 clampPixelSize(size: string, minPixels: number): string { + if (minPixels <= 0) return size; + const parsed = parsePixelSize(size); + if (!parsed || parsed.width * parsed.height >= minPixels) return size; + return pixelSizeForAspectRatio(minPixels, parsed); +} + +function resolveNewApiWGImageSize( + parameters: Record | undefined, + modelId: string +): string { + const minPixels = isSeedreamImageModel(modelId) ? NEWAPIWG_SEEDREAM_MIN_IMAGE_PIXELS : 0; const requestedSize = getStringParameter(parameters, "imageSize") ?? getStringParameter(parameters, "resolution"); if (requestedSize) { - if (/^\d+x\d+$/i.test(requestedSize)) return requestedSize; + if (/^\d+x\d+$/i.test(requestedSize)) return clampPixelSize(requestedSize, minPixels); const targetPixels = NEWAPIWG_IMAGE_RESOLUTION_PIXELS[requestedSize]; if (targetPixels) { @@ -506,15 +539,13 @@ function resolveNewApiWGImageSize(parameters: Record | undefine 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}`; + return pixelSizeForAspectRatio(Math.max(targetPixels, minPixels), ratio); } } const explicitSize = getStringParameter(parameters, "size"); - if (explicitSize) return explicitSize; - return "1024x1024"; + if (explicitSize) return clampPixelSize(explicitSize, minPixels); + return clampPixelSize("1024x1024", minPixels); } function isImagenGenerationModel(modelId: string): boolean { @@ -940,7 +971,7 @@ export async function generateWithNewApiWG( } const endpoint = `${normalizedBaseUrl}/${isVideo ? "video" : "images"}/generations`; - const imageSize = resolveNewApiWGImageSize(input.parameters); + const imageSize = resolveNewApiWGImageSize(input.parameters, input.model.id); const imageParameters = normalizeNewApiWGImageGenerationParameters(input.parameters); const rawDynamicInputs = dynamicInputsWithoutPrompt(input); const normalizedVideoInputs = isVideo