Browse Source

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
feature_login^2
jiajia 2 months ago
parent
commit
27c76b502f
  1. 23
      src/app/api/generate/providers/__tests__/newapiwg.test.ts
  2. 47
      src/app/api/generate/providers/newapiwg.ts

23
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: {

47
src/app/api/generate/providers/newapiwg.ts

@ -420,6 +420,7 @@ const NEWAPIWG_IMAGE_RESOLUTION_PIXELS: Record<string, number> = {
"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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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

Loading…
Cancel
Save