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

  1. Read the embedded reason — provider error messages are usually specific (content filter, invalid input, quota).
  2. If content/safety related, reword ctx.prompt or change the source image and resubmit.
  3. 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.
  4. If 'cancelled' was unexpected, check whether another session or the OpenRouter dashboard cancelled the job.
  5. 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

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


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