Skip to content

Commit bc047da

Browse files
committed
Add README and MIT license
Comprehensive docs covering install, quick start, programmatic API, fixture matching, fixture responses, fixture files, CLI usage, and advanced usage patterns.
1 parent 2e2ccec commit bc047da

2 files changed

Lines changed: 328 additions & 0 deletions

File tree

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 CopilotKit
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
# @copilotkit/mock-openai
2+
3+
Deterministic mock OpenAI server for testing. Streams SSE responses in real OpenAI chat completion format, driven entirely by fixtures. Zero runtime dependencies — built on Node.js builtins only.
4+
5+
## Install
6+
7+
```bash
8+
npm install @copilotkit/mock-openai
9+
```
10+
11+
## Quick Start
12+
13+
```typescript
14+
import { MockOpenAI } from "@copilotkit/mock-openai";
15+
16+
const mock = new MockOpenAI();
17+
18+
mock.onMessage("hello", { content: "Hi there!" });
19+
20+
const url = await mock.start();
21+
// Point your OpenAI client at `url` instead of https://api.openai.com
22+
23+
// ... run your tests ...
24+
25+
await mock.stop();
26+
```
27+
28+
## Programmatic API
29+
30+
### `new MockOpenAI(options?)`
31+
32+
Create a new mock server instance.
33+
34+
| Option | Type | Default | Description |
35+
|---|---|---|---|
36+
| `port` | `number` | `0` (random) | Port to listen on |
37+
| `host` | `string` | `"127.0.0.1"` | Host to bind to |
38+
| `latency` | `number` | `0` | Default ms delay between SSE chunks |
39+
| `chunkSize` | `number` | `20` | Default characters per SSE chunk |
40+
41+
### `MockOpenAI.create(options?)`
42+
43+
Static factory — creates an instance and starts it in one call. Returns `Promise<MockOpenAI>`.
44+
45+
### Server Lifecycle
46+
47+
| Method | Returns | Description |
48+
|---|---|---|
49+
| `start()` | `Promise<string>` | Start the server, returns the base URL |
50+
| `stop()` | `Promise<void>` | Stop the server |
51+
| `url` | `string` | Base URL (throws if not started) |
52+
| `baseUrl` | `string` | Alias for `url` |
53+
| `port` | `number` | Listening port (throws if not started) |
54+
55+
### Fixture Registration
56+
57+
All registration methods return `this` for chaining.
58+
59+
#### `on(match, response, opts?)`
60+
61+
Register a fixture with full control over match criteria.
62+
63+
```typescript
64+
mock.on(
65+
{ userMessage: /weather/i, model: "gpt-4" },
66+
{ content: "It's sunny!" },
67+
{ latency: 50 },
68+
);
69+
```
70+
71+
#### `onMessage(pattern, response, opts?)`
72+
73+
Shorthand — matches on the last user message.
74+
75+
```typescript
76+
mock.onMessage("hello", { content: "Hi!" });
77+
mock.onMessage(/greet/i, { content: "Hey there!" });
78+
```
79+
80+
#### `onToolCall(name, response, opts?)`
81+
82+
Shorthand — matches when the request contains a tool call with the given name.
83+
84+
```typescript
85+
mock.onToolCall("get_weather", {
86+
toolCalls: [{ name: "get_weather", arguments: '{"location":"SF"}' }],
87+
});
88+
```
89+
90+
#### `onToolResult(id, response, opts?)`
91+
92+
Shorthand — matches when a tool result message has the given `tool_call_id`.
93+
94+
```typescript
95+
mock.onToolResult("call_abc123", { content: "Temperature is 72F" });
96+
```
97+
98+
#### `addFixture(fixture)` / `addFixtures(fixtures)`
99+
100+
Add raw `Fixture` objects directly.
101+
102+
#### `loadFixtureFile(path)` / `loadFixtureDir(path)`
103+
104+
Load fixtures from JSON files on disk. See [Fixture Files](#fixture-files) below.
105+
106+
#### `clearFixtures()`
107+
108+
Remove all registered fixtures.
109+
110+
### Error Injection
111+
112+
#### `nextRequestError(status, errorBody?)`
113+
114+
Queue a one-shot error for the very next request. The error fires once, then auto-removes itself.
115+
116+
```typescript
117+
mock.nextRequestError(429, {
118+
message: "Rate limited",
119+
type: "rate_limit_error",
120+
});
121+
122+
// Next request → 429 error
123+
// Subsequent requests → normal fixture matching
124+
```
125+
126+
### Request Journal
127+
128+
Every request to `/v1/chat/completions` is recorded in a journal.
129+
130+
#### Programmatic Access
131+
132+
| Method | Returns | Description |
133+
|---|---|---|
134+
| `getRequests()` | `JournalEntry[]` | All recorded requests |
135+
| `getLastRequest()` | `JournalEntry \| null` | Most recent request |
136+
| `clearRequests()` | `void` | Clear the journal |
137+
| `journal` | `Journal` | Direct access to the journal instance |
138+
139+
```typescript
140+
await fetch(mock.url + "/v1/chat/completions", { ... });
141+
142+
const last = mock.getLastRequest();
143+
expect(last?.body.messages).toContainEqual({
144+
role: "user",
145+
content: "hello",
146+
});
147+
```
148+
149+
#### HTTP Endpoints
150+
151+
The server also exposes journal data over HTTP (useful in CLI mode):
152+
153+
- `GET /v1/_requests` — returns all journal entries as JSON. Supports `?limit=N`.
154+
- `DELETE /v1/_requests` — clears the journal. Returns 204.
155+
156+
### Reset
157+
158+
#### `reset()`
159+
160+
Clear all fixtures **and** the journal in one call. Works before or after the server is started.
161+
162+
```typescript
163+
afterEach(() => {
164+
mock.reset();
165+
});
166+
```
167+
168+
## Fixture Matching
169+
170+
Fixtures are evaluated in registration order (first match wins). A fixture matches when **all** specified fields match the incoming request (AND logic).
171+
172+
| Field | Type | Matches on |
173+
|---|---|---|
174+
| `userMessage` | `string \| RegExp` | Content of the last `role: "user"` message |
175+
| `toolName` | `string` | Name of a tool in the request's `tools` array |
176+
| `toolCallId` | `string` | `tool_call_id` on a `role: "tool"` message |
177+
| `model` | `string \| RegExp` | The `model` field in the request |
178+
| `predicate` | `(req) => boolean` | Arbitrary matching function |
179+
180+
## Fixture Responses
181+
182+
### Text
183+
184+
```typescript
185+
{ content: "Hello world" }
186+
```
187+
188+
Streams as SSE chunks, splitting `content` by `chunkSize`.
189+
190+
### Tool Calls
191+
192+
```typescript
193+
{
194+
toolCalls: [
195+
{ name: "get_weather", arguments: '{"location":"SF"}' }
196+
]
197+
}
198+
```
199+
200+
### Errors
201+
202+
```typescript
203+
{
204+
error: { message: "Rate limited", type: "rate_limit_error" },
205+
status: 429
206+
}
207+
```
208+
209+
## Fixture Files
210+
211+
Fixtures can be defined in JSON files for use with the CLI or `loadFixtureFile`/`loadFixtureDir`.
212+
213+
```json
214+
{
215+
"fixtures": [
216+
{
217+
"match": { "userMessage": "hello" },
218+
"response": { "content": "Hello! How can I help you today?" }
219+
},
220+
{
221+
"match": { "toolName": "get_weather" },
222+
"response": {
223+
"toolCalls": [
224+
{
225+
"name": "get_weather",
226+
"arguments": "{\"location\":\"San Francisco\"}"
227+
}
228+
]
229+
}
230+
}
231+
]
232+
}
233+
```
234+
235+
Each entry can also include `latency` and `chunkSize` overrides.
236+
237+
## CLI
238+
239+
The package includes a standalone server binary:
240+
241+
```bash
242+
mock-openai [options]
243+
```
244+
245+
| Option | Short | Default | Description |
246+
|---|---|---|---|
247+
| `--port` | `-p` | `4010` | Port to listen on |
248+
| `--host` | `-h` | `127.0.0.1` | Host to bind to |
249+
| `--fixtures` | `-f` | `./fixtures` | Path to fixtures directory or file |
250+
| `--latency` | `-l` | `0` | Latency between SSE chunks (ms) |
251+
| `--chunk-size` | `-c` | `20` | Characters per SSE chunk |
252+
| `--help` | | | Show help |
253+
254+
```bash
255+
# Start with bundled example fixtures
256+
mock-openai
257+
258+
# Custom fixtures on a specific port
259+
mock-openai -p 8080 -f ./my-fixtures
260+
261+
# Simulate slow responses
262+
mock-openai --latency 100 --chunk-size 5
263+
```
264+
265+
## Advanced Usage
266+
267+
### Low-level Server
268+
269+
If you need the raw HTTP server without the `MockOpenAI` wrapper:
270+
271+
```typescript
272+
import { createServer } from "@copilotkit/mock-openai";
273+
274+
const fixtures = [
275+
{ match: { userMessage: "hi" }, response: { content: "Hello!" } },
276+
];
277+
278+
const { server, journal, url } = await createServer(fixtures, { port: 0 });
279+
// ... use it ...
280+
server.close();
281+
```
282+
283+
### Custom Matching with Predicates
284+
285+
```typescript
286+
mock.on(
287+
{
288+
predicate: (req) =>
289+
req.messages.length > 5 && req.model.startsWith("gpt-4"),
290+
},
291+
{ content: "You've been chatting a while!" },
292+
);
293+
```
294+
295+
### Per-Fixture Timing
296+
297+
```typescript
298+
mock.on(
299+
{ userMessage: "slow" },
300+
{ content: "Finally..." },
301+
{ latency: 200, chunkSize: 5 },
302+
);
303+
```
304+
305+
## License
306+
307+
MIT

0 commit comments

Comments
 (0)