Grok Imagine Video
+
+ aimock mocks xAI's native Grok Imagine async video-generation API — submit a job
+ under POST /v1/videos/generations, then poll it through
+ pending → done | failed | expired under
+ GET /v1/videos/{request_id}. It draws from the same
+ endpoint: "video" fixture pool as the other video surfaces, disambiguated by
+ the grok-imagine-* model string.
+
Endpoints
+| Method | +Path | +Response | +
|---|---|---|
| POST | +/v1/videos/generations |
+
+ { request_id } job envelope (JSON only — a
+ multipart/form-data body is rejected with 400 before the body is
+ parsed); the matched fixture's video drives the job's terminal state
+ |
+
| GET | +/v1/videos/{request_id} |
+
+ { request_id, status, progress } — plus video.url /
+ video.duration + usage.cost_in_usd_ticks once
+ done, or those same
+ { request_id, status, progress } fields plus code /
+ error once failed
+ |
+
Sora-safe dispatch
+
+ Grok's status route, GET /v1/videos/{request_id}, shares its path shape with
+ the OpenAI-shaped Sora /v1/videos/{id} surface. aimock
+ resolves this with a job-map-first lookup: a GET that matches a live Grok job is served by
+ the Grok handler, and any miss falls through to the unchanged Sora
+ handleVideoStatus byte-for-byte (the two id namespaces are disjoint —
+ Sora ids come from its own state map, Grok request ids are minted UUIDs). The literal
+ /v1/videos/generations submit path is guarded so it is never parsed as a
+ status id of "generations". Existing Sora behavior is unaffected.
+
Fixture Authoring
+
+ Submits are matched against endpoint: "video" fixtures on the request's
+ prompt (via match.userMessage, read from
+ body.prompt) and model (via match.model, default
+ grok-imagine-video). The grok-imagine-* model string is the
+ provider disambiguator — Grok, Veo, and OpenRouter video fixtures share one match
+ namespace, and their model tokens never overlap.
+
mock.onVideo("a cat playing piano", {
+ // `id` is required by the type but ignored on this surface (the request_id
+ // is always server-minted); `url` is served as-is, no byte proxying.
+ video: { id: "vid_1", status: "completed", url: "https://videos.x.ai/abc.mp4", duration: 6, cost: 0.12 },
+}, { model: "grok-imagine-video" });
+
+// A failed job:
+mock.onVideo("impossible prompt", {
+ video: { id: "vid_2", status: "failed", error: "content policy violation" },
+}, { model: "grok-imagine-video" });
+ The fixture's video object supports:
-
+
-
+
status—"completed"or"failed"sets the + job's terminal state. The wirestatusis derived from the stored value: a + stored"completed"serializes to the wire"done"; the stored + status is always"completed", never the wire"done". +
+ -
+
id— ignored on this surface (therequest_idis always a + server-minted UUID) +
+ -
+
url?— the video URL surfaced asvideo.urlon a done + poll, served as-is (no byte proxying) +
+ -
+
duration?— clip duration surfaced as +video.duration(defaults to 0) +
+ -
+
cost?— generation cost in USD, surfaced as +usage.cost_in_usd_tickson completion (see units below) +
+ -
+
error?— failure message surfaced as{ code, error }on + a failed poll +
+
Progress & Polling Realism
+
+ Grok reports a progress percentage on every poll; aimock
+ synthesizes it from the poll count (climbing toward 100, reaching
+ 100 when the job is done). By default a submitted job is seeded
+ terminal internally. To exercise client code that polls through intermediate states, pass
+ grokVideo with poll thresholds. The semantics are identical to
+ falQueue and the
+ OpenRouter video surface.
+
const mock = new LLMock({
+ port: 0,
+ grokVideo: { pollsBeforeInProgress: 1, pollsBeforeCompleted: 2 },
+});
+
+// Submit → { request_id }
+// poll 1 → { request_id, status: "pending", progress: … }
+// poll 2 → { request_id, status: "done", progress: 100, video: { url, duration }, usage: { cost_in_usd_ticks } }
+
+ Thresholds are sanitized exactly as on the other video surfaces (non-finite treated as
+ unset; negatives/fractions floored and clamped). createServer warns at
+ startup on invalid values.
+
JSON-only — multipart rejected
+
+ The Grok video API is JSON-only. A submit carrying a
+ Content-Type: multipart/form-data body is rejected with HTTP 400 and a
+ { code, error } envelope before the body is parsed — aimock
+ does not reuse Sora's accept-multipart create branch. A non-object or malformed JSON
+ body and an empty prompt likewise return
+ { code: "invalid_request", error }.
+
Cost units
+
+ Grok bills in cost_in_usd_ticks, an integer count of USD ticks where
+ 1 USD = 1e10 ticks. A fixture's video.cost is the cost in USD;
+ on a done poll aimock surfaces
+ usage.cost_in_usd_ticks = round(cost × 1e10). Record mode persists the
+ upstream cost back to USD (cost = cost_in_usd_ticks / 1e10) so it
+ round-trips.
+
Chaos & Metrics
+
+ Chaos injection applies to both routes. In
+ Prometheus metrics the per-job status path is templated as
+ /v1/videos/{request_id} to keep label cardinality bounded.
+
Record Mode
+
+ With record mode and the grok provider
+ configured, an unmatched submit becomes a live interactive proxy: the
+ /v1/videos/generations POST is forwarded to the real API and answered with a
+ mock-rewritten { request_id } envelope (a fresh aimock request id), and each
+ client poll is proxied upstream 1:1 with the mock request id substituted. The client's own
+ polling drives the upstream lifecycle.
+
{
+ "llm": {
+ "fixtures": "./fixtures",
+ "record": {
+ "providers": { "grok": "https://api.x.ai" }
+ }
+ }
+}
+ const mock = new LLMock({
+ record: {
+ providers: { grok: "https://api.x.ai" },
+ },
+});
+
+ When the upstream poll reports done, the completed poll body is relayed first
+ and the eager capture then runs synchronously on the request stack (there is no
+ byte download to detach): aimock reads the video URL, duration, and cost straight out of
+ the terminal poll body (no download — the URL is served as-is) and persists a normal
+ video fixture (match.userMessage = the prompt, match.model = the
+ submitted model under the standard model-normalization rules, video.id = the
+ upstream request id, video.status = "completed",
+ video.url / video.duration, plus cost converted
+ from cost_in_usd_ticks). The same submit then replays in-session and across
+ sessions. Relayed poll bodies are otherwise faithful: status, progress,
+ video.url, and usage pass through verbatim with only the request
+ id rewritten. A failed upstream persists a failed fixture ({ status: "failed", error }) and relays { request_id, status, progress, code, error };
+ expired (and any terminal status not representable in
+ video.status) passes through with a warning, persists nothing, and keeps
+ proxying.
+
+ The polling client's Bearer credential is forwarded only to the configured provider
+ origin: the submit response carries just a request_id (never an upstream poll
+ URL), so there is nothing off-origin to adopt or validate — the poll URL is always
+ constructed on the configured provider origin from that request id. Strict mode wins over
+ record: a strict no-match returns 503 and nothing is proxied. Without a configured
+ grok provider URL, --record warns and serves the normal no-match
+ 404. Under proxy-only mode (record.proxyOnly / --proxy-only)
+ nothing is persisted and a done job is never converted to a local replay job — every
+ poll keeps proxying upstream.
+
+ TTL caveat: record-mode jobs live in the same bounded job map as replay + jobs (1-hour TTL, 10,000 entries). Each successful proxied poll refreshes a record job's + TTL, so an actively-polled long render is never evicted mid-recording — but a poll + arriving more than an hour after the last successful poll finds the job evicted + and returns 404. +
+