Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
4176fa6
feat: extend video fixture fields and share fal progression resolver
jpr5 Jun 10, 2026
dbf15f6
feat: OpenRouter async video job map and submit endpoint
jpr5 Jun 10, 2026
2b18662
feat: OpenRouter video status poll with staged progression
jpr5 Jun 10, 2026
6bc32ff
feat: OpenRouter video content download endpoint
jpr5 Jun 10, 2026
f01fde2
feat: OpenRouter video model listing endpoint
jpr5 Jun 10, 2026
9083726
test: OpenRouter video cross-cutting conformance suite
jpr5 Jun 10, 2026
2060053
docs: changelog entry for the OpenRouter async video lifecycle mock
jpr5 Jun 10, 2026
a0859f6
fix: validate OpenRouter video request bodies and Bearer auth scheme
jpr5 Jun 10, 2026
81647d4
fix: scope OpenRouter video URLs by testId and honor x-forwarded-proto
jpr5 Jun 10, 2026
d5d1172
fix: correct chaos source label and cover the video models route
jpr5 Jun 10, 2026
b13b26b
fix: surface silent coercions and gaps on the OpenRouter video path
jpr5 Jun 10, 2026
e3ed14c
chore: clear videoStates on server close and drop dead job createdAt
jpr5 Jun 10, 2026
303b8a5
docs: tighten OpenRouter video changelog entry and routing/content co…
jpr5 Jun 10, 2026
e71d3c4
test: hygiene fixes and coverage adds for the OpenRouter video surface
jpr5 Jun 10, 2026
06cc980
fix: template video job paths in Prometheus metric labels
jpr5 Jun 10, 2026
666b063
fix: harden OpenRouter video validation, auth, and observability
jpr5 Jun 10, 2026
5192c0f
docs: clarify models-listing synthesis and FalQueueConfig state mapping
jpr5 Jun 10, 2026
b48c310
test: pin OpenRouter video behaviors and tighten test hygiene
jpr5 Jun 10, 2026
265f835
fix: OpenRouter video CR findings — journal shape, b64 warn, fwd host…
jpr5 Jun 10, 2026
c54df06
test: harden faulty-registry injection assertion and models-fallback …
jpr5 Jun 10, 2026
7bd577b
docs: document "internal" journal source and terminal-at-submit seeding
jpr5 Jun 10, 2026
d981dfb
fix: harden OpenRouter video b64 heuristic, forwarded-host gate, and …
jpr5 Jun 10, 2026
13bf83a
docs: correct OpenRouter video option/journal comments, dedupe video …
jpr5 Jun 10, 2026
f9174b6
test: tighten metrics path assertion, fix placeholder URL, harden cle…
jpr5 Jun 10, 2026
e68b3c1
fix: harden OpenRouter video proxy-host handling and error-path journ…
jpr5 Jun 10, 2026
ac88859
test: pin OpenRouter video poll-budget and threshold semantics, consu…
jpr5 Jun 10, 2026
0ecf2b5
docs: clarify FalQueueConfig threshold defaults and video failure mes…
jpr5 Jun 10, 2026
3fbcde7
fix: harden OpenRouter video journal, thresholds, and header/content …
jpr5 Jun 10, 2026
4541695
docs: clarify video threshold and default-model contracts
jpr5 Jun 10, 2026
a6181e6
test: pin OpenRouter video chaos-slot, proto-list, and chaos-source c…
jpr5 Jun 10, 2026
301d421
fix: harden OpenRouter video host handling and journal-failure logging
jpr5 Jun 10, 2026
af3b07f
docs: qualify threshold and double-journal notes; changelog falQueue …
jpr5 Jun 10, 2026
24fdd6a
fix: accept underscore hosts, CORS on submit errors, dedupe status RE…
jpr5 Jun 10, 2026
d005468
docs: clarify testIdSuffix header comment and chaos source-label test…
jpr5 Jun 10, 2026
de92c0d
fix: warn (not debug) when video fixtures contribute no string model
jpr5 Jun 10, 2026
cd886e7
test: fix invalid logLevel and loosen-prone warn assertion in video t…
jpr5 Jun 10, 2026
24231e6
docs: wording pass on OpenRouter video changelog entries
jpr5 Jun 10, 2026
bee2d13
docs: document OpenRouter video surface across docs site and README
jpr5 Jun 10, 2026
0f74b4f
docs: note video.id is ignored on the OpenRouter surface
jpr5 Jun 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,43 @@

## [Unreleased]

### Added

- **OpenRouter async video lifecycle** — mocks OpenRouter's dedicated video-generation job API
alongside the existing OpenAI-shaped `/v1/videos` handler; both draw from the same
`endpoint: "video"` fixture pool. `POST /api/v1/videos` matches fixtures on `prompt`/`model`
(endpoint `video`) and returns a `{ id, polling_url, status: "pending" }` job envelope (a
model-less submit assumes the default `bytedance/seedance-2.0` model for fixture matching); the
envelope reports `"pending"` for API fidelity even though, under the default thresholds, the job
is already seeded terminal internally at submit. `GET /api/v1/videos/{jobId}` advances
`pending → in_progress → completed | failed` per the new `openRouterVideo` poll-threshold config
(same poll-threshold semantics as `falQueue`), adding `unsigned_urls` + `usage.cost` on
completion and the fixture's `error` message on failure (a default "Video generation failed"
message is used when the fixture has no `error` field). Because the job is server-side terminal
at submit by default, content is downloadable with zero polls if the client constructs the
content URL itself — the documented client flow still needs one status poll to learn
`unsigned_urls`. `GET /api/v1/videos/{jobId}/content?index=0` requires Bearer auth (401
otherwise) and serves the fixture's `b64` bytes — or a built-in minimal MP4 `ftyp` placeholder —
always as `Content-Type: video/mp4` (matching production even when the client sends
`Accept: application/octet-stream`). Status polls and the models listing are served without auth
— only the content endpoint enforces Bearer (a deliberate, documented divergence). The `index`
query param is accepted but ignored (jobs are single-video), and the content endpoint does not
advance job state — content URLs are only learned from a completed status poll (API fidelity;
diverges from fal's advance-on-result). `GET /api/v1/videos/models` synthesizes the video-model
listing from loaded video fixtures that specify a string `match.model` (falling back to a
built-in default set otherwise). Video fixtures gain optional `error`, `b64`, and `cost` fields.
Replay-only for now (no record/proxy mode yet — lenient mode 404s on a miss instead of
proxying); record-mode proxying for this surface is a follow-up.

### Fixed

- **fal queue thresholds** — the progression resolver — now shared with the OpenRouter video
surface — also sanitizes the pre-existing `falQueue` config: non-finite
`pollsBeforeInProgress`/`pollsBeforeCompleted` values (NaN, Infinity) are treated as unset instead
of stranding jobs short of a terminal status, negative/fractional values are floored and clamped
to non-negative integers, and `createServer` now warns on invalid `falQueue`/`openRouterVideo`
threshold values.

## [1.30.0] - 2026-06-09

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Run them all on one port with `npx @copilotkit/aimock --config aimock.json`, or
- **Timing-aware recording and replay** — Recorded fixtures capture per-frame arrival timestamps; replay uses recorded timings for approximate timing reproduction based on recorded TTFT and inter-frame cadence (replay chunk count may differ from recording — TTFT and average pace are preserved, not per-token fidelity) with configurable `--replay-speed` multiplier
- **[Multi-turn Conversations](https://aimock.copilotkit.dev/multi-turn)** — Record and replay multi-turn traces with tool rounds; match distinct turns via `turnIndex`, `hasToolResult`, `toolCallId`, `sequenceIndex`, `systemMessage` (gate on host-supplied agent context), or custom predicates
- **[14 LLM Providers](https://aimock.copilotkit.dev/docs)** — OpenAI Chat, OpenAI Responses, OpenAI Realtime (GA + Beta shim), Claude, Gemini (REST + embedContent), Gemini Live, Gemini Interactions, Azure, Bedrock, Vertex AI, Ollama (chat + embeddings), Cohere (chat + embed), ElevenLabs TTS — full streaming support
- **Multimedia APIs** — [image generation](https://aimock.copilotkit.dev/images) (DALL-E, Imagen), [image editing](https://aimock.copilotkit.dev/images) (/v1/images/edits), [text-to-speech](https://aimock.copilotkit.dev/speech) (OpenAI + ElevenLabs), [audio transcription](https://aimock.copilotkit.dev/transcription), [audio translation](https://aimock.copilotkit.dev/transcription) (/v1/audio/translations), [video generation](https://aimock.copilotkit.dev/video), [fal.ai](https://aimock.copilotkit.dev/fal-ai) (image / video / audio with queue lifecycle)
- **Multimedia APIs** — [image generation](https://aimock.copilotkit.dev/images) (DALL-E, Imagen), [image editing](https://aimock.copilotkit.dev/images) (/v1/images/edits), [text-to-speech](https://aimock.copilotkit.dev/speech) (OpenAI + ElevenLabs), [audio transcription](https://aimock.copilotkit.dev/transcription), [audio translation](https://aimock.copilotkit.dev/transcription) (/v1/audio/translations), [video generation](https://aimock.copilotkit.dev/video), [OpenRouter video generation](https://aimock.copilotkit.dev/openrouter-video) (/api/v1/videos with async job lifecycle), [fal.ai](https://aimock.copilotkit.dev/fal-ai) (image / video / audio with queue lifecycle)
- **[MCP](https://aimock.copilotkit.dev/mcp-mock) / [A2A](https://aimock.copilotkit.dev/a2a-mock) / [AG-UI](https://aimock.copilotkit.dev/agui-mock) / [Vector](https://aimock.copilotkit.dev/vector-mock)** — Mock every protocol your AI agents use
- **[Chaos Testing](https://aimock.copilotkit.dev/chaos-testing)** — 500 errors, malformed JSON, mid-stream disconnects at any probability
- **Per-Request Strict Mode** — `X-AIMock-Strict` header overrides the server-level `--strict` flag per request (`true`/`1` = strict, `false`/`0` = lenient)
Expand Down
4 changes: 3 additions & 1 deletion docs/fal-ai/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,9 @@ <h2>Polling Realism</h2>

<p>
If <code>pollsBeforeCompleted</code> is set lower than <code>pollsBeforeInProgress</code>,
it is clamped up so <code>IN_PROGRESS</code> is never skipped.
it is clamped up so <code>IN_PROGRESS</code> is never skipped. Thresholds are also
sanitized: non-finite values are treated as unset, negative or fractional values are
floored and clamped to non-negative integers, and invalid values warn at server start.
</p>

<p>
Expand Down
9 changes: 7 additions & 2 deletions docs/fixtures/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,13 @@ <h2>Response Types</h2>
</tr>
<tr>
<td>Video</td>
<td>video.id, video.status, video.url?</td>
<td>Generated video URL with async polling</td>
<td>video.id, video.status, video.url?, video.error?, video.b64?, video.cost?</td>
<td>
Generated video with async polling &mdash; <code>error</code> is the failure message
surfaced by async video jobs, <code>b64</code> is base64-encoded video bytes served
by content-download endpoints, <code>cost</code> is the generation cost surfaced in
usage envelopes
</td>
</tr>
</tbody>
</table>
Expand Down
264 changes: 264 additions & 0 deletions docs/openrouter-video/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>OpenRouter Video — aimock</title>
<link rel="icon" type="image/svg+xml" href="../favicon.svg" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=Instrument+Sans:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="../style.css" />
<script src="/pixels.js" defer></script>
</head>
<body>
<nav class="top-nav">
<div class="nav-inner">
<div style="display: flex; align-items: center; gap: 1rem">
<button
class="sidebar-toggle"
onclick="document.querySelector('.sidebar').classList.toggle('open')"
aria-label="Toggle sidebar"
>
&#9776;
</button>
<a href="/" class="nav-brand"> <span class="prompt">$</span> aimock </a>
</div>
<ul class="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/docs" style="color: var(--accent)">Docs</a></li>
<li>
<a href="https://github.com/CopilotKit/aimock" class="gh-link" target="_blank">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path
d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
/>
</svg>
GitHub
</a>
</li>
</ul>
</div>
</nav>

<div class="docs-layout">
<aside class="sidebar" id="sidebar"></aside>

<main class="docs-content">
<h1>OpenRouter Video</h1>
<p class="lead">
aimock mocks OpenRouter's dedicated video-generation job API under
<code>/api/v1/videos</code> &mdash; submit a job, poll it through
<code>pending &rarr; in_progress &rarr; completed | failed</code>, and download the bytes.
It draws from the same <code>endpoint: "video"</code> fixture pool as the OpenAI-shaped
<a href="/video">/v1/videos</a> handler.
</p>

<h2>Endpoints</h2>
<table class="endpoint-table">
<thead>
<tr>
<th>Method</th>
<th>Path</th>
<th>Response</th>
</tr>
</thead>
<tbody>
<tr>
<td>POST</td>
<td><code>/api/v1/videos</code></td>
<td>
<code>{ id, polling_url, status: "pending" }</code> job envelope; the matched
fixture's <code>video</code> drives the job's terminal state
</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/v1/videos/{jobId}</code></td>
<td>
<code>{ id, status }</code> &mdash; plus <code>unsigned_urls</code> +
<code>usage.cost</code> once <code>completed</code>, or <code>error</code> once
<code>failed</code>
</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/v1/videos/{jobId}/content</code></td>
<td>
The video bytes as <code>video/mp4</code> (Bearer auth required; 400 before the job
completes)
</td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/v1/videos/models</code></td>
<td><code>{ data: [&hellip;] }</code> video-model listing</td>
</tr>
</tbody>
</table>

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

<div class="code-block">
<div class="code-block-header">
openrouter-video.test.ts <span class="lang-tag">ts</span>
</div>
<pre><code><span class="op">mock</span>.<span class="fn">onVideo</span>(<span class="str">"a cat playing piano"</span>, {
<span class="prop">video</span>: { <span class="prop">status</span>: <span class="str">"completed"</span>, <span class="prop">b64</span>: <span class="str">"AAAAGGZ0eXBpc29t..."</span>, <span class="prop">cost</span>: <span class="num">0.12</span> },
});

<span class="cm">// A failed job:</span>
<span class="op">mock</span>.<span class="fn">onVideo</span>(<span class="str">"impossible prompt"</span>, {
<span class="prop">video</span>: { <span class="prop">status</span>: <span class="str">"failed"</span>, <span class="prop">error</span>: <span class="str">"content policy violation"</span> },
});</code></pre>
</div>

<p>The fixture's <code>video</code> object supports:</p>
<ul>
<li>
<code>status</code> &mdash; <code>"completed"</code> or <code>"failed"</code> sets the
job's terminal state (any other status is coerced to completed, with a warning)
</li>
<li>
<code>id</code> &mdash; ignored on this surface (the job id is always a server-minted
UUID); the <code>/v1/videos</code> surface does use it
</li>
<li><code>error?</code> &mdash; failure message surfaced on a failed status poll</li>
<li>
<code>b64?</code> &mdash; base64-encoded video bytes served by the content endpoint
</li>
<li>
<code>cost?</code> &mdash; generation cost surfaced as <code>usage.cost</code> on
completion
</li>
<li>
<code>url?</code> &mdash; ignored on this surface (the content endpoint serves bytes,
not a redirect); use <code>b64</code>
</li>
</ul>

<h2>Polling Realism</h2>
<p>
By default a submitted job is seeded terminal internally &mdash; the submit envelope still
reports <code>"pending"</code> 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 <code>openRouterVideo</code> with poll thresholds. The
semantics are identical to <a href="/fal-ai#polling-realism"><code>falQueue</code></a
>, mapped onto <code>pending</code> / <code>in_progress</code> /
<code>completed | failed</code>.
</p>

<div class="code-block">
<div class="code-block-header">polling.test.ts <span class="lang-tag">ts</span></div>
<pre><code><span class="kw">const</span> <span class="op">mock</span> = <span class="kw">new</span> <span class="type">LLMock</span>({
<span class="prop">port</span>: <span class="num">0</span>,
<span class="prop">openRouterVideo</span>: { <span class="prop">pollsBeforeInProgress</span>: <span class="num">1</span>, <span class="prop">pollsBeforeCompleted</span>: <span class="num">2</span> },
});

<span class="cm">// Submit → { id, polling_url, status: "pending" }</span>
<span class="cm">// poll 1 → in_progress</span>
<span class="cm">// poll 2 → completed, unsigned_urls + usage.cost</span>
<span class="cm">// content → 200 video/mp4 bytes</span></code></pre>
</div>

<div class="info-box">
<p>
Unset and an explicit <code>0</code> differ: with both fields unset the job is terminal
at submit, but explicitly setting <code>pollsBeforeInProgress</code> &mdash; even to
<code>0</code> &mdash; enables progression, with
<code>pollsBeforeCompleted</code> defaulting to
<code>pollsBeforeInProgress + 1</code> so the job passes through
<code>in_progress</code>. An explicit <code>pollsBeforeCompleted</code> lower than
<code>pollsBeforeInProgress</code> is clamped up so <code>in_progress</code> is never
skipped.
</p>
</div>

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

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

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

<h2>Test Isolation</h2>
<p>
Generated URLs (<code>polling_url</code>, <code>unsigned_urls</code>) embed the request's
testId as a <code>?testId=</code> query param. The <code>@openrouter/sdk</code> 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.
</p>

<h2>Models Listing</h2>
<p>
<code>GET /api/v1/videos/models</code> synthesizes the listing from loaded video fixtures
that specify a string <code>match.model</code>. 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).
</p>

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

<div class="info-box">
<p>
<strong>Replay-only:</strong> this surface does not support record/proxy mode yet
&mdash; with <code>--record</code>, an unmatched submit warns and returns the normal
no-match response instead of proxying. Record-mode support is a follow-up.
</p>
</div>
</main>
<aside class="page-toc" id="page-toc"></aside>
</div>

<footer class="docs-footer">
<div class="footer-inner">
<div class="footer-left"><span>$</span> aimock &middot; MIT License</div>
<ul class="footer-links">
<li><a href="https://github.com/CopilotKit/aimock" target="_blank">GitHub</a></li>
<li>
<a href="https://www.npmjs.com/package/@copilotkit/aimock" target="_blank">npm</a>
</li>
</ul>
</div>
</footer>
<script src="../sidebar.js"></script>
<script src="../cli-tabs.js"></script>
</body>
</html>
Loading
Loading