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

  1. Inspect the truncated submit body in the error to see the actual envelope.
  2. If OpenRouter renamed the fields, update the extraction at apps/daemon/src/media/index.ts:2041-2042 (and fall back across aliases).
  3. If the body is an error envelope, address the underlying cause (credits, model, etc.) and retry.
  4. 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

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


AI-assisted analysis of nexu-io/open-design@5be4028344 (2026-08-12). Data as JSON: /api/errors/eaee25097e4e593c. Report an issue: GitHub.