From e4a94f82c075904448bff158b8b34717b604d70d Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Thu, 22 Jan 2026 17:56:15 +0100 Subject: [PATCH 1/4] feat(runware): use async delivery with polling for image generation Switch from synchronous requests to async delivery method to avoid timeouts on slower image generation models. The client now submits tasks with deliveryMethod: "async" and polls for results using the getResponse task type. --- .../src/runware/createRunwareClient.ts | 138 +++++++++++++++--- 1 file changed, 120 insertions(+), 18 deletions(-) diff --git a/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts b/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts index dda9d148..e3838744 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/createRunwareClient.ts @@ -2,8 +2,17 @@ * Runware HTTP REST API client * Uses the REST API instead of the WebSocket SDK * API documentation: https://runware.ai/docs/en/getting-started/how-to-connect + * + * Image generation uses async delivery with polling: + * 1. Submit task with deliveryMethod: "async" + * 2. Receive taskUUID in response + * 3. Poll with getResponse until status is "success" or "failed" */ +// Polling configuration +const POLL_INTERVAL_MS = 1000; // Poll every 1 second +const MAX_POLL_TIME_MS = 5 * 60 * 1000; // Maximum 5 minutes + export interface RunwareImageInferenceParams { model: string; positivePrompt: string; @@ -43,11 +52,13 @@ export type RunwareImageInferenceInput = export interface RunwareImageResult { taskType: string; taskUUID: string; - imageURL: string; + status?: 'processing' | 'success' | 'failed'; + imageURL?: string; imageUUID?: string; seed?: number; NSFWContent?: boolean; cost?: number; + errorMessage?: string; } export interface RunwareErrorResponse { @@ -77,10 +88,106 @@ function generateUUID(): string { }); } +function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); +} + export function createRunwareClient( proxyUrl: string, headers?: Record ): RunwareClient { + /** + * Poll for image generation results using getResponse task + */ + // eslint-disable-next-line no-await-in-loop + async function pollForResult( + taskUUID: string, + abortSignal?: AbortSignal + ): Promise { + const startTime = Date.now(); + + // Polling must be sequential - each request depends on the previous result + /* eslint-disable no-await-in-loop */ + while (Date.now() - startTime < MAX_POLL_TIME_MS) { + // Check if aborted + if (abortSignal?.aborted) { + throw new Error('Image generation aborted'); + } + + // Poll for results + const pollBody = [ + { + taskType: 'getResponse', + taskUUID + } + ]; + + const pollResponse = await fetch(proxyUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + ...headers + }, + body: JSON.stringify(pollBody), + signal: abortSignal + }); + + if (!pollResponse.ok) { + const errorText = await pollResponse.text(); + throw new Error( + `Runware API polling error: ${pollResponse.status} - ${errorText}` + ); + } + + const pollResult = await pollResponse.json(); + + // Check for errors + if (pollResult.errors != null && pollResult.errors.length > 0) { + const error = pollResult.errors[0] as RunwareErrorResponse; + throw new Error(`Runware API error: ${error.errorMessage}`); + } + + if (pollResult.error != null) { + throw new Error( + `Runware API error: ${ + pollResult.error.errorMessage ?? pollResult.error + }` + ); + } + + const data = pollResult.data; + if (data != null && Array.isArray(data) && data.length > 0) { + const imageResult = data.find( + (item: any) => + item.taskType === 'imageInference' && item.taskUUID === taskUUID + ) as RunwareImageResult | undefined; + + if (imageResult != null) { + // Check status + if (imageResult.status === 'success' && imageResult.imageURL) { + return imageResult; + } + + if (imageResult.status === 'failed') { + throw new Error( + imageResult.errorMessage ?? 'Image generation failed' + ); + } + + // Still processing, continue polling + } + } + + // Wait before next poll + await sleep(POLL_INTERVAL_MS); + } + /* eslint-enable no-await-in-loop */ + + throw new Error('Image generation timed out'); + } + return { imageInference: async ( params: RunwareImageInferenceInput, @@ -88,13 +195,14 @@ export function createRunwareClient( ): Promise => { const taskUUID = generateUUID(); - // Build the request body as a JSON array with the imageInference task + // Build the request body with async delivery to avoid timeouts const requestBody = [ { taskType: 'imageInference', taskUUID, model: params.model, positivePrompt: params.positivePrompt, + deliveryMethod: 'async', // Required to avoid timeouts outputType: params.outputType ?? 'URL', outputFormat: params.outputFormat ?? 'PNG', numberResults: params.numberResults ?? 1, @@ -120,6 +228,7 @@ export function createRunwareClient( } ]; + // Submit the image generation task const response = await fetch(proxyUrl, { method: 'POST', headers: { @@ -150,7 +259,7 @@ export function createRunwareClient( ); } - // The response contains a data array with image results + // Verify we got the task acknowledgment const data = result.data; if (data == null || !Array.isArray(data)) { throw new Error( @@ -158,26 +267,19 @@ export function createRunwareClient( ); } - // Filter for imageInference results that match our taskUUID - const imageResults = data.filter( + const taskAck = data.find( (item: any) => item.taskType === 'imageInference' && item.taskUUID === taskUUID - ) as RunwareImageResult[]; - - if (imageResults.length === 0) { - // Fallback: if no exact match, try to get any imageInference results - const anyImageResults = data.filter( - (item: any) => item.taskType === 'imageInference' - ) as RunwareImageResult[]; + ); - if (anyImageResults.length > 0) { - return anyImageResults; - } - - throw new Error('No image results in Runware API response'); + if (taskAck == null) { + throw new Error('Image generation task was not acknowledged'); } - return imageResults; + // Poll for the result + const imageResult = await pollForResult(taskUUID, abortSignal); + + return [imageResult]; } }; } From b317aebfa19e116afc479921f682cd6832a1c887 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Fri, 23 Jan 2026 16:26:56 +0100 Subject: [PATCH 2/4] fix(runware): use model-specific dimensions for NanoBanana2Pro and Seedream4 NanoBanana2Pro and Seedream4 have specific dimension requirements that don't match the generic ASPECT_RATIO_MAP. Using incorrect dimensions (e.g., 1344x768 for 16:9) caused API errors. - NanoBanana2Pro: Add custom dimension map with API-required values (e.g., 16:9 is 1376x768, not 1344x768) - NanoBanana2Pro I2I: Add resolution parameter for auto aspect detection - Seedream4: Use 2K dimensions since 1K only supports 1:1 aspect ratio --- .../src/runware/NanoBanana2Pro.image2image.ts | 2 ++ .../src/runware/NanoBanana2Pro.text2image.ts | 31 ++++++++++++++----- .../src/runware/Seedream4.text2image.ts | 26 +++++++++++----- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts index d73ba42b..43629fb3 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.image2image.ts @@ -144,10 +144,12 @@ export function NanoBanana2ProImage2Image( // Map to Runware API format // Nano Banana 2 Pro uses inputs.referenceImages for image-to-image // Supports up to 14 reference images + // Resolution parameter tells the API to auto-detect aspect ratio from reference image const referenceImages = input.image_urls ?? (input.image_url ? [input.image_url] : []); return { positivePrompt: input.prompt, + resolution: '2k', inputs: { referenceImages } diff --git a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts index cc3e6f04..0617d773 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/NanoBanana2Pro.text2image.ts @@ -8,10 +8,26 @@ import type CreativeEditorSDK from '@cesdk/cesdk-js'; // @ts-ignore - JSON import import NanoBanana2ProSchema from './NanoBanana2Pro.text2image.json'; import createImageProvider from './createImageProvider'; -import { - RunwareProviderConfiguration, - getImageDimensionsFromAspectRatio -} from './types'; +import { RunwareProviderConfiguration } from './types'; + +/** + * Nano Banana 2 Pro (google:4@2) dimension constraints + * These are model-specific dimensions required by the Runware API. + * Source: https://runware.ai/docs/providers/google.md + * + * Note: Generic ASPECT_RATIO_MAP dimensions (e.g., 1344x768 for 16:9) are NOT valid + * for this model - it requires specific dimension combinations per resolution tier. + */ +const NANO_BANANA_2_PRO_DIMENSIONS: Record< + string, + { width: number; height: number } +> = { + '1:1': { width: 1024, height: 1024 }, + '16:9': { width: 1376, height: 768 }, + '9:16': { width: 768, height: 1376 }, + '4:3': { width: 1200, height: 896 }, + '3:4': { width: 896, height: 1200 } +}; /** * Input interface for Nano Banana 2 Pro text-to-image @@ -56,11 +72,10 @@ export function NanoBanana2Pro( cesdk, middleware: config.middlewares ?? [], getImageSize: (input) => - getImageDimensionsFromAspectRatio(input.aspect_ratio ?? '1:1'), + NANO_BANANA_2_PRO_DIMENSIONS[input.aspect_ratio ?? '1:1'], mapInput: (input) => { - const dims = getImageDimensionsFromAspectRatio( - input.aspect_ratio ?? '1:1' - ); + const dims = + NANO_BANANA_2_PRO_DIMENSIONS[input.aspect_ratio ?? '1:1']; return { positivePrompt: input.prompt, width: dims.width, diff --git a/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts b/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts index 84407b10..615d52e4 100644 --- a/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts +++ b/packages/plugin-ai-image-generation-web/src/runware/Seedream4.text2image.ts @@ -8,10 +8,22 @@ import type CreativeEditorSDK from '@cesdk/cesdk-js'; // @ts-ignore - JSON import import Seedream4Schema from './Seedream4.text2image.json'; import createImageProvider from './createImageProvider'; -import { - RunwareProviderConfiguration, - getImageDimensionsFromAspectRatio -} from './types'; +import { RunwareProviderConfiguration } from './types'; + +/** + * Seedream 4.0 (bytedance:5@0) dimension constraints - 2K resolution + * At 1K resolution, only 1024×1024 (1:1) is supported. + * For other aspect ratios, we use 2K dimensions. + * Source: https://runware.ai/docs/providers/bytedance.md + */ +const SEEDREAM4_DIMENSIONS: Record = + { + '1:1': { width: 2048, height: 2048 }, + '16:9': { width: 2560, height: 1440 }, + '9:16': { width: 1440, height: 2560 }, + '4:3': { width: 2304, height: 1728 }, + '3:4': { width: 1728, height: 2304 } + }; /** * Input interface for Seedream 4.0 text-to-image @@ -56,11 +68,9 @@ export function Seedream4( cesdk, middleware: config.middlewares ?? [], getImageSize: (input) => - getImageDimensionsFromAspectRatio(input.aspect_ratio ?? '1:1'), + SEEDREAM4_DIMENSIONS[input.aspect_ratio ?? '1:1'], mapInput: (input) => { - const dims = getImageDimensionsFromAspectRatio( - input.aspect_ratio ?? '1:1' - ); + const dims = SEEDREAM4_DIMENSIONS[input.aspect_ratio ?? '1:1']; return { positivePrompt: input.prompt, width: dims.width, From ff007c03c83dad9cc9b8bcae8a08157432fb5cf1 Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Fri, 23 Jan 2026 16:32:17 +0100 Subject: [PATCH 3/4] docs(runware): document model-specific dimension requirements Add documentation for models that require specific dimension combinations instead of the generic ASPECT_RATIO_MAP values: - api-patterns.md: Add warning about model-specific dimensions - implementation-notes.md: Add dimension tables for NanoBanana2Pro, Seedream 4.0, and Seedream 4.5 --- specs/providers/runware/api-patterns.md | 8 ++++ .../providers/runware/implementation-notes.md | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/specs/providers/runware/api-patterns.md b/specs/providers/runware/api-patterns.md index ba3c157c..6036a15d 100644 --- a/specs/providers/runware/api-patterns.md +++ b/specs/providers/runware/api-patterns.md @@ -95,6 +95,14 @@ AIR (AI Resource Identifier) follows the pattern: All dimensions must be divisible by 64. Use the predefined mappings: +**⚠️ Important**: Some models require specific dimension combinations and do NOT accept +the generic `ASPECT_RATIO_MAP` values. Always check the model's documentation. Models +with specific requirements include: +- **Nano Banana 2 Pro** (`google:4@2`): Requires specific 1K/2K/4K dimension presets +- **Seedream 4.0** (`bytedance:5@0`): Only supports 1:1 at 1K; other ratios need 2K dimensions + +See `implementation-notes.md` for model-specific dimension tables. + ### Aspect Ratio Map ```typescript diff --git a/specs/providers/runware/implementation-notes.md b/specs/providers/runware/implementation-notes.md index bbd76992..6f97c8a0 100644 --- a/specs/providers/runware/implementation-notes.md +++ b/specs/providers/runware/implementation-notes.md @@ -518,11 +518,50 @@ getBlockInput: async (input) => { ### Known Model Constraints +#### Flexible Range Models (I2I) + +These models accept any dimensions within the specified range: + | Model | AIR | Width | Height | Multiple | |-------|-----|-------|--------|----------| | FLUX.2 [dev] | `runware:400@1` | 512-2048 | 512-2048 | 16 | | FLUX.2 [pro] | `bfl:5@1` | 256-1920 | 256-1920 | 16 | | FLUX.2 [flex] | `bfl:6@1` | 256-1920 | 256-1920 | 16 | +| Seedream 4.0 | `bytedance:5@0` | 128-2048 | 128-2048 | 64 | + +#### Fixed Dimension Models (T2I) + +These models require specific dimension combinations. The generic `ASPECT_RATIO_MAP` does NOT work: + +**Nano Banana 2 Pro** (`google:4@2`) - 1K Resolution: + +| Aspect Ratio | Width | Height | +|--------------|-------|--------| +| 1:1 | 1024 | 1024 | +| 16:9 | 1376 | 768 | +| 9:16 | 768 | 1376 | +| 4:3 | 1200 | 896 | +| 3:4 | 896 | 1200 | + +**Seedream 4.0** (`bytedance:5@0`) - 2K Resolution (1K only supports 1:1): + +| Aspect Ratio | Width | Height | +|--------------|-------|--------| +| 1:1 | 2048 | 2048 | +| 16:9 | 2560 | 1440 | +| 9:16 | 1440 | 2560 | +| 4:3 | 2304 | 1728 | +| 3:4 | 1728 | 2304 | + +**Seedream 4.5** (`bytedance:seedream@4.5`) - 2K Resolution: + +| Aspect Ratio | Width | Height | +|--------------|-------|--------| +| 1:1 | 2048 | 2048 | +| 16:9 | 2560 | 1440 | +| 9:16 | 1440 | 2560 | +| 4:3 | 2304 | 1728 | +| 3:4 | 1728 | 2304 | ### Extracting Constraints from API Docs From 9a1308fa616b73d70b58493362240c85f2765a0c Mon Sep 17 00:00:00 2001 From: Marcin Skirzynski Date: Fri, 23 Jan 2026 16:32:58 +0100 Subject: [PATCH 4/4] docs: add changelog entries for Runware dimension fixes --- CHANGELOG-AI.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG-AI.md b/CHANGELOG-AI.md index 51397c76..44e00255 100644 --- a/CHANGELOG-AI.md +++ b/CHANGELOG-AI.md @@ -2,6 +2,16 @@ ## [Unreleased] +### Fixed + +- [image-generation] **Runware NanoBanana2Pro Dimensions**: Fixed API errors when using non-square aspect ratios (16:9, 9:16, 4:3, 3:4) with Nano Banana 2 Pro provider. The model requires specific dimension combinations (e.g., 1376×768 for 16:9) that differ from the generic aspect ratio map. +- [image-generation] **Runware Seedream 4.0 Dimensions**: Fixed API errors when using non-square aspect ratios with Seedream 4.0 provider. At 1K resolution, only 1:1 is supported; now using 2K dimensions for all aspect ratios to ensure compatibility. + +### Improvements + +- [image-generation] **Runware Async Delivery**: Switched from synchronous requests to async delivery with polling to avoid timeouts on slower image generation models. +- [image-generation] **Runware NanoBanana2Pro I2I**: Added `resolution: '2k'` parameter to image-to-image generation for automatic aspect ratio detection from reference images. + ## [0.2.16] - 2026-01-16 ### New Features