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
    }
  ]
}

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)
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
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 at least one role: "tool" message is present; false when none are. Stateless alternative to ordering fixtures by toolCallId. See Multi-Turn Conversations
endpoint string Restrict to endpoint type: chat, image, speech, transcription, video, embedding. 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 LAST tool message

toolCallId is compared against the tool_call_id of the last role: "tool" message in the request — regardless of whether that’s the overall last message. If no tool message is present in the history, toolCallId never matches. 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.includesuserMessage: "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 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[], reasoning?, finishReason? Text and tool calls in a single response
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
Video video.id, video.status, video.url? Generated video URL with async polling

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

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: stopend_turn (Claude), STOP (Gemini), completed (Responses API); tool_callstool_use (Claude), FUNCTION_CALL (Gemini), completed (Responses API); lengthmax_tokens (Claude), MAX_TOKENS (Gemini), incomplete (Responses API); content_filterSAFETY (Gemini), failed (Responses API)
role string Override message role (default: "assistant")
systemFingerprint string Add system_fingerprint to response

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

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
Text Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Tool Calls Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Content + Tool Calls Yes Yes Yes Yes Yes Yes Yes Yes Yes Yes
Streaming SSE SSE SSE SSE SSE SSE Binary EventStream SSE NDJSON SSE
Reasoning Yes Yes Yes Yes Yes Yes Yes Yes
Web Searches Yes
Response Overrides 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.