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
{
"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
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
-
WebSocket Responses (
/v1/responsesover WebSocket) answers a raw fixture with an error event with the codeaimock_raw_mode_unavailable: "raw fixtures are not yet supported on the WebSocket transport". Serving raw over WebSocket is a follow-up. - Realtime, Gemini Live and non-LLM endpoints (embeddings, images, audio, video) never match raw fixtures. For a transport aimock does not cover, use mount().