Google Veo Video
aimock mocks Google's native Veo async video-generation API — submit a long-running
operation under POST /v1beta/models/{model}:predictLongRunning, then poll it
through done: false → done: true under
GET /v1beta/operations/{name}. It draws from the same
endpoint: "video" fixture pool as the other video surfaces, disambiguated by
the veo-* model string.
Endpoints
| Method | Path | Response |
|---|---|---|
| POST | /v1beta/models/{model}:predictLongRunning |
{ name: "operations/…" } — the long-running operation
handle; the matched fixture's video drives the operation's terminal
state
|
| GET | /v1beta/operations/{name} |
{ name, done: false } while running, then
{ name, done: true, response.generateVideoResponse.generatedSamples[0].video.uri
}
once complete — or { name, done: true, error } on failure
|
Veo has no collision with the Gemini :predict route (anchored on
:predict$, so :predictLongRunning never matches it) and
/v1beta/operations/… is a fresh namespace. There is no model-listing
endpoint, and — deliberately — no content/download endpoint (see
Files-API uri).
Fixture Authoring
Submits are matched against endpoint: "video" fixtures on the request's
prompt (via match.userMessage) and model (via
match.model). The Veo create body's prompt is read from
instances[0].prompt. The veo-* model string is the provider
disambiguator — Veo, Grok, 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 operation
// name is always server-minted); `url` is the Files-API uri served as-is.
video: { id: "vid_1", status: "completed", url: "https://generativelanguage.googleapis.com/v1beta/files/abc" },
}, { model: "veo-3.1-generate-preview" });
// A failed operation:
mock.onVideo("impossible prompt", {
video: { id: "vid_2", status: "failed", error: "content policy violation" },
}, { model: "veo-3.1-generate-preview" });
The fixture's video object supports:
-
status—"completed"or"failed"sets the operation's terminal state. The wire terminal signal is the booleandonefield: a stored"completed"or"failed"status serializes todone: true(and a non-terminal poll todone: false). There is no"done"status string on this surface — only the boolean. -
id— ignored on this surface (the operation name is always a server-mintedoperations/<uuid>) -
url?— the Files-API uri returned ingeneratedSamples[0].video.urion a completed poll; defaults to a Files-API-shaped placeholder when omitted error?— failure message surfaced on a failed-operation poll-
cost?— not applicable to Veo. The Veo operations envelope carries no cost field, so cost is neither captured in record mode nor round-tripped on this surface.
Polling Realism
By default a submitted operation is seeded terminal internally — the first status
poll already reports done: true. To exercise client code that polls through
intermediate states, pass veoVideo with poll thresholds. The semantics are
identical to falQueue and the
OpenRouter video surface: aimock keeps a
pending → in_progress → completed model internally and serializes
both non-terminal states to the Veo wire's done: false.
const mock = new LLMock({
port: 0,
veoVideo: { pollsBeforeInProgress: 1, pollsBeforeCompleted: 2 },
});
// Submit → { name: "operations/…" }
// poll 1 → { name, done: false }
// poll 2 → { name, done: true, response.generateVideoResponse.generatedSamples[0].video.uri }
Thresholds are sanitized exactly as on the other video surfaces: non-finite values
(NaN, Infinity) are treated as unset, negative or fractional
values are floored and clamped to non-negative integers, and an explicit
{ pollsBeforeInProgress: 0, pollsBeforeCompleted: 0 } still seeds the
operation terminal at submit. createServer warns at startup on invalid
values.
Files-API uri — served as-is, no byte proxying
aimock does not proxy or capture Veo video bytes. The Veo API returns
the rendered clip as a Files-API uri inside
generatedSamples[0].video.uri, and aimock serves that uri
verbatim — there is no content/download endpoint, no base64, and no byte
relay. A fixture stores the uri in video.url (a Files-API-shaped
https://generativelanguage.googleapis.com/v1beta/files/… placeholder
is served when omitted), and record mode persists the upstream uri as-is. Clients fetch
the actual bytes from the Files API themselves, exactly as against the real provider.
Errors
Malformed or non-object JSON, an empty prompt, and an unknown operation all return the
Gemini error envelope { error: { code, message, status } } — 400 for a
bad submit body, 404 for an unknown operation. A matched failed fixture polls
to { name, done: true, error: { code, message } }.
Chaos & Metrics
Chaos injection applies to both routes. In
Prometheus metrics the per-operation path is templated as
/v1beta/models/{model}:predictLongRunning and
/v1beta/operations/{name} to keep label cardinality bounded.
Record Mode
With record mode and the veo provider
configured, an unmatched submit becomes a live interactive proxy: the
:predictLongRunning POST is forwarded to the real API and answered with a
mock-rewritten { name } envelope (a fresh aimock operation name), and each
client poll is proxied upstream 1:1 with the mock operation name substituted. The client's
own polling drives the upstream lifecycle — there is no server-side queue walk.
{
"llm": {
"fixtures": "./fixtures",
"record": {
"providers": { "veo": "https://generativelanguage.googleapis.com" }
}
}
}
const mock = new LLMock({
record: {
providers: { veo: "https://generativelanguage.googleapis.com" },
},
});
When the upstream poll reports done: true, the body is relayed immediately
and an eager capture runs in the background: aimock reads the Files-API uri
straight out of the terminal poll body (no download — there are no bytes to fetch)
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 operation name, video.status =
"completed", video.url = the upstream uri). The Veo operations
envelope carries no cost field, so no cost is captured. The same submit then replays
in-session and across sessions. Relayed poll bodies are otherwise faithful: every upstream
field passes through verbatim — including the Files-API uri inside
response — with only the operation name rewritten to the mock id. A
failed operation persists { status: "failed", error }; any
terminal status that is not representable in video.status passes through with
a warning and is never recorded.
The polling client's Bearer credential is forwarded only to the configured provider
origin: the upstream poll URL is adopted only when same-origin (an off-origin or
unparseable URL falls back to the constructed path on the provider origin, with a
warning). Strict mode wins over record: a strict no-match returns 503 and nothing is
proxied. Without a configured veo provider URL, --record warns
and serves the normal no-match 404. Under proxy-only mode (record.proxyOnly /
--proxy-only) nothing is persisted and a completed operation is never
converted to a local replay job — every poll keeps proxying upstream.
TTL caveat: record-mode operations live in the same bounded job map as replay operations (1-hour TTL, 10,000 entries). Each successful proxied poll refreshes a record operation'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 operation evicted and returns 404.