Raw Passthrough Fixtures

Send provider events exactly as you write them. A raw fixture lists the events (streaming) or the JSON body (non-streaming) of one provider wire, and aimock adds only that wire's framing. Use it to test an SDK against a new event type, field or error shape before aimock models it.

Prefer typed fixtures. content, toolCalls, blocks and the other typed responses work on every provider, get usage, reasoning and override handling, and are checked for drift against the real APIs. Raw fixtures are wire-specific and are not checked for drift: when the provider changes, your raw events do not. Use raw only for what typed fixtures cannot express yet.

Shape

raw.json json
{
  "fixtures": [
    {
      "match": { "userMessage": "cite paris" },
      "response": {
        "raw": {
          "wire": "openai-responses",
          "events": [
            { "type": "response.created", "response": { "id": "resp_1", "status": "in_progress", "output": [] } },
            { "type": "response.output_text.delta", "delta": "Paris.", "output_index": 0, "content_index": 0 },
            { "type": "response.output_text.annotation.added", "annotation": { "type": "url_citation", "url": "https://example.com" } },
            { "type": "response.completed", "response": { "id": "resp_1", "status": "completed", "output": [] } }
          ],
          "json": { "id": "resp_1", "object": "response", "status": "completed", "output": [] },
          "headers": { "x-request-id": "req_123" }
        }
      }
    }
  ]
}
Field Type Description
wire string, required The wire the events belong to (see the framing table). A raw fixture matches only requests on that wire; every other handler skips it and keeps looking. A factory is the exception: its result is checked when it runs, and a raw result for another wire gets a 500 (see below).
events object[] Streamed body. Each entry is sent as one frame, framed per wire.
json any Non-streaming body, sent as JSON.stringify(json).
status integer 200-599 HTTP status for both streaming and non-streaming replies. Default 200; 204 and 304 are rejected (they cannot carry a body). A reply with status 400 or above is never delayed. A 4xx/5xx with json sends that JSON error body as written, even to a streaming request.
headers object of strings Extra response headers, for example x-request-id or anthropic-ratelimit-*. Unless you set Content-Type here, a stream gets the same default headers as a typed stream on its wire (text/event-stream, application/x-ndjson on Ollama, application/vnd.amazon.eventstream on Bedrock) and a non-streaming reply gets application/json on every wire. A header you set replaces the default of the same name in any letter case, so it is sent once. content-length, transfer-encoding, connection and content-encoding are rejected.
done boolean openai-chat only: send the terminal data: [DONE]. Default true. It must be a boolean; any other wire rejects it.

raw is recognized only when the response has no other type's key. A fixture such as { "content": "hi", "raw": { … } } stays a text fixture, as before. The exceptions are customToolCalls and responsesBlocks: a fixture that writes either next to raw is rejected. A key inside raw that is not one of the fields above is ignored with a warning.

Framing per wire

wire Endpoints Each event is sent as
openai-responses POST /v1/responses event: <type>\ndata: <json>\n\n (no terminal)
openai-chat POST /v1/chat/completions (also Azure and OpenRouter routes) data: <json>\n\n, then data: [DONE]\n\n
anthropic POST /v1/messages event: <type>\ndata: <json>\n\n
gemini generateContent, streamGenerateContent (also Vertex AI) data: <json>\n\n
gemini-interactions POST /v1beta/interactions data: <json>\n\n
cohere POST /v2/chat event: <type>\ndata: <json>\n\n (no terminal)
ollama POST /api/chat <json>\n (NDJSON)
ollama-generate POST /api/generate <json>\n (NDJSON)
bedrock-invoke /invoke, /invoke-with-response-stream An AWS event-stream chunk frame with base64 bytes
bedrock-converse /converse, /converse-stream An AWS event-stream frame: the event's single key is the event type, its value the payload ({ "contentBlockDelta": { … } })

On openai-responses, anthropic and cohere every event needs a string type; it names the SSE event. Each raw event is one frame on every wire, including Gemini Interactions (where typed fixtures split text by chunkSize).

Streaming and non-streaming requests

A streaming request gets events; a non-streaming request gets json. The exception is an error reply: with a status of 400 or above and a json, a streaming request also gets the JSON, as provider APIs answer errors. If the one the request needs is missing, aimock answers with that wire's native 500 error and the code aimock_raw_mode_unavailable. An invalid raw fixture (for example a factory that returns status: 99) gets the same 500 with the code aimock_raw_fixture_invalid, and so does a factory that returns a raw fixture for another wire. A raw fixture added as an object for another wire is never served there: the router skips it, and when no other fixture matches, the server log warns that a raw fixture matched everything except its wire. The error body uses each provider's own error shape (on Gemini, Ollama and OpenRouter the code is prefixed to the message).

In an OpenRouter models fallback chain, an openai-chat raw fixture with a status of 400 or above fails over to the next model, as a typed error fixture with the same status does.

Timing

latency, streamingProfile, recordedTimings and replaySpeed pace streamed raw events exactly as they pace typed frames (see Streaming Physics).

A non-streaming raw reply follows nonStreamingTiming like a typed fixture: nonStreamingTiming: true delays a raw stream: false reply on OpenAI Chat Completions only, and nonStreamingTimingAllProviders: true delays it on every raw wire (including OpenAI Chat Completions). Either flag needs a streamingProfile or recordedTimings; a bare latency never delays a non-streaming reply. The wait is sized from the served JSON, as a typed reply's is sized from its content. A raw reply with status 400 or above is never delayed, streaming or not.

Which features apply

Feature Raw fixtures
Matching (all match keys, sequenceIndex, turnIndex, predicate, factories) Yes. Plus the wire filter above.
truncateAfterChunks, disconnectAfterMs, chaos Yes. Every raw event counts as one chunk.
Streaming timing Yes (see Timing).
chunkSize No. Each event is one frame. Setting it warns at load.
Model misbehavior No: faults skip raw fixtures. A server-level fault is skipped, the raw reply is served, and the journal records the fault as skipped. An X-AIMock-Misbehavior header returns 501 aimock_misbehavior_not_applicable. A raw fixture that sets its own misbehavior is an invalid fixture: load validation reports it (with --misbehavior, its file fails to load), and when served it gets the wire's 500 with aimock_raw_fixture_invalid.
Overrides, reasoning gating, usage synthesis, drift checks No. The events are sent as written.
Record mode A matched raw fixture never proxies. Recording raw fixtures is a follow-up.

Validation

--validate-on-load, addFixturesFromJSON and POST /__aimock/fixtures check raw fixtures the same way: a known wire, events or json present, events that are plain JSON objects (no sparse-array holes, undefined, non-finite numbers, functions, Dates or cycles), the per-wire type / single-key rules (no CR or LF in a type; a bedrock-converse key of at most 65535 bytes), status 200-599 except 204 and 304, valid header names and values, a boolean done only on openai-chat, no customToolCalls or responsesBlocks next to raw, and no fixture misbehavior. Unknown keys inside raw and an empty events are warnings. on(), addFixture() and factories are not validated when added; an invalid raw fixture registered that way returns the wire's 500 at request time. The same check runs again on every raw fixture when it is served.

Programmatic use

raw.test.ts ts
const mock = new LLMock();
mock.on({ userMessage: "weather" }, {
  raw: {
    wire: "anthropic",
    events: [
      { type: "message_start", message: { /* … */ } },
      { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "Sunny." } },
      { type: "message_stop" },
    ],
  },
});

Limits