# Write Fixtures

> Use when writing test fixtures for @copilotkit/aimock — mock LLM responses, tool call sequences, error injection, multi-turn agent loops, embeddings, structured output, sequential responses, or debugging fixture mismatches

- Skill: `copilotkit-aimock/write-fixtures` (Agent Skill)
- Install (CLI): `npx skillmds@latest add copilotkit-aimock/write-fixtures`
- Raw SKILL.md: https://api.skillmd.com/api/skills/copilotkit-aimock/write-fixtures/raw
- Safety review: PASS (external: skill-scanner PASS, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: CopilotKit (https://skillmd.com/u/copilotkit-aimock)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/copilotkit-aimock/write-fixtures

---


# Writing aimock Test Fixtures

## What aimock Is

aimock is a zero-dependency mock infrastructure for AI apps. Fixture-driven. Multi-provider (OpenAI, Anthropic, Gemini, Gemini Interactions, AWS Bedrock, Azure OpenAI, Vertex AI, Ollama, Cohere, OpenRouter). Multimedia endpoints (image generation, text-to-speech, audio transcription, video generation). MCP, A2A, AG-UI, and vector DB mocking. Runs a real HTTP server on a real port — works across processes, unlike MSW-style interceptors. WebSocket support for OpenAI Responses/Realtime and Gemini Live APIs. Record-and-replay for all endpoints including multimedia. Chaos testing and Prometheus metrics.

## Core Mental Model

- **Fixtures** = match criteria + response
- **First-match-wins** — order matters
- All providers share one fixture pool (provider adapters normalize to `ChatCompletionRequest`)
- Fixtures are live — mutations after `start()` take effect immediately
- Sequential responses are supported via `sequenceIndex` (match count tracked per fixture)

## Match Field Reference

| Field                | Type                                      | Matches Against                                                                                                                                                                                                                                                                                                                                                                                                    |
| -------------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `userMessage`        | `string`                                  | Substring of last `role: "user"` message text                                                                                                                                                                                                                                                                                                                                                                      |
| `userMessage`        | `RegExp`                                  | Pattern test on last `role: "user"` message text                                                                                                                                                                                                                                                                                                                                                                   |
| `systemMessage`      | `string`                                  | Substring of the concatenated text of every `role: "system"` message in the request. Use to gate a fixture on host-supplied context (persona, agent-context entries) so changes to that context cause the fixture to fall through instead of returning a stale baked response                                                                                                                                      |
| `systemMessage`      | `string[]`                                | Array of substrings — ALL must be present in the joined system text (AND semantics). Use when the gate must combine multiple non-adjacent tokens whose serialisation order isn't stable                                                                                                                                                                                                                            |
| `systemMessage`      | `RegExp`                                  | Pattern test on the concatenated system-message text                                                                                                                                                                                                                                                                                                                                                               |
| `inputText`          | `string`                                  | Substring of embedding input text (concatenated if multiple inputs)                                                                                                                                                                                                                                                                                                                                                |
| `inputText`          | `RegExp`                                  | Pattern test on embedding input text                                                                                                                                                                                                                                                                                                                                                                               |
| `toolName`           | `string`                                  | Exact match on any tool in request's `tools[]` array (by `function.name`)                                                                                                                                                                                                                                                                                                                                          |
| `toolCallId`         | `string`                                  | Exact match on `tool_call_id` of last `role: "tool"` message                                                                                                                                                                                                                                                                                                                                                       |
| `toolResultContains` | `string`                                  | Substring of the last tool message's text content, gated on that message being the request's LAST message (same rule as `toolCallId`). Discriminates resume paths that share a `tool_call_id` and differ only inside the tool-result payload (e.g. approve `{"chosen_time": …}` vs cancel `{"cancelled": true}`)                                                                                                   |
| `model`              | `string`                                  | Exact match on `req.model`                                                                                                                                                                                                                                                                                                                                                                                         |
| `model`              | `RegExp`                                  | Pattern test on `req.model`                                                                                                                                                                                                                                                                                                                                                                                        |
| `responseFormat`     | `string`                                  | Exact match on `req.response_format.type` (`"json_object"`, `"json_schema"`)                                                                                                                                                                                                                                                                                                                                       |
| `sequenceIndex`      | `number`                                  | Matches only when this fixture's match count equals the given index (0-based)                                                                                                                                                                                                                                                                                                                                      |
| `turnIndex`          | `number`                                  | Stateless conversation-depth matching. Counts `role: "assistant"` messages in the request; matches when that count equals the value. `turnIndex: 0` = first turn (no prior assistant messages). Use instead of `sequenceIndex` for shared/deployed instances where stateful counters break under concurrency                                                                                                       |
| `hasToolResult`      | `boolean`                                 | Stateless tool-message presence matching, scoped to the CURRENT turn (messages after the last `role: "user"` message). `true` matches when a `role: "tool"` message appears after the last user message; `false` matches when none does. (If the request has no user message, the whole conversation is scanned.) Provider-consistent across all aimock handlers (OpenAI, Claude, Gemini, Bedrock, Ollama, Cohere) |
| `endpoint`           | `string`                                  | Restrict to endpoint type: `"chat"`, `"image"`, `"speech"`, `"transcription"`, `"video"`, `"embedding"`                                                                                                                                                                                                                                                                                                            |
| `predicate`          | `(req: ChatCompletionRequest) => boolean` | Custom function — full access to request                                                                                                                                                                                                                                                                                                                                                                           |

**AND logic**: all specified fields must match. Empty match `{}` = catch-all.

Multi-part content (e.g., `[{type: "text", text: "hello"}]`) is automatically extracted — `userMessage` matching works regardless of content format.

### When to Use Each Multi-turn Matching Approach

| Approach             | Stateless? | Best For                                                                                                                  |
| -------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- |
| `turnIndex`          | Yes        | Shared/deployed instances; matches on conversation depth (count of assistant messages in request)                         |
| `hasToolResult`      | Yes        | Simplest option for 2-step tool flows — boolean: does the current turn (after the last user message) carry a tool result? |
| `sequenceIndex`      | No         | Single-client unit tests with repeated identical requests (server-side counter, breaks under concurrency)                 |
| `toolCallId`         | Yes        | Matching specific tool result IDs in the conversation history                                                             |
| `toolResultContains` | Yes        | Same tool call id, different outcomes — match on the tool-result payload (approve vs cancel legs)                         |

**Prefer stateless approaches** (`turnIndex`, `hasToolResult`, `toolResultContains`) for shared aimock instances (deployed via Docker, used by multiple test runners). Use `sequenceIndex` only in isolated single-client unit tests where the counter won't be corrupted by concurrent requests.

### Multi-turn fixture examples

```jsonc
// 2-step HITL with turnIndex
{"match": {"userMessage": "trip to mars", "turnIndex": 0}, "response": {"toolCalls": [{"id": "call_001", "name": "generate_steps", "arguments": "{}"}]}}
{"match": {"userMessage": "trip to mars", "turnIndex": 1}, "response": {"content": "Great choices! Proceeding."}}

// Same thing with hasToolResult (simpler for 2-step)
{"match": {"userMessage": "trip to mars", "hasToolResult": false}, "response": {"toolCalls": [{"id": "call_001", "name": "generate_steps", "arguments": "{}"}]}}
{"match": {"userMessage": "trip to mars", "hasToolResult": true}, "response": {"content": "Great choices!"}}

// HITL suspend tool where approve and cancel resume with the SAME tool call id —
// discriminate on the tool-result payload; put the cancel leg first (first match wins)
{"match": {"toolCallId": "call_001", "toolResultContains": "\"cancelled\""}, "response": {"content": "No problem — nothing was booked."}}
{"match": {"toolCallId": "call_001"}, "response": {"content": "Booked: Monday 9:00 AM confirmed."}}
```

## Response Types

### Text

```typescript
{
  content: "Hello!";
}
```

### Tool Calls

```typescript
// Preferred: object form (auto-stringified by the fixture loader)
{
  toolCalls: [{ name: "get_weather", arguments: { city: "SF" } }];
}

// Also accepted: JSON string form (backward compatible)
{
  toolCalls: [{ name: "get_weather", arguments: '{"city":"SF"}' }];
}
```

**Both object and string forms are accepted** for `arguments`. The fixture loader auto-stringifies objects via `JSON.stringify()`. Object form is preferred for readability.

### Blocks (ordered text / tool-call streaming)

The optional `blocks` array expresses an explicit, ordered sequence of stream entries — something plain `content` + `toolCalls` cannot, since those imply text-then-tools. Each entry is either `{ "type": "text", "text": "..." }` or `{ "type": "toolCall", "name": "...", "arguments": "...", "id"?: "..." }`, streamed in array order. This enables tool-first ordering (a tool call before any text) and interleaved text/tool ordering.

```typescript
// Tool-first: tool call streams before the text
{
  blocks: [
    { type: "toolCall", name: "get_weather", arguments: { city: "SF" } },
    { type: "text", text: "Checking the weather for you…" },
  ];
}
```

When `blocks` is present it takes **precedence over `content`/`toolCalls`** for stream order; when absent, legacy behavior is unchanged. blocks-only fixtures are first-class — a response may be just `{ blocks: [...] }` with no `content` and no `toolCalls`, and builders derive the aggregate `content`/`tool_calls` from the blocks. A toolCall block's `arguments` may be a JSON object or a string (objects auto-stringify), exactly like top-level `toolCalls`.

Replay caveat: block order is observable on some providers and not others — see the [per-provider observability matrix](../../docs/fixtures/index.html#ordered-blocks).

### Embedding

```typescript
{
  embedding: [0.1, 0.2, 0.3, -0.5, 0.8];
}
```

The embedding vector is returned for each input in the request. If no embedding fixture matches, deterministic embeddings are auto-generated from the input text hash — you only need fixtures when you want specific vectors.

### Image

<!-- prettier-ignore -->
```typescript
// Single image
{
  image: {
    url: "https://example.com/generated.png"
  }
}
// Multiple images
{
  images: [{ url: "https://example.com/1.png" }, { b64Json: "iVBOR..." }]
}
```

Use `match: { endpoint: "image" }` to prevent cross-matching with chat fixtures.

### Speech (TTS)

```typescript
{ audio: "base64-encoded-audio-data" }
// With explicit format (default: mp3)
{ audio: "base64-data", format: "opus" }
```

### Transcription

```typescript
// Simple
{ transcription: { text: "Hello world" } }
// Verbose with timestamps
{ transcription: { text: "Hello world", language: "en", duration: 2.5, words: [...], segments: [...] } }
```

### Video

```typescript
{ video: { id: "vid-1", status: "completed", url: "https://example.com/video.mp4" } }
```

Video uses async polling — `POST /v1/videos` creates, `GET /v1/videos/{id}` checks status.

### Error

```typescript
{ error: { message: "Rate limited", type: "rate_limit_error" }, status: 429 }
```

### Chaos (Failure Injection)

The optional `chaos` field on a fixture enables probabilistic failure injection:

```typescript
{
  chaos?: {
    dropRate?: number;      // Probability (0-1) of returning a 500 error
    malformedRate?: number; // Probability (0-1) of returning malformed JSON
    disconnectRate?: number; // Probability (0-1) of disconnecting mid-stream
  }
}
```

Rates are evaluated per-request. When triggered, the chaos failure replaces the normal response.

## Common Patterns

### Basic text fixture

```typescript
mock.onMessage("hello", { content: "Hi there!" });
```

### Tool call → tool result → final response (3-step agent loop)

The most common pattern. Fixture 1 triggers the tool call, fixture 2 handles the tool result.

```typescript
// Step 1: User asks about weather → LLM calls tool
mock.onMessage("weather", {
  toolCalls: [{ name: "get_weather", arguments: { city: "SF" } }],
});

// Step 2: Tool result comes back → LLM responds with text
mock.addFixture({
  match: { predicate: (req) => req.messages.at(-1)?.role === "tool" },
  response: { content: "It's 72°F in San Francisco." },
});
```

**Why predicate, not userMessage?** After a tool call, the client replays the same conversation with the tool result appended. The user message hasn't changed — `userMessage: "weather"` would match the SAME fixture again, creating an infinite loop.

### Embedding fixture

```typescript
// Match specific input text
mock.onEmbedding("search query", {
  embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
});

// Match with regex
mock.onEmbedding(/product.*description/, {
  embedding: [0.9, -0.1, 0.5, 0.3, 0.2],
});
```

### Structured output / JSON mode

```typescript
// onJsonOutput auto-sets responseFormat: "json_object" and stringifies objects
mock.onJsonOutput("extract entities", {
  entities: [
    { name: "Acme Corp", type: "company" },
    { name: "Jane Doe", type: "person" },
  ],
});

// Equivalent manual form:
mock.addFixture({
  match: { userMessage: "extract entities", responseFormat: "json_object" },
  response: { content: '{"entities":[...]}' },
});
```

### Sequential responses (same match, different responses)

```typescript
// First call returns tool call, second returns text
mock.on(
  { userMessage: "status", sequenceIndex: 0 },
  { toolCalls: [{ name: "check_status", arguments: {} }] },
);
mock.on({ userMessage: "status", sequenceIndex: 1 }, { content: "All systems operational." });
```

Match counts are tracked per fixture group. Use `resetMatchCounts()` between tests to reset counts while keeping loaded fixtures. `reset()` also clears the fixture pool, so avoid it between tests that share a loaded fixture set.

### Streaming physics (realistic timing)

```typescript
mock.onMessage(
  "tell me a story",
  { content: "Once upon a time..." },
  {
    streamingProfile: {
      ttft: 200, // 200ms before first token
      tps: 30, // 30 tokens per second after that
      jitter: 0.1, // ±10% random variance
    },
  },
);
```

### Predicate-based routing (same user message, different context)

Common in supervisor/orchestrator patterns where the system prompt changes:

```typescript
mock.addFixture({
  match: {
    predicate: (req) => {
      const sys = req.messages.find((m) => m.role === "system")?.content ?? "";
      return typeof sys === "string" && sys.includes("Flights found: false");
    },
  },
  response: { toolCalls: [{ name: "search_flights", arguments: {} }] },
});
```

### Catch-all (always add one)

Prevents unmatched requests from returning 404 and crashing the test:

```typescript
mock.addFixture({
  match: { predicate: () => true },
  response: { content: "I understand. How can I help?" },
});
```

### Tool result catch-all with prependFixture

Must go at the front so it matches before substring-based fixtures:

```typescript
mock.prependFixture({
  match: { predicate: (req) => req.messages.at(-1)?.role === "tool" },
  response: { content: "Done!" },
});
```

### Stream interruption simulation (v1.3.0+)

```typescript
mock.onMessage(
  "long response",
  { content: "This will be cut short..." },
  {
    truncateAfterChunks: 3, // Stop after 3 SSE chunks
    disconnectAfterMs: 500, // Or disconnect after 500ms
  },
);
```

### Chaos testing (probabilistic failures)

```typescript
mock.addFixture({
  match: { userMessage: "flaky" },
  response: { content: "Sometimes works!" },
  chaos: { dropRate: 0.3 },
});
```

30% of requests matching this fixture will get a 500 error instead of the response. Can also use `malformedRate` (garbled JSON) or `disconnectRate` (connection dropped mid-stream).

Server-level chaos applies to ALL requests:

```typescript
mock.setChaos({ dropRate: 0.1 }); // 10% of all requests fail
mock.clearChaos(); // Remove server-level chaos
```

### Error injection (one-shot)

```typescript
mock.nextRequestError(429, { message: "Rate limited", type: "rate_limit_error" });
// Next request gets 429, then fixture auto-removes itself
```

### JSON fixture files

```json
{
  "fixtures": [
    {
      "match": { "userMessage": "hello" },
      "response": { "content": "Hi!" }
    },
    {
      "match": { "userMessage": "weather" },
      "response": {
        "toolCalls": [
          {
            "name": "get_weather",
            "arguments": { "city": "SF", "units": "fahrenheit" }
          }
        ]
      }
    },
    {
      "match": { "inputText": "search query" },
      "response": { "embedding": [0.1, 0.2, 0.3] }
    },
    {
      "match": { "userMessage": "status", "sequenceIndex": 0 },
      "response": { "content": "First response" }
    }
  ]
}
```

**JSON auto-stringify**: In JSON fixture files, `arguments` and `content` can be objects — the loader auto-stringifies them with `JSON.stringify()`. This also applies to a `blocks` entry's `arguments` — object form auto-stringifies just like top-level `toolCalls`. The escaped-string form (`"{\"city\":\"SF\"}"`) still works but objects are preferred for readability.

JSON files cannot use `RegExp` or `predicate` — those are code-only features. `streamingProfile` is supported in JSON fixture files.

Load with `mock.loadFixtureFile("./fixtures/greetings.json")` or `mock.loadFixtureDir("./fixtures/")`.

## API Endpoints

All providers share the same fixture pool — write fixtures once, they work for any endpoint.

| Endpoint                                                                                 | Provider      | Protocol  |
| ---------------------------------------------------------------------------------------- | ------------- | --------- |
| `POST /v1/chat/completions`                                                              | OpenAI        | HTTP      |
| `POST /v1/responses`                                                                     | OpenAI        | HTTP + WS |
| `POST /v1/messages`                                                                      | Anthropic     | HTTP      |
| `POST /v1/embeddings`                                                                    | OpenAI        | HTTP      |
| `POST /v1beta/models/{model}:{method}`                                                   | Google Gemini | HTTP      |
| `POST /model/{modelId}/invoke`                                                           | AWS Bedrock   | HTTP      |
| `POST /openai/deployments/{id}/chat/completions`                                         | Azure OpenAI  | HTTP      |
| `POST /openai/deployments/{id}/embeddings`                                               | Azure OpenAI  | HTTP      |
| `GET /health`                                                                            | —             | HTTP      |
| `GET /ready`                                                                             | —             | HTTP      |
| `POST /model/{modelId}/invoke-with-response-stream`                                      | AWS Bedrock   | HTTP      |
| `POST /model/{modelId}/converse`                                                         | AWS Bedrock   | HTTP      |
| `POST /model/{modelId}/converse-stream`                                                  | AWS Bedrock   | HTTP      |
| `POST /v1/projects/{p}/locations/{l}/publishers/google/models/{m}:generateContent`       | Vertex AI     | HTTP      |
| `POST /v1/projects/{p}/locations/{l}/publishers/google/models/{m}:streamGenerateContent` | Vertex AI     | HTTP      |
| `POST /api/chat`                                                                         | Ollama        | HTTP      |
| `POST /api/generate`                                                                     | Ollama        | HTTP      |
| `GET /api/tags`                                                                          | Ollama        | HTTP      |
| `POST /v2/chat`                                                                          | Cohere        | HTTP      |
| `POST /api/v1/chat/completions`                                                          | OpenRouter    | HTTP      |
| `GET /api/v1/models` · `/api/v1/key` · `/api/v1/credits`                                 | OpenRouter    | HTTP      |
| `GET /metrics`                                                                           | —             | HTTP      |
| `GET /v1/models`                                                                         | OpenAI-compat | HTTP      |
| `WS /v1/responses`                                                                       | OpenAI        | WebSocket |
| `WS /v1/realtime`                                                                        | OpenAI        | WebSocket |
| `WS /ws/google.ai...BidiGenerateContent`                                                 | Gemini Live   | WebSocket |
| `POST /v1/images/generations`                                                            | OpenAI        | HTTP      |
| `POST /v1beta/models/{model}:predict`                                                    | Gemini Imagen | HTTP      |
| `POST /v1/audio/speech`                                                                  | OpenAI        | HTTP      |
| `POST /v1/audio/transcriptions`                                                          | OpenAI        | HTTP      |
| `POST /v1/videos`                                                                        | OpenAI        | HTTP      |
| `GET /v1/videos/{id}`                                                                    | OpenAI        | HTTP      |

## Response Template Overrides

Fixture responses can include optional override fields to control auto-generated envelope values. These are merged into the provider-specific response format (OpenAI, Claude, Gemini, Responses API).

| Field                | Type   | Default                   | Description                                                                                                                                                                                                                                                                |
| -------------------- | ------ | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                 | string | auto-generated            | Override response ID (e.g., `chatcmpl-custom`)                                                                                                                                                                                                                             |
| `created`            | number | `Date.now()/1000`         | Override Unix timestamp                                                                                                                                                                                                                                                    |
| `model`              | string | echoes request            | Override model name in response                                                                                                                                                                                                                                            |
| `usage`              | object | zeroed                    | Override token counts: `{ prompt_tokens, completion_tokens, total_tokens }`. OpenAI Chat includes usage in response body; Responses API uses `response.usage`. When omitted, auto-computed from content length                                                             |
| `finishReason`       | string | `"stop"` / `"tool_calls"` | Override finish reason. Mappings: `stop` -> `end_turn` (Claude), `STOP` (Gemini); `tool_calls` -> `tool_use` (Claude), `FUNCTION_CALL` (Gemini); `length` -> `max_tokens` (Claude), `MAX_TOKENS` (Gemini); `content_filter` -> `SAFETY` (Gemini), `failed` (Responses API) |
| `role`               | string | `"assistant"`             | Override message role                                                                                                                                                                                                                                                      |
| `systemFingerprint`  | string | (omitted)                 | Add `system_fingerprint` to response                                                                                                                                                                                                                                       |
| `provider`           | string | slug author               | OpenRouter only: top-level serving-provider display name (default = the winning model slug's author). Override to assert who served the request                                                                                                                            |
| `nativeFinishReason` | string | mirrors `finishReason`    | OpenRouter only: the raw upstream `native_finish_reason` alongside the normalized `finish_reason`                                                                                                                                                                          |
| `usage.cost`         | number | (omitted)                 | OpenRouter only: per-request `usage.cost` (scriptable — powers budget-guard tests). When set, `usage.cost_details` is emitted too. Never fabricated when omitted                                                                                                           |
| `usage.is_byok`      | bool   | (omitted)                 | OpenRouter only: emit `usage.is_byok`. Also `usage.prompt_tokens_details`, `usage.completion_tokens_details` — emitted only when set                                                                                                                                       |

### Example

```typescript
mock.onMessage("hello", {
  content: "Hi!",
  model: "gpt-4-turbo-2024-04-09",
  usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
  systemFingerprint: "fp_abc123",
});
```

### In JSON fixtures

```json
{
  "match": { "userMessage": "hello" },
  "response": {
    "content": "Hi!",
    "model": "gpt-4-turbo-2024-04-09",
    "usage": { "prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15 },
    "systemFingerprint": "fp_abc123"
  }
}
```

These fields map correctly across all provider formats — for example, `finishReason: "stop"` becomes `finish_reason: "stop"` in OpenAI, `stop_reason: "end_turn"` in Claude, and `finishReason: "STOP"` in Gemini.

## OpenRouter (chat / router)

A request whose path starts with `/api/v1/` (point the OpenAI SDK at a `baseURL` ending `/api/v1`) is shaped as OpenRouter: `gen-` id, top-level `provider`, per-choice `native_finish_reason`, always-present `system_fingerprint`/`service_tier` (null by default), an always-present `message.reasoning` (null unless a fixture supplies reasoning and the model is reasoning-capable), and a rich `usage`. Requests on the plain `/v1/...` base are untouched OpenAI. Same fixture pool — the fields above are the only additions.

- **Scriptable cost / provider / finish reason**: set `provider`, `nativeFinishReason`, and `usage.cost` on the response (see the overrides table). `cost`/`cost_details` are emitted only when a fixture supplies `cost` — aimock never fabricates a cost.
- **`models[]` fallback (router failover)**: when the request body carries `models: [m1, m2, ...]`, aimock walks `[model, ...models]` in order and serves the first fixture that returns a NON-error response. A `429`/`503` error fixture on a candidate simulates a RUNTIME provider failure and falls through to the next candidate; the winning slug is echoed back as the top-level `model` (assert failover via `response.model`). Model the primary's "failure" as a 429/503 — an unknown/invalid model is just a fixture miss (aimock does not replicate OpenRouter's up-front invalid-model 400).
- **Terminal (non-failover) error class — `fallthrough: false`**: real OpenRouter fails over inconsistently by error class (a `403 budget-exceeded` / generic "provider returned error" is served as terminal and does NOT advance to the next candidate, while 429/503 usually do). Set `fallthrough: false` on an error fixture to make it terminal: the fallback loop stops and serves that error even when a good candidate follows. Absent / `true` keeps the default fall-through. Composes with `provider.allow_fallbacks`: fall-through happens only when both allow it (if either says stop, the error is terminal). Use it to reproduce the exact provider error a dev's app must handle itself. `{ error: { message: "budget exceeded" }, status: 403, fallthrough: false }`
- **Keepalive**: set the fixture option `openRouterProcessing: true` to emit one `: OPENROUTER PROCESSING` SSE comment before the first data frame (opt-in, default off).
- **Error envelope**: OpenRouter errors are `{ error: { message, code } }` (numeric `code` == HTTP status), with optional free-form `metadata`.

```typescript
// primary is a runtime 429, fallback answers
mock.on(
  { model: "openai/gpt-4o", userMessage: "route" },
  { error: { message: "rate limited" }, status: 429 },
);
mock.on(
  { model: "anthropic/claude-3.5-sonnet", userMessage: "route" },
  {
    content: "served by the fallback",
    provider: "Anthropic",
    usage: { cost: 0.0021 },
  },
);
// POST /api/v1/chat/completions { model: "openai/gpt-4o", models: [...], ... }
//   → response.model === "anthropic/claude-3.5-sonnet"
```

## Provider Support Matrix

| Feature              | OpenAI Chat | OpenAI Responses | Claude | Gemini | Gemini Int. | Bedrock | Azure | Ollama | Cohere | OpenRouter |
| -------------------- | ----------- | ---------------- | ------ | ------ | ----------- | ------- | ----- | ------ | ------ | ---------- |
| 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         | Binary  | SSE   | NDJSON | SSE    | SSE        |
| Reasoning            | Yes         | Yes              | Yes    | Yes    | --          | Yes     | Yes   | --     | --     | Yes        |
| Web Searches         | --          | Yes              | --     | --     | --          | --      | --    | --     | --     | --         |
| Response Overrides   | Yes         | Yes              | Yes    | Yes    | Yes         | --      | Yes   | --     | --     | Yes        |

## Critical Gotchas

1. **Order matters** — first match wins. Specific fixtures before general ones. Use `prependFixture()` to force priority.

2. **`arguments` accepts both objects and strings** — `"arguments": {"key":"value"}` (preferred, auto-stringified) or `"arguments": "{\"key\":\"value\"}"` (legacy). The same applies to `content` fields that contain JSON. The fixture loader detects `typeof === "object"` and calls `JSON.stringify()` automatically.

3. **Latency is per-chunk, not total** — `latency: 100` means 100ms between each SSE chunk, not 100ms total response time. Similarly, `truncateAfterChunks` and `disconnectAfterMs` are for simulating stream interruptions (added in v1.3.0).

4. **`streamingProfile` takes precedence over `latency`** — when both are set on a fixture, `streamingProfile` controls timing. Use one or the other.

5. **Tool result messages don't change the user message** — after a tool call, the client sends the same conversation + tool result. Matching on `userMessage` will hit the SAME fixture again → infinite loop. Always use `predicate` checking `role === "tool"` for tool results. Note: a whole-conversation `role === "tool"` check (e.g. `req.messages.some((m) => m.role === "tool")`) diverges from `hasToolResult`'s current-turn scoping in multi-turn flows — the built-in `hasToolResult` matcher only looks after the last user message, so a later turn whose history carries an earlier tool result still reads `false`.

6. **`clearFixtures()` preserves the array reference** — uses `.length = 0`, not reassignment. The running server reads the same array object.

7. **Journal records everything** — including 404 "no match" responses. Use `mock.getLastRequest()` to debug mismatches.

8. **All providers share fixtures** — a fixture matching "hello" works whether the request comes via `/v1/chat/completions` (OpenAI), `/v1/messages` (Anthropic), Gemini, Bedrock, or Azure endpoints.

9. **WebSocket uses the same fixture pool** — no special setup needed for WebSocket-based APIs (OpenAI Responses WS, Realtime, Gemini Live).

10. **Embeddings auto-generate if no fixture matches** — deterministic vectors are generated from the input text hash. You don't need a catch-all for embedding requests.

11. **Sequential response counts are tracked per fixture** — use `resetMatchCounts()` between tests to reset counts while keeping loaded fixtures; `reset()` also clears the fixture pool, so don't use it between tests that share a loaded fixture set. The count increments after each match of that fixture group (all fixtures sharing the same non-`sequenceIndex` match fields).

12. **Bedrock uses Anthropic Messages format internally** — the adapter normalizes Bedrock requests to `ChatCompletionRequest`, so the same fixtures work. Bedrock supports both non-streaming (`/invoke`, `/converse`) and streaming (`/invoke-with-response-stream`, `/converse-stream`) endpoints.

13. **Azure OpenAI routes through the same handlers** — `/openai/deployments/{id}/chat/completions` maps to the completions handler, `/openai/deployments/{id}/embeddings` maps to the embeddings handler. Fixtures work unchanged.

14. **Ollama defaults to streaming** — opposite of OpenAI. Set `stream: false` explicitly in the request for non-streaming responses.

15. **Ollama tool call `arguments` is an object, not a JSON string** — unlike OpenAI where `arguments` is a JSON string, Ollama sends and expects a plain object.

16. **Bedrock streaming uses binary Event Stream format** — not SSE. The `invoke-with-response-stream` and `conver

…(truncated)
