llm-mock 0.1.0__tar.gz
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.
- llm_mock-0.1.0/LICENSE +21 -0
- llm_mock-0.1.0/PKG-INFO +423 -0
- llm_mock-0.1.0/README.md +405 -0
- llm_mock-0.1.0/llm_mock/__init__.py +4 -0
- llm_mock-0.1.0/llm_mock/cli.py +85 -0
- llm_mock-0.1.0/llm_mock/context_manager.py +26 -0
- llm_mock-0.1.0/llm_mock/fixture_store.py +67 -0
- llm_mock-0.1.0/llm_mock/matcher.py +11 -0
- llm_mock-0.1.0/llm_mock/providers/__init__.py +0 -0
- llm_mock-0.1.0/llm_mock/providers/anthropic.py +60 -0
- llm_mock-0.1.0/llm_mock/providers/openai.py +56 -0
- llm_mock-0.1.0/llm_mock/pytest_plugin.py +40 -0
- llm_mock-0.1.0/llm_mock.egg-info/PKG-INFO +423 -0
- llm_mock-0.1.0/llm_mock.egg-info/SOURCES.txt +24 -0
- llm_mock-0.1.0/llm_mock.egg-info/dependency_links.txt +1 -0
- llm_mock-0.1.0/llm_mock.egg-info/entry_points.txt +5 -0
- llm_mock-0.1.0/llm_mock.egg-info/requires.txt +10 -0
- llm_mock-0.1.0/llm_mock.egg-info/top_level.txt +1 -0
- llm_mock-0.1.0/pyproject.toml +40 -0
- llm_mock-0.1.0/setup.cfg +4 -0
- llm_mock-0.1.0/tests/test_cli.py +115 -0
- llm_mock-0.1.0/tests/test_context_manager.py +212 -0
- llm_mock-0.1.0/tests/test_fixture_store.py +68 -0
- llm_mock-0.1.0/tests/test_hello.py +14 -0
- llm_mock-0.1.0/tests/test_matcher.py +36 -0
- llm_mock-0.1.0/tests/test_pytest_plugin.py +119 -0
llm_mock-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Roman Tokmakov
|
|
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.
|
llm_mock-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-mock
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Record and replay LLM API calls for deterministic testing
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Dist: httpx>=0.27
|
|
9
|
+
Requires-Dist: respx>=0.21
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
14
|
+
Requires-Dist: anthropic>=0.25; extra == "dev"
|
|
15
|
+
Requires-Dist: openai>=1.30; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# llm-mock
|
|
20
|
+
|
|
21
|
+
Record real LLM responses once, replay them in tests forever — no API key required, no cost, no non-determinism.
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
# Record once against the real API (run locally with your API key)
|
|
25
|
+
with llm_mock(mode="record", fixture="tests/fixtures/summarize"):
|
|
26
|
+
result = my_pipeline("Summarize this document...")
|
|
27
|
+
|
|
28
|
+
# Replay in tests — no API key, no cost, deterministic
|
|
29
|
+
@pytest.mark.llm_replay(fixture="summarize")
|
|
30
|
+
def test_summarize():
|
|
31
|
+
result = my_pipeline("Summarize this document...")
|
|
32
|
+
assert "key points" in result
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Why
|
|
38
|
+
|
|
39
|
+
- **API calls during tests are expensive.** A CI run hitting real LLM APIs can cost dollars per run at scale.
|
|
40
|
+
- **LLM outputs are non-deterministic.** Even at `temperature=0`, responses can vary across model versions.
|
|
41
|
+
- **Your production code stays untouched.** llm-mock intercepts at the HTTP transport layer — no changes to application code required.
|
|
42
|
+
|
|
43
|
+
llm-mock records and replays at the structured request level (model + messages + temperature), stores human-readable JSON fixtures, and integrates natively with pytest.
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# PyPI release coming soon — install from source for now:
|
|
51
|
+
git clone https://github.com/autopost/llm-mock.git
|
|
52
|
+
cd llm-mock
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Runtime dependencies:** `httpx`, `respx`, `pydantic`
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## How to use
|
|
61
|
+
|
|
62
|
+
### Your production code — untouched
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
# my_app/pipeline.py
|
|
66
|
+
import anthropic
|
|
67
|
+
|
|
68
|
+
client = anthropic.Anthropic()
|
|
69
|
+
|
|
70
|
+
def summarize(text: str) -> str:
|
|
71
|
+
message = client.messages.create(
|
|
72
|
+
model="claude-sonnet-4-6",
|
|
73
|
+
max_tokens=100,
|
|
74
|
+
messages=[{"role": "user", "content": f"Summarize: {text}"}],
|
|
75
|
+
)
|
|
76
|
+
return message.content[0].text
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`pipeline.py` has zero knowledge of llm-mock. No imports, no changes needed.
|
|
80
|
+
|
|
81
|
+
### Step 1 — Record (run once, locally)
|
|
82
|
+
|
|
83
|
+
Create a small script or a dedicated test that runs with `mode="record"`. You need a real API key for this step.
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
# record_fixtures.py
|
|
87
|
+
from llm_mock import llm_mock
|
|
88
|
+
from my_app.pipeline import summarize
|
|
89
|
+
|
|
90
|
+
with llm_mock(mode="record", fixture="tests/fixtures/summarize"):
|
|
91
|
+
result = summarize("Long article about climate change...")
|
|
92
|
+
print(result) # real response from the API
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
ANTHROPIC_API_KEY=sk-... python record_fixtures.py
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
This creates `tests/fixtures/summarize.json`. **Commit this file to git.**
|
|
100
|
+
|
|
101
|
+
### Step 2 — Replay (in tests, forever)
|
|
102
|
+
|
|
103
|
+
Use the pytest decorator — no `with` block needed inside the test:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
# tests/test_pipeline.py
|
|
107
|
+
import pytest
|
|
108
|
+
from my_app.pipeline import summarize
|
|
109
|
+
|
|
110
|
+
@pytest.mark.llm_replay(fixture="summarize")
|
|
111
|
+
def test_summarize():
|
|
112
|
+
result = summarize("Long article about climate change...")
|
|
113
|
+
assert "climate" in result
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pytest # no API key needed, runs offline, instant
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
The decorator auto-discovers the fixture path relative to the test file — `fixture="summarize"` looks for `tests/fixtures/summarize.json` when the test lives in `tests/`.
|
|
121
|
+
|
|
122
|
+
llm-mock intercepts the httpx call the Anthropic SDK makes internally and returns the saved response — your test code calls `summarize()` exactly as it would in production.
|
|
123
|
+
|
|
124
|
+
**Alternative:** use the context manager directly if you need more control:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from llm_mock import llm_mock
|
|
128
|
+
|
|
129
|
+
def test_summarize():
|
|
130
|
+
with llm_mock(mode="replay", fixture="tests/fixtures/summarize"):
|
|
131
|
+
result = summarize("Long article about climate change...")
|
|
132
|
+
assert "climate" in result
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Step 3 — Re-record when things change
|
|
136
|
+
|
|
137
|
+
If you change the prompt, update the model, or want to refresh fixtures:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
ANTHROPIC_API_KEY=sk-... python record_fixtures.py # overwrites old fixture
|
|
141
|
+
git add tests/fixtures/summarize.json
|
|
142
|
+
git commit -m "refresh summarize fixture"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Quick start (direct API usage)
|
|
148
|
+
|
|
149
|
+
A complete working example from scratch.
|
|
150
|
+
|
|
151
|
+
### 1. Install
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git clone https://github.com/yourname/llm-mock
|
|
155
|
+
cd llm-mock
|
|
156
|
+
python -m venv .venv && source .venv/bin/activate
|
|
157
|
+
pip install -e ".[dev]"
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 2. Save your API key
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
echo 'export ANTHROPIC_API_KEY=sk-ant-api03-...' > .env
|
|
164
|
+
echo '.env' >> .gitignore
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### 3. Create a record script
|
|
168
|
+
|
|
169
|
+
Create `try_record.py`:
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
import anthropic
|
|
173
|
+
from llm_mock import llm_mock
|
|
174
|
+
|
|
175
|
+
client = anthropic.Anthropic()
|
|
176
|
+
|
|
177
|
+
with llm_mock(mode="record", fixture="fixtures/hello"):
|
|
178
|
+
message = client.messages.create(
|
|
179
|
+
model="claude-haiku-4-5-20251001",
|
|
180
|
+
max_tokens=64,
|
|
181
|
+
messages=[{"role": "user", "content": "Say hello in one sentence."}],
|
|
182
|
+
)
|
|
183
|
+
print("Response:", message.content[0].text)
|
|
184
|
+
print("Fixture saved to fixtures/hello.json")
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 4. Run it
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
source .env && .venv/bin/python try_record.py
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
You should see the real response printed and `fixtures/hello.json` created.
|
|
194
|
+
|
|
195
|
+
### 5. Verify the fixture
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
llm-mock list tests/fixtures/hello
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 6. Replay without an API key
|
|
202
|
+
|
|
203
|
+
Create `try_replay.py`:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import anthropic
|
|
207
|
+
from llm_mock import llm_mock
|
|
208
|
+
|
|
209
|
+
client = anthropic.Anthropic(api_key="fake-key") # key is irrelevant in replay
|
|
210
|
+
|
|
211
|
+
with llm_mock(mode="replay", fixture="fixtures/hello"):
|
|
212
|
+
message = client.messages.create(
|
|
213
|
+
model="claude-haiku-4-5-20251001",
|
|
214
|
+
max_tokens=64,
|
|
215
|
+
messages=[{"role": "user", "content": "Say hello in one sentence."}],
|
|
216
|
+
)
|
|
217
|
+
print("Replayed:", message.content[0].text)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
.venv/bin/python try_replay.py
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The exact same response is returned instantly — no network call made.
|
|
225
|
+
|
|
226
|
+
### 7. Write a test with the pytest decorator
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
# tests/test_hello.py
|
|
230
|
+
import anthropic
|
|
231
|
+
import pytest
|
|
232
|
+
|
|
233
|
+
client = anthropic.Anthropic(api_key="fake-key")
|
|
234
|
+
|
|
235
|
+
@pytest.mark.llm_replay(fixture="hello")
|
|
236
|
+
def test_hello():
|
|
237
|
+
message = client.messages.create(
|
|
238
|
+
model="claude-haiku-4-5-20251001",
|
|
239
|
+
max_tokens=64,
|
|
240
|
+
messages=[{"role": "user", "content": "Say hello in one sentence."}],
|
|
241
|
+
)
|
|
242
|
+
assert message.content[0].text # replayed from fixtures/hello.json
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
.venv/bin/pytest tests/test_hello.py -v
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## CLI
|
|
252
|
+
|
|
253
|
+
Inspect and manage fixture files from the terminal.
|
|
254
|
+
|
|
255
|
+
> **Note:** activate your virtual environment first so `llm-mock` is on your PATH:
|
|
256
|
+
> ```bash
|
|
257
|
+
> source .venv/bin/activate
|
|
258
|
+
> ```
|
|
259
|
+
> Or run it directly with `.venv/bin/llm-mock <command>`.
|
|
260
|
+
|
|
261
|
+
### `llm-mock list <fixture>`
|
|
262
|
+
|
|
263
|
+
Show all recorded interactions in a fixture file:
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
$ llm-mock list tests/fixtures/summarize
|
|
267
|
+
|
|
268
|
+
Fixture : tests/fixtures/summarize.json
|
|
269
|
+
Provider: anthropic
|
|
270
|
+
Interactions: 2
|
|
271
|
+
|
|
272
|
+
1. a3f2c1d4e5b6… claude-sonnet-4-6 2026-04-23T10:00:00
|
|
273
|
+
"Summarize this document about climate change..."
|
|
274
|
+
2. b4g3d2e5f6c7… claude-haiku-4-5-20251001 2026-04-24T11:00:00
|
|
275
|
+
"What is the capital of France?"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `llm-mock clear <fixture>`
|
|
279
|
+
|
|
280
|
+
Delete an entire fixture file:
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
llm-mock clear tests/fixtures/summarize
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Delete a single interaction by hash:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
llm-mock clear tests/fixtures/summarize --hash a3f2c1d4e5b6
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## How it works
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
Record mode:
|
|
298
|
+
Your code → Anthropic/OpenAI SDK → httpx
|
|
299
|
+
→ llm-mock intercepts → forwards to real API
|
|
300
|
+
→ saves response to fixture JSON
|
|
301
|
+
→ returns response to your code
|
|
302
|
+
|
|
303
|
+
Replay mode:
|
|
304
|
+
Your code → Anthropic/OpenAI SDK → httpx
|
|
305
|
+
→ llm-mock intercepts → looks up fixture by SHA256(model + messages + temperature)
|
|
306
|
+
→ returns saved response — no network call made
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
**Request matching** uses SHA256 of `(model, messages, temperature)`. Same request always hits the same fixture entry. Different temperature or different message content → different fixture entry.
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## API reference
|
|
314
|
+
|
|
315
|
+
### `llm_mock(mode, fixture, provider="all")`
|
|
316
|
+
|
|
317
|
+
Context manager that activates record or replay mode.
|
|
318
|
+
|
|
319
|
+
| Parameter | Type | Description |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| `mode` | `"record"` \| `"replay"` | Whether to hit the real API and save, or return from fixture |
|
|
322
|
+
| `fixture` | `str` | Path to the fixture file. `.json` extension added automatically if omitted |
|
|
323
|
+
| `provider` | `"anthropic"` \| `"openai"` \| `"all"` | Which provider(s) to intercept. Default: `"all"` |
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
from llm_mock import llm_mock
|
|
327
|
+
|
|
328
|
+
with llm_mock(mode="replay", fixture="tests/fixtures/my_test", provider="anthropic"):
|
|
329
|
+
...
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Exceptions
|
|
333
|
+
|
|
334
|
+
| Exception | When raised |
|
|
335
|
+
|---|---|
|
|
336
|
+
| `FixtureNotFoundError` | Replay mode: fixture file missing, or no matching hash in file |
|
|
337
|
+
| `FixtureParseError` | Fixture file exists but contains invalid JSON |
|
|
338
|
+
|
|
339
|
+
```python
|
|
340
|
+
from llm_mock import llm_mock, FixtureNotFoundError
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
with llm_mock(mode="replay", fixture="tests/fixtures/missing"):
|
|
344
|
+
client.messages.create(...)
|
|
345
|
+
except FixtureNotFoundError as e:
|
|
346
|
+
print(e) # includes hint to run in record mode first
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Fixture file format
|
|
352
|
+
|
|
353
|
+
Fixture files are plain JSON — readable, diffable, committable.
|
|
354
|
+
|
|
355
|
+
```json
|
|
356
|
+
{
|
|
357
|
+
"version": "1.0",
|
|
358
|
+
"provider": "anthropic",
|
|
359
|
+
"interactions": [
|
|
360
|
+
{
|
|
361
|
+
"hash": "a3f2c1...",
|
|
362
|
+
"request": {
|
|
363
|
+
"model": "claude-sonnet-4-6",
|
|
364
|
+
"messages": [{"role": "user", "content": "Say hello."}],
|
|
365
|
+
"max_tokens": 64
|
|
366
|
+
},
|
|
367
|
+
"response": {
|
|
368
|
+
"id": "msg_01XYZ",
|
|
369
|
+
"type": "message",
|
|
370
|
+
"role": "assistant",
|
|
371
|
+
"content": [{"type": "text", "text": "Hello! How can I help you today?"}],
|
|
372
|
+
"model": "claude-sonnet-4-6",
|
|
373
|
+
"stop_reason": "end_turn",
|
|
374
|
+
"usage": {"input_tokens": 10, "output_tokens": 9}
|
|
375
|
+
},
|
|
376
|
+
"recorded_at": "2026-04-23T10:00:00+00:00"
|
|
377
|
+
}
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Multiple interactions (from different requests) are stored in the same file. Re-recording an existing hash overwrites only that entry.
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## Supported providers
|
|
387
|
+
|
|
388
|
+
| Provider | Intercepted endpoint | Status |
|
|
389
|
+
|---|---|---|
|
|
390
|
+
| Anthropic | `api.anthropic.com/v1/messages` | Supported |
|
|
391
|
+
| OpenAI | `api.openai.com/v1/chat/completions` | Supported |
|
|
392
|
+
| Streaming (`stream=True`) | — | v1.1 |
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Comparison
|
|
397
|
+
|
|
398
|
+
| Tool | Record mode | Native SDK support | In-process |
|
|
399
|
+
|---|---|---|---|
|
|
400
|
+
| **llm-mock** | yes | yes (Anthropic + OpenAI) | yes |
|
|
401
|
+
| [llm_recorder](https://github.com/zby/llm_recorder) | yes | no (LiteLLM only) | yes |
|
|
402
|
+
| [AIMock](https://github.com/CopilotKit/aimock) | no | yes | no (HTTP server) |
|
|
403
|
+
| [vcr-langchain](https://github.com/amosjyng/vcr-langchain) | yes | no (LangChain only) | yes |
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Development
|
|
408
|
+
|
|
409
|
+
```bash
|
|
410
|
+
git clone https://github.com/yourname/llm-mock
|
|
411
|
+
cd llm-mock
|
|
412
|
+
python -m venv .venv && source .venv/bin/activate
|
|
413
|
+
pip install -e ".[dev]"
|
|
414
|
+
pytest
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
---
|
|
418
|
+
|
|
419
|
+
## Roadmap
|
|
420
|
+
|
|
421
|
+
- **v0.2** — `auto` mode, disable via env var (`LLM_MOCK_DISABLED`)
|
|
422
|
+
- **v1.1** — streaming support
|
|
423
|
+
- **v2** — shared fixtures for teams, semantic matching, web dashboard
|