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