{"record":{"id":"eaee25097e4e593c","repo":"nexu-io/open-design","slug":"openrouter-video-submit-returned-no-job-id-or-poll","errorCode":null,"errorMessage":"openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}","messagePattern":"openrouter video submit returned no job id or polling_url: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/daemon/src/media/index.ts","lineNumber":2043,"sourceCode":"    body: JSON.stringify(body),\n  }));\n  const submitText = await submitResp.text();\n  if (!submitResp.ok) {\n    throw new Error(\n      `openrouter video submit ${submitResp.status}: ${truncate(submitText, 240)}`,\n    );\n  }\n  let submitData: any;\n  try {\n    submitData = JSON.parse(submitText);\n  } catch {\n    throw new Error(`openrouter video non-JSON: ${truncate(submitText, 200)}`);\n  }\n\n  const jobId = submitData?.id;\n  const pollingUrl = submitData?.polling_url;\n  if (!jobId || !pollingUrl) {\n    throw new Error(\n      `openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}`,\n    );\n  }\n\n  // ── Step 2: Poll until completion ──────────────────────────────────\n  const startedAt = Date.now();\n  const configuredMaxMs = Number(process.env.OD_OPENROUTER_VIDEO_MAX_POLL_MS);\n  const maxMs =\n    Number.isFinite(configuredMaxMs) && configuredMaxMs >= 60_000\n      ? configuredMaxMs\n      : 30 * 60 * 1000; // 30 minutes default\n  const configuredPollIntervalMs = Number(process.env.OD_OPENROUTER_VIDEO_POLL_INTERVAL_MS);\n  const pollIntervalMs =\n    Number.isFinite(configuredPollIntervalMs) && configuredPollIntervalMs >= 0\n      ? configuredPollIntervalMs\n      : DEFAULT_OPENROUTER_VIDEO_POLL_INTERVAL_MS;\n\n  let lastStatus = submitData?.status || 'pending';","sourceCodeStart":2025,"sourceCodeEnd":2061,"githubUrl":"https://github.com/nexu-io/open-design/blob/5be4028344c2eb4c667c5a97bda8f750c5597ef7/apps/daemon/src/media/index.ts#L2025-L2061","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst jobId = submitData?.id;\nconst pollingUrl = submitData?.polling_url;\nif (!jobId || !pollingUrl) {\n  throw new Error(`openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}`);\n}\n\n// after — accept field aliases for schema drift\nconst jobId = submitData?.id || submitData?.job_id;\nconst pollingUrl = submitData?.polling_url || submitData?.status_url || submitData?.urls?.polling;\nif (!jobId || !pollingUrl) {\n  throw new Error(`openrouter video submit returned no job id or polling_url: ${truncate(submitText, 200)}`);\n}","handlingStrategy":"type-guard","validationCode":"// Accept common field aliases and surface the real envelope\nfunction extractOpenRouterJobRef(submitData: any, rawText: string): { jobId: string; pollingUrl: string } {\n  const jobId = submitData?.id || submitData?.job_id || submitData?.request_id;\n  const pollingUrl = submitData?.polling_url || submitData?.status_url || submitData?.urls?.polling;\n  if (typeof jobId === 'string' && typeof pollingUrl === 'string') {\n    return { jobId, pollingUrl };\n  }\n  throw new Error(`openrouter video submit: no job id/polling_url (keys=${Object.keys(submitData || {}).join(',')}): ${truncate(rawText, 200)}`);\n}","typeGuard":"interface OpenRouterVideoJobRef { id?: string; job_id?: string; request_id?: string; polling_url?: string; status_url?: string; urls?: { polling?: string } }\nfunction isOpenRouterVideoJobRef(v: unknown): v is OpenRouterVideoJobRef {\n  if (typeof v !== 'object' || v === null) return false;\n  const o = v as any;\n  return Boolean(o.id || o.job_id || o.request_id) && Boolean(o.polling_url || o.status_url || o.urls?.polling);\n}","tryCatchPattern":"let jobId: string, pollingUrl: string;\ntry {\n  ({ jobId, pollingUrl } = extractOpenRouterJobRef(submitData, submitText));\n} catch (e) {\n  ctx.onProviderRequestSettled?.({ providerId: 'openrouter', ok: false, error: String(e) });\n  throw e;\n}","preventionTips":["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."],"tags":["openrouter","video-generation","response-shape","async-polling","provider-api"],"backgroundTag":null,"analyzedSha":"5be4028344c2eb4c667c5a97bda8f750c5597ef7","analyzedAt":"2026-08-12T12:03:58.812Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}