{"record":{"id":"8e1c4771d62be027","repo":"nexu-io/open-design","slug":"openrouter-video-timed-out-after-elapsedsec-s-wa","errorCode":null,"errorMessage":"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.","messagePattern":"openrouter video timed out after (.+?)s waiting for status=completed \\(last status: (.+?), ceiling (.+?)s\\)\\. If your jobs legitimately need longer, raise OD_OPENROUTER_VIDEO_MAX_POLL_MS\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2123,"sourceCode":"      break;\n    }\n    if (\n      lastStatus === 'failed'\n      || lastStatus === 'expired'\n      || lastStatus === 'cancelled'\n    ) {\n      const reasonRaw =\n        pollData?.error?.message || pollData?.error || lastStatus;\n      const reason =\n        typeof reasonRaw === 'string' ? reasonRaw : JSON.stringify(reasonRaw);\n      throw new Error(`openrouter job ${lastStatus}: ${reason}`);\n    }\n  }\n\n  if (!videoUrls || videoUrls.length === 0) {\n    const elapsedSec = Math.round((Date.now() - startedAt) / 1000);\n    const ceilingSec = Math.round(maxMs / 1000);\n    throw new Error(\n      `openrouter video timed out after ${elapsedSec}s waiting for status=completed `\n      + `(last status: ${lastStatus || 'pending'}, ceiling ${ceilingSec}s). `\n      + `If your jobs legitimately need longer, raise OD_OPENROUTER_VIDEO_MAX_POLL_MS.`,\n    );\n  }\n\n  // ── Step 3: Download the video binary ──────────────────────────────\n  // unsigned_urls are often third-party CDNs where sending our API key\n  // would leak credentials. However, sometimes OpenRouter returns a proxied\n  // openrouter.ai URL that still requires authorization. We only attach the\n  // auth header if the host is explicitly allowlisted as openrouter.ai.\n  const contentUrl = videoUrls[0]!;\n  const parsedContentUrl = new URL(contentUrl);\n\n  const dlHeaders: Record<string, string> = {};\n  if (parsedContentUrl.hostname === 'openrouter.ai') {\n    dlHeaders['authorization'] = `Bearer ${credentials.apiKey}`;\n  }","sourceCodeStart":2105,"sourceCodeEnd":2141,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2105-L2141","documentation":"Thrown by the OpenRouter video polling loop when the daemon never observed status=completed (and never collected any videoUrls) before OD_OPENROUTER_VIDEO_MAX_POLL_MS elapsed. It is a wall-clock ceiling guard, distinct from upstream failure errors: the job may still be running server-side when this fires. The message reports elapsed seconds, the last observed status, and the configured ceiling so an operator can decide whether to raise the limit.","triggerScenarios":"OpenRouter video generation (renderOpenRouterVideo) where the GET status endpoint keeps returning a non-complete status (PENDING/PROCESSING) for longer than maxMs. Triggered when the while-loop exits the poll window with videoUrls still empty and lastStatus never reaching 'completed'.","commonSituations":"Long video jobs (high duration, complex prompt) that legitimately exceed the default ceiling; upstream OpenRouter slowness or queueing; network stalls that stretch individual poll latency; default ceiling too low for the model/duration combination a user selected.","solutions":["Raise the ceiling: set OD_OPENROUTER_VIDEO_MAX_POLL_MS to a higher value (e.g. 1800000 for 30 min) in the daemon environment if your jobs legitimately run long.","Shorten the requested video length or pick a faster OpenRouter video model so upstream finishes inside the default window.","Check OpenRouter status/dashboard to confirm the job actually stalled vs. is still processing; retry the request if it was a transient queue backlog.","If lastStatus shows a non-pending terminal value, inspect the upstream API contract — a status string the parser doesn't map to 'completed' will loop forever until timeout."],"exampleFix":"// before\n// OD_OPENROUTER_VIDEO_MAX_POLL_MS unset → default ceiling\n\n// after (operator)\nexport OD_OPENROUTER_VIDEO_MAX_POLL_MS=1800000\n# restart the daemon so the env var is picked up","handlingStrategy":"retry","validationCode":"// Before invoking renderOpenRouterVideo, sanity-check the configured ceiling\n// against the requested job size so callers get a fast config error instead of a\n// 10-minute timeout.\nfunction assertOpenRouterCeilingOk(requestedLengthSec: number) {\n  const maxMs = Number(process.env.OD_OPENROUTER_VIDEO_MAX_POLL_MS) || 10 * 60 * 1000;\n  // OpenRouter video jobs scale roughly linearly with length; flag anything\n  // where the default ceiling is clearly too small.\n  const estimatedMs = requestedLengthSec * 60 * 1000; // ~1min/s, conservative\n  if (estimatedMs > maxMs) {\n    throw new Error(\n      `requested ${requestedLengthSec}s video likely exceeds OD_OPENROUTER_VIDEO_MAX_POLL_MS (${Math.round(maxMs / 1000)}s); raise the env var or shorten the request.`,\n    );\n  }\n}","typeGuard":null,"tryCatchPattern":"// Catch the timeout specifically and surface the raise-the-ceiling hint to the user.\ntry {\n  await renderOpenRouterVideo(ctx, creds, onProgress);\n} catch (e) {\n  const msg = String((e as Error).message || e);\n  if (msg.startsWith('openrouter video timed out')) {\n    // Surface the operator action; do NOT silently retry a job that already ran long.\n    throw new Error(`Video generation timed out. Ask the operator to raise OD_OPENROUTER_VIDEO_MAX_POLL_MS. Detail: ${msg}`);\n  }\n  throw e;\n}","preventionTips":["Set OD_OPENROUTER_VIDEO_MAX_POLL_MS explicitly in the daemon environment based on your typical job length, do not rely on the default.","Track p95 job duration per model in your observability and alert before it approaches the ceiling.","Keep the requested video length modest; long single jobs are more fragile than concatenating shorter ones."],"tags":["openrouter","video-generation","polling-timeout","configuration"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}