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

Run aimock shell
$ npx @copilotkit/aimock --config aimock.json --port 4010
Run aimock shell
# 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.

aimock.json json
{
  "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
Recording config example json
{
  "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

Run with config shell
$ npx @copilotkit/aimock --config aimock.json --host 0.0.0.0
Run with config (docker) shell
# 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

Convert fixtures shell
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

Converter examples shell
# 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:

A path validated twice shell
$ 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):

validate --json json
{
  "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):

validate --json, usage error json
{
  "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):

validate --json, a skipped file json
{
  "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

Validate fixtures shell
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

Validation examples shell
# 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.

docker-compose.yml yaml
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