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/report 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.

Pick the narrower route — 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).

Full reset shell
$ curl -X POST http://localhost:4010/__aimock/reset
Response json
{ "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.

Journal-only reset shell
$ curl -X POST http://localhost:4010/__aimock/reset/journal
Response json
{ "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.

Deprecated reset shell
$ curl -i -X POST http://localhost:4010/__aimock/reset/fixtures
Response json
// 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.

Health check shell
$ curl http://localhost:4010/__aimock/health
Response json
{ "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.

Read the journal shell
$ 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.

Filter and paginate shell
$ curl -i "http://localhost:4010/__aimock/journal?path=chat&status=200&limit=10"
// X-Total-Count: 42   (response header)
Response json
[
  /* 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.

Count fixtures shell
$ curl http://localhost:4010/__aimock/fixtures
Response json
{ "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, …).

Dump fixtures shell
$ curl "http://localhost:4010/__aimock/fixtures?include=fixtures"
Response json
{
  "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>.

Add MCP fakes shell
curl -X POST http://localhost:4010/__aimock/fixtures \
  -H "Content-Type: application/json" \
  -d @account/read-only.json
Response json
{
  "added": 0,
  "mcpFakesAdded": 1
}
Add fixtures shell
$ curl -X POST http://localhost:4010/__aimock/fixtures \
  -H "Content-Type: application/json" \
  -d '{ "fixtures": [{ "match": { ... }, "response": { ... } }] }'
Response json
{ "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.

Clear fixtures shell
$ curl -X DELETE http://localhost:4010/__aimock/fixtures
Response json
{ "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):

List MCP fakes shell
curl "http://localhost:4010/__aimock/mcp/fakes?testId=account%20%E2%80%BA%20read%20only"
Response json
{
  "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:

Read the fakes report shell
curl "http://localhost:4010/__aimock/mcp/fakes/report?testId=tickets%20%E2%80%BA%20retry%20on%20timeout"
Response json
{
  "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.

Queue an error shell
$ curl -X POST http://localhost:4010/__aimock/error \
  -H "Content-Type: application/json" \
  -d '{ "status": 429, "body": { "message": "rate limited", "type": "rate_limit_error" } }'
Response json
{ "queued": true }

The payload is validated, and an invalid one is rejected with 400 and { "error": "…" } without queueing anything:

Chaos

Send your 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).

Read chaos shell
$ curl http://localhost:4010/__aimock/chaos
Response json
{ "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.

Set chaos shell
$ curl -X POST http://localhost:4010/__aimock/chaos \
  -H "Content-Type: application/json" \
  -H "X-Test-Id: t1" \
  -d '{ "dropRate": 0.25 }'
Response json
{ "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.

Clear chaos shell
$ curl -X DELETE http://localhost:4010/__aimock/chaos \
  -H "X-Test-Id: t1"
Response json
{ "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.

Read a scope shell
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.

Fail the first tool response shell
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:

Validation error json
{
  "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.