aimock CLI
Configure all your mocks in one JSON file and serve them on a single port — LLM providers, MCP, A2A, AG-UI, vector databases, and services.
Quick Start
$ npx @copilotkit/aimock --config aimock.json --port 4010
# The image's default ENTRYPOINT is the flag-driven llmock bin; override it
# to run the config-driven aimock CLI that ships in the same image:
$ docker run -d -p 4010:4010 \
-v ./aimock.json:/app/aimock.json:ro \
-v ./fixtures:/app/fixtures:ro \
--entrypoint node ghcr.io/copilotkit/aimock \
dist/aimock-cli.js --config /app/aimock.json --port 4010 --host 0.0.0.0
# If you only need flag-driven fixture serving, the default entrypoint works:
$ docker run -d -p 4010:4010 \
-v ./fixtures:/fixtures \
ghcr.io/copilotkit/aimock \
-f /fixtures -h 0.0.0.0
Note: The published Docker image (ghcr.io/copilotkit/aimock)
starts the flag-driven llmock CLI (ENTRYPOINT node dist/cli.js),
which does not accept --config or the convert and
validate subcommands. The aimock bin is in the image as
dist/aimock-cli.js: pass --entrypoint node and put
dist/aimock-cli.js first in the command to run config-driven mode. Paths in
the mounted config resolve against the container working directory /app.
Config File Format
The config file is a JSON object describing which services to run and how to configure
them. The llm section configures the core LLMock server. Additional services
are mounted at path prefixes.
{
"port": 4010,
"host": "127.0.0.1",
"auth": { "apiKeys": ["test-key"] },
"llm": {
"fixtures": "./fixtures",
"latency": 0,
"chunkSize": 20,
"replaySpeed": 1,
"logLevel": "info"
},
"mcp": {
"path": "/mcp",
"tools": [
{ "name": "echo", "description": "Echo the input", "result": "echoed" }
]
},
"a2a": {
"agents": [
{
"name": "helper",
"messages": [{ "pattern": "hello", "parts": [{ "text": "Hi!" }] }]
}
]
},
"agui": {
"fixtures": [{ "match": { "message": "hello" }, "text": "Hello from AG-UI" }]
},
"vector": {
"collections": [{ "name": "docs", "dimension": 3 }]
},
"services": { "search": true, "rerank": false, "moderate": true },
"metrics": true,
"strict": false
}
Config Fields
| Field | Type | Description |
|---|---|---|
port |
number |
Port to listen on. --port overrides it; when neither is given the
server takes an ephemeral port (0).
|
host |
string |
Bind address. --host overrides it; when neither is given the server
binds 127.0.0.1.
|
auth |
object |
Inbound API-key auth: { "apiKeys": ["..."] }, a non-empty array of
distinct non-blank strings. When set, every request except GET /health,
GET /ready and GET /metrics must carry one of the keys. If
the AIMOCK_API_KEYS environment variable is present (comma-separated),
it replaces this section entirely, and the file's auth is not parsed.
|
llm |
object |
LLMock configuration. Accepts fixtures, latency,
chunkSize, replaySpeed, logLevel,
chaos, record. Per-fixture knobs such as
streamingProfile live on the fixture entries.
|
mcp |
object |
Mounts an MCP mock. path (default /mcp),
serverInfo, and inline tools, resources,
prompts arrays. A tool's optional result string is
returned by every call to it.
|
a2a |
object |
Mounts an A2A mock. path (default /a2a) and an inline
agents array; each agent carries its card fields plus optional
messages, tasks, streamingTasks pattern
lists.
|
agui |
object |
Mounts an AG-UI mock. path (default /agui) and an inline
fixtures array of { match, text | events, delayMs }. A
fixture with neither text nor events is skipped with a
warning.
|
vector |
object |
Mounts a vector-database mock. path (default /vector) and
an inline collections array of
{ name, dimension, vectors?, queryResults? }.
|
services |
object |
Booleans search, rerank, moderate. Each
true installs a catch-all default response on the root-mounted service
endpoint (empty results for search and rerank, unflagged for moderation).
|
metrics |
boolean | Serve Prometheus metrics at GET /metrics (404 when off). |
strict |
boolean | Return 503 instead of 404 when no fixture matches. |
Recording Config (llm.record)
When llm.record is present, aimock proxies unmatched requests to real
providers and saves the responses as fixtures. See
Record & Replay for full details.
| Option | Type | Default | Description |
|---|---|---|---|
providers |
object | — |
Map of provider names to upstream base URLs (e.g.
{ "openai": "https://api.openai.com" }). Values are URLs, never API
keys.
|
providerKeys |
object | — |
Map of provider names to aimock's own upstream API keys, injected on a fixture-miss
passthrough when the caller sent no credential or a dummy placeholder; a real caller
key always wins. Under aimock --config this map is read from the JSON
file as written; the AIMOCK_PROVIDER_*_KEY environment variables are
read only by the flag-driven llmock bin, not by
aimock --config.
|
fixturePath |
string | ./fixtures/recorded |
Directory where recorded fixtures are saved |
proxyOnly |
boolean | false |
Proxy without saving fixtures to disk or caching in memory |
recordFullModelVersion |
boolean | false |
Record the exact model string without stripping date/version suffixes. Use when tests depend on exact model version matching. See Model-Aware Recording |
{
"llm": {
"fixtures": "./fixtures",
"record": {
"providers": {
"openai": "https://api.openai.com",
"anthropic": "https://api.anthropic.com"
},
"fixturePath": "./fixtures/recorded",
"recordFullModelVersion": false
}
}
}
CLI Flags
| Option | Default | Description |
|---|---|---|
-c, --config |
— (required) |
Path to JSON config file. There is no default: without it the run exits 1 with
Error: --config is required.
|
-p, --port |
config port, else 0 |
Port to listen on (overrides config); 0 takes an ephemeral port |
--host |
config host, else 127.0.0.1 |
Host to bind to (overrides config). Long form only on this bin, where
-h is help; the llmock bin keeps its own
-h, --host
|
-h, --help |
— | Show help |
convert <format> <input> [output] |
— | Subcommand: convert third-party mock configs to aimock format (see Fixture Converters below) |
validate [--strict] [--json] [--] <path> [more paths ...]
|
— | Subcommand: validate fixture files or directories offline (see Fixture Validation below) |
An option given an empty value is a usage error naming the option (Error: --port requires a non-empty value.), not a silently absent one.
Single-Port Routing
All services share one port. Requests are routed by path prefix. LLM endpoints live at the root, mounted services at their configured prefix:
| Path | Service |
|---|---|
/v1/chat/completions |
LLMock (OpenAI Chat Completions) |
/v1/messages |
LLMock (Anthropic Claude) |
/v1/embeddings |
LLMock (Embeddings) |
/mcp/* |
MCP mock service |
/a2a/* |
A2A mock service |
/agui/* |
AG-UI mock service |
/vector/* |
Vector-database mock service |
/health |
Unified health check (all services) |
/ready |
Readiness probe: 200 { "status": "ready" } |
/metrics |
Prometheus metrics (if enabled) |
Path stripping is automatic — a request to /mcp/tools/list arrives at
the MCP service as /tools/list.
Docker Usage
$ npx @copilotkit/aimock --config aimock.json --host 0.0.0.0
# The image's ENTRYPOINT is the flag-driven llmock bin (node dist/cli.js).
# dist/aimock-cli.js ships in the same image: override the entrypoint to use --config.
$ docker run -d -p 4010:4010 \
-v ./aimock.json:/app/aimock.json:ro \
-v ./fixtures:/app/fixtures:ro \
--entrypoint node ghcr.io/copilotkit/aimock \
dist/aimock-cli.js --config /app/aimock.json --port 4010 --host 0.0.0.0
Fixture Converters
Convert fixtures from other mock tools to aimock format.
Usage
npx @copilotkit/aimock convert <format> <input> [output]
Supported Formats
| Format | Source | Description |
|---|---|---|
vidaimock |
VidaiMock Tera templates |
Converts .tera / .json / .txt templates to
aimock fixture JSON
|
mockllm |
mock-llm YAML config | Converts mock-llm YAML configs to aimock fixture JSON. Also extracts MCP tools if present. |
Examples
# Convert a directory of VidaiMock templates
$ npx @copilotkit/aimock convert vidaimock ./templates/ ./fixtures/converted.json
# Convert a mock-llm YAML config
$ npx @copilotkit/aimock convert mockllm ./config.yaml ./fixtures/converted.json
# Print to stdout (omit output path)
$ npx @copilotkit/aimock convert vidaimock ./templates/
Fixture Validation
Lint fixture files offline, without starting a server. Each argument is a fixture file or
a directory of them (walked recursively for *.json, like
--fixtures <dir>).
Files are checked one at a time and then again as one combined set in load order, so the
rules that span files — a userMessage duplicated across two files, an
empty catch-all match that is last in its own file but not last overall
— fire here as they do on the server. Cross-file findings are prefixed
cross-file:, attributed to the file owning the fixture, and name the other
file. One that restates a finding the per-file pass already made against the same fixture
never prints a second line: naming the same fixtures it is dropped, and naming others
— a shadow that reaches into another load — it replaces the per-file wording
in place, at the same position.
Against the server's --validate-on-load this is stricter in one direction and
weaker in another. Stricter: there an unreadable, unparseable, or
wrong-shape file only warns on stderr and contributes zero fixtures, and startup continues
as long as something loaded, whereas here every such file fails the run, so a broken
fixture cannot slip into CI unnoticed. The one exception, which matches the loader: a
*.json file found by walking a directory and having no
top-level "fixtures" key at all is not a fixture file (an aimock config,
say), so it is passed over with a skipped (...) line instead of failing the
run — a file you name on the command line is still validated, and still fails on the
wrong shape. Two walk hazards the server does not survive are refused outright rather than
mirrored: a *.json path that is not a regular file (a FIFO, a socket, a
device) is a per-file error here, where the server's readFileSync blocks on
it forever; and a directory symlink pointing back at a directory already being walked is
reported once as a cycle, where the server recurses through it until the kernel returns
ELOOP and loads those levels' fixtures over and over.
Equal: the fixture rules themselves, and the zero-fixture rule — a
run whose inputs yield no fixtures at all is an error, matching the server's startup
abort, while a single {"fixtures": []} file alongside files that do load
fixtures is fine in both. Equal too in what the inputs expand to: a path named twice, and
a directory reached both directly and through an acyclic symlink to it, are validated once
per mention, exactly as the server loads one --fixtures source per mention,
and each fixture-and-rule pair is reported once — the count the server logs.
Weaker: this is a static lint that starts no server and reads local paths
only, so it says nothing about binding, remote
--fixtures <url> sources, --watch reloads, or any runtime
behaviour. An http:// or https:// path is refused by name
—
Remote fixture source: this lint reads local paths only (the server's --fixtures
accepts a URL; validate does not) — download the file and validate the local
copy
— rather than reported as a missing file. "The server" in that message is the
llmock bin (dist/cli.js, the Docker entrypoint): its
-f, --fixtures takes a filesystem path or an https:// /
http:// URL to a .json fixture file. The aimock bin
has no --fixtures flag at all; it loads fixtures through
--config, whose llm.fixtures is resolved as a filesystem path
only.
Anything the filesystem or a fixture entry can throw is contained in the report instead of
crashing the run: a malformed entry, an unreadable file or directory, a stat failure, or a
symlink cycle each becomes a per-file [error] line and exits 1. One stat
failure is deliberately silent: a directory entry that vanishes between the
readdir and the stat (ENOENT — a dangling
symlink, or a file deleted mid-walk) is skipped without a finding, exactly as the server's
loader skips it; every other stat failure is surfaced. A directory argument that walks
cleanly but holds no *.json files is likewise a per-path
[error] and exits 1. A crash inside the validator itself is contained too: a
per-file one becomes a file-level [error] naming no entry, and one in the
combined pass becomes a run-level [error] printed after the per-file output.
Errors go to stderr while OK and summary lines go to stdout; warnings follow
the exit they produce — stdout by default, stderr under --strict. Every
file gets exactly one stdout line — including a file that failed fatally, and a file
the walk skipped (<file>: skipped (...)) — so a reader tallying
stdout never loses a file. OK is withheld in one case: on a run that failed
because it loaded no fixtures at all, the file that loaded none reads
<file>: 0 fixture(s), loaded nothing — see the run-level error.
With --json, stdout carries the report alone; stderr carries one line,
Error: fixture validation failed — <first finding> [<counts>]., when the run failed on its inputs. A usage error (no paths, an unknown option) writes
Error: <reason>, a blank line, and the full subcommand help to stderr
— in both output modes, not a one-line reason. A path given twice is validated
twice, because the server loads one set of fixtures per --fixtures it is
given; the duplicate fixtures that produces are reported as the server reports them, one
finding per fixture and rule. Such a path is printed as
<path>[<n>], numbering its mentions from 1, so each load —
and every cross-file reference to its fixtures — names one of them:
$ npx @copilotkit/aimock validate dup.json dup.json
dup.json[1]: OK (2 fixture(s))
dup.json[2]: [warning] #0 cross-file: duplicate userMessage 'hi' — shadows fixture dup.json[1] #0
dup.json[2]: [warning] #1 cross-file: duplicate userMessage 'bye' — shadows fixture dup.json[1] #1
dup.json[2]: 2 fixture(s), 0 error(s), 2 warning(s)
MCP fakes
A fixture file may hold an
mcpFakes key.
validate checks its blocks by the rules the server uses at load: each bad
block is an error that names its rule (for example
[mcp-fakes/bad-block:a] block has no scope), and an args entry
after an anyArgs entry is a warning. Entry ids are also checked across all
files of the run. Mount conflicts are not checked, because they depend on the running
server. A file that holds only mcpFakes is a fixture file: a directory walk
checks it instead of skipping it, and a run whose files hold only fakes does not fail the
zero-fixture rule. The OK line of a file with fakes counts its blocks, for
example account/read-only.json: OK (0 fixture(s), 1 mcpFakes block(s)).
The --json Report
--json writes one document to stdout and nothing else — a usage error
included. stderr still gets the failure: one
Error: fixture validation failed — ... line for a run that failed on
its inputs, or Error: <reason> plus the full help for a usage error.
Validating a directory holding a file with invalid JSON and a file with one bad entry and
a duplicate userMessage (exit 1):
{
"strict": false,
"failed": true,
"files": [
{
"file": "fixtures/badjson.json",
"mention": 1,
"fixtures": 0,
"errors": [
{
"message": "Invalid JSON: Expected property name or '}' in JSON at position 1 (line 1 column 2)"
}
],
"warnings": [],
"fatal": "Invalid JSON: Expected property name or '}' in JSON at position 1 (line 1 column 2)"
},
{
"file": "fixtures/mixed.json",
"mention": 1,
"fixtures": 2,
"errors": [
{
"index": 0,
"message": "Invalid fixture entry #0: missing or non-object \"match\" and \"response\" — every entry needs { \"match\": { ... }, \"response\": { ... } }"
}
],
"warnings": [
{
"index": 2,
"message": "duplicate userMessage 'hi' — shadows fixture 1"
}
]
}
],
"run": {
"fixtures": 2,
"mcpFakeBlocks": 0,
"errors": []
}
}
A usage error carries the reason in run.errors (aimock validate --json, exit 1):
{
"strict": false,
"failed": true,
"files": [],
"run": {
"fixtures": 0,
"mcpFakeBlocks": 0,
"errors": [
"no fixture paths given."
]
}
}
A *.json file the walk passed over carries skipped and no
findings (aimock validate --json wk/, exit 0):
{
"strict": false,
"failed": false,
"files": [
{
"file": "wk/aimock.json",
"mention": 1,
"fixtures": 0,
"errors": [],
"warnings": [],
"skipped": "not a fixture file — an aimock config file (llm, services), which \"aimock --config\" serves and \"--fixtures\" does not load"
},
{
"file": "wk/dup.json",
"mention": 1,
"fixtures": 2,
"errors": [],
"warnings": []
}
],
"run": {
"fixtures": 2,
"mcpFakeBlocks": 0,
"errors": []
}
}
| Field | Meaning |
|---|---|
failed |
The boolean the exit code follows |
strict |
Echoes the --strict flag failed was computed under |
files[] |
One entry per path the run touched, in argv / walk order: file,
mention (which load of that path this is, 1-based, always present
— human output prints it as <path>[<n>] only when the
path repeats), fixtures (how many entries converted),
errors, warnings, skipped — present
only when the walk passed the file over — and fatal —
present only when the file produced no report at all. The fatal reason
also appears in errors, so a tally of files[].errors
matches the error count on the file's stdout line. A file with an
mcpFakes key also has mcpFakeBlocks, the number of blocks
it adds (0 when a block is bad)
|
files[].errors[], files[].warnings[] |
A finding always has message. index is the fixture entry
it belongs to and is absent on a file-level finding.
detail carries the raw thrown text when message is a
rephrasing of it
|
run |
The combined pass: fixtures is the total fixture count across every
file, mcpFakeBlocks (always present, 0 when no file has
mcpFakes) is the total number of mcpFakes blocks, and
errors holds run-level strings such as
"Cross-file validation failed: ...",
"No fixtures loaded from any input — the server aborts startup on this
under --validate-on-load/--strict", or the reason a usage error failed
|
Usage
npx @copilotkit/aimock validate [--strict] [--json] [--] <path> [more paths ...]
Options
| Flag | Description |
|---|---|
--strict |
Treat warnings as errors (exit 1 on warnings) |
--json |
Emit a JSON report instead of human-readable lines |
-h, --help |
Show the subcommand help and exit 0. With --json the help is the one
field of a JSON document on stdout: { "help": "<help text>" }
|
-- |
Stop option parsing; every later argument is a path (use this for a path that begins
with -)
|
Exit Codes
| Exit code | Meaning |
|---|---|
0 |
All files valid (warnings allowed unless --strict) |
1 |
Validation errors, unreadable or unparseable files, a named path of
the wrong shape (a walked one is skipped instead), a malformed fixture entry, a
remote http(s):// path, a stat failure or symlink cycle while walking a
directory, a *.json path that is not a regular file, a directory
holding no *.json files, no fixtures loaded at all,
--strict warnings, or a usage error (no paths given, or an unknown
option)
|
Validation State and Reset
validate keeps no state. It constructs no LLMock and no server,
touches no journal, match-count, chaos or file store, and reads only the paths it is
given, so there is nothing for LLMock.reset() or
POST /__aimock/reset to clear.
Examples
# Validate a whole fixture directory (recursive). Non-fixture JSON the
# walk turns up (aimock configs, say) is skipped with a note, not failed:
# fixtures/examples/mcp/mcp-config.json: skipped (not a fixture file — an aimock config file (mcp), which "aimock --config" serves and "--fixtures" does not load)
$ npx @copilotkit/aimock validate ./fixtures/
# Validate specific files, failing on warnings too (--strict on the whole shipped tree exits 1: cross-file duplicate userMessage)
$ npx @copilotkit/aimock validate --strict ./fixtures/example-greeting.json ./fixtures/examples/llm/
# Machine-readable report for CI
$ npx @copilotkit/aimock validate --json ./fixtures/
Docker Compose
The published image's ENTRYPOINT runs the flag-driven
llmock CLI — it supports -f/--fixtures,
-p/--port, -h/--host, and the chaos / record flags, but not
--config. The snippet below uses that default for flag-driven fixture
serving; for config-driven mode set
entrypoint: ["node", "dist/aimock-cli.js"] and
command: ["--config", "/app/aimock.json", "--host", "0.0.0.0"] and mount the
config file. The image declares no HEALTHCHECK, so the compose file supplies
one against GET /ready (busybox wget is in the image) and the
app waits on condition: service_healthy instead of on container start, which
races the listener.
services:
aimock:
image: ghcr.io/copilotkit/aimock:latest
command: ["-f", "/app/fixtures", "-h", "0.0.0.0"]
ports:
- "4010:4010"
volumes:
- ./fixtures:/app/fixtures:ro
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:4010/ready"]
interval: 2s
timeout: 2s
retries: 15
app:
build: .
environment:
OPENAI_BASE_URL: http://aimock:4010/v1
depends_on:
aimock:
condition: service_healthy