nexu-io/open-design · error · Error
openrouter video submit returned no job id or polling_url: $
Error message
openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)} What it means
Thrown when the parsed submit body lacks either an `id` or a `polling_url` field. Both are required: jobId drives the progress log and the (fallback) status poll, and pollingUrl is the primary polling target. Missing either means the async contract was not honoured, so the daemon cannot track the job.
Source
Thrown at apps/daemon/src/media/index.ts:2043
body: JSON.stringify(body),
}));
const submitText = await submitResp.text();
if (!submitResp.ok) {
throw new Error(
`openrouter video submit ${submitResp.status}: ${truncate(submitText, 240)}`,
);
}
let submitData: any;
try {
submitData = JSON.parse(submitText);
} catch {
throw new Error(`openrouter video non-JSON: ${truncate(submitText, 200)}`);
}
const jobId = submitData?.id;
const pollingUrl = submitData?.polling_url;
if (!jobId || !pollingUrl) {
throw new Error(
`openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}`,
);
}
// ── Step 2: Poll until completion ──────────────────────────────────
const startedAt = Date.now();
const configuredMaxMs = Number(process.env.OD_OPENROUTER_VIDEO_MAX_POLL_MS);
const maxMs =
Number.isFinite(configuredMaxMs) && configuredMaxMs >= 60_000
? configuredMaxMs
: 30 * 60 * 1000; // 30 minutes default
const configuredPollIntervalMs = Number(process.env.OD_OPENROUTER_VIDEO_POLL_INTERVAL_MS);
const pollIntervalMs =
Number.isFinite(configuredPollIntervalMs) && configuredPollIntervalMs >= 0
? configuredPollIntervalMs
: DEFAULT_OPENROUTER_VIDEO_POLL_INTERVAL_MS;
let lastStatus = submitData?.status || 'pending';View on GitHub (pinned to 5be4028344)
Solutions
- Inspect the truncated submit body in the error to see the actual envelope.
- If OpenRouter renamed the fields, update the extraction at apps/daemon/src/media/index.ts:2041-2042 (and fall back across aliases).
- If the body is an error envelope, address the underlying cause (credits, model, etc.) and retry.
- Retry once in case of a transient partial response.
Example fix
// before
const jobId = submitData?.id;
const pollingUrl = submitData?.polling_url;
if (!jobId || !pollingUrl) {
throw new Error(`openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}`);
}
// after — accept field aliases for schema drift
const jobId = submitData?.id || submitData?.job_id;
const pollingUrl = submitData?.polling_url || submitData?.status_url || submitData?.urls?.polling;
if (!jobId || !pollingUrl) {
throw new Error(`openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}`);
} Defensive patterns
Strategy: type-guard
Validate before calling
// Accept common field aliases and surface the real envelope
function extractOpenRouterJobRef(submitData: any, rawText: string): { jobId: string; pollingUrl: string } {
const jobId = submitData?.id || submitData?.job_id || submitData?.request_id;
const pollingUrl = submitData?.polling_url || submitData?.status_url || submitData?.urls?.polling;
if (typeof jobId === 'string' && typeof pollingUrl === 'string') {
return { jobId, pollingUrl };
}
throw new Error(`openrouter video submit: no job id/polling_url (keys=${Object.keys(submitData || {}).join(',')}): ${truncate(rawText, 200)}`);
} Type guard
interface OpenRouterVideoJobRef { id?: string; job_id?: string; request_id?: string; polling_url?: string; status_url?: string; urls?: { polling?: string } }
function isOpenRouterVideoJobRef(v: unknown): v is OpenRouterVideoJobRef {
if (typeof v !== 'object' || v === null) return false;
const o = v as any;
return Boolean(o.id || o.job_id || o.request_id) && Boolean(o.polling_url || o.status_url || o.urls?.polling);
} Try / catch
let jobId: string, pollingUrl: string;
try {
({ jobId, pollingUrl } = extractOpenRouterJobRef(submitData, submitText));
} catch (e) {
ctx.onProviderRequestSettled?.({ providerId: 'openrouter', ok: false, error: String(e) });
throw e;
} Prevention
- Accept field aliases (id/job_id/request_id, polling_url/status_url) to absorb schema drift.
- Log the response keys when extraction fails so drift is diagnosable.
- Treat a 200 with an embedded `error` object as a failure.
- Retry once — transient partial responses have been seen.
When it happens
Trigger: OpenRouter changed the submit response schema (renamed id→job_id or polling_url→status_url), returned an error envelope with 200 status, or the underlying provider returned a synchronous result instead of an async stub.
Common situations: Schema drift on OpenRouter's async video API, a new video provider integrated by OpenRouter with a different envelope, or a 200 with an embedded error object.
Related errors
- openrouter poll non-JSON: ${truncate(pollText, 200)}
- openrouter video non-JSON: ${truncate(submitText, 200)}
- openrouter poll ${pollResp.status}: ${truncate(pollText, 240
- openrouter job ${lastStatus}: ${reason}
- 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/eaee25097e4e593c.
Report an issue: GitHub.