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 |
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 and the chaos warning latch |
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 |
Reset Routes
aimock keeps several kinds of in-memory state between requests: the loaded
fixtures, 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).
$ 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 as the server resolves it: 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 */
]
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.
$ 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. 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 }
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": {} }