nexu-io/open-design · error · Error
openrouter image ${resp.status}: ${truncate(text, 240)}
Error message
openrouter image ${resp.status}: ${truncate(text, 240)} What it means
Thrown by renderOpenRouterImage when POST /api/v1/chat/completions returns non-2xx. The request carries authorization bearer, HTTP-Referer, X-Title attribution headers, and an AbortSignal.timeout capped by max(OPENAI_IMAGE_HEADERS_TIMEOUT_MS, OPENAI_IMAGE_BODY_TIMEOUT_MS). The error embeds status plus the first 240 chars of the body.
Source
Thrown at apps/daemon/src/media/index.ts:1870
aspect_ratio: aspectRatio,
image_size: '1K',
};
body.image_config = imageConfig;
const resp = await fetch(`${baseUrl}/chat/completions`, withMediaRequestInit(ctx, {
method: 'POST',
headers: {
'authorization': `Bearer ${credentials.apiKey}`,
'content-type': 'application/json',
'HTTP-Referer': 'https://opendesign.dev',
'X-Title': 'Open Design',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(Math.max(OPENAI_IMAGE_HEADERS_TIMEOUT_MS, OPENAI_IMAGE_BODY_TIMEOUT_MS)),
}));
const text = await resp.text();
if (!resp.ok) {
throw new Error(`openrouter image ${resp.status}: ${truncate(text, 240)}`);
}
let data: any;
try {
data = JSON.parse(text);
} catch {
throw new Error(`openrouter image non-JSON response: ${truncate(text, 200)}`);
}
// Extract the first generated image from the response.
const images: any[] | undefined =
data?.choices?.[0]?.message?.images;
if (!images || images.length === 0) {
throw new Error(
`openrouter image response contained no images for model ${wireModel}: `
+ truncate(text, 200),
);
}View on GitHub (pinned to 5be4028344)
Solutions
- Read the embedded status+body: 401→fix key, 402→add credits, 404→check the model slug, 429→slow down, 400→check modality support.
- Confirm the resolved wireModel (after stripping the openrouter/ prefix) matches a slug listed at openrouter.ai/models with image output.
- Verify the OpenRouter account has credits for the chosen model tier.
- If 5xx, retry with backoff or pick an alternate image model.
Example fix
// before
if (!resp.ok) {
throw new Error(`openrouter image ${resp.status}: ${truncate(text, 240)}`);
}
// after — actionable hints by status
if (!resp.ok) {
const hint = resp.status === 401 ? ' (bad key)'
: resp.status === 402 ? ' (no credits)'
: resp.status === 404 ? ' (unknown model)'
: resp.status === 429 ? ' (rate limited)'
: '';
throw new Error(`openrouter image ${resp.status}${hint}: ${truncate(text, 240)}`);
} Defensive patterns
Strategy: try-catch
Validate before calling
function describeOpenRouterImageError(status: number, body: string): string {
const hint = status === 401 ? 'bad key'
: status === 402 ? 'no credits'
: status === 404 ? 'unknown model'
: status === 429 ? 'rate limited'
: status >= 500 ? 'upstream provider outage — retry'
: 'see body';
return `openrouter image ${status} (${hint}): ${truncate(body, 240)}`;
}
function isRetryableOpenRouterStatus(status: number): boolean {
return status === 429 || status >= 500;
} Type guard
function isOpenRouterModelCapable(model: string): boolean {
// Caller should consult the OpenRouter catalogue; this is a last-mile sanity check
return /image|vision|gemini|dall-e|flux|seedream/i.test(model);
} Try / catch
for (let attempt = 0; attempt < 2; attempt++) {
const resp = await fetch(/* ... */);
if (resp.ok) { /* parse and return */ }
if (attempt === 0 && isRetryableOpenRouterStatus(resp.status)) {
await sleep(1500 * (attempt + 1));
continue;
}
throw new Error(describeOpenRouterImageError(resp.status, await resp.text()));
} Prevention
- Classify 402 (credits) distinctly so users know to top up rather than debug.
- Confirm the model slug supports image output at openrouter.ai/models.
- Retry on 429/5xx with exponential backoff before surfacing.
- Keep attribution headers (HTTP-Referer, X-Title) present — some models 400 without them.
When it happens
Trigger: 401 (bad/missing key), 402 (insufficient credits on the OpenRouter account), 404 (model slug not in the OpenRouter catalogue or not enabled for the key), 429 (rate limit), 400 (model does not support the requested modalities or the prompt exceeds limits), or upstream 5xx from the underlying provider routed through OpenRouter.
Common situations: Free OpenRouter key with no credits hitting a paid image model, model id with the wrong prefix, regional rate limits, or the underlying provider (e.g. Gemini-via-OpenRouter) incident surfacing as a 5xx.
Related errors
- nano-banana image ${resp.status}: ${truncate(text, 240)}
- no OpenRouter API key — configure it in Settings or set OPEN
- openrouter video submit ${submitResp.status}: ${truncate(sub
- no Nano Banana API key — configure it in Settings or set OD_
- openrouter image non-JSON response: ${truncate(text, 200)}
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/caf7c6b80352c774.
Report an issue: GitHub.