Mock infrastructure for AI application testing — point your SDK at one local port and every provider, protocol, and service answers deterministically.
aimock_sans.3.mp4
One package, one port, zero dependencies. LLM APIs, image generation and editing, text-to-speech, transcription, audio translation and generation, video generation, embeddings, MCP tools, A2A agents, AG-UI event streams, vector databases, search, rerank, and moderation — no keys, no network, no surprise bills.
npm install @copilotkit/aimock// The class is still named `LLMock` for back-compat after the v1.7.0 package
// rename from `@copilotkit/llmock` to `@copilotkit/aimock`.
import { LLMock } from "@copilotkit/aimock";
const mock = new LLMock({ port: 0 });
mock.onMessage("hello", { content: "Hi there!" });
await mock.start();
// Set env BEFORE importing/constructing the OpenAI (or other provider) client.
// Many SDKs cache the base URL at construction time — if the client is built
// before these are set, it will talk to the real API (surprise bills) instead
// of aimock.
process.env.OPENAI_BASE_URL = `${mock.url}/v1`;
process.env.OPENAI_API_KEY = "mock"; // SDK requires a value, even when base URL is mocked
// ... run your tests ...
await mock.stop();aimock mocks everything your AI app talks to:
| Tool | What it mocks | Docs |
|---|---|---|
| LLMock | OpenAI (Chat/Responses/Realtime GA/GPT-Live), Claude, Gemini (REST/Live/Interactions/Embeddings), Bedrock, Azure, Vertex AI, Ollama (chat/embeddings), Cohere (chat/embed), OpenRouter (chat/router/video), BytePlus Ark (chat/images/video), Grok (video), Veo (video), fal.ai, ElevenLabs (TTS/Voice Design) | Providers |
| MCPMock | MCP tools, resources, prompts with session management | MCP |
| A2AMock | Agent-to-agent protocol with SSE streaming | A2A |
| AGUIMock | AG-UI agent-to-UI event streams for frontend testing | AG-UI |
| VectorMock | Pinecone, Qdrant, ChromaDB compatible endpoints | Vector |
| Services | Tavily search, Cohere rerank, OpenAI moderation | Services |
Run them all on one port with npx @copilotkit/aimock --config aimock.json, or use the programmatic API to compose exactly what you need.
- Record & Replay — Proxy real APIs, save as fixtures, replay deterministically forever
- Timing-aware recording and replay — Recorded fixtures capture per-frame arrival timestamps; replay uses recorded timings for approximate timing reproduction based on recorded TTFT and inter-frame cadence (replay chunk count may differ from recording — TTFT and average pace are preserved, not per-token fidelity) with configurable
--replay-speedmultiplier - Recorded token usage and cost — Recording captures the final usage frame of a streaming completion, so replayed fixtures serve real
prompt_tokens/completion_tokensinstead of a length estimate; OpenRouter's provider-reportedusage.costand itscost_details/*_tokens_detailsbreakdowns are captured too, so an app that bills from real provider cost can e2e-test its wallet path from a tape - Multi-turn Conversations — Record and replay multi-turn traces with tool rounds; match distinct turns via
turnIndex,hasToolResult,toolCallId,toolResultContains(gate on the tool-result payload),sequenceIndex,systemMessage(gate on host-supplied agent context), or custom predicates - 11 providers across 22 API surfaces — OpenAI Chat, OpenAI Responses, OpenAI Realtime (GA; an
OpenAI-Beta: realtime=v1request gets the real sunset rejection), GPT-Live, Claude, Gemini REST, Gemini embedContent, Gemini Live, Gemini Interactions, Azure, Bedrock, Vertex AI, Ollama (chat + embeddings), Cohere (chat + embed), OpenRouter (chat + router failover), BytePlus Ark (OpenAI-compatible chat + images under/api/v3), ElevenLabs TTS and Voice Design — full streaming support - Multimedia APIs — image generation (
gpt-image-1default, Imagen;/v1/images/variationsreplays OpenAI's removal 404), image editing (/v1/images/edits), text-to-speech (OpenAI + ElevenLabs TTS and Voice Design), audio transcription, audio translation (/v1/audio/translations), video generation, OpenRouter video generation (/api/v1/videos with async job lifecycle), Google Veo video generation (:predictLongRunning + /v1beta/operations async lifecycle), Grok Imagine video generation (/v1/videos/generations with async job lifecycle), BytePlus Ark video generation (Seedance, /api/v3/contents/generations/tasks with async task lifecycle), fal.ai (image / video / audio with queue lifecycle) - MCP / A2A / AG-UI / Vector — Mock every protocol your AI agents use
- Chaos Testing — 500 errors, malformed JSON, mid-stream disconnects and 429 rate limits (
--chaos-ratelimit, withRetry-After) at any probability, plus a fixed pre-handling delay (--chaos-latency <ms>); read, set and clear the rates at runtime viaGET/POST/DELETE /__aimock/chaos, scoped to the caller'sX-Test-Idso one test cannot destabilise the suite running beside it - Request journal —
GET /__aimock/journalreturns every recorded request, filterable bypath,method,status,service,testIdandrequestIdwithlimit/offsetpaging and anX-Total-Countheader carrying the pre-pagination total - Request IDs — every response carries
X-Request-Id, echoing the caller's header or minting one; the journal stores it on each entry andGET /__aimock/journal?requestId=<id>returns that request. A minted id is never forwarded upstream in record/proxy mode - Per-Request Strict Mode —
X-AIMock-Strictheader overrides the server-level--strictflag per request (true/1= strict,false/0= lenient) - Context-Based Fixture Routing —
X-AIMock-Contextheader scopes fixtures per integration; fixtures withmatch.contextonly match requests carrying that context, fixtures without it remain shared - Drift Detection — Daily CI validation against real APIs
- Streaming Physics — Configurable
ttft,tps, andjitter - WebSocket APIs — OpenAI Realtime (GA protocol with models: gpt-realtime, gpt-realtime-2, gpt-realtime-1.5, gpt-realtime-mini; transcription/translation via gpt-4o-transcribe, gpt-4o-mini-transcribe, whisper-1; image input; commentary phase), Responses WS, Gemini Live
- GPT-Live — Mock, record, and replay primary WebSocket sessions at
/v1/live/sessionsin client and managed modes, with fixture-backed audio and backend tool continuation. Saved fixtures replay offline; aimock does not synthesize speech. This voice surface is separate from OpenAI text-model family support; WebRTC/SDP and sideband connections are outside this scope. - Prometheus Metrics — Request counts, latencies, fixture match rates
- Docker + Helm — Container image and Helm chart for CI/CD
- Vitest & Jest Plugins — Zero-config
useAimock()with auto lifecycle and env patching - Response Overrides — Control
id,model,usage,finishReasonin fixture responses - Ordered Blocks — A
blocksarray streams text and tool calls in any order (tool-first or interleaved); blocks-only fixtures are first-class, and the recorder captures order from genuinely tool-first/interleaved streams - Streaming Usage Chunks —
stream_options.include_usagesupport emits a final chunk with token counts, matching OpenAI's streaming usage protocol - Rate Limiting Headers —
x-ratelimit-*headers on every response andRetry-Afteron 429 errors for testing retry/backoff logic - Zero dependencies — Everything from Node.js builtins
- uses: CopilotKit/aimock@v1
with:
fixtures: ./test/fixtures
- run: npm test
env:
OPENAI_BASE_URL: http://127.0.0.1:4010/v1See the GitHub Action docs for all inputs and examples.
# LLM mocking only
npx -p @copilotkit/aimock llmock -p 4010 -f ./fixtures
# Remote fixtures — load JSON from an HTTPS URL (repeatable)
npx -p @copilotkit/aimock llmock -p 4010 \
-f https://raw.githubusercontent.com/acme/mocks/main/openai.json \
-f ./fixtures/local-overrides.json
# Full suite from config
npx @copilotkit/aimock --config aimock.json
# Record mode: proxy to real APIs, save fixtures
npx -p @copilotkit/aimock llmock --record --provider-openai https://api.openai.com
# Record with extended timeout for reasoning models
npx -p @copilotkit/aimock llmock --record --provider-openai https://api.openai.com \
--body-timeout-ms 180000
# Replay recorded fixtures at 2× speed
npx -p @copilotkit/aimock llmock -p 4010 -f ./fixtures --replay-speed 2
# Convert fixtures from other tools
npx @copilotkit/aimock convert vidaimock ./templates/ ./fixtures/vidaimock.json
npx @copilotkit/aimock convert mockllm ./config.yaml ./fixtures/mockllm.json
# Validate fixtures offline (file or directory, recursive)
npx @copilotkit/aimock validate ./fixtures/
# --strict on the whole shipped tree exits 1 (adk/mastra repeat a userMessage across files)
npx @copilotkit/aimock validate --strict --json ./fixtures/examples/llm/blocks-tool-first.json
# Docker
docker run -d -p 4010:4010 -v "$(pwd)/fixtures:/fixtures" ghcr.io/copilotkit/aimock -f /fixtures -h 0.0.0.0Note on
llmockvsaimockCLIs. Thellmockbin is retained as a compat alias for users of the pre-1.7.0@copilotkit/llmockpackage. It runs a narrower flag-driven CLI without--configor theconvertandvalidatesubcommands. New projects should useaimock(ornpx @copilotkit/aimock) for full feature support.
aimock validate [--strict] [--json] [--] <path> [more paths ...] lints fixtures without starting a server. Each path is a fixture file or a directory of them (walked recursively for *.json, like --fixtures <dir>). --strict treats warnings as errors; --json emits a machine-readable report for CI; -- stops option parsing so a path beginning with - is still reachable. 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. --json carries that number as mention on every file, whether or not the path repeats.
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 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 it 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 the run loaded at least one fixture from somewhere, whereas validate fails the run on every such file. 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: it 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" there is the llmock bin (dist/cli.js, the Docker entrypoint), whose -f, --fixtures takes a filesystem path or an https:///http:// URL to a .json fixture file; the aimock bin has no --fixtures flag, and the llm.fixtures in its --config file 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 and the one-line failure reason goes to stderr; a usage error is reported the same way, as a document with files: [] and the reason in run.errors. Exit codes: 0 clean; 1 everything else — validation errors, a malformed fixture entry, unreadable/unparseable files, a NAMED path of the wrong shape (a walked one is skipped instead), 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). There is no exit code 2; every other CLI in this package reports a usage error as 1 too.
--json emits one document on stdout, and only that document — a usage error included. Validating a directory holding a file with invalid JSON and a file with one bad entry and a duplicate userMessage:
{
"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,
"errors": []
}
}A usage error carries the reason in run.errors (aimock validate --json, exit 1):
{
"strict": false,
"failed": true,
"files": [],
"run": {
"fixtures": 0,
"errors": ["no fixture paths given."]
}
}failed is the boolean the exit code follows, and strict echoes the flag it was computed under. files has one entry per path the run touched, in argv/walk order: mention is which load of that path this is (1-based, always present); fixtures is how many entries converted; skipped (present only when set) is why a walked non-fixture file was passed over; fatal (present only when set) is the reason the file produced no report at all, and that same reason also appears in errors, so a tally of files[].errors matches the error count on the file's stdout line; and errors/warnings hold findings. A finding always has message; index is the 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 covers the combined pass: fixtures is the total fixture count across every file, and errors holds run-level strings such as "Cross-file validation failed: ...", the "No fixtures loaded from any input — the server aborts startup on this under --validate-on-load/--strict" line, or the reason a usage error failed.
--fixtures accepts https:// and http:// URLs pointing at JSON fixture files in addition to filesystem paths, and the flag is repeatable so you can layer remote and local sources in argv order. Fetched fixtures are cached on disk at ~/.cache/aimock/fixtures/<sha256-of-url>/ (honors $XDG_CACHE_HOME); when paired with --validate-on-load, a fetch failure with a valid cached copy logs a warning and continues — without a cache, the process exits non-zero. HTTP fetches have a 10s timeout and a 50 MB body cap; redirects are rejected fail-loud, so configure your upstream to serve the final URL directly (GitHub raw content URLs already do).
Private and link-local addresses (loopback, RFC1918, CGNAT, cloud metadata, ULA, multicast) are rejected by default to prevent SSRF. For local development or tests that need to hit 127.0.0.1, opt out with AIMOCK_ALLOW_PRIVATE_URLS=1. Tarball and zip URL support is intentionally deferred.
On replay, turnIndex is a non-fatal disambiguator, not a hard reject gate: a content-matching fixture is served even when its scripted turnIndex differs from the request's assistant-message count. This kills false "no fixture matched" misses for multi-bubble agent runs (multi-step agents emit several assistant bubbles per logical turn). When a served fixture diverges from its scripted turnIndex, the match diagnostic carries turnIndexRelaxed: true and aimock logs a one-shot warning (at the warn log level, so it is silent under the programmatic default, which is silent, and printed by the llmock CLI, which defaults --log-level to info). To restore the legacy strict behavior where a defined turnIndex must equal the assistant count exactly, set AIMOCK_STRICT_TURN_INDEX=1. The record path is always strict regardless of this flag.
By default aimock accepts all requests. Opt into inbound test-client validation with a programmatic option, top-level aimock.json field, or environment-only key list:
await createServer(fixtures, { auth: { apiKeys: ["test-key"] } });{ "auth": { "apiKeys": ["test-key"] } }AIMOCK_API_KEYS=test-key,rotated-key npx @copilotkit/aimock --config aimock.jsonUse Authorization: Bearer <key>, Authorization: Key <key>, x-api-key, x-goog-api-key, api-key, or xi-api-key. Every supplied credential must resolve to one configured key; mismatches return 401 with an OpenAI-compatible authentication error. HTTP routes, control APIs, mounts, and WebSocket upgrades are protected. Genuine CORS preflights plus GET /health, GET /ready, and GET /metrics remain public. This is inbound test access control, distinct from record.providerKeys; when enabled, proxying strips test credentials and requires a configured static provider credential before egress.
In record or --proxy-only mode, aimock forwards the caller's auth header to the real provider unchanged. If your tests can only send a dummy placeholder key (e.g. an SDK that refuses to start without a non-empty API key), aimock can inject its own configured upstream key on a fixture-miss passthrough so the proxied call actually authenticates. Each provider has an independent env var, and the key is applied with the provider-correct wire scheme:
| Env var | Provider | Injected header |
|---|---|---|
AIMOCK_PROVIDER_OPENAI_KEY |
OpenAI | Authorization: Bearer <key> |
AIMOCK_PROVIDER_OPENROUTER_KEY |
OpenRouter | Authorization: Bearer <key> |
AIMOCK_PROVIDER_COHERE_KEY |
Cohere | Authorization: Bearer <key> |
AIMOCK_PROVIDER_GROK_KEY |
Grok (xAI) | Authorization: Bearer <key> |
AIMOCK_PROVIDER_BYTEPLUS_KEY |
BytePlus Ark (ModelArk) | Authorization: Bearer <key> |
AIMOCK_PROVIDER_OLLAMA_KEY |
Ollama (Cloud / bearer-gated) | Authorization: Bearer <key> |
AIMOCK_PROVIDER_ANTHROPIC_KEY |
Anthropic | x-api-key: <key> |
AIMOCK_PROVIDER_GEMINI_KEY |
Gemini (and Gemini Interactions) | x-goog-api-key: <key> |
AIMOCK_PROVIDER_VEO_KEY |
Veo | x-goog-api-key: <key> |
AIMOCK_PROVIDER_AZURE_KEY |
Azure OpenAI | api-key: <key> |
AIMOCK_PROVIDER_ELEVENLABS_KEY |
ElevenLabs | xi-api-key: <key> |
AIMOCK_PROVIDER_FAL_KEY |
fal.ai | Authorization: Key <key> |
The Gemini interactions provider mode reuses AIMOCK_PROVIDER_GEMINI_KEY (same upstream API as Gemini). An empty-string value is treated as unset.
Which binary reads them. Only the
llmockbin readsAIMOCK_PROVIDER_*_KEYfrom the environment (it fillsrecord.providerKeysat startup).aimock --configdoes not read these variables: the config loader passesllm.recordthrough verbatim, so underaimock --configsetllm.record.providerKeysin the JSON instead. The programmatic API likewise takesrecord.providerKeysinLLMockoptions.
This is opt-in and backward-compatible: with no key configured the feature is inert and the caller's header passes through as-is. Injection fires only when the caller sent no credential or a dummy credential prefixed with sk-aimock- (overridable via AIMOCK_DUMMY_KEY_MARKER); a real caller key never starting with that marker is always forwarded verbatim, so the caller overrides aimock. Signed and exchanged credentials — AWS Bedrock (SigV4) and Vertex AI (OAuth) — are never rewritten and always forwarded unchanged. (Azure's static api-key is injected; a real Microsoft Entra ID Authorization: Bearer token from the caller is never dummy-prefixed, so it too passes through verbatim.)
Test your AI agents with aimock — no API keys, no network calls: LangChain · CrewAI · PydanticAI · LlamaIndex · Mastra · Google ADK · Microsoft Agent Framework
Step-by-step migration guides: MSW · VidaiMock · mock-llm · piyook/llm-mock · Python mocks · openai-responses · Mokksy
https://aimock.copilotkit.dev · Example fixtures
AG-UI uses aimock for its end-to-end test suite, verifying AI agent behavior across LLM providers with fixture-driven responses.
aimock keeps mocking provider surfaces that upstream has deprecated — that is when test scaffolding matters most, since your code still calls them while you migrate. It declines to mock the success path of APIs that have actually been removed: a mock that answers 200 where the real endpoint is gone manufactures a passing test for code that cannot work. Full policy: Deprecated & Removed APIs.
MIT