Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Latest commit

 

History

1,680 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

aimock

Mock infrastructure for AI application testing — point your SDK at one local port and every provider, protocol, and service answers deterministically.

Quick start · The suite · Record & replay · Docs

npm Unit Tests Drift Tests License: MIT

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.

Quick Start

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();

The aimock Suite

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.

Features

  • 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-speed multiplier
  • Recorded token usage and cost — Recording captures the final usage frame of a streaming completion, so replayed fixtures serve real prompt_tokens / completion_tokens instead of a length estimate; OpenRouter's provider-reported usage.cost and its cost_details / *_tokens_details breakdowns 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=v1 request 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 APIsimage generation (gpt-image-1 default, Imagen; /v1/images/variations replays 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, with Retry-After) at any probability, plus a fixed pre-handling delay (--chaos-latency <ms>); read, set and clear the rates at runtime via GET / POST / DELETE /__aimock/chaos, scoped to the caller's X-Test-Id so one test cannot destabilise the suite running beside it
  • Request journalGET /__aimock/journal returns every recorded request, filterable by path, method, status, service, testId and requestId with limit / offset paging and an X-Total-Count header 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 and GET /__aimock/journal?requestId=<id> returns that request. A minted id is never forwarded upstream in record/proxy mode
  • Per-Request Strict ModeX-AIMock-Strict header overrides the server-level --strict flag per request (true/1 = strict, false/0 = lenient)
  • Context-Based Fixture RoutingX-AIMock-Context header scopes fixtures per integration; fixtures with match.context only match requests carrying that context, fixtures without it remain shared
  • Drift Detection — Daily CI validation against real APIs
  • Streaming Physics — Configurable ttft, tps, and jitter
  • 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/sessions in 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, finishReason in fixture responses
  • Ordered Blocks — A blocks array 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 Chunksstream_options.include_usage support emits a final chunk with token counts, matching OpenAI's streaming usage protocol
  • Rate Limiting Headersx-ratelimit-* headers on every response and Retry-After on 429 errors for testing retry/backoff logic
  • Zero dependencies — Everything from Node.js builtins

GitHub Action

- uses: CopilotKit/aimock@v1
  with:
    fixtures: ./test/fixtures

- run: npm test
  env:
    OPENAI_BASE_URL: http://127.0.0.1:4010/v1

See the GitHub Action docs for all inputs and examples.

CLI

# 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.0

Note on llmock vs aimock CLIs. The llmock bin is retained as a compat alias for users of the pre-1.7.0 @copilotkit/llmock package. It runs a narrower flag-driven CLI without --config or the convert and validate subcommands. New projects should use aimock (or npx @copilotkit/aimock) for full feature support.

Offline fixture validation

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.

Remote fixture URLs

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

Replay matching & AIMOCK_STRICT_TURN_INDEX

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.

API-key validation

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.json

Use 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.

aimock-owned upstream keys — AIMOCK_PROVIDER_*_KEY

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 llmock bin reads AIMOCK_PROVIDER_*_KEY from the environment (it fills record.providerKeys at startup). aimock --config does not read these variables: the config loader passes llm.record through verbatim, so under aimock --config set llm.record.providerKeys in the JSON instead. The programmatic API likewise takes record.providerKeys in LLMock options.

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

Framework Guides

Test your AI agents with aimock — no API keys, no network calls: LangChain · CrewAI · PydanticAI · LlamaIndex · Mastra · Google ADK · Microsoft Agent Framework

Switching from other tools?

Step-by-step migration guides: MSW · VidaiMock · mock-llm · piyook/llm-mock · Python mocks · openai-responses · Mokksy

Documentation

https://aimock.copilotkit.dev · Example fixtures

Real-World Usage

AG-UI uses aimock for its end-to-end test suite, verifying AI agent behavior across LLM providers with fixture-driven responses.

Deprecated & removed APIs

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.

License

MIT

About

Mock everything your AI app talks to — LLM APIs, MCP, A2A, AG-UI, vector DBs, search. One package, one port, zero dependencies.

Topics

Resources

Stars

932 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages