@copilotkit/aimock 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +17 -0
- package/.claude-plugin/plugin.json +12 -0
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/_virtual/_rolldown/runtime.cjs +29 -0
- package/dist/a2a-handler.cjs +203 -0
- package/dist/a2a-handler.cjs.map +1 -0
- package/dist/a2a-handler.js +199 -0
- package/dist/a2a-handler.js.map +1 -0
- package/dist/a2a-mock.cjs +292 -0
- package/dist/a2a-mock.cjs.map +1 -0
- package/dist/a2a-mock.d.cts +41 -0
- package/dist/a2a-mock.d.cts.map +1 -0
- package/dist/a2a-mock.d.ts +41 -0
- package/dist/a2a-mock.d.ts.map +1 -0
- package/dist/a2a-mock.js +290 -0
- package/dist/a2a-mock.js.map +1 -0
- package/dist/a2a-stub.cjs +4 -0
- package/dist/a2a-stub.d.cts +3 -0
- package/dist/a2a-stub.d.ts +3 -0
- package/dist/a2a-stub.js +3 -0
- package/dist/a2a-types.d.cts +68 -0
- package/dist/a2a-types.d.cts.map +1 -0
- package/dist/a2a-types.d.ts +68 -0
- package/dist/a2a-types.d.ts.map +1 -0
- package/dist/aimock-cli.cjs +112 -0
- package/dist/aimock-cli.cjs.map +1 -0
- package/dist/aimock-cli.d.cts +19 -0
- package/dist/aimock-cli.d.cts.map +1 -0
- package/dist/aimock-cli.d.ts +19 -0
- package/dist/aimock-cli.d.ts.map +1 -0
- package/dist/aimock-cli.js +110 -0
- package/dist/aimock-cli.js.map +1 -0
- package/dist/aws-event-stream.cjs +117 -0
- package/dist/aws-event-stream.cjs.map +1 -0
- package/dist/aws-event-stream.d.cts +38 -0
- package/dist/aws-event-stream.d.cts.map +1 -0
- package/dist/aws-event-stream.d.ts +38 -0
- package/dist/aws-event-stream.d.ts.map +1 -0
- package/dist/aws-event-stream.js +114 -0
- package/dist/aws-event-stream.js.map +1 -0
- package/dist/bedrock-converse.cjs +445 -0
- package/dist/bedrock-converse.cjs.map +1 -0
- package/dist/bedrock-converse.d.cts +50 -0
- package/dist/bedrock-converse.d.cts.map +1 -0
- package/dist/bedrock-converse.d.ts +50 -0
- package/dist/bedrock-converse.d.ts.map +1 -0
- package/dist/bedrock-converse.js +443 -0
- package/dist/bedrock-converse.js.map +1 -0
- package/dist/bedrock.cjs +557 -0
- package/dist/bedrock.cjs.map +1 -0
- package/dist/bedrock.d.cts +41 -0
- package/dist/bedrock.d.cts.map +1 -0
- package/dist/bedrock.d.ts +41 -0
- package/dist/bedrock.d.ts.map +1 -0
- package/dist/bedrock.js +553 -0
- package/dist/bedrock.js.map +1 -0
- package/dist/chaos.cjs +114 -0
- package/dist/chaos.cjs.map +1 -0
- package/dist/chaos.d.cts +27 -0
- package/dist/chaos.d.cts.map +1 -0
- package/dist/chaos.d.ts +27 -0
- package/dist/chaos.d.ts.map +1 -0
- package/dist/chaos.js +113 -0
- package/dist/chaos.js.map +1 -0
- package/dist/cli.cjs +268 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +268 -0
- package/dist/cli.js.map +1 -0
- package/dist/cohere.cjs +434 -0
- package/dist/cohere.cjs.map +1 -0
- package/dist/cohere.d.cts +34 -0
- package/dist/cohere.d.cts.map +1 -0
- package/dist/cohere.d.ts +34 -0
- package/dist/cohere.d.ts.map +1 -0
- package/dist/cohere.js +433 -0
- package/dist/cohere.js.map +1 -0
- package/dist/config-loader.cjs +111 -0
- package/dist/config-loader.cjs.map +1 -0
- package/dist/config-loader.d.cts +100 -0
- package/dist/config-loader.d.cts.map +1 -0
- package/dist/config-loader.d.ts +100 -0
- package/dist/config-loader.d.ts.map +1 -0
- package/dist/config-loader.js +107 -0
- package/dist/config-loader.js.map +1 -0
- package/dist/embeddings.cjs +150 -0
- package/dist/embeddings.cjs.map +1 -0
- package/dist/embeddings.d.cts +12 -0
- package/dist/embeddings.d.cts.map +1 -0
- package/dist/embeddings.d.ts +12 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +150 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/fixture-loader.cjs +269 -0
- package/dist/fixture-loader.cjs.map +1 -0
- package/dist/fixture-loader.d.cts +17 -0
- package/dist/fixture-loader.d.cts.map +1 -0
- package/dist/fixture-loader.d.ts +17 -0
- package/dist/fixture-loader.d.ts.map +1 -0
- package/dist/fixture-loader.js +265 -0
- package/dist/fixture-loader.js.map +1 -0
- package/dist/gemini.cjs +403 -0
- package/dist/gemini.cjs.map +1 -0
- package/dist/gemini.d.cts +10 -0
- package/dist/gemini.d.cts.map +1 -0
- package/dist/gemini.d.ts +10 -0
- package/dist/gemini.d.ts.map +1 -0
- package/dist/gemini.js +403 -0
- package/dist/gemini.js.map +1 -0
- package/dist/helpers.cjs +276 -0
- package/dist/helpers.cjs.map +1 -0
- package/dist/helpers.d.cts +39 -0
- package/dist/helpers.d.cts.map +1 -0
- package/dist/helpers.d.ts +39 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +259 -0
- package/dist/helpers.js.map +1 -0
- package/dist/index.cjs +113 -0
- package/dist/index.d.cts +42 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +39 -0
- package/dist/interruption.cjs +40 -0
- package/dist/interruption.cjs.map +1 -0
- package/dist/interruption.d.cts +15 -0
- package/dist/interruption.d.cts.map +1 -0
- package/dist/interruption.d.ts +15 -0
- package/dist/interruption.d.ts.map +1 -0
- package/dist/interruption.js +39 -0
- package/dist/interruption.js.map +1 -0
- package/dist/journal.cjs +65 -0
- package/dist/journal.cjs.map +1 -0
- package/dist/journal.d.cts +23 -0
- package/dist/journal.d.cts.map +1 -0
- package/dist/journal.d.ts +23 -0
- package/dist/journal.d.ts.map +1 -0
- package/dist/journal.js +65 -0
- package/dist/journal.js.map +1 -0
- package/dist/jsonrpc.cjs +91 -0
- package/dist/jsonrpc.cjs.map +1 -0
- package/dist/jsonrpc.d.cts +24 -0
- package/dist/jsonrpc.d.cts.map +1 -0
- package/dist/jsonrpc.d.ts +24 -0
- package/dist/jsonrpc.d.ts.map +1 -0
- package/dist/jsonrpc.js +90 -0
- package/dist/jsonrpc.js.map +1 -0
- package/dist/llmock.cjs +223 -0
- package/dist/llmock.cjs.map +1 -0
- package/dist/llmock.d.cts +70 -0
- package/dist/llmock.d.cts.map +1 -0
- package/dist/llmock.d.ts +70 -0
- package/dist/llmock.d.ts.map +1 -0
- package/dist/llmock.js +223 -0
- package/dist/llmock.js.map +1 -0
- package/dist/logger.cjs +29 -0
- package/dist/logger.cjs.map +1 -0
- package/dist/logger.d.cts +14 -0
- package/dist/logger.d.cts.map +1 -0
- package/dist/logger.d.ts +14 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +28 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp-handler.cjs +189 -0
- package/dist/mcp-handler.cjs.map +1 -0
- package/dist/mcp-handler.js +188 -0
- package/dist/mcp-handler.js.map +1 -0
- package/dist/mcp-mock.cjs +169 -0
- package/dist/mcp-mock.cjs.map +1 -0
- package/dist/mcp-mock.d.cts +40 -0
- package/dist/mcp-mock.d.cts.map +1 -0
- package/dist/mcp-mock.d.ts +40 -0
- package/dist/mcp-mock.d.ts.map +1 -0
- package/dist/mcp-mock.js +167 -0
- package/dist/mcp-mock.js.map +1 -0
- package/dist/mcp-stub.cjs +4 -0
- package/dist/mcp-stub.d.cts +3 -0
- package/dist/mcp-stub.d.ts +3 -0
- package/dist/mcp-stub.js +3 -0
- package/dist/mcp-types.d.cts +65 -0
- package/dist/mcp-types.d.cts.map +1 -0
- package/dist/mcp-types.d.ts +65 -0
- package/dist/mcp-types.d.ts.map +1 -0
- package/dist/messages.cjs +489 -0
- package/dist/messages.cjs.map +1 -0
- package/dist/messages.d.cts +10 -0
- package/dist/messages.d.cts.map +1 -0
- package/dist/messages.d.ts +10 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +489 -0
- package/dist/messages.js.map +1 -0
- package/dist/metrics.cjs +160 -0
- package/dist/metrics.cjs.map +1 -0
- package/dist/metrics.d.cts +24 -0
- package/dist/metrics.d.cts.map +1 -0
- package/dist/metrics.d.ts +24 -0
- package/dist/metrics.d.ts.map +1 -0
- package/dist/metrics.js +158 -0
- package/dist/metrics.js.map +1 -0
- package/dist/moderation.cjs +91 -0
- package/dist/moderation.cjs.map +1 -0
- package/dist/moderation.d.cts +23 -0
- package/dist/moderation.d.cts.map +1 -0
- package/dist/moderation.d.ts +23 -0
- package/dist/moderation.d.ts.map +1 -0
- package/dist/moderation.js +91 -0
- package/dist/moderation.js.map +1 -0
- package/dist/ndjson-writer.cjs +31 -0
- package/dist/ndjson-writer.cjs.map +1 -0
- package/dist/ndjson-writer.d.cts +17 -0
- package/dist/ndjson-writer.d.cts.map +1 -0
- package/dist/ndjson-writer.d.ts +17 -0
- package/dist/ndjson-writer.d.ts.map +1 -0
- package/dist/ndjson-writer.js +31 -0
- package/dist/ndjson-writer.js.map +1 -0
- package/dist/ollama.cjs +519 -0
- package/dist/ollama.cjs.map +1 -0
- package/dist/ollama.d.cts +34 -0
- package/dist/ollama.d.cts.map +1 -0
- package/dist/ollama.d.ts +34 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +517 -0
- package/dist/ollama.js.map +1 -0
- package/dist/recorder.cjs +311 -0
- package/dist/recorder.cjs.map +1 -0
- package/dist/recorder.d.cts +23 -0
- package/dist/recorder.d.cts.map +1 -0
- package/dist/recorder.d.ts +23 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +305 -0
- package/dist/recorder.js.map +1 -0
- package/dist/rerank.cjs +71 -0
- package/dist/rerank.cjs.map +1 -0
- package/dist/rerank.d.cts +22 -0
- package/dist/rerank.d.cts.map +1 -0
- package/dist/rerank.d.ts +22 -0
- package/dist/rerank.d.ts.map +1 -0
- package/dist/rerank.js +71 -0
- package/dist/rerank.js.map +1 -0
- package/dist/responses.cjs +637 -0
- package/dist/responses.cjs.map +1 -0
- package/dist/responses.d.cts +16 -0
- package/dist/responses.d.cts.map +1 -0
- package/dist/responses.d.ts +16 -0
- package/dist/responses.d.ts.map +1 -0
- package/dist/responses.js +634 -0
- package/dist/responses.js.map +1 -0
- package/dist/router.cjs +68 -0
- package/dist/router.cjs.map +1 -0
- package/dist/router.d.cts +16 -0
- package/dist/router.d.cts.map +1 -0
- package/dist/router.d.ts +16 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +65 -0
- package/dist/router.js.map +1 -0
- package/dist/search.cjs +59 -0
- package/dist/search.cjs.map +1 -0
- package/dist/search.d.cts +23 -0
- package/dist/search.d.cts.map +1 -0
- package/dist/search.d.ts +23 -0
- package/dist/search.d.ts.map +1 -0
- package/dist/search.js +59 -0
- package/dist/search.js.map +1 -0
- package/dist/server.cjs +935 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +28 -0
- package/dist/server.d.cts.map +1 -0
- package/dist/server.d.ts +28 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +933 -0
- package/dist/server.js.map +1 -0
- package/dist/sse-writer.cjs +59 -0
- package/dist/sse-writer.cjs.map +1 -0
- package/dist/sse-writer.d.cts +19 -0
- package/dist/sse-writer.d.cts.map +1 -0
- package/dist/sse-writer.d.ts +19 -0
- package/dist/sse-writer.d.ts.map +1 -0
- package/dist/sse-writer.js +55 -0
- package/dist/sse-writer.js.map +1 -0
- package/dist/stream-collapse.cjs +496 -0
- package/dist/stream-collapse.cjs.map +1 -0
- package/dist/stream-collapse.d.cts +70 -0
- package/dist/stream-collapse.d.cts.map +1 -0
- package/dist/stream-collapse.d.ts +70 -0
- package/dist/stream-collapse.d.ts.map +1 -0
- package/dist/stream-collapse.js +489 -0
- package/dist/stream-collapse.js.map +1 -0
- package/dist/suite.cjs +46 -0
- package/dist/suite.cjs.map +1 -0
- package/dist/suite.d.cts +31 -0
- package/dist/suite.d.cts.map +1 -0
- package/dist/suite.d.ts +31 -0
- package/dist/suite.d.ts.map +1 -0
- package/dist/suite.js +46 -0
- package/dist/suite.js.map +1 -0
- package/dist/types.d.cts +243 -0
- package/dist/types.d.cts.map +1 -0
- package/dist/types.d.ts +243 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/url.cjs +21 -0
- package/dist/url.cjs.map +1 -0
- package/dist/url.d.cts +16 -0
- package/dist/url.d.cts.map +1 -0
- package/dist/url.d.ts +16 -0
- package/dist/url.d.ts.map +1 -0
- package/dist/url.js +20 -0
- package/dist/url.js.map +1 -0
- package/dist/vector-handler.cjs +239 -0
- package/dist/vector-handler.cjs.map +1 -0
- package/dist/vector-handler.js +238 -0
- package/dist/vector-handler.js.map +1 -0
- package/dist/vector-mock.cjs +229 -0
- package/dist/vector-mock.cjs.map +1 -0
- package/dist/vector-mock.d.cts +39 -0
- package/dist/vector-mock.d.cts.map +1 -0
- package/dist/vector-mock.d.ts +39 -0
- package/dist/vector-mock.d.ts.map +1 -0
- package/dist/vector-mock.js +227 -0
- package/dist/vector-mock.js.map +1 -0
- package/dist/vector-stub.cjs +4 -0
- package/dist/vector-stub.d.cts +3 -0
- package/dist/vector-stub.d.ts +3 -0
- package/dist/vector-stub.js +3 -0
- package/dist/vector-types.d.cts +32 -0
- package/dist/vector-types.d.cts.map +1 -0
- package/dist/vector-types.d.ts +32 -0
- package/dist/vector-types.d.ts.map +1 -0
- package/dist/watcher.cjs +59 -0
- package/dist/watcher.cjs.map +1 -0
- package/dist/watcher.js +58 -0
- package/dist/watcher.js.map +1 -0
- package/dist/ws-framing.cjs +187 -0
- package/dist/ws-framing.cjs.map +1 -0
- package/dist/ws-framing.d.cts +26 -0
- package/dist/ws-framing.d.cts.map +1 -0
- package/dist/ws-framing.d.ts +26 -0
- package/dist/ws-framing.d.ts.map +1 -0
- package/dist/ws-framing.js +184 -0
- package/dist/ws-framing.js.map +1 -0
- package/dist/ws-gemini-live.cjs +364 -0
- package/dist/ws-gemini-live.cjs.map +1 -0
- package/dist/ws-gemini-live.d.cts +18 -0
- package/dist/ws-gemini-live.d.cts.map +1 -0
- package/dist/ws-gemini-live.d.ts +18 -0
- package/dist/ws-gemini-live.d.ts.map +1 -0
- package/dist/ws-gemini-live.js +364 -0
- package/dist/ws-gemini-live.js.map +1 -0
- package/dist/ws-realtime.cjs +435 -0
- package/dist/ws-realtime.cjs.map +1 -0
- package/dist/ws-realtime.d.cts +17 -0
- package/dist/ws-realtime.d.cts.map +1 -0
- package/dist/ws-realtime.d.ts +17 -0
- package/dist/ws-realtime.d.ts.map +1 -0
- package/dist/ws-realtime.js +435 -0
- package/dist/ws-realtime.js.map +1 -0
- package/dist/ws-responses.cjs +164 -0
- package/dist/ws-responses.cjs.map +1 -0
- package/dist/ws-responses.d.cts +18 -0
- package/dist/ws-responses.d.cts.map +1 -0
- package/dist/ws-responses.d.ts +18 -0
- package/dist/ws-responses.d.ts.map +1 -0
- package/dist/ws-responses.js +164 -0
- package/dist/ws-responses.js.map +1 -0
- package/fixtures/example-greeting.json +12 -0
- package/fixtures/example-multi-turn.json +14 -0
- package/fixtures/example-tool-call.json +15 -0
- package/package.json +118 -0
- package/skills/write-fixtures/SKILL.md +625 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: write-fixtures
|
|
3
|
+
description: Use when writing test fixtures for @copilotkit/aimock — mock LLM responses, tool call sequences, error injection, multi-turn agent loops, embeddings, structured output, sequential responses, or debugging fixture mismatches
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing aimock Test Fixtures
|
|
7
|
+
|
|
8
|
+
## What aimock Is
|
|
9
|
+
|
|
10
|
+
aimock is a zero-dependency mock LLM server. Fixture-driven. Multi-provider (OpenAI, Anthropic, Gemini, AWS Bedrock, Azure OpenAI, Vertex AI, Ollama, Cohere). Runs a real HTTP server on a real port — works across processes, unlike MSW-style interceptors. WebSocket support for OpenAI Responses/Realtime and Gemini Live APIs. Chaos testing and Prometheus metrics.
|
|
11
|
+
|
|
12
|
+
## Core Mental Model
|
|
13
|
+
|
|
14
|
+
- **Fixtures** = match criteria + response
|
|
15
|
+
- **First-match-wins** — order matters
|
|
16
|
+
- All providers share one fixture pool (provider adapters normalize to `ChatCompletionRequest`)
|
|
17
|
+
- Fixtures are live — mutations after `start()` take effect immediately
|
|
18
|
+
- Sequential responses are supported via `sequenceIndex` (match count tracked per fixture)
|
|
19
|
+
|
|
20
|
+
## Match Field Reference
|
|
21
|
+
|
|
22
|
+
| Field | Type | Matches Against |
|
|
23
|
+
| ---------------- | ----------------------------------------- | ----------------------------------------------------------------------------- |
|
|
24
|
+
| `userMessage` | `string` | Substring of last `role: "user"` message text |
|
|
25
|
+
| `userMessage` | `RegExp` | Pattern test on last `role: "user"` message text |
|
|
26
|
+
| `inputText` | `string` | Substring of embedding input text (concatenated if multiple inputs) |
|
|
27
|
+
| `inputText` | `RegExp` | Pattern test on embedding input text |
|
|
28
|
+
| `toolName` | `string` | Exact match on any tool in request's `tools[]` array (by `function.name`) |
|
|
29
|
+
| `toolCallId` | `string` | Exact match on `tool_call_id` of last `role: "tool"` message |
|
|
30
|
+
| `model` | `string` | Exact match on `req.model` |
|
|
31
|
+
| `model` | `RegExp` | Pattern test on `req.model` |
|
|
32
|
+
| `responseFormat` | `string` | Exact match on `req.response_format.type` (`"json_object"`, `"json_schema"`) |
|
|
33
|
+
| `sequenceIndex` | `number` | Matches only when this fixture's match count equals the given index (0-based) |
|
|
34
|
+
| `predicate` | `(req: ChatCompletionRequest) => boolean` | Custom function — full access to request |
|
|
35
|
+
|
|
36
|
+
**AND logic**: all specified fields must match. Empty match `{}` = catch-all.
|
|
37
|
+
|
|
38
|
+
Multi-part content (e.g., `[{type: "text", text: "hello"}]`) is automatically extracted — `userMessage` matching works regardless of content format.
|
|
39
|
+
|
|
40
|
+
## Response Types
|
|
41
|
+
|
|
42
|
+
### Text
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
{
|
|
46
|
+
content: "Hello!";
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Tool Calls
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
{
|
|
54
|
+
toolCalls: [{ name: "get_weather", arguments: '{"city":"SF"}' }];
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**`arguments` MUST be a JSON string**, not an object. This is the #1 mistake.
|
|
59
|
+
|
|
60
|
+
### Embedding
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
{
|
|
64
|
+
embedding: [0.1, 0.2, 0.3, -0.5, 0.8];
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The embedding vector is returned for each input in the request. If no embedding fixture matches, deterministic embeddings are auto-generated from the input text hash — you only need fixtures when you want specific vectors.
|
|
69
|
+
|
|
70
|
+
### Error
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
{ error: { message: "Rate limited", type: "rate_limit_error" }, status: 429 }
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Chaos (Failure Injection)
|
|
77
|
+
|
|
78
|
+
The optional `chaos` field on a fixture enables probabilistic failure injection:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
{
|
|
82
|
+
chaos?: {
|
|
83
|
+
dropRate?: number; // Probability (0-1) of returning a 500 error
|
|
84
|
+
malformedRate?: number; // Probability (0-1) of returning malformed JSON
|
|
85
|
+
disconnectRate?: number; // Probability (0-1) of disconnecting mid-stream
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Rates are evaluated per-request. When triggered, the chaos failure replaces the normal response.
|
|
91
|
+
|
|
92
|
+
## Common Patterns
|
|
93
|
+
|
|
94
|
+
### Basic text fixture
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
mock.onMessage("hello", { content: "Hi there!" });
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Tool call → tool result → final response (3-step agent loop)
|
|
101
|
+
|
|
102
|
+
The most common pattern. Fixture 1 triggers the tool call, fixture 2 handles the tool result.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Step 1: User asks about weather → LLM calls tool
|
|
106
|
+
mock.onMessage("weather", {
|
|
107
|
+
toolCalls: [{ name: "get_weather", arguments: '{"city":"SF"}' }],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Step 2: Tool result comes back → LLM responds with text
|
|
111
|
+
mock.addFixture({
|
|
112
|
+
match: { predicate: (req) => req.messages.at(-1)?.role === "tool" },
|
|
113
|
+
response: { content: "It's 72°F in San Francisco." },
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Why predicate, not userMessage?** After a tool call, the client replays the same conversation with the tool result appended. The user message hasn't changed — `userMessage: "weather"` would match the SAME fixture again, creating an infinite loop.
|
|
118
|
+
|
|
119
|
+
### Embedding fixture
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Match specific input text
|
|
123
|
+
mock.onEmbedding("search query", {
|
|
124
|
+
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Match with regex
|
|
128
|
+
mock.onEmbedding(/product.*description/, {
|
|
129
|
+
embedding: [0.9, -0.1, 0.5, 0.3, 0.2],
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Structured output / JSON mode
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// onJsonOutput auto-sets responseFormat: "json_object" and stringifies objects
|
|
137
|
+
mock.onJsonOutput("extract entities", {
|
|
138
|
+
entities: [
|
|
139
|
+
{ name: "Acme Corp", type: "company" },
|
|
140
|
+
{ name: "Jane Doe", type: "person" },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// Equivalent manual form:
|
|
145
|
+
mock.addFixture({
|
|
146
|
+
match: { userMessage: "extract entities", responseFormat: "json_object" },
|
|
147
|
+
response: { content: '{"entities":[...]}' },
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Sequential responses (same match, different responses)
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
// First call returns tool call, second returns text
|
|
155
|
+
mock.on(
|
|
156
|
+
{ userMessage: "status", sequenceIndex: 0 },
|
|
157
|
+
{ toolCalls: [{ name: "check_status", arguments: "{}" }] },
|
|
158
|
+
);
|
|
159
|
+
mock.on({ userMessage: "status", sequenceIndex: 1 }, { content: "All systems operational." });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Match counts are tracked per fixture group and reset with `reset()` or `resetMatchCounts()`.
|
|
163
|
+
|
|
164
|
+
### Streaming physics (realistic timing)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
mock.onMessage(
|
|
168
|
+
"tell me a story",
|
|
169
|
+
{ content: "Once upon a time..." },
|
|
170
|
+
{
|
|
171
|
+
streamingProfile: {
|
|
172
|
+
ttft: 200, // 200ms before first token
|
|
173
|
+
tps: 30, // 30 tokens per second after that
|
|
174
|
+
jitter: 0.1, // ±10% random variance
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Predicate-based routing (same user message, different context)
|
|
181
|
+
|
|
182
|
+
Common in supervisor/orchestrator patterns where the system prompt changes:
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
mock.addFixture({
|
|
186
|
+
match: {
|
|
187
|
+
predicate: (req) => {
|
|
188
|
+
const sys = req.messages.find((m) => m.role === "system")?.content ?? "";
|
|
189
|
+
return typeof sys === "string" && sys.includes("Flights found: false");
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
response: { toolCalls: [{ name: "search_flights", arguments: "{}" }] },
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Catch-all (always add one)
|
|
197
|
+
|
|
198
|
+
Prevents unmatched requests from returning 404 and crashing the test:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
mock.addFixture({
|
|
202
|
+
match: { predicate: () => true },
|
|
203
|
+
response: { content: "I understand. How can I help?" },
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Tool result catch-all with prependFixture
|
|
208
|
+
|
|
209
|
+
Must go at the front so it matches before substring-based fixtures:
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
mock.prependFixture({
|
|
213
|
+
match: { predicate: (req) => req.messages.at(-1)?.role === "tool" },
|
|
214
|
+
response: { content: "Done!" },
|
|
215
|
+
});
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Stream interruption simulation (v1.3.0+)
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
mock.onMessage(
|
|
222
|
+
"long response",
|
|
223
|
+
{ content: "This will be cut short..." },
|
|
224
|
+
{
|
|
225
|
+
truncateAfterChunks: 3, // Stop after 3 SSE chunks
|
|
226
|
+
disconnectAfterMs: 500, // Or disconnect after 500ms
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Chaos testing (probabilistic failures)
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
mock.addFixture({
|
|
235
|
+
match: { userMessage: "flaky" },
|
|
236
|
+
response: { content: "Sometimes works!" },
|
|
237
|
+
chaos: { dropRate: 0.3 },
|
|
238
|
+
});
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
30% of requests matching this fixture will get a 500 error instead of the response. Can also use `malformedRate` (garbled JSON) or `disconnectRate` (connection dropped mid-stream).
|
|
242
|
+
|
|
243
|
+
Server-level chaos applies to ALL requests:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
mock.setChaos({ dropRate: 0.1 }); // 10% of all requests fail
|
|
247
|
+
mock.clearChaos(); // Remove server-level chaos
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Error injection (one-shot)
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
mock.nextRequestError(429, { message: "Rate limited", type: "rate_limit_error" });
|
|
254
|
+
// Next request gets 429, then fixture auto-removes itself
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### JSON fixture files
|
|
258
|
+
|
|
259
|
+
```json
|
|
260
|
+
{
|
|
261
|
+
"fixtures": [
|
|
262
|
+
{
|
|
263
|
+
"match": { "userMessage": "hello" },
|
|
264
|
+
"response": { "content": "Hi!" }
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
"match": { "inputText": "search query" },
|
|
268
|
+
"response": { "embedding": [0.1, 0.2, 0.3] }
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
"match": { "userMessage": "status", "sequenceIndex": 0 },
|
|
272
|
+
"response": { "content": "First response" }
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
JSON files cannot use `RegExp` or `predicate` — those are code-only features. `streamingProfile` is supported in JSON fixture files.
|
|
279
|
+
|
|
280
|
+
Load with `mock.loadFixtureFile("./fixtures/greetings.json")` or `mock.loadFixtureDir("./fixtures/")`.
|
|
281
|
+
|
|
282
|
+
## API Endpoints
|
|
283
|
+
|
|
284
|
+
All providers share the same fixture pool — write fixtures once, they work for any endpoint.
|
|
285
|
+
|
|
286
|
+
| Endpoint | Provider | Protocol |
|
|
287
|
+
| ---------------------------------------------------------------------------------------- | ------------- | --------- |
|
|
288
|
+
| `POST /v1/chat/completions` | OpenAI | HTTP |
|
|
289
|
+
| `POST /v1/responses` | OpenAI | HTTP + WS |
|
|
290
|
+
| `POST /v1/messages` | Anthropic | HTTP |
|
|
291
|
+
| `POST /v1/embeddings` | OpenAI | HTTP |
|
|
292
|
+
| `POST /v1beta/models/{model}:{method}` | Google Gemini | HTTP |
|
|
293
|
+
| `POST /model/{modelId}/invoke` | AWS Bedrock | HTTP |
|
|
294
|
+
| `POST /openai/deployments/{id}/chat/completions` | Azure OpenAI | HTTP |
|
|
295
|
+
| `POST /openai/deployments/{id}/embeddings` | Azure OpenAI | HTTP |
|
|
296
|
+
| `GET /health` | — | HTTP |
|
|
297
|
+
| `GET /ready` | — | HTTP |
|
|
298
|
+
| `POST /model/{modelId}/invoke-with-response-stream` | AWS Bedrock | HTTP |
|
|
299
|
+
| `POST /model/{modelId}/converse` | AWS Bedrock | HTTP |
|
|
300
|
+
| `POST /model/{modelId}/converse-stream` | AWS Bedrock | HTTP |
|
|
301
|
+
| `POST /v1/projects/{p}/locations/{l}/publishers/google/models/{m}:generateContent` | Vertex AI | HTTP |
|
|
302
|
+
| `POST /v1/projects/{p}/locations/{l}/publishers/google/models/{m}:streamGenerateContent` | Vertex AI | HTTP |
|
|
303
|
+
| `POST /api/chat` | Ollama | HTTP |
|
|
304
|
+
| `POST /api/generate` | Ollama | HTTP |
|
|
305
|
+
| `GET /api/tags` | Ollama | HTTP |
|
|
306
|
+
| `POST /v2/chat` | Cohere | HTTP |
|
|
307
|
+
| `GET /metrics` | — | HTTP |
|
|
308
|
+
| `GET /v1/models` | OpenAI-compat | HTTP |
|
|
309
|
+
| `WS /v1/responses` | OpenAI | WebSocket |
|
|
310
|
+
| `WS /v1/realtime` | OpenAI | WebSocket |
|
|
311
|
+
| `WS /ws/google.ai...BidiGenerateContent` | Gemini Live | WebSocket |
|
|
312
|
+
|
|
313
|
+
## Critical Gotchas
|
|
314
|
+
|
|
315
|
+
1. **Order matters** — first match wins. Specific fixtures before general ones. Use `prependFixture()` to force priority.
|
|
316
|
+
|
|
317
|
+
2. **`arguments` must be a JSON string** — `"arguments": "{\"key\":\"value\"}"` not `"arguments": {"key":"value"}`. The type system enforces this but JSON fixtures can get it wrong silently.
|
|
318
|
+
|
|
319
|
+
3. **Latency is per-chunk, not total** — `latency: 100` means 100ms between each SSE chunk, not 100ms total response time. Similarly, `truncateAfterChunks` and `disconnectAfterMs` are for simulating stream interruptions (added in v1.3.0).
|
|
320
|
+
|
|
321
|
+
4. **`streamingProfile` takes precedence over `latency`** — when both are set on a fixture, `streamingProfile` controls timing. Use one or the other.
|
|
322
|
+
|
|
323
|
+
5. **Tool result messages don't change the user message** — after a tool call, the client sends the same conversation + tool result. Matching on `userMessage` will hit the SAME fixture again → infinite loop. Always use `predicate` checking `role === "tool"` for tool results.
|
|
324
|
+
|
|
325
|
+
6. **`clearFixtures()` preserves the array reference** — uses `.length = 0`, not reassignment. The running server reads the same array object.
|
|
326
|
+
|
|
327
|
+
7. **Journal records everything** — including 404 "no match" responses. Use `mock.getLastRequest()` to debug mismatches.
|
|
328
|
+
|
|
329
|
+
8. **All providers share fixtures** — a fixture matching "hello" works whether the request comes via `/v1/chat/completions` (OpenAI), `/v1/messages` (Anthropic), Gemini, Bedrock, or Azure endpoints.
|
|
330
|
+
|
|
331
|
+
9. **WebSocket uses the same fixture pool** — no special setup needed for WebSocket-based APIs (OpenAI Responses WS, Realtime, Gemini Live).
|
|
332
|
+
|
|
333
|
+
10. **Embeddings auto-generate if no fixture matches** — deterministic vectors are generated from the input text hash. You don't need a catch-all for embedding requests.
|
|
334
|
+
|
|
335
|
+
11. **Sequential response counts are tracked per fixture** — counts reset with `reset()` or `resetMatchCounts()`. The count increments after each match of that fixture group (all fixtures sharing the same non-`sequenceIndex` match fields).
|
|
336
|
+
|
|
337
|
+
12. **Bedrock uses Anthropic Messages format internally** — the adapter normalizes Bedrock requests to `ChatCompletionRequest`, so the same fixtures work. Bedrock supports both non-streaming (`/invoke`, `/converse`) and streaming (`/invoke-with-response-stream`, `/converse-stream`) endpoints.
|
|
338
|
+
|
|
339
|
+
13. **Azure OpenAI routes through the same handlers** — `/openai/deployments/{id}/chat/completions` maps to the completions handler, `/openai/deployments/{id}/embeddings` maps to the embeddings handler. Fixtures work unchanged.
|
|
340
|
+
|
|
341
|
+
14. **Ollama defaults to streaming** — opposite of OpenAI. Set `stream: false` explicitly in the request for non-streaming responses.
|
|
342
|
+
|
|
343
|
+
15. **Ollama tool call `arguments` is an object, not a JSON string** — unlike OpenAI where `arguments` is a JSON string, Ollama sends and expects a plain object.
|
|
344
|
+
|
|
345
|
+
16. **Bedrock streaming uses binary Event Stream format** — not SSE. The `invoke-with-response-stream` and `converse-stream` endpoints use AWS Event Stream binary encoding.
|
|
346
|
+
|
|
347
|
+
17. **Vertex AI routes to the same handler as consumer Gemini** — the same fixtures work for both Vertex AI (`/v1/projects/.../models/{m}:generateContent`) and consumer Gemini (`/v1beta/models/{model}:generateContent`).
|
|
348
|
+
|
|
349
|
+
18. **Cohere requires `model` field** — returns 400 if `model` is missing from the request body.
|
|
350
|
+
|
|
351
|
+
## Mount & Composition
|
|
352
|
+
|
|
353
|
+
### mount() API
|
|
354
|
+
|
|
355
|
+
Mount additional mock services onto a running LLMock server. All services share one port, one health endpoint, and one request journal.
|
|
356
|
+
|
|
357
|
+
```typescript
|
|
358
|
+
const llm = new LLMock({ port: 5555 });
|
|
359
|
+
llm.mount("/mcp", mcpMock); // MCP tools at /mcp
|
|
360
|
+
llm.mount("/a2a", a2aMock); // A2A agents at /a2a
|
|
361
|
+
llm.mount("/vector", vectorMock); // Vector DB at /vector
|
|
362
|
+
await llm.start();
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Any object implementing the `Mountable` interface (a `handleRequest` method that returns `boolean`) can be mounted. Path prefixes are stripped before the service sees the request — `/mcp/tools/list` arrives as `/tools/list`.
|
|
366
|
+
|
|
367
|
+
### createMockSuite()
|
|
368
|
+
|
|
369
|
+
Unified lifecycle for LLMock + mounted services:
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import { createMockSuite } from "@copilotkit/aimock";
|
|
373
|
+
|
|
374
|
+
const suite = createMockSuite({
|
|
375
|
+
port: 0,
|
|
376
|
+
fixtures: "./fixtures",
|
|
377
|
+
services: { "/mcp": mcpMock, "/a2a": a2aMock },
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
await suite.start();
|
|
381
|
+
// suite.llm — the LLMock instance
|
|
382
|
+
// suite.url — base URL
|
|
383
|
+
|
|
384
|
+
afterEach(() => suite.reset()); // resets everything
|
|
385
|
+
afterAll(() => suite.stop());
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### aimock CLI config file
|
|
389
|
+
|
|
390
|
+
The `aimock` CLI reads a JSON config and serves all services on one port:
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
aimock --config aimock.json --port 4010
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Config format:
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"llm": {
|
|
401
|
+
"fixtures": "./fixtures",
|
|
402
|
+
"latency": 0,
|
|
403
|
+
"metrics": true
|
|
404
|
+
},
|
|
405
|
+
"services": {
|
|
406
|
+
"/mcp": { "type": "mcp", "tools": "./mcp-tools.json" },
|
|
407
|
+
"/a2a": { "type": "a2a", "agents": "./a2a-agents.json" }
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
## VectorMock
|
|
413
|
+
|
|
414
|
+
Mock vector database server for testing RAG pipelines. Supports Pinecone, Qdrant, and ChromaDB API formats.
|
|
415
|
+
|
|
416
|
+
```typescript
|
|
417
|
+
import { VectorMock } from "@copilotkit/aimock";
|
|
418
|
+
|
|
419
|
+
const vector = new VectorMock();
|
|
420
|
+
|
|
421
|
+
// Create a collection and register query results
|
|
422
|
+
vector.addCollection("docs", { dimension: 1536 });
|
|
423
|
+
vector.onQuery("docs", [
|
|
424
|
+
{ id: "doc-1", score: 0.95, metadata: { title: "Getting Started" } },
|
|
425
|
+
{ id: "doc-2", score: 0.87, metadata: { title: "API Reference" } },
|
|
426
|
+
]);
|
|
427
|
+
|
|
428
|
+
// Upsert vectors
|
|
429
|
+
vector.upsert("docs", [
|
|
430
|
+
{ id: "v1", values: [0.1, 0.2, ...], metadata: { title: "Intro" } },
|
|
431
|
+
]);
|
|
432
|
+
|
|
433
|
+
// Dynamic query handler
|
|
434
|
+
vector.onQuery("docs", (query) => {
|
|
435
|
+
return [{ id: "result", score: 1.0, metadata: { topK: query.topK } }];
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Standalone or mounted
|
|
439
|
+
const url = await vector.start();
|
|
440
|
+
// Or: llm.mount("/vector", vector);
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
### VectorMock endpoints
|
|
444
|
+
|
|
445
|
+
| Provider | Endpoints |
|
|
446
|
+
| -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
447
|
+
| Pinecone | `POST /query`, `POST /vectors/upsert`, `POST /vectors/delete`, `GET /describe-index-stats` |
|
|
448
|
+
| Qdrant | `POST /collections/{name}/points/search`, `PUT /collections/{name}/points`, `POST /collections/{name}/points/delete` |
|
|
449
|
+
| ChromaDB | `POST /api/v1/collections/{id}/query`, `POST /api/v1/collections/{id}/add`, `GET /api/v1/collections`, `DELETE /api/v1/collections/{id}` |
|
|
450
|
+
|
|
451
|
+
## Service Mocks (Search / Rerank / Moderation)
|
|
452
|
+
|
|
453
|
+
Built-in mocks for common AI-adjacent services. Registered on the LLMock instance directly — no separate server needed.
|
|
454
|
+
|
|
455
|
+
### Search (Tavily-compatible)
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
// POST /search — matches request `query` field
|
|
459
|
+
mock.onSearch("weather", [
|
|
460
|
+
{ title: "Weather Report", url: "https://example.com", content: "Sunny today" },
|
|
461
|
+
]);
|
|
462
|
+
mock.onSearch(/stock\s+price/i, [
|
|
463
|
+
{ title: "ACME Stock", url: "https://example.com", content: "$42", score: 0.95 },
|
|
464
|
+
]);
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Rerank (Cohere-compatible)
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
// POST /v2/rerank — matches request `query` field
|
|
471
|
+
mock.onRerank("machine learning", [
|
|
472
|
+
{ index: 0, relevance_score: 0.99 },
|
|
473
|
+
{ index: 2, relevance_score: 0.85 },
|
|
474
|
+
]);
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### Moderation (OpenAI-compatible)
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
// POST /v1/moderations — matches request `input` field
|
|
481
|
+
mock.onModerate("violent", {
|
|
482
|
+
flagged: true,
|
|
483
|
+
categories: { violence: true, hate: false },
|
|
484
|
+
category_scores: { violence: 0.95, hate: 0.01 },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Catch-all — everything passes
|
|
488
|
+
mock.onModerate(/.*/, { flagged: false, categories: {} });
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Pattern matching
|
|
492
|
+
|
|
493
|
+
All three services use the same matching logic:
|
|
494
|
+
|
|
495
|
+
- **String patterns** — case-insensitive substring match
|
|
496
|
+
- **RegExp patterns** — full regex test
|
|
497
|
+
- **First match wins** — register specific patterns before catch-alls
|
|
498
|
+
|
|
499
|
+
## Debugging Fixture Mismatches
|
|
500
|
+
|
|
501
|
+
When a fixture doesn't match:
|
|
502
|
+
|
|
503
|
+
1. **Inspect what the server received**: `mock.getLastRequest()` → check `body.messages` array
|
|
504
|
+
2. **Check fixture order**: `mock.getFixtures()` returns fixtures in registration order
|
|
505
|
+
3. **For `userMessage`**: match is against the LAST `role: "user"` message only, substring match (not exact)
|
|
506
|
+
4. **Check the journal**: `mock.getRequests()` shows all requests including which fixture matched (or `null` for 404)
|
|
507
|
+
|
|
508
|
+
## E2E Test Setup Pattern
|
|
509
|
+
|
|
510
|
+
```typescript
|
|
511
|
+
import { LLMock } from "@copilotkit/aimock";
|
|
512
|
+
|
|
513
|
+
// Setup — port: 0 picks a random available port
|
|
514
|
+
const mock = new LLMock({ port: 0 });
|
|
515
|
+
mock.loadFixtureDir("./fixtures");
|
|
516
|
+
await mock.start();
|
|
517
|
+
process.env.OPENAI_BASE_URL = `${mock.url}/v1`;
|
|
518
|
+
|
|
519
|
+
// Per-test cleanup
|
|
520
|
+
afterEach(() => mock.reset()); // clears fixtures AND journal
|
|
521
|
+
|
|
522
|
+
// Teardown
|
|
523
|
+
afterAll(async () => await mock.stop());
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Static factory shorthand
|
|
527
|
+
|
|
528
|
+
```typescript
|
|
529
|
+
const mock = await LLMock.create({ port: 0 }); // creates + starts in one call
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
## API Quick Reference
|
|
533
|
+
|
|
534
|
+
| Method | Purpose |
|
|
535
|
+
| --------------------------------------- | ------------------------------------------- |
|
|
536
|
+
| `addFixture(f)` | Append fixture (last priority) |
|
|
537
|
+
| `addFixtures(f[])` | Append multiple |
|
|
538
|
+
| `prependFixture(f)` | Insert at front (highest priority) |
|
|
539
|
+
| `clearFixtures()` | Remove all fixtures |
|
|
540
|
+
| `getFixtures()` | Read current fixture list |
|
|
541
|
+
| `on(match, response, opts?)` | Shorthand for `addFixture` |
|
|
542
|
+
| `onMessage(pattern, response, opts?)` | Match by user message |
|
|
543
|
+
| `onEmbedding(pattern, response, opts?)` | Match by embedding input text |
|
|
544
|
+
| `onJsonOutput(pattern, json, opts?)` | Match by user message with `responseFormat` |
|
|
545
|
+
| `onToolCall(name, response, opts?)` | Match by tool name in `tools[]` |
|
|
546
|
+
| `onToolResult(id, response, opts?)` | Match by `tool_call_id` |
|
|
547
|
+
| `nextRequestError(status, body?)` | One-shot error, auto-removes |
|
|
548
|
+
| `loadFixtureFile(path)` | Load JSON fixture file |
|
|
549
|
+
| `loadFixtureDir(path)` | Load all JSON files in directory |
|
|
550
|
+
| `start()` | Start server, returns URL |
|
|
551
|
+
| `stop()` | Stop server |
|
|
552
|
+
| `reset()` | Clear fixtures + journal + match counts |
|
|
553
|
+
| `resetMatchCounts()` | Clear sequence match counts only |
|
|
554
|
+
| `getRequests()` | All journal entries |
|
|
555
|
+
| `getLastRequest()` | Most recent journal entry |
|
|
556
|
+
| `clearRequests()` | Clear journal only |
|
|
557
|
+
| `setChaos(opts)` | Set server-level chaos rates |
|
|
558
|
+
| `clearChaos()` | Remove server-level chaos |
|
|
559
|
+
| `onSearch(pattern, results)` | Match search requests by query |
|
|
560
|
+
| `onRerank(pattern, results)` | Match rerank requests by query |
|
|
561
|
+
| `onModerate(pattern, result)` | Match moderation requests by input |
|
|
562
|
+
| `mount(path, handler)` | Mount a Mountable (VectorMock, etc.) |
|
|
563
|
+
| `url` / `baseUrl` | Server URL (throws if not started) |
|
|
564
|
+
| `port` | Server port number |
|
|
565
|
+
|
|
566
|
+
Sequential responses use `on()` with `sequenceIndex` in the match — there is no dedicated convenience method.
|
|
567
|
+
|
|
568
|
+
## Record-and-Replay (VCR Mode)
|
|
569
|
+
|
|
570
|
+
llmock supports a VCR-style record-and-replay workflow: unmatched requests are proxied to real provider APIs, and the responses are saved as standard llmock fixture files for deterministic replay.
|
|
571
|
+
|
|
572
|
+
### CLI usage
|
|
573
|
+
|
|
574
|
+
```bash
|
|
575
|
+
# Record mode: proxy unmatched requests to real OpenAI and Anthropic APIs
|
|
576
|
+
llmock --record \
|
|
577
|
+
--provider-openai https://api.openai.com \
|
|
578
|
+
--provider-anthropic https://api.anthropic.com \
|
|
579
|
+
-f ./fixtures
|
|
580
|
+
|
|
581
|
+
# Strict mode: fail on unmatched requests (no proxying, no catch-all 404)
|
|
582
|
+
llmock --strict -f ./fixtures
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
- `--record` enables proxy-on-miss. Requires at least one `--provider-*` flag.
|
|
586
|
+
- `--strict` returns a 503 error when no fixture matches AND no proxy is configured (or the proxy attempt fails), instead of silently returning a 404. The proxy is still tried first when `--record` is set. Use this in CI to prevent unmatched requests from slipping through as silent 404s.
|
|
587
|
+
- Provider flags: `--provider-openai`, `--provider-anthropic`, `--provider-gemini`, `--provider-vertexai`, `--provider-bedrock`, `--provider-azure`, `--provider-ollama`, `--provider-cohere`.
|
|
588
|
+
|
|
589
|
+
### How it works
|
|
590
|
+
|
|
591
|
+
1. **Existing fixtures are served first** — the router checks all loaded fixtures before considering the proxy.
|
|
592
|
+
2. **Misses are proxied** — if no fixture matches and recording is enabled, the request is forwarded to the real provider API. Upstream URL path prefixes are preserved (e.g., `https://gateway.company.com/llm/v1` correctly proxies to `/llm/v1/chat/completions`).
|
|
593
|
+
3. **All request headers are forwarded (auth headers NOT saved)** — all client request headers are passed through to the upstream provider, except hop-by-hop headers and `host`/`content-length`/`cookie`/`accept-encoding`. Auth headers (`Authorization`, `x-api-key`, `api-key`) are forwarded but stripped from the recorded fixture.
|
|
594
|
+
4. **Responses are saved as standard fixtures** — recorded files land in `{fixturePath}/recorded/` and use the same JSON format as hand-written fixtures. Nothing special about them.
|
|
595
|
+
5. **Streaming responses are collapsed** — SSE streams are collapsed into a single text or tool-call response for the fixture. The original streaming format is preserved in the live proxy response.
|
|
596
|
+
6. **Base64 embedding decoding** — when the upstream returns base64-encoded embeddings (the default `encoding_format` in Python's openai SDK), the recorder decodes them into float arrays so fixtures contain readable numeric data instead of opaque base64 strings.
|
|
597
|
+
7. **Loud logging** — every proxy hit logs at `warn` level so you can see exactly which requests are being forwarded.
|
|
598
|
+
|
|
599
|
+
### Programmatic API
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
const mock = new LLMock({ port: 0 });
|
|
603
|
+
await mock.start();
|
|
604
|
+
|
|
605
|
+
// Enable recording at runtime
|
|
606
|
+
mock.enableRecording({
|
|
607
|
+
providers: {
|
|
608
|
+
openai: "https://api.openai.com",
|
|
609
|
+
anthropic: "https://api.anthropic.com",
|
|
610
|
+
},
|
|
611
|
+
fixturePath: "./fixtures/recorded",
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// ... run tests that hit real APIs for uncovered cases ...
|
|
615
|
+
|
|
616
|
+
// Disable recording (back to fixture-only mode)
|
|
617
|
+
mock.disableRecording();
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### Workflow
|
|
621
|
+
|
|
622
|
+
1. **Bootstrap**: Run your test suite with `--record` and provider URLs. All requests that don't match existing fixtures are proxied and recorded.
|
|
623
|
+
2. **Review**: Check the recorded fixtures in `{fixturePath}/recorded/`. Edit or reorganize as needed.
|
|
624
|
+
3. **Lock down**: Run your test suite with `--strict` to ensure every request hits a fixture. No network calls escape.
|
|
625
|
+
4. **Maintain**: When APIs change, delete stale fixtures and re-record.
|