nexu-io/open-design · error · Error

openrouter video submit ${submitResp.status}: ${truncate(sub

Error message

openrouter video submit ${submitResp.status}: ${truncate(submitText, 240)}

What it means

Thrown by renderOpenRouterVideo when the POST to submit an async video generation job (with frame_images for i2v when ctx.imageRef is set) returns non-2xx. The error embeds submitResp.status and the first 240 chars of the body. Unlike the image chat-completion path, video uses a dedicated async submit endpoint and returns a job id plus polling_url on success.

Source

Thrown at apps/daemon/src/media/index.ts:2029

    ];
  }

  // ── Step 1: Submit the generation request ──────────────────────────
  const submitResp = await fetch(`${baseUrl}/videos`, withMediaRequestInit(ctx, {
    method: 'POST',
    headers: {
      'authorization': `Bearer ${credentials.apiKey}`,
      'content-type': 'application/json',
      // OpenRouter attribution headers per
      // https://openrouter.ai/docs/app-attribution
      'HTTP-Referer': 'https://opendesign.dev',
      'X-Title': 'Open Design',
    },
    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)}`,
    );
  }

View on GitHub (pinned to 5be4028344)

Solutions

  1. Read status+body: 402→add credits, 404→fix model slug, 413→reduce image size, 429→slow down, 5xx→retry/backoff.
  2. Confirm wireModel is a video-capable OpenRouter slug and that the account is enabled for it.
  3. For i2v, verify ctx.imageRef resolved to a valid base64 data URL within the provider's size limits.
  4. Retry once on 5xx; video backends are flaky under load.

Example fix

// before
if (!submitResp.ok) {
  throw new Error(`openrouter video submit ${submitResp.status}: ${truncate(submitText, 240)}`);
}

// after — status hints
if (!submitResp.ok) {
  const hint = submitResp.status === 402 ? ' (no credits)'
    : submitResp.status === 413 ? ' (image too large)'
    : submitResp.status === 404 ? ' (unknown video model)'
    : submitResp.status === 429 ? ' (rate limited)'
    : '';
  throw new Error(`openrouter video submit ${submitResp.status}${hint}: ${truncate(submitText, 240)}`);
}
Defensive patterns

Strategy: try-catch

Validate before calling

function describeOpenRouterVideoSubmitError(status: number, body: string): string {
  const hint = status === 401 ? 'bad key'
    : status === 402 ? 'no credits for premium video model'
    : status === 404 ? 'unknown video model slug'
    : status === 413 ? 'frame_images payload too large for i2v'
    : status === 429 ? 'rate limited'
    : status >= 500 ? 'upstream video provider outage — retry'
    : 'see body';
  return `openrouter video submit ${status} (${hint}): ${truncate(body, 240)}`;
}

function isRetryableSubmitStatus(status: number): boolean {
  return status === 429 || status >= 500;
}

Type guard

function isOpenRouterVideoModelSlug(model: string): boolean {
  return /seedance|kling|wan|sora|veo|cogvideo|hailuo|ltx-video|minimax/i.test(model);
}

Try / catch

for (let attempt = 0; attempt < 2; attempt++) {
  const r = await submitOpenRouterVideo(ctx, credentials);
  if (r.ok) { /* parse and poll */ }
  if (attempt === 0 && isRetryableSubmitStatus(r.status)) { await sleep(2000); continue; }
  throw new Error(describeOpenRouterVideoSubmitError(r.status, await r.text()));
}

Prevention

When it happens

Trigger: 401/403 (bad key), 402 (no credits — video models are typically premium), 404 (model slug not enabled for video on the key), 413/422 (frame_images base64 too large or malformed for i2v), 429 (rate limit), or upstream 5xx from the underlying video provider.

Common situations: Account has no credits for the premium video model, ctx.imageRef base64 exceeds the provider size cap, model id not opted into video, or a provider-side (Seedance/Kling/etc.) incident.

Related errors


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