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 asvideo.duration(defaults to 0) -
cost?— generation cost in USD, surfaced asusage.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.