|
1 | | -# Mocktopus 🐙 |
| 1 | +# 🐙 Mocktopus |
2 | 2 |
|
3 | | -**Multi‑armed mocks for LLM apps.** Deterministic, dataset‑driven mocks for OpenAI‑style chat |
4 | | -completions (plus tool calls & streaming simulation). Designed for evals, CI, and reproducible tests. |
| 3 | +> Multi-armed mocks for LLM apps |
5 | 4 |
|
6 | | -## Why |
7 | | -- Make flaky LLM tests deterministic. |
8 | | -- Run evals offline and in CI without credentials. |
9 | | -- Record once, replay many times (golden fixtures). |
10 | | -- Simulate streaming and tool calls without hitting real APIs. |
| 5 | +**Mocktopus** is a drop-in replacement for OpenAI/Anthropic APIs, designed to make your LLM application tests fast, deterministic, and cost-free. |
11 | 6 |
|
12 | | -## Features (MVP) |
13 | | -- 🧪 `Scenario` that loads rules from YAML and returns canned LLM responses. |
14 | | -- 🧵 Streaming simulation compatible with `stream=True` style iteration. |
15 | | -- 🧰 Tool call stubs (`assistant.tool_calls`) with optional structured args. |
16 | | -- 🧩 Two ways to use: |
17 | | - 1. **Dependency‑injected client**: `OpenAIStubClient(scenario)` |
18 | | - 2. **Monkey‑patch** (best‑effort): `with patch_openai(scenario): ...` |
| 7 | +[](https://github.com/evalops/mocktopus/actions) |
| 8 | +[](https://pypi.org/project/mocktopus/) |
| 9 | +[](https://opensource.org/licenses/MIT) |
19 | 10 |
|
20 | | -> Patching SDK internals can be brittle across versions. Prefer dependency injection for reliability. |
| 11 | +## Why Mocktopus? |
21 | 12 |
|
22 | | -Roadmap: HTTP fixtures (VCR‑like), vector/RAG stubs, Anthropic adapter, record mode. |
| 13 | +Testing LLM applications is challenging: |
| 14 | +- **Non-deterministic**: Same prompt, different responses |
| 15 | +- **Expensive**: Every test run costs API credits |
| 16 | +- **Slow**: API calls add latency to test suites |
| 17 | +- **Network-dependent**: Can't run tests offline |
| 18 | +- **Complex workflows**: Tool calls and streaming complicate testing |
| 19 | + |
| 20 | +Mocktopus solves these problems by providing a local mock server that perfectly mimics LLM APIs. |
| 21 | + |
| 22 | +## Features |
| 23 | + |
| 24 | +✅ **Drop-in replacement** - Just change your base URL |
| 25 | +✅ **Deterministic responses** - Same input → same output |
| 26 | +✅ **Tool/function calling** - Full support for complex workflows |
| 27 | +✅ **Streaming** - Server-sent events (SSE) support |
| 28 | +✅ **Multiple providers** - OpenAI and Anthropic compatible |
| 29 | +✅ **Zero cost** - No API charges for tests |
| 30 | +✅ **Fast** - No network latency |
| 31 | +✅ **Offline** - Run tests without internet |
| 32 | + |
| 33 | +## Installation |
23 | 34 |
|
24 | | -## Install |
25 | 35 | ```bash |
26 | | -pip install -e ".[dev]" # when cloned locally |
27 | | -# or just install from source once published: |
28 | | -# pip install mocktopus |
| 36 | +pip install mocktopus |
29 | 37 | ``` |
30 | 38 |
|
31 | | -## Quick start |
| 39 | +## Quick Start |
32 | 40 |
|
33 | | -1) Define a fixture: |
| 41 | +### 1. Create a scenario file (`scenario.yaml`): |
34 | 42 |
|
35 | 43 | ```yaml |
36 | | -# examples/haiku.yaml |
37 | 44 | version: 1 |
38 | 45 | rules: |
39 | 46 | - type: llm.openai |
40 | 47 | when: |
41 | | - messages_contains: "haiku" |
42 | | - model: "*" |
| 48 | + model: "gpt-4*" |
| 49 | + messages_contains: "hello" |
43 | 50 | respond: |
44 | | - content: "Silent bay at dusk\nEight arms fold into the deep\nTides keep time for stars." |
45 | | - usage: |
46 | | - input_tokens: 12 |
47 | | - output_tokens: 17 |
48 | | - stream: false |
| 51 | + content: "Hello! How can I help you today?" |
| 52 | +``` |
| 53 | +
|
| 54 | +### 2. Start the mock server: |
| 55 | +
|
| 56 | +```bash |
| 57 | +mocktopus serve -s scenario.yaml |
49 | 58 | ``` |
50 | 59 |
|
51 | | -2) Use the dependency‑injected stub client: |
| 60 | +### 3. Point your app to Mocktopus: |
52 | 61 |
|
53 | 62 | ```python |
54 | | -from mocktopus import Scenario, OpenAIStubClient, load_yaml |
| 63 | +from openai import OpenAI |
55 | 64 |
|
56 | | -scenario = load_yaml("examples/haiku.yaml") |
57 | | -client = OpenAIStubClient(scenario) |
| 65 | +# Instead of the real API: |
| 66 | +# client = OpenAI(api_key="sk-...") |
58 | 67 |
|
59 | | -resp = client.chat.completions.create( |
60 | | - model="gpt-4o-mini", |
61 | | - messages=[{"role": "user", "content": "Write a haiku about an octopus"}], |
| 68 | +# Use Mocktopus: |
| 69 | +client = OpenAI( |
| 70 | + base_url="http://localhost:8080/v1", |
| 71 | + api_key="mock-key" # Any string works |
62 | 72 | ) |
63 | 73 |
|
64 | | -print(resp.choices[0].message.content) |
65 | | -# -> Silent bay at dusk ... |
| 74 | +response = client.chat.completions.create( |
| 75 | + model="gpt-4", |
| 76 | + messages=[{"role": "user", "content": "hello"}] |
| 77 | +) |
| 78 | +print(response.choices[0].message.content) |
| 79 | +# Output: "Hello! How can I help you today?" |
66 | 80 | ``` |
67 | 81 |
|
68 | | -3) Or (beta) patch the OpenAI SDK dynamically: |
| 82 | +## Usage Modes |
69 | 83 |
|
70 | | -```python |
71 | | -from mocktopus import load_yaml, patch_openai |
72 | | -from openai import OpenAI |
| 84 | +### Mock Mode (Default) |
| 85 | +Use predefined YAML scenarios for deterministic responses: |
73 | 86 |
|
74 | | -scenario = load_yaml("examples/haiku.yaml") |
75 | | -with patch_openai(scenario): |
76 | | - client = OpenAI() |
77 | | - resp = client.chat.completions.create( |
78 | | - model="gpt-4o-mini", |
79 | | - messages=[{"role": "user", "content": "Write a haiku about an octopus"}], |
80 | | - ) |
81 | | - print(resp.choices[0].message.content) |
| 87 | +```bash |
| 88 | +mocktopus serve -s examples/chat-basic.yaml |
82 | 89 | ``` |
83 | 90 |
|
84 | | -4) Streaming simulation: |
| 91 | +### Record Mode (Coming Soon) |
| 92 | +Proxy and record real API calls for later replay: |
85 | 93 |
|
86 | | -```python |
87 | | -resp = client.chat.completions.create( |
88 | | - model="gpt-4o-mini", |
89 | | - messages=[{"role": "user", "content": "haiku please"}], |
90 | | - stream=True, |
91 | | -) |
92 | | -for event in resp: |
93 | | - delta = event.choices[0].delta.content or "" |
94 | | - print(delta, end="") |
| 94 | +```bash |
| 95 | +mocktopus serve --mode record --recordings-dir ./recordings |
95 | 96 | ``` |
96 | 97 |
|
97 | | -## Pytest usage |
| 98 | +### Replay Mode (Coming Soon) |
| 99 | +Replay previously recorded API interactions: |
98 | 100 |
|
99 | | -Add to your test conftest: |
100 | | -```python |
101 | | -pytest_plugins = ["mocktopus.pytest_plugin"] |
| 101 | +```bash |
| 102 | +mocktopus serve --mode replay --recordings-dir ./recordings |
102 | 103 | ``` |
103 | 104 |
|
104 | | -Example test: |
| 105 | +## Scenario Examples |
| 106 | + |
| 107 | +### Basic Chat Response |
| 108 | + |
| 109 | +```yaml |
| 110 | +version: 1 |
| 111 | +rules: |
| 112 | + - type: llm.openai |
| 113 | + when: |
| 114 | + messages_contains: "weather" |
| 115 | + respond: |
| 116 | + content: "It's sunny today!" |
| 117 | +``` |
| 118 | +
|
| 119 | +### Function Calling |
| 120 | +
|
| 121 | +```yaml |
| 122 | +version: 1 |
| 123 | +rules: |
| 124 | + - type: llm.openai |
| 125 | + when: |
| 126 | + messages_contains: "weather" |
| 127 | + respond: |
| 128 | + tool_calls: |
| 129 | + - id: "call_123" |
| 130 | + type: "function" |
| 131 | + function: |
| 132 | + name: "get_weather" |
| 133 | + arguments: '{"location": "San Francisco"}' |
| 134 | +``` |
| 135 | +
|
| 136 | +### Streaming Response |
| 137 | +
|
| 138 | +```yaml |
| 139 | +version: 1 |
| 140 | +rules: |
| 141 | + - type: llm.openai |
| 142 | + when: |
| 143 | + model: "*" |
| 144 | + respond: |
| 145 | + content: "This will be streamed..." |
| 146 | + delay_ms: 50 # Delay between chunks |
| 147 | + chunk_size: 5 # Characters per chunk |
| 148 | +``` |
| 149 | +
|
| 150 | +### Limited Usage |
| 151 | +
|
| 152 | +```yaml |
| 153 | +version: 1 |
| 154 | +rules: |
| 155 | + - type: llm.openai |
| 156 | + when: |
| 157 | + messages_contains: "test" |
| 158 | + times: 3 # Only responds 3 times |
| 159 | + respond: |
| 160 | + content: "Limited response" |
| 161 | +``` |
| 162 | +
|
| 163 | +## CLI Commands |
| 164 | +
|
| 165 | +### Start Server |
| 166 | +```bash |
| 167 | +# Basic usage |
| 168 | +mocktopus serve -s scenario.yaml |
| 169 | + |
| 170 | +# Custom port |
| 171 | +mocktopus serve -s scenario.yaml -p 9000 |
| 172 | + |
| 173 | +# Verbose logging |
| 174 | +mocktopus serve -s scenario.yaml -v |
| 175 | +``` |
| 176 | + |
| 177 | +### Test Scenarios |
| 178 | +```bash |
| 179 | +# Validate a scenario file |
| 180 | +mocktopus validate scenario.yaml |
| 181 | + |
| 182 | +# Simulate a request without starting server |
| 183 | +mocktopus simulate -s scenario.yaml --prompt "Hello" |
| 184 | + |
| 185 | +# Generate example scenarios |
| 186 | +mocktopus example --type basic > my-scenario.yaml |
| 187 | +mocktopus example --type tools > tools-scenario.yaml |
| 188 | +``` |
| 189 | + |
| 190 | +## Testing with Mocktopus |
| 191 | + |
| 192 | +### Pytest Integration |
| 193 | + |
105 | 194 | ```python |
106 | | -def test_haiku(use_mocktopus): |
107 | | - use_mocktopus.load_yaml("examples/haiku.yaml") |
108 | | - client = use_mocktopus.openai_client() # OpenAIStubClient bound to the scenario |
109 | | - out = client.chat.completions.create( |
110 | | - model="gpt-4o-mini", |
111 | | - messages=[{"role": "user", "content": "haiku"}], |
| 195 | +import pytest |
| 196 | +from mocktopus import use_mocktopus |
| 197 | + |
| 198 | +def test_my_llm_app(use_mocktopus): |
| 199 | + # Load scenario |
| 200 | + use_mocktopus.load_yaml("tests/scenarios/test.yaml") |
| 201 | + |
| 202 | + # Get a client |
| 203 | + client = use_mocktopus.openai_client() |
| 204 | + |
| 205 | + # Test your app |
| 206 | + response = client.chat.completions.create( |
| 207 | + model="gpt-4", |
| 208 | + messages=[{"role": "user", "content": "test"}] |
112 | 209 | ) |
113 | | - assert "Silent bay" in out.choices[0].message.content |
| 210 | + assert "expected" in response.choices[0].message.content |
114 | 211 | ``` |
115 | 212 |
|
116 | | -## CLI |
| 213 | +### Continuous Integration |
117 | 214 |
|
118 | | -```bash |
119 | | -mocktopus simulate --fixture examples/haiku.yaml --prompt "haiku about octopus" |
| 215 | +```yaml |
| 216 | +# .github/workflows/test.yml |
| 217 | +name: Tests |
| 218 | +on: [push, pull_request] |
| 219 | + |
| 220 | +jobs: |
| 221 | + test: |
| 222 | + runs-on: ubuntu-latest |
| 223 | + steps: |
| 224 | + - uses: actions/checkout@v4 |
| 225 | + - uses: actions/setup-python@v5 |
| 226 | + - run: pip install -e . |
| 227 | + - run: mocktopus serve -s tests/scenarios.yaml & |
| 228 | + - run: pytest # Your tests hit localhost:8080 |
120 | 229 | ``` |
121 | 230 |
|
122 | | -## Caveats |
123 | | -- The OpenAI SDK patcher targets modern `openai` Python SDKs and may break across versions. Prefer the stub client where possible. |
124 | | -- YAML matching is currently simple (substring + model glob). Extend as needed. |
| 231 | +## Advanced Features |
| 232 | +
|
| 233 | +### Pattern Matching |
| 234 | +
|
| 235 | +Mocktopus supports multiple matching strategies: |
| 236 | +
|
| 237 | +- **Exact match**: `messages_contains: "exact phrase"` |
| 238 | +- **Regex**: `messages_regex: "\\d+ items?"` |
| 239 | +- **Glob**: `model: "gpt-4*"` |
| 240 | + |
| 241 | +### Response Configuration |
| 242 | + |
| 243 | +```yaml |
| 244 | +respond: |
| 245 | + content: "Response text" |
| 246 | + delay_ms: 100 # Simulate latency |
| 247 | + usage: |
| 248 | + input_tokens: 10 |
| 249 | + output_tokens: 20 |
| 250 | + # For streaming |
| 251 | + chunk_size: 10 # Characters per chunk |
| 252 | +``` |
| 253 | + |
| 254 | +## Roadmap |
| 255 | + |
| 256 | +- [x] OpenAI chat completions API |
| 257 | +- [x] Streaming support (SSE) |
| 258 | +- [x] Function/tool calling |
| 259 | +- [x] Anthropic messages API |
| 260 | +- [ ] Recording & replay |
| 261 | +- [ ] Embeddings API |
| 262 | +- [ ] Assistants API |
| 263 | +- [ ] Image generation |
| 264 | +- [ ] Semantic similarity matching |
| 265 | +- [ ] Response templating |
| 266 | +- [ ] Load testing mode |
| 267 | + |
| 268 | +## Contributing |
| 269 | + |
| 270 | +We welcome contributions! See our [Contributing Guide](CONTRIBUTING.md) for details. |
125 | 271 |
|
126 | 272 | ## License |
127 | | -MIT |
| 273 | + |
| 274 | +MIT - See [LICENSE](LICENSE) for details. |
| 275 | + |
| 276 | +## Links |
| 277 | + |
| 278 | +- [GitHub Repository](https://github.com/evalops/mocktopus) |
| 279 | +- [PyPI Package](https://pypi.org/project/mocktopus/) |
| 280 | +- [Documentation](https://github.com/evalops/mocktopus/wiki) |
| 281 | +- [Issue Tracker](https://github.com/evalops/mocktopus/issues) |
| 282 | + |
| 283 | +--- |
| 284 | + |
| 285 | +Made with 🐙 by [EvalOps](https://github.com/evalops) |
0 commit comments