Control API
aimock exposes a small HTTP control surface under the /__aimock/* prefix for
inspecting recorded traffic and resetting server state between test runs — no
restart required.
All control routes are exact-match and live alongside your mocked LLM endpoints on the
same port. They are intended for use from test harnesses (directly via
fetch / curl, or through the
aimock-pytest client).
Route Overview
| Method | Path | Description |
|---|---|---|
GET |
/__aimock/health |
Liveness probe |
GET |
/__aimock/journal |
Read-only snapshot of recorded requests, optionally filtered and paginated |
GET |
/__aimock/fixtures |
How many fixtures are loaded, and optionally what they match on |
POST |
/__aimock/fixtures |
Add fixtures at runtime |
DELETE |
/__aimock/fixtures |
Clear all fixtures and MCP fakes |
GET |
/__aimock/mcp/fakes |
The MCP fakes that apply to a test id and context, with each entry’s state |
GET |
/__aimock/mcp/fakes/
|
The per-test MCP fake report: served, unused and failed fakes for a test id |
POST |
/__aimock/reset |
Full reset: fixtures, journal entries, fixture match-counts (sequence position), video + fal.ai job state, the Files store, the Gemini interaction / event-id counters, every runtime chaos override, the chaos warning latch and all MCP fakes |
POST |
/__aimock/reset/journal |
Clear only the request journal entries |
POST |
/__aimock/reset/fixtures |
Deprecated. Alias for /reset |
POST |
/__aimock/error |
Queue a one-shot error injection |
GET |
/__aimock/chaos |
Read the chaos config in effect for your test |
POST |
/__aimock/chaos |
Replace the chaos config at runtime, scoped to your test |
DELETE |
/__aimock/chaos |
Drop the chaos override |
GET |
/__aimock/misbehavior |
Read semantic fault configuration for your test |
POST |
/__aimock/misbehavior |
Replace semantic fault configuration for your test |
DELETE |
/__aimock/misbehavior |
Remove your semantic fault override |
GET |
/__aimock/misbehavior/catalog |
Read the fault catalog and wire support |
Reset Routes
aimock keeps several kinds of in-memory state between requests: the loaded
fixtures and MCP fakes, the per-provider
generation state (video and fal.ai jobs, plus the Gemini interaction and
event-id counters), the fixture match-counts (sequence position), and the
request journal (recorded requests). Two routes clear it:
POST /__aimock/reset clears all of it — including every runtime chaos
override, per-test and untagged alike, which reverts the server to the chaos config it was
started with, and the latch on [chaos] … rejected warnings for invalid
fixture or server chaos values, so a bad static value is reported again after the reset
— and POST /__aimock/reset/journal clears only the recorded requests
and leaves everything else intact, chaos included. A third,
POST /__aimock/reset/fixtures, is a deprecated alias for the full reset.
POST /__aimock/reset wipes the loaded fixtures along with everything else,
and what happens to the next request then depends on the mode. In replay mode it fails
with 404, and in strict mode with 503 — both carry
code: "no_fixture_match". But
in record mode, with a provider key configured, an unmatched request is proxied to the
real provider
— so a full reset mid-recording means live upstream calls and real spend, not an
error. If all you want is a clean read between test runs, use
POST /__aimock/reset/journal — it leaves your fixtures intact.
POST /__aimock/reset
Full reset. Returns the server to a pristine, fixture-free state. It
clears the in-memory fixtures, the journal entries and the per-test fixture
match-counts (so sequenced fixtures rewind to their first response), the video and fal.ai
job and queue state, the Gemini interaction and event-id counters, every runtime chaos
override set via POST /__aimock/chaos (every X-Test-Id scope and
the untagged baseline, reverting to the construction-time chaos config), and this server's
latch of once-per-value [chaos] rejection warnings so the next request
reports them again. It also clears the Files API store
(clearFileStore) and unloads every MCP fake (see
Reset effects on MCP fakes).
$ curl -X POST http://localhost:4010/__aimock/reset
{ "reset": true }
POST /__aimock/reset/journal
Journal only. Clears only the request journal entries. Your loaded fixtures, generation state, and fixture match-counts (sequence position) are preserved, so the next request still matches. This is the recommended call for a clean read between test runs.
$ curl -X POST http://localhost:4010/__aimock/reset/journal
{ "reset": true }
POST /__aimock/reset/fixtures (Deprecated)
Deprecated alias for /__aimock/reset. The name promises a
fixtures-only reset, but it performs the same full reset — journal, match-counts,
job state and counters all go with it. It additionally sets a
Deprecation: true response header and adds deprecated /
deprecation fields to the body. Use /reset for a full reset, or
/reset/journal for a journal-only one; to clear fixtures and nothing else,
use DELETE /__aimock/fixtures.
$ curl -i -X POST http://localhost:4010/__aimock/reset/fixtures
// Deprecation: true (response header)
{
"reset": true,
"deprecated": true,
"deprecation": "POST /__aimock/reset/fixtures is deprecated; use POST /__aimock/reset (full reset) or POST /__aimock/reset/journal (journal only)"
}
Inspection
GET /__aimock/health
A simple liveness probe. Returns 200 once the server is accepting requests.
$ curl http://localhost:4010/__aimock/health
{ "status": "ok" }
GET /__aimock/journal
Returns a read-only snapshot of the recorded request entries as a JSON array. The journal
records each incoming request so tests can assert on what was sent. Clearing it does not
affect fixtures — see /__aimock/reset/journal above.
$ curl http://localhost:4010/__aimock/journal
With no query parameters the full array is returned. These parameters filter and paginate
it; any other parameter is rejected with 400, so a typo can never quietly return the whole journal:
| Param | Matching | Notes |
|---|---|---|
path |
Substring | ?path=chat matches a chat completions path |
method |
Exact, case-insensitive | ?method=post |
status |
Exact integer | Response status code |
service |
Exact | Not a substring: ?service=sear matches nothing |
testId |
Exact |
Resolved per entry. An MCP entry stores the test id that its mount resolved: the
decoded X-Test-Id header, else the decoded ?testId= on the
MCP URL, else the id bound to the session at initialize. That stored id
is used. Other entries use the X-Test-Id header, else
?testId= in the recorded path
|
requestId |
Exact |
Matches the entry's recorded x-request-id: the caller's
X-Request-Id when it was well-formed, else the id aimock minted and
echoed in that response's X-Request-Id header
|
limit, offset |
Integers ≥ 0 | Applied last, after filtering |
Every response carries an X-Total-Count header with the number of entries
that matched the filters, before limit / offset were
applied — so a paging caller can tell when it is done. The body stays a bare array.
$ curl -i "http://localhost:4010/__aimock/journal?path=chat&status=200&limit=10"
// X-Total-Count: 42 (response header)
[
/* recorded request entries */
]
Legacy GET /v1/_requests
The legacy journal route accepts a positive safe integer for limit. Leading
zeros, surrounding whitespace, and an encoded plus sign remain accepted. Suffixes,
fractional values, exponent notation, and unsafe integers return HTTP 400. An absent or
empty parameter leaves the result unlimited. This rule does not change
/__aimock/journal, where limit=0 remains valid.
Fixtures
GET /__aimock/fixtures
Read-only: how many fixtures are currently registered. Useful for asserting a CI job or
test harness actually loaded its tape before it starts making requests, without having to
send a probe request and infer the answer from the reply. Returns the count only by
default.
Any query parameter other than include is rejected with
400, so a typo can never quietly return the count-only body.
$ curl http://localhost:4010/__aimock/fixtures
{ "count": 2 }
Add ?include=fixtures to also dump what each fixture matches on, so a harness
can assert which fixture would serve a request without sending one. Any other
include value is rejected with 400. Match criteria are
JSON-safe: regexps are stringified and predicate functions become
"[function]". Response bodies are never serialized — each fixture
reports only a one-word responseKind (text,
toolCalls, error, audio, image,
factory, …).
$ curl "http://localhost:4010/__aimock/fixtures?include=fixtures"
{
"count": 1,
"fixtures": [
{
"index": 0,
"match": { "userMessage": "hello" },
"responseKind": "text"
}
]
}
POST /__aimock/fixtures
Add fixtures at runtime without restarting the server. The body is an object with a
"fixtures" array of fixtures to register. Returns the number of fixtures
added.
The body may also carry an mcpFakes key, in the same form as in a fixture
file (see MCPMock: Scenario fakes). Then
"fixtures" is optional, and the response is
{ "added": <LLM fixtures>, "mcpFakesAdded": <blocks> }. A request
without mcpFakes gets { "added": n } as before. Each block goes
to the MCP mount at its mount path; when no mount serves that path, aimock
mounts an MCPMock there. The whole body is checked first. If any block is
invalid, or its path is held by a mount that is not an MCPMock, the response
is 400 {"error":"Validation failed","details":[...]} with one detail per
error, and nothing is added, LLM fixtures included.
Add the fakes of a fixture file (here
account/read-only.json, which has no LLM fixtures). Blocks added this way get the block id
control-api#<n>.
curl -X POST http://localhost:4010/__aimock/fixtures \
-H "Content-Type: application/json" \
-d @account/read-only.json
{
"added": 0,
"mcpFakesAdded": 1
}
$ curl -X POST http://localhost:4010/__aimock/fixtures \
-H "Content-Type: application/json" \
-d '{ "fixtures": [{ "match": { ... }, "response": { ... } }] }'
{ "added": 1 }
DELETE /__aimock/fixtures
Clears all registered fixtures and every MCP fake on every mount. Generation state and the
journal are left untouched. To clear everything at once, use
/__aimock/reset instead.
$ curl -X DELETE http://localhost:4010/__aimock/fixtures
{ "cleared": true }
GET /__aimock/mcp/fakes
Read-only: for each MCP mount, the mcpFakes blocks that apply to a test id
and context, with every entry’s id and whether it is used for that test id. To check
a test at its end, use the report below. Query parameters:
testId, context and mount (a mount path; default:
all MCP mounts), all optional. An empty value counts as not sent. Any other query
parameter is rejected with 400, and a mount with no MCP mock
gets 404.
The response is { "testId", "context", "mounts": [{ "mount", "blocks" }] }.
Each block has blockId, source, mount,
scope, undeclaredTools, evicted (true when the
test-id cap dropped this test id’s state) and tools. Each tool has a
name and entries, and each entry has id,
args or anyArgs, and consumed.
After the request above and one get_account call in the test
account › read only (the test id is percent-encoded in the URL):
curl "http://localhost:4010/__aimock/mcp/fakes?testId=account%20%E2%80%BA%20read%20only"
{
"testId": "account › read only",
"context": null,
"mounts": [
{
"mount": "/mcp",
"blocks": [
{
"blockId": "control-api#1",
"source": "control-api#1",
"mount": "/mcp",
"scope": {
"testId": "account › read only"
},
"undeclaredTools": "deny",
"evicted": false,
"tools": [
{
"name": "get_account",
"entries": [
{
"id": "control-api#1:get_account#0",
"args": {
"id": "u1"
},
"consumed": true
}
]
}
]
}
]
}
]
}
GET /__aimock/mcp/fakes/report
Read-only: the report for one test id and context,
across every MCP mount. Query parameters: testId and context,
both optional; an empty value counts as not sent. Any other query parameter is rejected
with 400. Otherwise the answer is 200 with
{ "testId", "context", "ok", "evicted", "served", "unconsumed", "unfaked", "failures",
"sharedUnconsumed" }. Reading it changes no state.
After one create_ticket call in the test
tickets › retry on timeout (the
tickets/retry.json sample), the first
entry is served and the second is unused, so ok is false:
curl "http://localhost:4010/__aimock/mcp/fakes/report?testId=tickets%20%E2%80%BA%20retry%20on%20timeout"
{
"testId": "tickets › retry on timeout",
"context": null,
"ok": false,
"evicted": false,
"served": [
{
"entryId": "tickets/retry.json:first-try",
"mount": "/mcp",
"tool": "create_ticket",
"args": {
"title": "Refund"
}
}
],
"unconsumed": [
{
"entryId": "tickets/retry.json:retry",
"mount": "/mcp",
"tool": "create_ticket",
"args": {
"title": "Refund"
}
}
],
"unfaked": [],
"failures": [],
"sharedUnconsumed": []
}
Reset effects on MCP fakes
POST /__aimock/reset, its deprecated alias
POST /__aimock/reset/fixtures, and
DELETE /__aimock/fixtures unload every MCP fake along with the LLM fixtures,
and clear the fakes report’s event log.
POST /__aimock/reset/journal
leaves fakes and their state as they are. None of them closes MCP sessions.
Error Injection
POST /__aimock/error
Queues a one-shot error injection. The next matching request returns the queued error instead of a normal response, after which the injection is consumed. See Error Injection for the full request shape and options.
$ curl -X POST http://localhost:4010/__aimock/error \
-H "Content-Type: application/json" \
-d '{ "status": 429, "body": { "message": "rate limited", "type": "rate_limit_error" } }'
{ "queued": true }
The payload is validated, and an invalid one is rejected with 400 and
{ "error": "…" } without queueing anything:
-
statusmust be an integer from 200 to 999. It may be omitted ornull(both mean 500). Outside that range the status could never reach the client — 1xx cannot terminate a response and hangs the request, and anything below 100, above 999 or fractional makes Node throw and degrade to a generic 500. A string such as"500"is rejected: send a number. -
body, when present, must be an object —nullmeans absent; itsmessage,typeandcodemust be strings.nullis accepted for any of them and means “absent”, so an error envelope captured from aimock or from OpenAI (both emit"code": null) can be pasted back verbatim.
Chaos
X-Test-Id —
chaos overrides are scoped to it, like fixture match-counts and video job state. An
override installed with X-Test-Id: t1 applies only to traffic tagged
t1, so one test turning chaos on cannot fail the tests running beside it on a
shared aimock process. A call with no tag sets the server-wide
baseline, which applies to every test that has no override of its own. Both sides —
the control call and the traffic it affects — resolve the tag the same way the rest
of the server does: the X-Test-Id header, else ?testId= in the
query string. An X-Test-Id that is present but empty is rejected
with 400 rather than silently treated as untagged. For chaos on a single
request, the X-AIMock-Chaos-Drop, X-AIMock-Chaos-Malformed,
X-AIMock-Chaos-Ratelimit, X-AIMock-Chaos-Disconnect and
X-AIMock-Chaos-Latency request headers still override everything, field by
field.
WebSocket connections are different. The WebSocket endpoints (the
Responses, Realtime and Gemini Live upgrades) read X-Test-Id,
X-AIMock-Strict and X-AIMock-Context from the upgrade request,
once, and hold that copy along with the server defaults for the life of the socket. The
WebSocket handlers do not evaluate chaos at all, so POST /__aimock/chaos and
the X-AIMock-Chaos-* headers affect HTTP requests only; a socket that is
already open keeps the strict and record settings it was opened with.
GET /__aimock/chaos
Reads the chaos config in effect for the caller's X-Test-Id: that test's
override if one is installed, otherwise the server-wide baseline — an untagged
override, or whatever the server was started with (--chaos-drop and friends).
$ curl http://localhost:4010/__aimock/chaos
{ "chaos": { "dropRate": 0.1 } }
POST /__aimock/chaos
Replaces the chaos config at runtime, no restart, for the caller's
X-Test-Id. The body is a JSON object with any subset of the five chaos
fields: dropRate, malformedRate, rateLimitRate and
disconnectRate, each a number in [0, 1], and
latencyMs, a whole number of milliseconds in [0, 30000]. Unknown
fields and out-of-range values are rejected with 400.
POST {} means “explicitly no chaos”.
The body replaces the config for that scope wholesale — it is not merged with the
server baseline.
A server started with --chaos-latency 500 that is sent
{ "dropRate": 1 } for X-Test-Id: t1 gives t1's
traffic a drop and no latency, because latencyMs was not restated.
Restate every field you want to keep. This is what makes
POST {} (“explicitly no chaos for my test”) different from
DELETE
(“fall back to what I was shadowing”) — under a merge,
POST {} would do nothing at all. The 200 body echoes the config now in effect
for the scope, and any field that was in effect and is not restated is named in a server
warning, so a dropped baseline rate is never silent.
$ curl -X POST http://localhost:4010/__aimock/chaos \
-H "Content-Type: application/json" \
-H "X-Test-Id: t1" \
-d '{ "dropRate": 0.25 }'
{ "chaos": { "dropRate": 0.25 } }
DELETE /__aimock/chaos
Drops the override for the caller's X-Test-Id (or the server-wide baseline
when untagged), falling back to whatever it was shadowing. Symmetric with
POST: an untagged DELETE touches the baseline only and leaves
every per-test override in place, so one test's cleanup cannot revoke another's.
POST /__aimock/reset drops every override at once, restoring the
config the server was started with.
$ curl -X DELETE http://localhost:4010/__aimock/chaos \
-H "X-Test-Id: t1"
{ "chaos": {} }
Model misbehavior
Misbehavior changes model output, such as tool arguments or stop reasons. Transport chaos
runs first. If chaos fires, no semantic fault fires for that request. Semantic fault
rendering currently supports the openai-chat wire, including Azure and
OpenRouter chat endpoints. Other wires remain unsupported.
Scope identity uses X-Test-Id, then ?testId=. A present but
blank header returns 400. A named scope uses its override, or the current
server baseline if no override exists. An untagged POST replaces the runtime baseline for
scopes without their own override. Selection order is request header, fixture, runtime
scope, then server baseline. Configurations replace one another; they do not merge.
GET /__aimock/misbehavior
Returns 200 with
{ "misbehavior": <effective configuration> }. With no configuration,
the value is { "faults": [] }. A configured
seed: "random" appears as its chosen numeric seed for replay.
curl 'http://localhost:4010/__aimock/misbehavior?testId=retry-tool'
POST /__aimock/misbehavior
Replaces the selected scope's entire configuration and returns the effective configuration
with 200. The body accepts a fault ID string or
{ "seed": 42, "faults": [...] }. Only this route accepts {} as
shorthand for { "faults": [] }. That empty list opts out at this scope; a
fixture or request header still takes precedence.
curl -X POST http://localhost:4010/__aimock/misbehavior \
-H 'Content-Type: application/json' -H 'X-Test-Id: retry-tool' \
-d '{"seed":42,"faults":[{"fault":"tool-args-invalid-json","tool":"weather","times":1}]}'
Send the same test ID on model requests. times: 1 spends its budget only when
the fault fires. Once exhausted, the fault is skipped and the normal fixture response can
pass through. A skipped fault does not automatically retry the application request.
Invalid configuration returns 400 and leaves the previous scope unchanged.
Unknown keys and invalid values fail validation before installation. For example,
{ "faults": [], "typo": true } returns:
{
"error": "Validation failed",
"rule": "misbehavior/unknown-key",
"message": "misbehavior/unknown-key at misbehavior.typo: invalid value true"
}
DELETE /__aimock/misbehavior
Removes only the selected override and returns 200 with the effective
configuration. A named DELETE reveals the current baseline. An untagged DELETE reveals the
construction or setter baseline. Other named scopes remain unchanged. Unlike POST with an
empty list, DELETE restores the fallback.
GET /__aimock/misbehavior/catalog
Returns 200 with { "catalog": {...}, "WIRE_SUPPORT": {...} }.
Read support by wire, fault and mode before selecting a fault. JSON serialization omits
the catalog's parameter validator functions; this response is not a JSON Schema.
Request headers and WebSocket scope
For one HTTP request, use
X-AIMock-Misbehavior: tool-args-invalid-json; style=trailing-comma; tool=weather. The grammar is one fault ID followed by semicolon-separated scalar
key=value pairs. Values are literal text; quotes are not removed. Duplicate
keys are invalid. Headers do not accept times, providers or a
configuration-level seed. Send one header value. Repeated values are parsed
together, not as independent faults. Invalid header input returns 400 with
code aimock_misbehavior_invalid.
Do not send this header on a WebSocket upgrade. Use the runtime POST route with the same
X-Test-Id or ?testId= as the socket instead. Runtime scope does
not enable unsupported wires: Responses, Realtime and Gemini Live semantic fault rendering
remains unsupported.