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.

Native catalog shell
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.

Count a prompt shell
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:

Default inbound rejection json
{"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:

Native inbound rejection json
{"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

gemini-text.test.ts ts
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

gemini-tools.test.ts ts
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:

Vertex AI URL pattern text
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.