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 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.
@@ -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