nexu-io/open-design · error · Error
openrouter job ${lastStatus}: ${reason}
Error message
openrouter job ${lastStatus}: ${reason} What it means
Thrown when pollData.status is one of the terminal failure states — 'failed', 'expired', or 'cancelled' — and the loop aborts immediately rather than continuing to poll. The message embeds the status and a reason string derived from pollData.error.message (preferred), pollData.error, or the bare status, so the operator sees why the underlying provider killed the job.
Source
Thrown at apps/daemon/src/media/index.ts:2116
onProgress(
`openrouter job ${jobId} status=${lastStatus || 'pending'} (elapsed ${elapsedSec}s)`,
);
}
if (lastStatus === 'completed') {
videoUrls = pollData?.unsigned_urls || null;
break;
}
if (
lastStatus === 'failed'
|| lastStatus === 'expired'
|| lastStatus === 'cancelled'
) {
const reasonRaw =
pollData?.error?.message || pollData?.error || lastStatus;
const reason =
typeof reasonRaw === 'string' ? reasonRaw : JSON.stringify(reasonRaw);
throw new Error(`openrouter job ${lastStatus}: ${reason}`);
}
}
if (!videoUrls || videoUrls.length === 0) {
const elapsedSec = Math.round((Date.now() - startedAt) / 1000);
const ceilingSec = Math.round(maxMs / 1000);
throw new Error(
`openrouter video timed out after ${elapsedSec}s waiting for status=completed `
+ `(last status: ${lastStatus || 'pending'}, ceiling ${ceilingSec}s). `
+ `If your jobs legitimately need longer, raise OD_OPENROUTER_VIDEO_MAX_POLL_MS.`,
);
}
// ── Step 3: Download the video binary ──────────────────────────────
// unsigned_urls are often third-party CDNs where sending our API key
// would leak credentials. However, sometimes OpenRouter returns a proxied
// openrouter.ai URL that still requires authorization. We only attach the
// auth header if the host is explicitly allowlisted as openrouter.ai.View on GitHub (pinned to 5be4028344)
Solutions
- Read the embedded reason — provider error messages are usually specific (content filter, invalid input, quota).
- If content/safety related, reword ctx.prompt or change the source image and resubmit.
- If 'expired', resubmit and consider a faster model or higher OD_OPENROUTER_VIDEO_MAX_POLL_MS so the daemon does not give up before the provider.
- If 'cancelled' was unexpected, check whether another session or the OpenRouter dashboard cancelled the job.
- On provider incidents, retry after the incident resolves.
Example fix
// before
const reasonRaw = pollData?.error?.message || pollData?.error || lastStatus;
const reason = typeof reasonRaw === 'string' ? reasonRaw : JSON.stringify(reasonRaw);
throw new Error(`openrouter job ${lastStatus}: ${reason}`);
// after — preserve structured error for telemetry, still throw readable text
const reasonRaw = pollData?.error?.message || pollData?.error || lastStatus;
const reason = typeof reasonRaw === 'string' ? reasonRaw : JSON.stringify(reasonRaw);
const err = new Error(`openrouter job ${lastStatus}: ${reason}`);
(err as any).providerError = pollData?.error;
(err as any).jobStatus = lastStatus;
throw err; Defensive patterns
Strategy: try-catch
Validate before calling
// Distinguish failure classes so the user gets actionable guidance
function classifyOpenRouterVideoFailure(status: string, reason: string): { kind: string; message: string } {
if (/block|policy|safety|nsfw|content/i.test(reason)) {
return { kind: 'content-filter', message: `Video rejected by provider safety filter: ${reason}` };
}
if (status === 'expired') {
return { kind: 'expired', message: `Video job expired before completing: ${reason}` };
}
if (/quota|credit|billing|payment/i.test(reason)) {
return { kind: 'billing', message: `Video failed for billing reasons: ${reason}` };
}
return { kind: 'failed', message: `openrouter job ${status}: ${reason}` };
} Type guard
type OpenRouterTerminalStatus = 'failed' | 'expired' | 'cancelled';
function isTerminalFailure(status: string): status is OpenRouterTerminalStatus {
return status === 'failed' || status === 'expired' || status === 'cancelled';
} Try / catch
if (isTerminalFailure(lastStatus)) {
const cls = classifyOpenRouterVideoFailure(lastStatus, reason);
const err = new Error(cls.message);
(err as any).failureKind = cls.kind;
(err as any).providerError = pollData?.error;
throw err;
} Prevention
- Classify the failure reason (content filter / expired / billing) so the user gets targeted guidance.
- Reword prompts or change the i2v source image on content-filter failures rather than retrying verbatim.
- On 'expired', resubmit and consider a faster model or longer poll ceiling.
- Preserve pollData.error on the thrown error for telemetry/support.
When it happens
Trigger: Underlying video provider (Seedance, Kling, etc.) returned status=failed with an error detail, the job exceeded OpenRouter's TTL and went expired, the user/account cancelled the job out-of-band, or the provider rejected the input (e.g. disallowed content in the prompt or the i2v frame).
Common situations: Prompt tripped the underlying provider's safety policy, the source image for i2v was rejected, the job ran past OpenRouter's max lifetime, or a provider incident marked in-flight jobs as failed.
Related errors
- openrouter video submit returned no job id or polling_url: $
- openrouter poll ${pollResp.status}: ${truncate(pollText, 240
- openrouter poll non-JSON: ${truncate(pollText, 200)}
- openrouter image response contained no images for model ${wi
- openrouter video submit ${submitResp.status}: ${truncate(sub
AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12).
Data as JSON: /api/errors/61f0b565135fe904.
Report an issue: GitHub.