CopilotKit aimock

repository·main·Indexed 20 days ago

https://github.com/copilotkit/aimock

A comprehensive mock infrastructure for AI application testing. It allows developers to deterministically simulate LLM APIs (OpenAI, Claude, Gemini, etc.), multimedia generation, vector databases (Pinecone, Qdrant, ChromaDB), and agent protocols including MCP, A2A, and AG-UI event streams. The suite includes the LLMock class for programmatic mocking, a CLI for running the mocking suite, a GitHub Action for CI/CD integration, and the aimock-pytest plugin for Python testing.

Tokens
33.1K
Snippets
72
Records
130
Agent score
70%

What's inside @copilotkit/aimock

  1. What aimock can mock

    main

    aimock provides comprehensive mocking capabilities for various AI components:

    • LLM Providers: OpenAI, Anthropic, Gemini, Gemini Interactions, AWS Bedrock, Azure OpenAI, Vertex AI, Ollama, Cohere, and OpenRouter.
    • Multimedia Endpoints: Image generation, text-to-speech, audio transcription, and video generation.
    • Advanced Protocols & Interfaces: MCP, A2A, AG-UI, and vector DB mocking.
    • Real-time Communication: WebSocket support for OpenAI Responses/Realtime and Gemini Live APIs.
    • Testing Features: Record-and-replay for all endpoints, chaos testing, and Prometheus metrics.
  2. Overview of the aimock Suite

    main

    The aimock suite provides specialized mocks for various parts of the AI application stack. You can run them all on a single port using a configuration file or compose them programmatically.

    ToolDescription
    LLMockMocks LLM providers: OpenAI (Chat/Responses/Realtime), Claude, Gemini, Bedrock, Azure, Vertex AI, Ollama, Cohere, OpenRouter, and ElevenLabs TTS.
    MCPMockMocks Model Context Protocol (MCP) tools, resources, and prompts with session management.
    A2AMockMocks the Agent-to-Agent (A2A) protocol with SSE streaming.
    AGUIMockMocks AG-UI agent-to-UI event streams for frontend testing.
    VectorMockProvides compatible endpoints for Pinecone, Qdrant, and ChromaDB.
    ServicesMocks external services like Tavily search, Cohere rerank, OpenAI moderation, and ElevenLabs TTS.

    To run the full suite from a config file:

    npx @copilotkit/aimock --config aimock.json
  3. Choosing a Multi-turn Matching Approach

    main

    When designing multi-turn conversations (e.g., Human-in-the-loop or tool-calling flows), choose a matching strategy based on your deployment environment.

    Recommendation: Prefer stateless approaches (turnIndex, hasToolResult, toolResultContains) for shared aimock instances (e.g., deployed via Docker) to avoid issues with concurrency. Use sequenceIndex only in isolated, single-client unit tests.

    ApproachStateless?Best For
    turnIndexYesShared/deployed instances; matches on conversation depth (count of assistant messages).
    hasToolResultYesSimple 2-step tool flows; checks if the current turn carries a tool result.
    sequenceIndexNoSingle-client unit tests with repeated identical requests (uses a server-side counter).
    toolCallIdYesMatching specific tool result IDs in the conversation history.
    toolResultContainsYesDiscriminating between different outcomes (e.g., approve vs cancel) that share the same tool_call_id.
  4. Critical Gotchas when writing fixtures

    main

    Avoid these common pitfalls when configuring aimock:

    1. Order Matters: The first matching fixture wins. Use prependFixture() to ensure specific fixtures take priority over general ones.
    2. JSON Arguments: arguments in tool calls can be objects (preferred, auto-stringified) or strings. The fixture loader handles typeof === "object" automatically.
    3. Latency: latency: 100 defines the delay between SSE chunks, not the total response time.
    4. Tool Result Loops: Matching on userMessage for tool results will cause an infinite loop because the client sends the same conversation history. Always use a predicate checking role === "tool" for tool results.
    5. Sequence Counts: Use resetMatchCounts() to reset counts between tests without clearing the fixtures. Use reset() only if you want to clear the entire fixture pool.
    6. Embeddings: If no fixture matches an embedding request, aimock automatically generates deterministic vectors based on the input text hash.
  5. Understand API Drift Severity Levels

    main

    Drift detection uses a three-layer approach (SDK vs. Real API vs. aimock) to categorize mismatches:

    • critical: The test fails. aimock produces a different shape than the real API for a field that both the SDK and real API agree on. Action required: Update aimock response builders.
    • warning: The test passes (unless STRICT_DRIFT=1). The real API has a field that neither the SDK nor aimock knows about, or the SDK and real API disagree. This usually indicates a provider added a new feature.
    • info: Always passes. Represents known intentional differences (e.g., usage fields being zeroed out in mocks).
  6. Understand replay matching and turnIndex behavior

    main

    By default, aimock uses turnIndex as a non-fatal disambiguator during replay. If a content-matching fixture is found, it will be served even if its scripted turnIndex does not exactly match the current assistant-message count. This is designed to support multi-step agents that emit multiple assistant bubbles per logical turn.

    When this happens, aimock logs a warning with turnIndexRelaxed: true.

    To restore legacy strict behavior (where turnIndex must match the assistant count exactly), set the environment variable AIMOCK_STRICT_TURN_INDEX=1. Note that the record path is always strict regardless of this setting.

  7. Understand the aimock Core Mental Model

    main

    aimock is a fixture-driven, zero-dependency mock infrastructure for AI applications. It operates by running a real HTTP server on a real port, allowing it to work across processes (unlike MSW-style interceptors).

    Key concepts include:

    • Fixtures: A combination of match criteria and the corresponding response.
    • First-match-wins: The order of fixtures in your configuration matters; the first fixture that matches the incoming request is the one used.
    • Unified Provider Pool: All providers (OpenAI, Anthropic, etc.) share a single fixture pool because provider adapters normalize requests to a standard ChatCompletionRequest format.
    • Live Fixtures: Mutations made to fixtures after calling start() take effect immediately.
    • Sequential Responses: You can provide multiple responses for the same match criteria using sequenceIndex, which tracks the match count per fixture.
  8. Understand Automated Drift Remediation

    main

    aimock distinguishes between two types of API drift:

    1. General Drift (Non-model-churn): This includes changes to API response shapes or behavior. This is not automatically fixed. It is detected by daily drift tests and must be remediated manually by a human, similar to a standard bug.
    2. Model-Family Sync (Model churn): This is the only automated remediation. It handles cases where a provider adds or retires a model family. This process is deterministic and does not use an LLM.

    The fix-drift.yml workflow manages this sync and can be triggered via workflow_dispatch, a daily scheduled cron, or automatically upon a failed Drift Tests run.

  9. Understanding the AG-UI multimodal schema bug in aimock

    main

    In versions of aimock prior to the fix (specifically v1.26.1), the recorder failed to capture user messages when the content field was a structured array instead of a simple string. This occurs when using the AG-UI multimodal specification (e.g., @ag-ui/core), where content contains multiple parts like text and document (with embedded data sources).

    When this bug is present, the recorded fixture will show match.message: "__NO_USER_MESSAGE__" instead of the actual text content. After the fix, match.message correctly contains the joined text content from the parts array (e.g., "summarize this").

  10. How the Model-Family Sync process works

    main

    The model-family sync follows a three-step process to keep the model-registry.ts up to date:

    1. Sync (scripts/drift-sync.ts): Fetches live /models listings from providers and compares them against src/__tests__/drift/model-registry.ts.
      • Mechanical Removal: If a classified family is absent from live listings and has zero remaining aimock references, it is automatically marked for removal.
      • Human Decision Required: If a family is still referenced but deprecated, or if a new/unclassified family is found, the system never auto-edits the registry. Instead, it writes a note file in drift-proposals/ and routes the task to a human.
    2. Gate (scripts/drift-sync-check.ts): Re-verifies any mechanical edits. It uses a changed-file allowlist (limited to model-registry.ts data literals and drift-proposals/ notes) and checksum-pinning to ensure safety.
    3. PR (Pull Request): The workflow opens a PR for human review. There are two types:
      • ok-applied: A successful mechanical registry edit. A human reviews the diff and merges.
      • needs-human: A routed decision. The workflow pushes a drift-needs-human/* branch containing the proposal note. To approve a new family, a human must set the note's Decision: include line and merge the PR. The next sync run will then see the approved note on main and create an ok-applied PR to update the registry.
  11. Set up an E2E test pattern with LLMock

    main

    To integrate LLMock into an end-to-end test suite, follow this pattern for setup, per-test cleanup, and teardown. Using port: 0 allows the OS to pick an available random port.

    Note: When using LLMock in tests, you must point your LLM client's base URL to the mock server's URL (e.g., setting OPENAI_BASE_URL).

    import { LLMock } from "@copilotkit/aimock";
    
    // Setup
    const mock = new LLMock({ port: 0 });
    mock.loadFixtureDir("./fixtures");
    await mock.start();
    process.env.OPENAI_BASE_URL = `${mock.url}/v1`;
    
    // Per-test cleanup: resets sequence match counts but keeps loaded fixtures
    afterEach(() => mock.resetMatchCounts());
    
    // Teardown
    afterAll(async () => await mock.stop());