Google Gemini
aimock supports both generateContent (non-streaming) and
streamGenerateContent (SSE) endpoints, plus Gemini Live over WebSocket. The
same fixtures drive all three transports.
To send events or fields aimock does not model yet, use a
raw passthrough fixture with wire: "gemini".
Endpoints
| Method | Path | Format |
|---|---|---|
| POST | /v1beta/models/:model:generateContent | JSON |
| POST | /v1beta/models/:model:streamGenerateContent | SSE (data:) |
| GET | /v1beta/models | Paged model JSON |
| GET | /v1beta/models/:model | Model JSON |
| POST | /v1beta/models/:model:countTokens | JSON: totalTokens |
| WS | /ws/google.ai.generativelanguage.* | WebSocket JSON |
If the client hangs up before the reply is complete, aimock stops delivery and journals
the request with interruptReason: "client aborted" (see
Client Disconnects). Gemini Live records
the turn the same way. To delay generateContent replies, set
nonStreamingTimingAllProviders: true with a streamingProfile or
recordedTimings (see
All Providers).
Model Catalog
The native catalog contains Google IDs and unrecognized IDs from fixtures, in first-seen order. Empty fixture sets use the established default models. Known OpenAI and Anthropic IDs are excluded. Custom test IDs remain visible.
GET /v1beta/models returns models and a
nextPageToken when another page exists. The default pageSize is
50. Zero uses that default, and values greater than 1000 use 1000. Send the returned token
as pageToken for the next page.
A client that ignores nextPageToken sees only the first page. Negative,
noninteger, or nonnumeric sizes and invalid tokens return 400
INVALID_ARGUMENT. aimock uses base64 offset tokens. Exact error details are
local contracts. Authenticated upstream catalog errors were not measured.
Model metadata is synthetic: name is models/<id>,
baseModelId and displayName equal the ID, and
version is "1". Token limits are 128000 input and 8192 output.
supportedGenerationMethods contains generateContent and
countTokens. These values do not describe upstream capacity.
Detail paths accept raw or percent-encoded slashes in IDs. Missing IDs, malformed escapes,
and IDs outside the Google catalog return 404 NOT_FOUND. The examples assume
fixtures with gemini-test and additional Gemini IDs.
curl 'http://localhost:4010/v1beta/models?pageSize=1'
curl 'http://localhost:4010/v1beta/models?pageSize=1&pageToken=MQ=='
curl 'http://localhost:4010/v1beta/models/gemini-test'
In this example, the first page returns MQ==. Use the actual returned token
for your fixture set.
Approximate Token Counts
POST /v1beta/models/:model:countTokens accepts top-level
contents or a generateContentRequest wrapper. It returns
{"totalTokens": N}. Wrapped requests use the fields inside the wrapper,
including system text and tools.
aimock converts the request through its existing translator, then estimates about 1 token per 4 characters of messages (plus tool definitions), minimum 1. The estimate includes converted messages, system text, and serialized converted tools. This deterministic estimate is not a tokenizer or an upstream count. Optional cache counts and token-detail arrays are omitted.
curl http://localhost:4010/v1beta/models/gemini-test:countTokens \
-H 'Content-Type: application/json' \
--data '{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}'
curl http://localhost:4010/v1beta/models/gemini-test:countTokens \
-H 'Content-Type: application/json' \
--data '{"generateContentRequest":{"contents":[{"role":"user","parts":[{"text":"hello"}]}]}}'
Counts ignore fixture usage. Generate responses use fixture counts and otherwise default
to zero. To make a response agree with an estimate, set its fixture usage explicitly.
Missing or invalid contents return 400 INVALID_ARGUMENT.
Catalog and count routes never proxy or record. Count POSTs create internal journal entries. Catalog GETs do not. See Record & Replay.
Authentication Errors
With inbound API-key authentication enabled, rejected keys on Gemini and Vertex Google HTTP paths return 401 with the same body as every other HTTP route, unchanged from 1.45:
{"error":{"message":"Invalid API key","type":"authentication_error","code":"invalid_api_key"}}
To get the provider-native body instead, set policy.authErrors to
"native", pass --policy authErrors=native, or send the request
header X-AIMock-Policy: authErrors=native. Gemini and Vertex Google HTTP
paths then return:
{"error":{"code":401,"message":"Invalid API key","status":"UNAUTHENTICATED"}}
The native body uses the numeric error.code: 401 and
error.status: "UNAUTHENTICATED" in place of the string
error.code: "invalid_api_key". With the native policy, check HTTP 401 or the
status string.
A live unauthenticated Gemini count probe returned 403 PERMISSION_DENIED
for an unregistered caller. The native aimock body intentionally keeps 401
UNAUTHENTICATED for configured inbound authentication. These policies differ.
WebSocket authentication always returns the default body.
Unit Test: Streaming Text
const textFixture = {
match: { userMessage: "hello" },
response: { content: "Hi there!" },
};
const instance = await createServer([textFixture]);
const res = await post(
`${instance.url}/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse`,
{
contents: [{ role: "user", parts: [{ text: "hello" }] }],
}
);
// Parse Gemini SSE chunks
const chunks = res.body.split("\n")
.filter(l => l.startsWith("data: "))
.map(l => JSON.parse(l.slice(6)));
// Gemini response shape
expect(chunks[0].candidates[0].content.parts[0].text).toBeDefined();
// Reassemble text
const text = chunks
.map(c => c.candidates[0].content.parts[0].text ?? "")
.join("");
expect(text).toBe("Hi there!");
Unit Test: Tool Call
const toolFixture = {
match: { userMessage: "weather" },
response: {
toolCalls: [{ name: "get_weather", arguments: { city: "NYC" } }]
},
};
const instance = await createServer([toolFixture]);
const res = await post(
`${instance.url}/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse`,
{
contents: [{ role: "user", parts: [{ text: "what is the weather?" }] }],
}
);
const chunks = parseGeminiSSEChunks(res.body);
const parts = chunks[0].candidates[0].content.parts;
expect(parts[0].functionCall.name).toBe("get_weather");
Request Translation
Gemini uses a different request format (contents with parts)
than OpenAI. aimock translates Gemini requests to the unified format via
geminiToCompletionRequest() so the same fixture
match.userMessage works regardless of which provider endpoint the request
arrives on.
Gemini Live (WebSocket)
Gemini Live uses WebSocket at /ws/google.ai.generativelanguage.* for
bidirectional streaming. See the WebSocket APIs page for details.
Gemini Live text support is unverified against a real model — no text-capable Gemini Live model existed at time of writing. The implementation follows the API specification.
Vertex AI
Google Cloud's Vertex AI provides access to Gemini models through a different URL pattern than the AI Studio API. aimock supports Vertex AI requests using the same Gemini handler — the URL pattern is different, but the request and response formats are identical.
Vertex AI URLs follow the pattern:
POST /v1/projects/{project}/locations/{location}/publishers/google/models/{model}:generateContent
POST /v1/projects/{project}/locations/{location}/publishers/google/models/{model}:streamGenerateContent
The same fixtures work for both Gemini AI Studio and Vertex AI endpoints. See the Vertex AI page for configuration details.
Ordered blocks (tool-first)
A combined content + toolCalls fixture accepts an optional
blocks array to control stream order — see
Ordered blocks. Gemini has
full support: ordered parts/candidate chunks carry
functionCall and text in array order, so tool-first and interleaved ordering
are observable to clients.