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.

veo-video.test.ts ts
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:

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.

polling.test.ts ts
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.

aimock.config.json json
{
  "llm": {
    "fixtures": "./fixtures",
    "record": {
      "providers": { "veo": "https://generativelanguage.googleapis.com" }
    }
  }
}
Programmatic config ts
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.