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.
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:
-
status—"completed"or"failed"sets the job's terminal state (any other status is coerced to completed, with a warning) -
id— ignored on this surface (the job id is always a server-minted UUID); the/v1/videossurface does use it error?— failure message surfaced on a failed status poll-
b64?— base64-encoded video bytes served by the content endpoint -
cost?— generation cost surfaced asusage.coston completion -
url?— ignored on this surface (the content endpoint serves bytes, not a redirect); useb64
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.
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.