OpenRouter Video

aimock mocks OpenRouter's dedicated video-generation job API under /api/v1/videos — submit a job, poll it through pending → in_progress → completed | failed, and download the bytes. It draws from the same endpoint: "video" fixture pool as the OpenAI-shaped /v1/videos handler.

Endpoints

Method Path Response
POST /api/v1/videos { id, polling_url, status: "pending" } job envelope; the matched fixture's video drives the job's terminal state
GET /api/v1/videos/{jobId} { id, status } — plus unsigned_urls + usage.cost once completed, or error once failed
GET /api/v1/videos/{jobId}/content The video bytes as video/mp4 (Bearer auth required; 400 before the job completes)
GET /api/v1/videos/models { data: […] } video-model listing

Fixture Authoring

Submits are matched against endpoint: "video" fixtures on the request's prompt (via match.userMessage) and model (via match.model). A submit without a model assumes the default bytedance/seedance-2.0 for matching, so a fixture restricted to that model still matches model-less submits.

openrouter-video.test.ts ts
mock.onVideo("a cat playing piano", {
  video: { status: "completed", b64: "AAAAGGZ0eXBpc29t...", cost: 0.12 },
});

// A failed job:
mock.onVideo("impossible prompt", {
  video: { status: "failed", error: "content policy violation" },
});

The fixture's video object supports:

Polling Realism

By default a submitted job is seeded terminal internally — the submit envelope still reports "pending" for API fidelity, but content is downloadable with zero polls and the first status poll reports the terminal status. To exercise client code that reacts to intermediate states, pass openRouterVideo with poll thresholds. The semantics are identical to falQueue, mapped onto pending / in_progress / completed | failed.

polling.test.ts ts
const mock = new LLMock({
  port: 0,
  openRouterVideo: { pollsBeforeInProgress: 1, pollsBeforeCompleted: 2 },
});

// Submit  → { id, polling_url, status: "pending" }
// poll 1  → in_progress
// poll 2  → completed, unsigned_urls + usage.cost
// content → 200 video/mp4 bytes

Unset and an explicit 0 differ: with both fields unset the job is terminal at submit, but explicitly setting pollsBeforeInProgress — even to 0 — enables progression, with pollsBeforeCompleted defaulting to pollsBeforeInProgress + 1 so the job passes through in_progress. An explicit pollsBeforeCompleted lower than pollsBeforeInProgress is clamped up so in_progress is never skipped.

Thresholds are sanitized: non-finite values (NaN, Infinity) are treated as unset, and negative or fractional values are floored and clamped to non-negative integers. createServer warns at startup on invalid values.

Authentication

Only the content endpoint enforces auth: GET /api/v1/videos/{jobId}/content requires a Bearer Authorization header (any non-empty credential) and returns 401 otherwise, matching the real API. Status polls and the models listing are served without auth — a deliberate divergence to keep test polling loops friction-free.

Content Serving

The content endpoint serves the fixture's b64 bytes when present, or a built-in minimal MP4 ftyp placeholder otherwise — always as Content-Type: video/mp4, even when the client sends Accept: application/octet-stream (matching production). The index query param is accepted but ignored (jobs are single-video), and fetching content never advances job state — clients learn the content URL only from a completed status poll.

Test Isolation

Generated URLs (polling_url, unsigned_urls) embed the request's testId as a ?testId= query param. The @openrouter/sdk fetches these URLs with standard Authorization but no aimock-specific headers, so the testId must travel in the URL for job state to resolve to the right test scope. The default testId is omitted to keep single-tenant URLs clean.

Models Listing

GET /api/v1/videos/models synthesizes the listing from loaded video fixtures that specify a string match.model. When no video fixture contributes a string model, a built-in default set is served instead (with a warning if video fixtures are loaded but none has a string model).

Chaos & Metrics

Chaos injection applies to all four routes; journal entries for chaos served without a matched fixture carry source: "internal". In Prometheus metrics, per-job paths are templated as /api/v1/videos/{jobId} and /api/v1/videos/{jobId}/content to keep label cardinality bounded.

Replay-only: this surface does not support record/proxy mode yet — with --record, an unmatched submit warns and returns the normal no-match response instead of proxying. Record-mode support is a follow-up.