llmock 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.
- llmock-0.1.0/.github/CODEOWNERS +1 -0
- llmock-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +59 -0
- llmock-0.1.0/.github/ISSUE_TEMPLATE/config.yml +1 -0
- llmock-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +39 -0
- llmock-0.1.0/.github/dependabot.yml +13 -0
- llmock-0.1.0/.github/pull_request_template.md +16 -0
- llmock-0.1.0/.github/workflows/ci.yml +71 -0
- llmock-0.1.0/.github/workflows/release.yml +87 -0
- llmock-0.1.0/.gitignore +45 -0
- llmock-0.1.0/ARCHITECTURE.md +216 -0
- llmock-0.1.0/CHANGELOG.md +12 -0
- llmock-0.1.0/CODE_OF_CONDUCT.md +27 -0
- llmock-0.1.0/CONTRIBUTING.md +52 -0
- llmock-0.1.0/LICENSE +21 -0
- llmock-0.1.0/PKG-INFO +378 -0
- llmock-0.1.0/README.md +338 -0
- llmock-0.1.0/RELEASING.md +125 -0
- llmock-0.1.0/SECURITY.md +20 -0
- llmock-0.1.0/examples/README.md +107 -0
- llmock-0.1.0/examples/chaos_scenarios.sh +107 -0
- llmock-0.1.0/examples/llmock.example.json +19 -0
- llmock-0.1.0/examples/llmock.example.yaml +15 -0
- llmock-0.1.0/examples/retry_with_openai.py +81 -0
- llmock-0.1.0/llmock/__init__.py +5 -0
- llmock-0.1.0/llmock/chaos.py +187 -0
- llmock-0.1.0/llmock/cli.py +275 -0
- llmock-0.1.0/llmock/errors.py +91 -0
- llmock-0.1.0/llmock/main.py +53 -0
- llmock-0.1.0/llmock/routers/__init__.py +0 -0
- llmock-0.1.0/llmock/routers/ai21.py +219 -0
- llmock-0.1.0/llmock/routers/anthropic.py +212 -0
- llmock-0.1.0/llmock/routers/base.py +45 -0
- llmock-0.1.0/llmock/routers/batch.py +678 -0
- llmock-0.1.0/llmock/routers/cohere.py +218 -0
- llmock-0.1.0/llmock/routers/gemini.py +208 -0
- llmock-0.1.0/llmock/routers/groq.py +254 -0
- llmock-0.1.0/llmock/routers/mistral.py +250 -0
- llmock-0.1.0/llmock/routers/openai.py +313 -0
- llmock-0.1.0/llmock/routers/perplexity.py +293 -0
- llmock-0.1.0/llmock/routers/registry.py +36 -0
- llmock-0.1.0/llmock/routers/together.py +245 -0
- llmock-0.1.0/llmock/routers/xai.py +254 -0
- llmock-0.1.0/llmock/simulation.py +355 -0
- llmock-0.1.0/pyproject.toml +70 -0
- llmock-0.1.0/tests/__init__.py +0 -0
- llmock-0.1.0/tests/conftest.py +33 -0
- llmock-0.1.0/tests/test_anthropic.py +69 -0
- llmock-0.1.0/tests/test_batch.py +605 -0
- llmock-0.1.0/tests/test_chaos.py +169 -0
- llmock-0.1.0/tests/test_cli.py +209 -0
- llmock-0.1.0/tests/test_error_profiles.py +102 -0
- llmock-0.1.0/tests/test_error_shapes.py +42 -0
- llmock-0.1.0/tests/test_errors.py +223 -0
- llmock-0.1.0/tests/test_mistral.py +50 -0
- llmock-0.1.0/tests/test_openai.py +73 -0
- llmock-0.1.0/tests/test_provider_batches.py +182 -0
- llmock-0.1.0/tests/test_providers.py +228 -0
- llmock-0.1.0/tests/test_responses.py +26 -0
- llmock-0.1.0/tests/test_simulation_behaviors.py +86 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @JulienRabault
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Report a runtime, CLI, docs, or provider-shape issue
|
|
3
|
+
title: "[Bug] "
|
|
4
|
+
labels: ["bug"]
|
|
5
|
+
body:
|
|
6
|
+
- type: textarea
|
|
7
|
+
id: summary
|
|
8
|
+
attributes:
|
|
9
|
+
label: Summary
|
|
10
|
+
description: What is wrong, and what did you expect instead?
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
|
|
14
|
+
- type: dropdown
|
|
15
|
+
id: area
|
|
16
|
+
attributes:
|
|
17
|
+
label: Area
|
|
18
|
+
options:
|
|
19
|
+
- OpenAI-compatible routes
|
|
20
|
+
- Anthropic routes
|
|
21
|
+
- Mistral routes
|
|
22
|
+
- Cohere routes
|
|
23
|
+
- Gemini routes
|
|
24
|
+
- Groq routes
|
|
25
|
+
- Together routes
|
|
26
|
+
- Perplexity routes
|
|
27
|
+
- AI21 routes
|
|
28
|
+
- xAI routes
|
|
29
|
+
- Batch endpoints
|
|
30
|
+
- Chaos / config / CLI
|
|
31
|
+
- Packaging / release / CI
|
|
32
|
+
- Docs / examples
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
|
|
36
|
+
- type: textarea
|
|
37
|
+
id: steps
|
|
38
|
+
attributes:
|
|
39
|
+
label: Reproduction
|
|
40
|
+
description: Include request payloads, config, and commands when possible.
|
|
41
|
+
placeholder: |
|
|
42
|
+
1. Start LLMock with ...
|
|
43
|
+
2. Send request to ...
|
|
44
|
+
3. Observe ...
|
|
45
|
+
validations:
|
|
46
|
+
required: true
|
|
47
|
+
|
|
48
|
+
- type: textarea
|
|
49
|
+
id: expected
|
|
50
|
+
attributes:
|
|
51
|
+
label: Expected behavior
|
|
52
|
+
validations:
|
|
53
|
+
required: true
|
|
54
|
+
|
|
55
|
+
- type: textarea
|
|
56
|
+
id: environment
|
|
57
|
+
attributes:
|
|
58
|
+
label: Environment
|
|
59
|
+
description: Python version, install method, OS, and LLMock version if relevant.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: true
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Propose a scoped improvement to LLMock
|
|
3
|
+
title: "[Feature] "
|
|
4
|
+
labels: ["enhancement"]
|
|
5
|
+
body:
|
|
6
|
+
- type: textarea
|
|
7
|
+
id: problem
|
|
8
|
+
attributes:
|
|
9
|
+
label: Problem
|
|
10
|
+
description: What real user or testing problem does this solve?
|
|
11
|
+
validations:
|
|
12
|
+
required: true
|
|
13
|
+
|
|
14
|
+
- type: textarea
|
|
15
|
+
id: proposal
|
|
16
|
+
attributes:
|
|
17
|
+
label: Proposed solution
|
|
18
|
+
description: Keep it concrete and scoped.
|
|
19
|
+
validations:
|
|
20
|
+
required: true
|
|
21
|
+
|
|
22
|
+
- type: textarea
|
|
23
|
+
id: tradeoffs
|
|
24
|
+
attributes:
|
|
25
|
+
label: Tradeoffs
|
|
26
|
+
description: Maintenance cost, compatibility impact, or scope concerns.
|
|
27
|
+
|
|
28
|
+
- type: dropdown
|
|
29
|
+
id: fit
|
|
30
|
+
attributes:
|
|
31
|
+
label: Why it fits LLMock
|
|
32
|
+
options:
|
|
33
|
+
- Improves resilience testing
|
|
34
|
+
- Improves API fidelity
|
|
35
|
+
- Improves onboarding or adoption
|
|
36
|
+
- Improves packaging or maintainability
|
|
37
|
+
- Not sure
|
|
38
|
+
validations:
|
|
39
|
+
required: true
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
- package-ecosystem: "pip"
|
|
4
|
+
directory: "/"
|
|
5
|
+
schedule:
|
|
6
|
+
interval: "weekly"
|
|
7
|
+
open-pull-requests-limit: 5
|
|
8
|
+
|
|
9
|
+
- package-ecosystem: "github-actions"
|
|
10
|
+
directory: "/"
|
|
11
|
+
schedule:
|
|
12
|
+
interval: "weekly"
|
|
13
|
+
open-pull-requests-limit: 5
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
## Summary
|
|
2
|
+
|
|
3
|
+
- what changed
|
|
4
|
+
- why it changed
|
|
5
|
+
- any provider or endpoint impact
|
|
6
|
+
|
|
7
|
+
## Validation
|
|
8
|
+
|
|
9
|
+
- [ ] `python -m ruff check llmock tests examples/retry_with_openai.py`
|
|
10
|
+
- [ ] `pytest -q`
|
|
11
|
+
- [ ] docs updated if install, config, or API behavior changed
|
|
12
|
+
|
|
13
|
+
## Risks
|
|
14
|
+
|
|
15
|
+
- behavior or compatibility risks
|
|
16
|
+
- release or migration impact
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
lint:
|
|
9
|
+
name: Lint
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Check out repository
|
|
13
|
+
uses: actions/checkout@v6
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v6
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.11"
|
|
19
|
+
cache: pip
|
|
20
|
+
|
|
21
|
+
- name: Install dependencies
|
|
22
|
+
run: python -m pip install -e ".[dev]"
|
|
23
|
+
|
|
24
|
+
- name: Run Ruff
|
|
25
|
+
run: python -m ruff check llmock tests
|
|
26
|
+
|
|
27
|
+
tests:
|
|
28
|
+
name: Tests
|
|
29
|
+
runs-on: ubuntu-latest
|
|
30
|
+
strategy:
|
|
31
|
+
fail-fast: false
|
|
32
|
+
matrix:
|
|
33
|
+
python-version: ["3.11", "3.12", "3.13"]
|
|
34
|
+
|
|
35
|
+
steps:
|
|
36
|
+
- name: Check out repository
|
|
37
|
+
uses: actions/checkout@v6
|
|
38
|
+
|
|
39
|
+
- name: Set up Python
|
|
40
|
+
uses: actions/setup-python@v6
|
|
41
|
+
with:
|
|
42
|
+
python-version: ${{ matrix.python-version }}
|
|
43
|
+
cache: pip
|
|
44
|
+
|
|
45
|
+
- name: Install dependencies
|
|
46
|
+
run: python -m pip install -e ".[dev]"
|
|
47
|
+
|
|
48
|
+
- name: Run tests
|
|
49
|
+
run: pytest
|
|
50
|
+
|
|
51
|
+
package:
|
|
52
|
+
name: Build package
|
|
53
|
+
runs-on: ubuntu-latest
|
|
54
|
+
steps:
|
|
55
|
+
- name: Check out repository
|
|
56
|
+
uses: actions/checkout@v6
|
|
57
|
+
|
|
58
|
+
- name: Set up Python
|
|
59
|
+
uses: actions/setup-python@v6
|
|
60
|
+
with:
|
|
61
|
+
python-version: "3.11"
|
|
62
|
+
cache: pip
|
|
63
|
+
|
|
64
|
+
- name: Install build tooling
|
|
65
|
+
run: python -m pip install -e ".[dev]"
|
|
66
|
+
|
|
67
|
+
- name: Build distributions
|
|
68
|
+
run: python -m build
|
|
69
|
+
|
|
70
|
+
- name: Check package metadata
|
|
71
|
+
run: python -m twine check dist/*
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: release-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build:
|
|
14
|
+
name: Build and verify release
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Check out repository
|
|
19
|
+
uses: actions/checkout@v6
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
uses: actions/setup-python@v6
|
|
23
|
+
with:
|
|
24
|
+
python-version: "3.11"
|
|
25
|
+
cache: pip
|
|
26
|
+
|
|
27
|
+
- name: Install release tooling
|
|
28
|
+
run: python -m pip install -e ".[dev]"
|
|
29
|
+
|
|
30
|
+
- name: Lint
|
|
31
|
+
run: python -m ruff check llmock tests examples/retry_with_openai.py
|
|
32
|
+
|
|
33
|
+
- name: Test
|
|
34
|
+
run: pytest -q
|
|
35
|
+
|
|
36
|
+
- name: Build distributions
|
|
37
|
+
run: python -m build
|
|
38
|
+
|
|
39
|
+
- name: Check package metadata
|
|
40
|
+
run: python -m twine check dist/*
|
|
41
|
+
|
|
42
|
+
- name: Upload distributions
|
|
43
|
+
uses: actions/upload-artifact@v7
|
|
44
|
+
with:
|
|
45
|
+
name: dist
|
|
46
|
+
path: dist/*
|
|
47
|
+
if-no-files-found: error
|
|
48
|
+
|
|
49
|
+
publish-pypi:
|
|
50
|
+
name: Publish to PyPI
|
|
51
|
+
needs: build
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
environment:
|
|
54
|
+
name: pypi
|
|
55
|
+
url: https://pypi.org/project/llmock/
|
|
56
|
+
permissions:
|
|
57
|
+
id-token: write
|
|
58
|
+
|
|
59
|
+
steps:
|
|
60
|
+
- name: Download distributions
|
|
61
|
+
uses: actions/download-artifact@v8
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist
|
|
65
|
+
|
|
66
|
+
- name: Publish package to PyPI
|
|
67
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
68
|
+
|
|
69
|
+
github-release:
|
|
70
|
+
name: Create GitHub release
|
|
71
|
+
needs: publish-pypi
|
|
72
|
+
runs-on: ubuntu-latest
|
|
73
|
+
permissions:
|
|
74
|
+
contents: write
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
- name: Download distributions
|
|
78
|
+
uses: actions/download-artifact@v8
|
|
79
|
+
with:
|
|
80
|
+
name: dist
|
|
81
|
+
path: dist
|
|
82
|
+
|
|
83
|
+
- name: Create GitHub release
|
|
84
|
+
uses: softprops/action-gh-release@v2
|
|
85
|
+
with:
|
|
86
|
+
generate_release_notes: true
|
|
87
|
+
files: dist/*
|
llmock-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
|
|
7
|
+
# Virtual environments
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
|
|
12
|
+
# Distribution / packaging
|
|
13
|
+
dist/
|
|
14
|
+
build/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
*.egg
|
|
17
|
+
MANIFEST
|
|
18
|
+
|
|
19
|
+
# Testing
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.ruff_cache/
|
|
22
|
+
.coverage
|
|
23
|
+
htmlcov/
|
|
24
|
+
.tox/
|
|
25
|
+
|
|
26
|
+
# Environment variables
|
|
27
|
+
.env
|
|
28
|
+
.env.*
|
|
29
|
+
!.env.example
|
|
30
|
+
|
|
31
|
+
# IDE
|
|
32
|
+
.vscode/
|
|
33
|
+
.idea/
|
|
34
|
+
*.swp
|
|
35
|
+
*.swo
|
|
36
|
+
|
|
37
|
+
# Internal agent files
|
|
38
|
+
agents/
|
|
39
|
+
|
|
40
|
+
# OS
|
|
41
|
+
.DS_Store
|
|
42
|
+
Thumbs.db
|
|
43
|
+
|
|
44
|
+
# Logs
|
|
45
|
+
*.log
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# LLMock Architecture
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
LLMock is a zero-cost local mock server that presents OpenAI-compatible (and provider-native) HTTP APIs. Developers point their LLM SDK at `http://localhost:8000` instead of a real provider endpoint, then use environment variables or runtime settings to inject latency, rate-limit errors (429), and server errors (500 / 503). This lets integration tests exercise retry logic, backoff strategies, and error-handling paths without spending tokens or requiring network access.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Request Flow
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
Client (SDK / curl)
|
|
13
|
+
│
|
|
14
|
+
▼
|
|
15
|
+
┌───────────────────────────────────────────────┐
|
|
16
|
+
│ FastAPI Application │
|
|
17
|
+
│ │
|
|
18
|
+
│ ┌───────────────────────────────────────┐ │
|
|
19
|
+
│ │ ChaosMiddleware │ │
|
|
20
|
+
│ │ (latency injection + error sampling) │ │
|
|
21
|
+
│ └─────────────────┬─────────────────────┘ │
|
|
22
|
+
│ │ │
|
|
23
|
+
│ ┌─────────────────▼─────────────────────┐ │
|
|
24
|
+
│ │ API Router │ │
|
|
25
|
+
│ │ (provider-specific prefix & schema) │ │
|
|
26
|
+
│ └─────────────────┬─────────────────────┘ │
|
|
27
|
+
│ │ │
|
|
28
|
+
│ ┌─────────────────▼─────────────────────┐ │
|
|
29
|
+
│ │ Mock Response │ │
|
|
30
|
+
│ │ (deterministic, schema-correct JSON) │ │
|
|
31
|
+
│ └───────────────────────────────────────┘ │
|
|
32
|
+
└───────────────────────────────────────────────┘
|
|
33
|
+
│
|
|
34
|
+
▼
|
|
35
|
+
Client receives response (or injected error)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The `/health` endpoint bypasses the middleware entirely so monitoring systems remain reliable during chaos tests.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Chaos Middleware
|
|
43
|
+
|
|
44
|
+
**File:** `llmock/chaos.py`
|
|
45
|
+
|
|
46
|
+
### `ChaosSettings` dataclass
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
@dataclass
|
|
50
|
+
class ChaosSettings:
|
|
51
|
+
latency_ms: int = 0
|
|
52
|
+
error_rate_429: float = 0.0
|
|
53
|
+
error_rate_500: float = 0.0
|
|
54
|
+
error_rate_503: float = 0.0
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Loaded from environment variables at startup via `ChaosSettings.from_env()`:
|
|
58
|
+
|
|
59
|
+
| Variable | Default | Effect |
|
|
60
|
+
|-----------------------|---------|---------------------------------------------|
|
|
61
|
+
| `LLMOCK_LATENCY_MS` | `0` | Fixed delay added before every response |
|
|
62
|
+
| `LLMOCK_ERROR_RATE_429` | `0.0` | Probability (0–1) of returning a 429 |
|
|
63
|
+
| `LLMOCK_ERROR_RATE_500` | `0.0` | Probability (0–1) of returning a 500 |
|
|
64
|
+
| `LLMOCK_ERROR_RATE_503` | `0.0` | Probability (0–1) of returning a 503 |
|
|
65
|
+
|
|
66
|
+
Each app instance receives a `ChaosSettings` object through `create_app(chaos=...)`. The module-level `chaos_settings` value is only used to build the default exported `app`.
|
|
67
|
+
|
|
68
|
+
### Sampling Logic
|
|
69
|
+
|
|
70
|
+
On each request the middleware:
|
|
71
|
+
|
|
72
|
+
1. Sleeps for `latency_ms / 1000` seconds (if > 0).
|
|
73
|
+
2. Draws a single random float `r ∈ [0, 1)`.
|
|
74
|
+
3. Walks error rates in priority order `429 → 503 → 500`, accumulating a cumulative probability. If `r < cumulative`, returns that error immediately.
|
|
75
|
+
|
|
76
|
+
This cumulative approach means rates are additive and predictable: setting all three to `0.33` gives roughly equal chance of each error and ~1% pass-through.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Router Pattern
|
|
81
|
+
|
|
82
|
+
**Directory:** `llmock/routers/`
|
|
83
|
+
|
|
84
|
+
Each provider has its own Python module with a consistent structure:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
llmock/routers/
|
|
88
|
+
├── openai.py # /v1/... (OpenAI-compatible)
|
|
89
|
+
├── anthropic.py # /anthropic/v1/...
|
|
90
|
+
├── mistral.py # /v1/... (Mistral-compatible)
|
|
91
|
+
├── gemini.py # /v1beta/models/...
|
|
92
|
+
├── cohere.py # /v2/...
|
|
93
|
+
├── groq.py # /openai/v1/...
|
|
94
|
+
├── together.py # /together/v1/...
|
|
95
|
+
├── perplexity.py # /pplx/v1/...
|
|
96
|
+
├── ai21.py # /ai21/v1/...
|
|
97
|
+
├── xai.py # /xai/v1/...
|
|
98
|
+
└── batch.py # /v1/files, /v1/batches (Batch API)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Module structure (example: `openai.py`)
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from fastapi import APIRouter
|
|
105
|
+
from pydantic import BaseModel
|
|
106
|
+
|
|
107
|
+
router = APIRouter(prefix="/v1", tags=["openai"])
|
|
108
|
+
|
|
109
|
+
# 1. Request models (Pydantic)
|
|
110
|
+
class ChatCompletionRequest(BaseModel): ...
|
|
111
|
+
|
|
112
|
+
# 2. Response models (Pydantic)
|
|
113
|
+
class ChatCompletionResponse(BaseModel): ...
|
|
114
|
+
|
|
115
|
+
# 3. Endpoint handlers
|
|
116
|
+
@router.post("/chat/completions", response_model=ChatCompletionResponse)
|
|
117
|
+
def chat_completions(request: ChatCompletionRequest) -> ChatCompletionResponse:
|
|
118
|
+
# Build and return a deterministic mock response
|
|
119
|
+
...
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Routers are registered in `llmock/main.py` via `app.include_router(...)`.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Batch API Simulation
|
|
127
|
+
|
|
128
|
+
**File:** `llmock/routers/batch.py`
|
|
129
|
+
|
|
130
|
+
Simulates the full OpenAI Batch API async JSONL workflow:
|
|
131
|
+
|
|
132
|
+
| Step | Request | Description |
|
|
133
|
+
|------|---------|-------------|
|
|
134
|
+
| 1 | `POST /v1/files` | Upload a JSONL request file |
|
|
135
|
+
| 2 | `POST /v1/batches` | Create a batch job referencing the file |
|
|
136
|
+
| 3 | `GET /v1/batches/{id}` | Poll status (`validating → in_progress → completed`) |
|
|
137
|
+
| 4 | `GET /v1/files/{id}/content` | Download the JSONL results file |
|
|
138
|
+
| 5 | `POST /v1/batches/{id}/cancel` | Cancel an in-progress batch |
|
|
139
|
+
|
|
140
|
+
All state is held in in-memory dictionaries (`_files`, `_batches`). Batches complete automatically after a configurable delay (`_BATCH_DELAY`, default 3 s) via a FastAPI `BackgroundTasks` coroutine.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Adding a New Provider
|
|
145
|
+
|
|
146
|
+
1. **Create the router file** at `llmock/routers/myprovider.py`:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from fastapi import APIRouter
|
|
150
|
+
from pydantic import BaseModel
|
|
151
|
+
|
|
152
|
+
router = APIRouter(prefix="/myprovider/v1", tags=["myprovider"])
|
|
153
|
+
|
|
154
|
+
class MyRequest(BaseModel):
|
|
155
|
+
model: str
|
|
156
|
+
prompt: str
|
|
157
|
+
|
|
158
|
+
@router.post("/generate")
|
|
159
|
+
def generate(request: MyRequest) -> dict:
|
|
160
|
+
return {
|
|
161
|
+
"id": "mock-id",
|
|
162
|
+
"model": request.model,
|
|
163
|
+
"output": f"Mock response from {request.model}.",
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
2. **Register the router** in `llmock/main.py`:
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from llmock.routers import myprovider as myprovider_router
|
|
171
|
+
# ...
|
|
172
|
+
app.include_router(myprovider_router.router)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
3. **Add tests** in `tests/test_myprovider.py`:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from fastapi.testclient import TestClient
|
|
179
|
+
from llmock.main import create_app
|
|
180
|
+
|
|
181
|
+
client = TestClient(create_app())
|
|
182
|
+
|
|
183
|
+
def test_generate():
|
|
184
|
+
r = client.post("/myprovider/v1/generate", json={"model": "my-model-1", "prompt": "Hello"})
|
|
185
|
+
assert r.status_code == 200
|
|
186
|
+
assert r.json()["model"] == "my-model-1"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Test Architecture
|
|
192
|
+
|
|
193
|
+
**Directory:** `tests/`
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
tests/
|
|
197
|
+
├── test_openai.py # OpenAI chat completions, embeddings, models
|
|
198
|
+
├── test_anthropic.py # Anthropic Messages API
|
|
199
|
+
├── test_mistral.py # Mistral chat completions
|
|
200
|
+
├── test_providers.py # Gemini, Cohere, Groq, Together, Perplexity, AI21, xAI
|
|
201
|
+
├── test_batch.py # Full Batch API workflow (upload → create → poll → results)
|
|
202
|
+
└── test_chaos.py # ChaosMiddleware latency and error injection
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Key patterns
|
|
206
|
+
|
|
207
|
+
- **`TestClient`** (synchronous): All tests use `fastapi.testclient.TestClient` wrapping `create_app()`. This avoids the async overhead of `httpx.AsyncClient` while remaining ASGI-compatible.
|
|
208
|
+
- **`create_app(chaos=...)`**: The app factory accepts an optional `ChaosSettings` argument, allowing tests to inject specific chaos configurations without touching global state or environment variables.
|
|
209
|
+
- **Direct state injection**: Batch API tests that need a specific pre-condition (e.g., `in_progress` status) write directly into the router's `_batches` dict rather than racing against background tasks.
|
|
210
|
+
|
|
211
|
+
### Running tests
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
pip install -e ".[dev]"
|
|
215
|
+
pytest
|
|
216
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project should be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-03-20
|
|
6
|
+
|
|
7
|
+
- initial public release of LLMock
|
|
8
|
+
- multi-provider mock API support across OpenAI, Anthropic, Mistral, Cohere, Gemini, Groq, Together AI, Perplexity, AI21, and xAI
|
|
9
|
+
- configurable chaos injection with latency and per-status error probabilities
|
|
10
|
+
- configurable success payload styles
|
|
11
|
+
- provider-specific batch endpoint simulation
|
|
12
|
+
- PyPI-oriented CLI packaging and release workflows
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Code of Conduct
|
|
2
|
+
|
|
3
|
+
This project expects respectful, direct, and constructive collaboration.
|
|
4
|
+
|
|
5
|
+
## Expected behavior
|
|
6
|
+
|
|
7
|
+
- discuss technical points in good faith
|
|
8
|
+
- focus on concrete behavior, tradeoffs, and evidence
|
|
9
|
+
- be respectful in disagreement
|
|
10
|
+
- assume contributors are trying to improve the project
|
|
11
|
+
- keep review feedback specific and actionable
|
|
12
|
+
|
|
13
|
+
## Unacceptable behavior
|
|
14
|
+
|
|
15
|
+
- harassment, intimidation, or personal attacks
|
|
16
|
+
- discriminatory or hateful language
|
|
17
|
+
- repeated bad-faith argumentation
|
|
18
|
+
- doxxing or sharing private information without consent
|
|
19
|
+
- derailing collaboration with hostility or abuse
|
|
20
|
+
|
|
21
|
+
## Scope
|
|
22
|
+
|
|
23
|
+
This applies to issues, pull requests, discussions, and any other project spaces.
|
|
24
|
+
|
|
25
|
+
## Enforcement
|
|
26
|
+
|
|
27
|
+
Maintainers may edit, remove, or reject contributions and interactions that harm the project environment.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Contributing to LLMock
|
|
2
|
+
|
|
3
|
+
Thanks for contributing.
|
|
4
|
+
|
|
5
|
+
LLMock is intentionally narrow in scope: it is a local mock server for testing LLM client resilience, retries, fallbacks, and integration behavior. Contributions are most useful when they improve fidelity, reliability, or adoption without turning the project into a full provider emulator.
|
|
6
|
+
|
|
7
|
+
## Good contribution targets
|
|
8
|
+
|
|
9
|
+
- fix mismatches between runtime behavior and provider docs
|
|
10
|
+
- add tests for provider-specific request and error shapes
|
|
11
|
+
- improve batch endpoint coverage
|
|
12
|
+
- improve docs, examples, and onboarding
|
|
13
|
+
- add deterministic testing capabilities
|
|
14
|
+
- tighten packaging, release automation, and CI
|
|
15
|
+
|
|
16
|
+
## Changes that need extra care
|
|
17
|
+
|
|
18
|
+
- large new provider surfaces
|
|
19
|
+
- features that increase maintenance cost a lot
|
|
20
|
+
- behavior that diverges from official provider APIs without being documented
|
|
21
|
+
- breaking CLI, config, or response-shape changes
|
|
22
|
+
|
|
23
|
+
If a change is large or changes public behavior, open an issue first.
|
|
24
|
+
|
|
25
|
+
## Local setup
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e ".[dev]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Run checks before opening a pull request:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m ruff check llmock tests examples/retry_with_openai.py
|
|
35
|
+
pytest -q
|
|
36
|
+
python -m build
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Pull request expectations
|
|
40
|
+
|
|
41
|
+
- keep changes scoped and explain the user-facing impact clearly
|
|
42
|
+
- add or update tests for behavioral changes
|
|
43
|
+
- update [README.md](/C:/Users/julie/Documents/Code_space/LLMock/README.md) when install, config, or API behavior changes
|
|
44
|
+
- preserve provider-specific response and error shapes where possible
|
|
45
|
+
- avoid unrelated refactors in the same pull request
|
|
46
|
+
|
|
47
|
+
## Design principles
|
|
48
|
+
|
|
49
|
+
- local-first and cheap to run
|
|
50
|
+
- explicit over magical
|
|
51
|
+
- useful for resilience testing before it is exhaustive
|
|
52
|
+
- predictable defaults, configurable failure modes
|
llmock-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Julie
|
|
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.
|