Fixtures

Fixtures define what the mock server returns. Each fixture has a match criteria and a response. Load them from JSON files, register them programmatically, or mix both approaches.

File Format

fixtures/example.json json
{
  "fixtures": [
    {
      "match": {
        "userMessage": "hello",
        "model": "gpt-4"
      },
      "response": {
        "content": "Hello!"
      },
      "latency": 200,
      "chunkSize": 10
    }
  ]
}

A fixture file may also hold an optional top-level mcpFakes key, which scripts MCP tool answers for one test scenario next to its LLM turns. A file may hold fixtures, mcpFakes, or both. See MCPMock: Scenario fakes.

Match Fields

Field Type Description
userMessage string | RegExp Match on the last user message — string (substring, or exact when requestTransform is set) or regex (pattern match)
systemMessage string | RegExp | string[] Match on the joined text of the request’s system messages — string (case-sensitive substring, or exact when requestTransform is set), regex (pattern match), or an array of strings (all substrings must be present). Gate a fixture on host-supplied context
inputText string | RegExp Match on embedding input text
toolCallId string Match on tool_call_id of the last role: "tool" message. The onToolResult(id, response) helper is sugar over this field
toolResultContains string Substring match on the text content of the last message when it is a role: "tool" result. Discriminates fixtures whose requests differ only inside the tool-result payload (e.g. approve vs cancel resuming with the same tool_call_id). See Multi-Turn Conversations
toolName string Match on tool function name — compared against the names of tool definitions in the request’s tools: array
model string | RegExp Match on the requested model name
responseFormat string Match on response_format.type (e.g. "json_object")
sequenceIndex number Match on the Nth occurrence of this pattern
turnIndex number Count of role: "assistant" messages in the request. Stateless — derived from request content, safe for shared instances. See Multi-Turn Conversations
hasToolResult boolean true when the current turn (the messages after the last role: "user" message) contains a role: "tool" result; false when it does not. Stateless alternative to ordering fixtures by toolCallId. See Multi-Turn Conversations
endpoint string Restrict to endpoint type: chat, image, speech, transcription, video, embedding, openai-live. Search, rerank, and moderation services (added in 1.7.0) are registered through their own fixture APIs rather than via this field
context string Restrict to a named context via X-AIMock-Context header. Fixtures with context only match requests carrying that exact value; fixtures without context match any request. Same opt-in semantics as endpoint
predicate function Custom function: (req) => boolean (programmatic only)

Matching Semantics

These are the rules the router uses to pick a fixture for a given request. All fields on match are AND-ed — every one must pass for the fixture to be selected.

1. userMessage matches only the LAST user message

userMessage is compared against the content of the last message with role: "user" in the request. Earlier user messages in the conversation history are ignored. A request that contains ten turns of prior history plus one new user turn only matches against that final turn — never against anything earlier.

This is the single rule that trips people up most often. If you need to differentiate conversations by earlier context (for example, to return a different response on the second round of a tool-using conversation), use toolCallId, sequenceIndex, or a predicate instead of piling keywords into userMessage. See Multi-Turn Conversations for the tool-round idiom.

2. toolCallId matches the overall last message

toolCallId matches only when the request’s overall last message is a role: "tool" result whose tool_call_id equals the given value. This mirrors the API contract: a tool result is the last message only while the model is answering that tool round. If the last message is anything else — a newer user turn, or no tool result at all — toolCallId never matches, so a stale tool round can’t shadow userMessage matchers. This is the same overall-last-message rule as toolResultContains. See Multi-Turn Conversations for the tool-round idiom.

3. First match wins, in file order

Fixtures are evaluated in the order they were registered. The first fixture whose match criteria all pass is returned — subsequent fixtures are not consulted. For file-loaded fixtures, that means order within the JSON array. For loadFixtureDir(), files are loaded in sorted filename order, so a 00-catchall.json loaded before 10-specific.json will shadow the specific fixture. Put more specific fixtures before broader ones.

sequenceIndex lets a single pattern return different responses on repeated matches — see Sequential Responses.

4. Substring by default, exact when a requestTransform is set

By default, string userMessage (and inputText) match via String.includes — userMessage: "hello" matches "say hello world". Pass a RegExp when you need pattern matching. When a requestTransform is configured, this behavior flips to strict equality — see the next paragraph for why.

If the router is configured with a requestTransform (typically used to strip dynamic data like timestamps or UUIDs from the request before matching), string userMessage, systemMessage, and inputText flip to strict equality (===). The rationale: transforms normalize requests to a canonical form, and once normalized, the sensible comparison is exact — substring matching on a normalized string is more likely to hide bugs than catch flexible input.

5. Validation warnings surface shadowing at load time

validateFixtures() runs when fixtures are loaded and emits warnings for common shadowing mistakes:

6. Use predicate for arbitrary logic

When the built-in match fields can't express the condition you need, a predicate function receives the full request and returns a boolean. It is the escape hatch for anything from inspecting the assistant's prior tool call arguments to gating on system-prompt content. Predicates are programmatic-only — JSON fixture files cannot serialize functions.

predicate.ts ts
mock.on(
  { predicate: (req) => req.messages.at(-1)?.role === "tool" },
  { content: "Done!" }
);

Response Types

Type Fields Description
Text content, role?, finishReason?, reasoning?, webSearches? Plain text response
Tool Call toolCalls[], finishReason? Function call(s) with name + arguments
Content + Tool Calls content, toolCalls[], blocks?, reasoning?, finishReason? Text and tool calls in a single response. Add an optional blocks array to control stream order (e.g. tool-first) — see Ordered blocks below.
Error error.message, error.type?, status? Error response with HTTP status
Embedding embedding[] Vector of numbers
Image image.url or images[].url Generated image URL(s) or base64 data
Speech audio Base64-encoded audio data
Transcription transcription.text, words?, segments? Transcribed text with optional timestamps
Live live, liveTiming? A versioned bidirectional transcript. See GPT-Live transcripts.
Video video.id, video.status, video.url?, video.error?, video.b64?, video.cost? Generated video with async polling — error is the failure message surfaced by async video jobs, b64 is base64-encoded video bytes served by content-download endpoints, cost is the generation cost surfaced in usage envelopes

Override fields: Text, Tool Call, and Content + Tool Calls responses also accept the override fields listed below (id, model, usage, finishReason, role, systemFingerprint, created).

GPT-Live transcripts

A Live fixture uses match.endpoint: "openai-live" and response: { live: transcript, liveTiming: "immediate" }. It describes a complete conversation on GET /v1/live/sessions. The WebSocket guide explains the connection and session commands.

live-fixture.ts ts
import { readFileSync } from "node:fs";
import { LLMock, type LiveTranscript } from "@copilotkit/aimock";

// Run from the aimock repository. Choose one mode per server.
const path = "./fixtures/openai-live-client.json";
const mock = new LLMock();
mock.loadFixtureFile(path);
await mock.start();
// Connect your application to mock.url at /v1/live/sessions.
// After the conversation, close its socket and await mock.stop().

// Alternative registration on a separate server:
const document = JSON.parse(readFileSync(path, "utf8"));
const transcript: LiveTranscript = document.fixtures[0].response.live;
const builderMock = new LLMock();
builderMock.onLive({ model: "gpt-live-1" }, transcript);

The repository includes fixtures/openai-live-client.json and fixtures/openai-live-managed.json. Both match the same endpoint and model, so load one per server. These examples contain authored events and synthetic PCM samples. Their timing and usage values are illustrative. They are not provider recordings or speech. The builder takes the transcript directly and defaults to recorded timing. The example files explicitly select immediate timing.

Transcript format

Field Meaning
version, model, mode version: 1, model: "gpt-live-1", and mode: "client" or "managed".
audio { encoding: "pcm16le", sampleRateHz: 24000, channels: 1 }. Audio event payloads contain base64-encoded PCM bytes.
configuration The session configuration. It must equal the first session.start command's session value.
entries Ordered events, each with direction ("client" or "server"), atMs, and event. Every server entry also has a barrier.
bindings Identifier definitions and references. Each binding supplies entry, pointer, name, action, and owner.
capture source is "authored" or "provider". complete must be true. terminalEntry is the zero-based index of the final session.closed. Provider captures include provenance.

The final close reason must be close_requested or remote_hangup. Partial captures and error probes are rejected. Server configuration snapshots must agree with startup and accepted updates. Only managed backend instructions updates are supported. Mode and audio format cannot change during a session.

Causal barriers and PCM chunks

A server barrier uses { command, audioBytes } to identify required client input. command counts accepted non-audio commands, including startup. audioBytes counts decoded input bytes since that command. Barriers must increase or stay equal and must refer to preceding client input. A server event cannot run before client input that defines an identifier it references.

Replay compares decoded input PCM bytes across adjacent audio appends with identical metadata. Clients can split or combine these chunks at sample boundaries. Each PCM16 sample uses two bytes. A non-audio command or metadata change ends the run. Annotated audio commands retain their original frame boundary. Rechunking cannot move bytes across these boundaries. Aimock replays fixture output audio and does not synthesize speech from transcript text.

Identifier bindings

A binding's pointer is a JSON Pointer relative to its entry's event. action is "define" or "reference". owner is "client" or "server". One symbolic name connects each definition to its later references. Replay creates fresh server identifiers for each connection. Clients must use the identifiers received on that connection, including delegation IDs and tool-call IDs.

The limited origin: "imported-context" exception defines a first-seen server response ID at /event/response/previous_response_id in a response lifecycle event. This represents earlier context outside the capture. It applies only to a non-null identifier without an earlier definition. Later occurrences reference that same name. Other forward references remain invalid, and null values have no binding.

Replay timing

liveTiming: "recorded" is the default. Replay uses each server event's atMs offset from accepted startup, divided by the fixture's replaySpeed, or the server value when omitted (default 1). A speed of 2 halves those offsets. liveTiming: "immediate" removes the recorded delays. Both modes preserve server order and wait for causal barriers. Faster timing does not bypass required client commands, tool results, or input audio. Session limits and timeouts still apply. See Record & Replay for capture and retention.

Tool argument JSON

Malformed JSON in a tool call's arguments is no longer silently replaced with {}. The response depends on how the selected API carries arguments:

Valid JSON retains its existing behavior, including arrays, primitives, and null. Empty or omitted legacy tool-call arguments still default to {}; whitespace-only strings are malformed JSON. Fixture validation is unchanged: validateFixtures() can still reject malformed arguments before replay, and ordered tool-call blocks still require an arguments field.

Ordered blocks (tool-first & interleaved streaming)

By default a Content + Tool Calls response streams its text first, then its tool calls. To control that order — for example to emit a tool call before any text (“tool-first”), or to interleave text and tool calls — add an optional blocks array. Each entry is one of:

When blocks is present it takes precedence over the content and toolCalls fields for stream ordering: the blocks are streamed in array order. When blocks is absent, legacy { content, toolCalls } fixtures with valid JSON arguments stream as before — text-first, byte-identical to prior releases. The field is purely additive.

Blocks-only fixtures (first-class)

A fixture can be written with only a blocks array — no content or toolCalls needed. A non-empty blocks array is a first-class response shape: the builders derive the aggregate text and tool calls from the blocks themselves, and validateFixtures() accepts it without requiring the legacy fields. This is the cleanest way to author a tool-first or interleaved response — you express the order once, in one place, with no duplicated aggregate to keep in sync.

tool-first.json json
{
  "blocks": [
    { "type": "toolCall", "name": "get_weather", "arguments": { "city": "SF" }, "id": "call_1" },
    { "type": "text", "text": "Here is the weather." }
  ]
}

The example above streams the get_weather tool call before the text, with no separate content / toolCalls fields. For an interleaved stream, list blocks in the desired order, e.g. [toolCall, text, toolCall].

You may still supply content and toolCalls alongside blocks if you want an explicit aggregate — for example to assert a specific merged shape independently of the order. Both forms are supported; blocks always wins for stream ordering.

Validation: validateFixtures() checks a blocks array at load time so a malformed array is rejected before it reaches a builder — blocks must be an array; each entry must be an object with type "text" or "toolCall"; a text block needs a non-empty string text; a toolCall block needs a non-empty name, arguments that are a valid-JSON string or an object, and an optional string id. If a fixture carries both blocks and legacy content/toolCalls that disagree, loading warns (the redundant legacy fields are ignored in favor of blocks).

Per-provider observability

How faithfully “tool-first” / interleaved order is observable depends on each provider's wire protocol — and, for some providers, on whether the request is streaming. The mock always emits in block order; what a client can reconstruct from the result varies. A shape is Full when the wire carries the blocks in a single positionally-ordered structure (indexed content blocks, ordered output items, ordered steps); it is Non-observable when text and tool calls land in separate top-level fields that the client merges without a shared order. It is Partial when block order is carried on the wire (chunk arrival order) but the structure is not positionally indexed, so some clients reassemble positionally rather than honoring arrival order — observable best-effort, not guaranteed. The classifications below were verified against each provider's builder.

Provider / shape Block-order support Notes
Anthropic (Claude Messages) Full Typed text / tool_use content blocks at incrementing indices — tool-first and interleaved are natively observable, streaming and non-streaming alike.
OpenAI Responses API Full Ordered output items (message vs function_call) carry output_index — SDKs honor the order, so a tool call can precede the message.
Gemini Full Ordered parts/candidate chunks carry functionCall and text in any order.
Gemini Interactions (replay) Full One step per block in array order — a function_call step takes a lower index than a later model_output step, streaming (step.* events) and non-streaming (steps[]) alike. Record side is args-normalization only — see the note below.
Bedrock invoke Full Mirrors the Anthropic Messages content array: ordered text / tool_use entries non-streaming, indexed content_block_* events streaming — tool-first is wire-expressible on both.
Bedrock Converse Full Positional content[] blocks non-streaming, indexed contentBlock* events (carrying contentBlockIndex) streaming — a toolUse can precede the text on both.
Cohere (streaming) Full SSE emits content-* and tool-call-* events in block array order, each carrying an index — tool-first / interleaved is observable on the stream.
Ollama (streaming) Partial A tool_calls chunk can be emitted before content on the wire, but some clients reassemble positionally. Best-effort.
OpenAI chat-completions Non-observable delta.content and delta.tool_calls (streaming), or message.content and message.tool_calls (non-streaming), are separate channels/fields the client merges. The mock emits in block order and the streamed wire order is assertable, but the merge is not positionally interleaved, so tool-first is not semantically observable to clients on this channel.
Cohere (non-streaming) Non-observable The non-streaming body keeps text in message.content[] and tool calls in the separate message.tool_calls[] field — the relative order of a text vs. a toolCall block is not on the wire. Use the streaming shape when order matters.
Ollama (non-streaming) Non-observable The aggregated reply carries message.content and message.tool_calls as separate fields — no positional ordering between a text and a toolCall block. Use the streaming shape when order matters.

Recording: In record mode the recorder only persists a blocks array when the recorded upstream stream was genuinely tool-first or interleaved (a tool-call delta arrives before the first content delta, or content arrives after a tool-call delta). Ordinary text-then-tools streams are saved in the legacy { content, toolCalls } shape with no blocks key, so existing golden recordings round-trip byte-identically. The Cohere and Bedrock collapsers capture block order this way alongside the original providers.

Gemini Interactions is the exception: its record-side collapser normalizes tool-call arguments only and does not reorder blocks on capture — its step-index protocol can't reconcile arrival-order blocks at record time. Ordering is still honored on replay from a hand-authored blocks fixture; it is simply not reconstructed automatically from a recording.

JSON auto-stringify: In fixture files and programmatic API, arguments and content fields accept both objects and strings. Objects are automatically stringified via JSON.stringify(). Use the object form for readability — no more escaped JSON strings.

Dynamic responses: Responses can also be sync or async functions that receive the request and return the response dynamically. See Dynamic Responses on the Examples page.

Response Override Fields

Fixture responses can include optional fields to override auto-generated envelope values. These map correctly across all provider formats (OpenAI, Claude, Gemini, Responses API).

Field Type Description
id string Override auto-generated response ID
created number Override Unix timestamp
model string Override model name in response
usage object Override token counts: { prompt_tokens, completion_tokens, total_tokens }. Also accepts Anthropic field names (input_tokens, output_tokens) and Gemini field names (promptTokenCount, candidatesTokenCount, totalTokenCount). OpenAI Chat Completions includes usage in the response body; the Responses API uses a separate response.usage object. When omitted, token counts are auto-computed from content length
finishReason string Override finish reason (default: "stop" or "tool_calls"). Provider mappings: stop → end_turn (Claude), STOP (Gemini), completed (Responses API); tool_calls → tool_use (Claude), FUNCTION_CALL (Gemini), completed (Responses API); length → max_tokens (Claude), MAX_TOKENS (Gemini), incomplete (Responses API); content_filter → SAFETY (Gemini), failed (Responses API)
role string Override message role (default: "assistant")
systemFingerprint string Add system_fingerprint to response
provider string OpenRouter only: top-level serving-provider display name (default: the winning model slug's author)
nativeFinishReason string OpenRouter only: raw upstream native_finish_reason (mirrors finishReason by default)
usage.cost number OpenRouter only: scriptable usage.cost (+ cost_details / is_byok / token-detail objects); emitted only when supplied, never fabricated

Fixture Options

Field Type Description
latency number Milliseconds delay between SSE chunks (streaming)
chunkSize number Characters per SSE chunk (streaming)
truncateAfterChunks number Abort stream after N chunks (error injection)
disconnectAfterMs number Disconnect after N ms (error injection)
streamingProfile object Streaming physics profile: { ttft, tps, jitter }. See Streaming Physics
chaos object Per-fixture chaos config: { dropRate, malformedRate, disconnectRate }. See Chaos Testing

Loading Fixtures

addFixturesFromJSON() accepts an array of fixture entries, either as a JSON string or as a parsed array. Fixture files use the { "fixtures": [...] } envelope shown above; load those files with loadFixtureFile().

Each JSON fixture needs an object-valued match. An explicit empty object ({}) remains a catch-all. Scalar and array values are invalid.

LLMock.loadFixtureFile(), LLMock.loadFixtureDir() and llmock --fixtures also load a file’s mcpFakes. When no mount serves a block’s mount path (default /mcp), aimock mounts an MCPMock there, so fakes need no other setup. The free functions loadFixtureFile() and loadFixturesFromDir() return LLM fixtures only and throw on a file with mcpFakes; use loadFixtureFileWithServices() or loadFixturesFromDirWithServices() for such files. See MCPMock: Scenario fakes.

From a file

load-file.ts ts
const mock = new LLMock();
mock.loadFixtureFile("./fixtures/chat.json");
mock.loadFixtureFile("./fixtures/tools.json");

From a directory

load-dir.ts ts
// Loads all .json files in the directory (non-recursive)
mock.loadFixtureDir("./fixtures");

Snapshot-style recording: When recording with X-Test-Id, fixtures are automatically organized into per-test directories (<fixturePath>/<test-slug>/<provider>.json). See Snapshot-Style Recording for details.

Context-scoped fixtures

fixtures/context-example.json json
{
  "fixtures": [
    {
      "match": { "userMessage": "hello", "context": "langgraph-python" },
      "response": { "content": "Hi from LangGraph!" }
    },
    {
      "match": { "userMessage": "hello" },
      "response": { "content": "Hi from the shared fallback!" }
    }
  ]
}

Requests with X-AIMock-Context: langgraph-python match the first fixture; all other requests fall through to the shared fixture.

Programmatically

programmatic.ts ts
// Shorthand methods
mock.onMessage("hello", { content: "Hi!" });
mock.onToolCall("get_weather", { content: "72F" });
mock.onEmbedding("my text", { embedding: [0.1, 0.2] });
mock.onImage("sunset", { image: { url: "https://example.com/sunset.png" } });
mock.onSpeech("hello", { audio: "SGVsbG8=" });
mock.onTranscription({ transcription: { text: "Hello" } });
mock.onVideo("cats", { video: { id: "vid-1", status: "completed", url: "https://example.com/cats.mp4" } });
mock.onJsonOutput("data", { key: "value" });
mock.onToolResult("call_123", { content: "Done" });

// Full fixture object
mock.addFixture({
  match: { userMessage: "hello", model: "gpt-4" },
  response: { content: "Hi!" },
  latency: 100,
  chunkSize: 5,
});

// Predicate-based routing
mock.on(
  { predicate: (req) => req.messages.at(-1)?.role === "tool" },
  { content: "Done!" }
);

JSON files cannot use predicate (functions can't be serialized). Use programmatic registration for predicate-based routing.

onTranscription takes the response object directly — there is no user-provided input to match against, unlike onMessage / onToolCall / onEmbedding. Every transcription request matches the same fixture.

Provider Support Matrix

Feature OpenAI Chat OpenAI Responses Claude Gemini Gemini Int. Vertex AI Bedrock Azure Ollama Cohere OpenRouter
Text Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Tool Calls Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Content + Tool Calls Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Streaming SSE SSE SSE SSE SSE SSE Binary EventStream SSE NDJSON SSE SSE
Reasoning Yes Yes Yes Yes Record only† Yes Yes Yes Yes Yes Yes
Web Searches — Yes — — — — — — — — —
Response Overrides Yes Yes Yes Yes Yes Yes — Yes* — — Yes

* Azure inherits OpenAI’s override support because Azure OpenAI routes through the OpenAI Chat Completions response format internally.

† Gemini Interactions captures reasoning on record (its collapser assembles thought_summary deltas into reasoning), but its replay builders do not re-emit reasoning, so a replayed turn carries none.