llm-cascade 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_cascade-0.1.0/.env.example +18 -0
- llm_cascade-0.1.0/.github/workflows/ci.yml +29 -0
- llm_cascade-0.1.0/.gitignore +41 -0
- llm_cascade-0.1.0/CHANGELOG.md +22 -0
- llm_cascade-0.1.0/LICENSE +21 -0
- llm_cascade-0.1.0/PKG-INFO +196 -0
- llm_cascade-0.1.0/README.md +163 -0
- llm_cascade-0.1.0/config/routes.example.toml +56 -0
- llm_cascade-0.1.0/examples/quickstart.py +47 -0
- llm_cascade-0.1.0/pyproject.toml +75 -0
- llm_cascade-0.1.0/src/llm_cascade/__init__.py +26 -0
- llm_cascade-0.1.0/src/llm_cascade/__main__.py +7 -0
- llm_cascade-0.1.0/src/llm_cascade/cli.py +72 -0
- llm_cascade-0.1.0/src/llm_cascade/config.py +123 -0
- llm_cascade-0.1.0/src/llm_cascade/pricing.py +27 -0
- llm_cascade-0.1.0/src/llm_cascade/providers/__init__.py +6 -0
- llm_cascade-0.1.0/src/llm_cascade/providers/anthropic_provider.py +123 -0
- llm_cascade-0.1.0/src/llm_cascade/providers/local_provider.py +104 -0
- llm_cascade-0.1.0/src/llm_cascade/providers/openrouter_provider.py +93 -0
- llm_cascade-0.1.0/src/llm_cascade/py.typed +0 -0
- llm_cascade-0.1.0/src/llm_cascade/router.py +277 -0
- llm_cascade-0.1.0/tests/conftest.py +119 -0
- llm_cascade-0.1.0/tests/test_cli.py +58 -0
- llm_cascade-0.1.0/tests/test_config.py +82 -0
- llm_cascade-0.1.0/tests/test_pick.py +55 -0
- llm_cascade-0.1.0/tests/test_providers_anthropic.py +81 -0
- llm_cascade-0.1.0/tests/test_providers_openrouter.py +118 -0
- llm_cascade-0.1.0/tests/test_router_complete.py +171 -0
- llm_cascade-0.1.0/tests/test_stats_logging.py +53 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Copy this file to `.env` and fill in the keys you plan to use.
|
|
2
|
+
# llm-cascade never requires any of these -- with none set, only the local
|
|
3
|
+
# Ollama tier works; each key you add unlocks the next tier in the cascade.
|
|
4
|
+
|
|
5
|
+
# Paid tier -- required for the `light`/`heavy` tiers and as the final safety net.
|
|
6
|
+
# Get a key at https://console.anthropic.com/
|
|
7
|
+
ANTHROPIC_API_KEY=
|
|
8
|
+
|
|
9
|
+
# Free tier -- required to use OpenRouter's `:free` models ($0, rate-limited).
|
|
10
|
+
# Get a key at https://openrouter.ai/
|
|
11
|
+
OPENROUTER_API_KEY=
|
|
12
|
+
|
|
13
|
+
# Optional: sent as the `HTTP-Referer` header on OpenRouter requests (some
|
|
14
|
+
# integrations use this for attribution). Omitted if unset.
|
|
15
|
+
LLM_CASCADE_HTTP_REFERER=
|
|
16
|
+
|
|
17
|
+
# Optional: sent as the `X-Title` header on OpenRouter requests. Omitted if unset.
|
|
18
|
+
LLM_CASCADE_APP_TITLE=
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: ${{ matrix.python-version }}
|
|
21
|
+
|
|
22
|
+
- name: Install package
|
|
23
|
+
run: python -m pip install -e ".[dev,anthropic]"
|
|
24
|
+
|
|
25
|
+
- name: Lint (ruff)
|
|
26
|
+
run: ruff check .
|
|
27
|
+
|
|
28
|
+
- name: Test (pytest)
|
|
29
|
+
run: pytest tests/ -v
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# Distribution / packaging
|
|
7
|
+
.Python
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
*.egg
|
|
12
|
+
wheels/
|
|
13
|
+
pip-wheel-metadata/
|
|
14
|
+
|
|
15
|
+
# Environments
|
|
16
|
+
.env
|
|
17
|
+
.venv
|
|
18
|
+
env/
|
|
19
|
+
venv/
|
|
20
|
+
ENV/
|
|
21
|
+
|
|
22
|
+
# Testing / coverage
|
|
23
|
+
.pytest_cache/
|
|
24
|
+
.coverage
|
|
25
|
+
.coverage.*
|
|
26
|
+
htmlcov/
|
|
27
|
+
.tox/
|
|
28
|
+
.mypy_cache/
|
|
29
|
+
.ruff_cache/
|
|
30
|
+
|
|
31
|
+
# IDE
|
|
32
|
+
.vscode/
|
|
33
|
+
.idea/
|
|
34
|
+
*.swp
|
|
35
|
+
|
|
36
|
+
# OS
|
|
37
|
+
.DS_Store
|
|
38
|
+
Thumbs.db
|
|
39
|
+
|
|
40
|
+
# llm-cascade runtime state (savings ledger, local config)
|
|
41
|
+
.llm_cascade/
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-07-15
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release.
|
|
13
|
+
- `route()` / `complete()` / `complete_json()` core API with a free-first cascade:
|
|
14
|
+
local Ollama -> OpenRouter free models -> paid Anthropic API.
|
|
15
|
+
- Automatic fallback on provider overload, and a final degrade-to-free safety net
|
|
16
|
+
if the paid API is unreachable for any reason.
|
|
17
|
+
- `pick()` to resolve a task or tier name to its routing config, and `stats()` to
|
|
18
|
+
read the cumulative savings ledger.
|
|
19
|
+
- `llm-cascade` CLI (`--task`, `--routes`, `--stats`, `--json`).
|
|
20
|
+
- Configuration via optional TOML file (`~/.llm_cascade/routes.toml` or
|
|
21
|
+
`LLM_CASCADE_CONFIG`) with environment variable overrides.
|
|
22
|
+
- Zero required third-party dependencies (optional `[anthropic]` extra).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 luandv92
|
|
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,196 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-cascade
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Free-first LLM routing for Python: local Ollama -> OpenRouter free models -> paid Anthropic API, with automatic fallback and a savings ledger.
|
|
5
|
+
Project-URL: Homepage, https://github.com/luandv92/llm-cascade
|
|
6
|
+
Project-URL: Repository, https://github.com/luandv92/llm-cascade
|
|
7
|
+
Project-URL: Issues, https://github.com/luandv92/llm-cascade/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/luandv92/llm-cascade/blob/main/CHANGELOG.md
|
|
9
|
+
Author: luandv92
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,anthropic,claude,cost-optimization,llm,ollama,openrouter,router
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
27
|
+
Provides-Extra: anthropic
|
|
28
|
+
Requires-Dist: anthropic>=0.40.0; extra == 'anthropic'
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# llm-cascade
|
|
35
|
+
|
|
36
|
+
[](https://github.com/luandv92/llm-cascade/actions/workflows/ci.yml)
|
|
37
|
+
[](https://pypi.org/project/llm-cascade/)
|
|
38
|
+
[](LICENSE)
|
|
39
|
+
[](https://pypi.org/project/llm-cascade/)
|
|
40
|
+
|
|
41
|
+
Free-first LLM routing for Python: local Ollama ($0) -> OpenRouter free models
|
|
42
|
+
($0) -> paid Anthropic API, with automatic fallback and a savings ledger.
|
|
43
|
+
Zero required dependencies.
|
|
44
|
+
|
|
45
|
+
## Why
|
|
46
|
+
|
|
47
|
+
Most teams route every prompt to the same frontier model regardless of how
|
|
48
|
+
hard the task actually is, and the API bill grows with usage instead of with
|
|
49
|
+
value delivered. llm-cascade flips that: it tries the cheapest tier that can
|
|
50
|
+
plausibly do the job first, and only escalates when that tier is unreachable
|
|
51
|
+
or genuinely too weak for the task. In production, that means classification,
|
|
52
|
+
extraction, and summarization run for $0 on a local model or a free cloud
|
|
53
|
+
model, while code review, planning, and long-form writing still get a
|
|
54
|
+
frontier model -- and if the paid API is ever down or out of credits, your
|
|
55
|
+
scheduled jobs degrade to a free tier instead of crashing.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
pip install llm-cascade
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The base package has **zero required third-party dependencies**. To use the
|
|
64
|
+
paid Anthropic tier, install the extra:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install "llm-cascade[anthropic]"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Without it, the local (Ollama) and free-cloud (OpenRouter) tiers still work
|
|
71
|
+
end to end -- you'll only see an error if a task actually routes to the paid
|
|
72
|
+
tier and the SDK isn't installed.
|
|
73
|
+
|
|
74
|
+
## Quickstart
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from llm_cascade import route
|
|
78
|
+
|
|
79
|
+
result = route("summarize", "Paste a long document here...")
|
|
80
|
+
print(result.text) # the completion
|
|
81
|
+
print(result.tier) # which tier served it: trivial/local/cheap/light/heavy
|
|
82
|
+
print(result.model) # e.g. "local:ollama", "openrouter:qwen/...", "claude-sonnet-5"
|
|
83
|
+
print(result.cost_usd) # 0.0 for local/free-cloud tiers
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Or from the command line:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
llm-cascade "Summarize this text: ..." --task summarize
|
|
90
|
+
llm-cascade --routes # show the tier ladder and task -> tier table
|
|
91
|
+
llm-cascade --stats # show cumulative cost and free-vs-paid call counts
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## How routing works
|
|
95
|
+
|
|
96
|
+
Each call resolves a **task name** (or a tier name directly) to a **tier**,
|
|
97
|
+
then runs that tier's provider chain in order until one succeeds:
|
|
98
|
+
|
|
99
|
+
| Tier | Chain | Use for |
|
|
100
|
+
|-----------|---------------------------------------------------|-------------------------------------------|
|
|
101
|
+
| `trivial` | local Ollama only ($0) | high-volume micro-tasks, never worth the network |
|
|
102
|
+
| `local` | local Ollama ($0) -> OpenRouter free ($0) -> paid | mechanical NLP: classify, extract, summarize, translate |
|
|
103
|
+
| `cheap` | OpenRouter free ($0) -> paid | manual opt-in tier, invoke directly with `task="cheap"` |
|
|
104
|
+
| `light` | paid (Sonnet); free-cloud only if `LLM_CASCADE_LIGHT_FREE=1` | customer-facing copy that needs reliable polish |
|
|
105
|
+
| `heavy` | paid (Opus), adaptive thinking, high effort | real reasoning: code, review, planning, long-form |
|
|
106
|
+
|
|
107
|
+
If the paid API call fails for *any* reason -- bad or revoked key, no
|
|
108
|
+
credits, a provider outage, a network blip -- llm-cascade degrades to the
|
|
109
|
+
free tiers instead of raising, so scheduled or automated callers don't break
|
|
110
|
+
outright. It only raises once every tier has been tried and failed.
|
|
111
|
+
|
|
112
|
+
The default task -> tier table:
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
classify, extract, summarize, translate, chat -> local
|
|
116
|
+
rewrite -> light
|
|
117
|
+
code, review, plan, creative -> heavy
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Anything not in that table falls back to `default_task_tier` (`light` by
|
|
121
|
+
default). See `config/routes.example.toml` for the full, commented schema.
|
|
122
|
+
|
|
123
|
+
## Configuration
|
|
124
|
+
|
|
125
|
+
Configuration is resolved with this precedence (lowest to highest):
|
|
126
|
+
|
|
127
|
+
1. Built-in defaults.
|
|
128
|
+
2. An optional TOML file: `~/.llm_cascade/routes.toml`, or the path given in
|
|
129
|
+
`LLM_CASCADE_CONFIG`.
|
|
130
|
+
3. Environment variables, for the handful of settings that make sense as env
|
|
131
|
+
vars.
|
|
132
|
+
|
|
133
|
+
Copy [`config/routes.example.toml`](config/routes.example.toml) to
|
|
134
|
+
`~/.llm_cascade/routes.toml` to add your own task names, retune which tier
|
|
135
|
+
handles a task, or override model ids and pricing.
|
|
136
|
+
|
|
137
|
+
Environment variables:
|
|
138
|
+
|
|
139
|
+
| Variable | Purpose |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `ANTHROPIC_API_KEY` | Enables the paid tier (`light`/`heavy`, and the final safety net). |
|
|
142
|
+
| `OPENROUTER_API_KEY` | Enables the free-cloud tier (OpenRouter `:free` models). |
|
|
143
|
+
| `LLM_CASCADE_CONFIG` | Path to a TOML config file, instead of `~/.llm_cascade/routes.toml`. |
|
|
144
|
+
| `LLM_CASCADE_LOG_PATH` | Path to the savings ledger, instead of `~/.llm_cascade/log.jsonl`. |
|
|
145
|
+
| `LLM_CASCADE_LIGHT_FREE` | Set to `1` to try free-cloud first on the `light` tier too. |
|
|
146
|
+
| `LLM_CASCADE_NO_FREE_CLOUD` | Set to any value to disable the free-cloud tier globally. |
|
|
147
|
+
| `LLM_CASCADE_HTTP_REFERER` / `LLM_CASCADE_APP_TITLE` | Sent as `HTTP-Referer` / `X-Title` on OpenRouter requests (optional; omitted if unset). |
|
|
148
|
+
| `LLM_CASCADE_OPENROUTER_MODEL` / `LLM_CASCADE_OPENROUTER_MODELS` | Override the free model (singular) or ordered list (comma-separated) tried on OpenRouter. |
|
|
149
|
+
| `LLM_CASCADE_OLLAMA_BASE_URL` | Ollama server URL (default `http://localhost:11434`). |
|
|
150
|
+
|
|
151
|
+
See [`.env.example`](.env.example) for a copy-pasteable starting point.
|
|
152
|
+
|
|
153
|
+
## Savings ledger
|
|
154
|
+
|
|
155
|
+
Every call -- free or paid -- appends one line to a JSONL ledger
|
|
156
|
+
(`~/.llm_cascade/log.jsonl` by default). `stats()` / `llm-cascade --stats`
|
|
157
|
+
summarize it:
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
$ llm-cascade --stats
|
|
161
|
+
{
|
|
162
|
+
"paid_calls": 3,
|
|
163
|
+
"free_calls": 41,
|
|
164
|
+
"total_cost_usd": 0.0842,
|
|
165
|
+
"by_model": {
|
|
166
|
+
"local:ollama": 30,
|
|
167
|
+
"openrouter:qwen/qwen3-next-80b-a3b-instruct:free": 11,
|
|
168
|
+
"claude-sonnet-5": 3
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Comparison
|
|
174
|
+
|
|
175
|
+
Unlike heavier multi-provider routers, llm-cascade ships with zero required
|
|
176
|
+
dependencies and a single opinionated cascade -- it's a routing policy you can
|
|
177
|
+
read in one file, not a platform.
|
|
178
|
+
|
|
179
|
+
## Contributing
|
|
180
|
+
|
|
181
|
+
Issues and pull requests are welcome at
|
|
182
|
+
[github.com/luandv92/llm-cascade](https://github.com/luandv92/llm-cascade).
|
|
183
|
+
Before opening a PR:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
pip install -e ".[dev,anthropic]"
|
|
187
|
+
ruff check .
|
|
188
|
+
pytest tests/ -v
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Tests never touch real networks -- Ollama, OpenRouter, and the Anthropic SDK
|
|
192
|
+
are all mocked (see `tests/conftest.py`).
|
|
193
|
+
|
|
194
|
+
## License
|
|
195
|
+
|
|
196
|
+
MIT -- see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# llm-cascade
|
|
2
|
+
|
|
3
|
+
[](https://github.com/luandv92/llm-cascade/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/llm-cascade/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://pypi.org/project/llm-cascade/)
|
|
7
|
+
|
|
8
|
+
Free-first LLM routing for Python: local Ollama ($0) -> OpenRouter free models
|
|
9
|
+
($0) -> paid Anthropic API, with automatic fallback and a savings ledger.
|
|
10
|
+
Zero required dependencies.
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
Most teams route every prompt to the same frontier model regardless of how
|
|
15
|
+
hard the task actually is, and the API bill grows with usage instead of with
|
|
16
|
+
value delivered. llm-cascade flips that: it tries the cheapest tier that can
|
|
17
|
+
plausibly do the job first, and only escalates when that tier is unreachable
|
|
18
|
+
or genuinely too weak for the task. In production, that means classification,
|
|
19
|
+
extraction, and summarization run for $0 on a local model or a free cloud
|
|
20
|
+
model, while code review, planning, and long-form writing still get a
|
|
21
|
+
frontier model -- and if the paid API is ever down or out of credits, your
|
|
22
|
+
scheduled jobs degrade to a free tier instead of crashing.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pip install llm-cascade
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The base package has **zero required third-party dependencies**. To use the
|
|
31
|
+
paid Anthropic tier, install the extra:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install "llm-cascade[anthropic]"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Without it, the local (Ollama) and free-cloud (OpenRouter) tiers still work
|
|
38
|
+
end to end -- you'll only see an error if a task actually routes to the paid
|
|
39
|
+
tier and the SDK isn't installed.
|
|
40
|
+
|
|
41
|
+
## Quickstart
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from llm_cascade import route
|
|
45
|
+
|
|
46
|
+
result = route("summarize", "Paste a long document here...")
|
|
47
|
+
print(result.text) # the completion
|
|
48
|
+
print(result.tier) # which tier served it: trivial/local/cheap/light/heavy
|
|
49
|
+
print(result.model) # e.g. "local:ollama", "openrouter:qwen/...", "claude-sonnet-5"
|
|
50
|
+
print(result.cost_usd) # 0.0 for local/free-cloud tiers
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or from the command line:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
llm-cascade "Summarize this text: ..." --task summarize
|
|
57
|
+
llm-cascade --routes # show the tier ladder and task -> tier table
|
|
58
|
+
llm-cascade --stats # show cumulative cost and free-vs-paid call counts
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## How routing works
|
|
62
|
+
|
|
63
|
+
Each call resolves a **task name** (or a tier name directly) to a **tier**,
|
|
64
|
+
then runs that tier's provider chain in order until one succeeds:
|
|
65
|
+
|
|
66
|
+
| Tier | Chain | Use for |
|
|
67
|
+
|-----------|---------------------------------------------------|-------------------------------------------|
|
|
68
|
+
| `trivial` | local Ollama only ($0) | high-volume micro-tasks, never worth the network |
|
|
69
|
+
| `local` | local Ollama ($0) -> OpenRouter free ($0) -> paid | mechanical NLP: classify, extract, summarize, translate |
|
|
70
|
+
| `cheap` | OpenRouter free ($0) -> paid | manual opt-in tier, invoke directly with `task="cheap"` |
|
|
71
|
+
| `light` | paid (Sonnet); free-cloud only if `LLM_CASCADE_LIGHT_FREE=1` | customer-facing copy that needs reliable polish |
|
|
72
|
+
| `heavy` | paid (Opus), adaptive thinking, high effort | real reasoning: code, review, planning, long-form |
|
|
73
|
+
|
|
74
|
+
If the paid API call fails for *any* reason -- bad or revoked key, no
|
|
75
|
+
credits, a provider outage, a network blip -- llm-cascade degrades to the
|
|
76
|
+
free tiers instead of raising, so scheduled or automated callers don't break
|
|
77
|
+
outright. It only raises once every tier has been tried and failed.
|
|
78
|
+
|
|
79
|
+
The default task -> tier table:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
classify, extract, summarize, translate, chat -> local
|
|
83
|
+
rewrite -> light
|
|
84
|
+
code, review, plan, creative -> heavy
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Anything not in that table falls back to `default_task_tier` (`light` by
|
|
88
|
+
default). See `config/routes.example.toml` for the full, commented schema.
|
|
89
|
+
|
|
90
|
+
## Configuration
|
|
91
|
+
|
|
92
|
+
Configuration is resolved with this precedence (lowest to highest):
|
|
93
|
+
|
|
94
|
+
1. Built-in defaults.
|
|
95
|
+
2. An optional TOML file: `~/.llm_cascade/routes.toml`, or the path given in
|
|
96
|
+
`LLM_CASCADE_CONFIG`.
|
|
97
|
+
3. Environment variables, for the handful of settings that make sense as env
|
|
98
|
+
vars.
|
|
99
|
+
|
|
100
|
+
Copy [`config/routes.example.toml`](config/routes.example.toml) to
|
|
101
|
+
`~/.llm_cascade/routes.toml` to add your own task names, retune which tier
|
|
102
|
+
handles a task, or override model ids and pricing.
|
|
103
|
+
|
|
104
|
+
Environment variables:
|
|
105
|
+
|
|
106
|
+
| Variable | Purpose |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `ANTHROPIC_API_KEY` | Enables the paid tier (`light`/`heavy`, and the final safety net). |
|
|
109
|
+
| `OPENROUTER_API_KEY` | Enables the free-cloud tier (OpenRouter `:free` models). |
|
|
110
|
+
| `LLM_CASCADE_CONFIG` | Path to a TOML config file, instead of `~/.llm_cascade/routes.toml`. |
|
|
111
|
+
| `LLM_CASCADE_LOG_PATH` | Path to the savings ledger, instead of `~/.llm_cascade/log.jsonl`. |
|
|
112
|
+
| `LLM_CASCADE_LIGHT_FREE` | Set to `1` to try free-cloud first on the `light` tier too. |
|
|
113
|
+
| `LLM_CASCADE_NO_FREE_CLOUD` | Set to any value to disable the free-cloud tier globally. |
|
|
114
|
+
| `LLM_CASCADE_HTTP_REFERER` / `LLM_CASCADE_APP_TITLE` | Sent as `HTTP-Referer` / `X-Title` on OpenRouter requests (optional; omitted if unset). |
|
|
115
|
+
| `LLM_CASCADE_OPENROUTER_MODEL` / `LLM_CASCADE_OPENROUTER_MODELS` | Override the free model (singular) or ordered list (comma-separated) tried on OpenRouter. |
|
|
116
|
+
| `LLM_CASCADE_OLLAMA_BASE_URL` | Ollama server URL (default `http://localhost:11434`). |
|
|
117
|
+
|
|
118
|
+
See [`.env.example`](.env.example) for a copy-pasteable starting point.
|
|
119
|
+
|
|
120
|
+
## Savings ledger
|
|
121
|
+
|
|
122
|
+
Every call -- free or paid -- appends one line to a JSONL ledger
|
|
123
|
+
(`~/.llm_cascade/log.jsonl` by default). `stats()` / `llm-cascade --stats`
|
|
124
|
+
summarize it:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
$ llm-cascade --stats
|
|
128
|
+
{
|
|
129
|
+
"paid_calls": 3,
|
|
130
|
+
"free_calls": 41,
|
|
131
|
+
"total_cost_usd": 0.0842,
|
|
132
|
+
"by_model": {
|
|
133
|
+
"local:ollama": 30,
|
|
134
|
+
"openrouter:qwen/qwen3-next-80b-a3b-instruct:free": 11,
|
|
135
|
+
"claude-sonnet-5": 3
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Comparison
|
|
141
|
+
|
|
142
|
+
Unlike heavier multi-provider routers, llm-cascade ships with zero required
|
|
143
|
+
dependencies and a single opinionated cascade -- it's a routing policy you can
|
|
144
|
+
read in one file, not a platform.
|
|
145
|
+
|
|
146
|
+
## Contributing
|
|
147
|
+
|
|
148
|
+
Issues and pull requests are welcome at
|
|
149
|
+
[github.com/luandv92/llm-cascade](https://github.com/luandv92/llm-cascade).
|
|
150
|
+
Before opening a PR:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install -e ".[dev,anthropic]"
|
|
154
|
+
ruff check .
|
|
155
|
+
pytest tests/ -v
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Tests never touch real networks -- Ollama, OpenRouter, and the Anthropic SDK
|
|
159
|
+
are all mocked (see `tests/conftest.py`).
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT -- see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Example llm-cascade configuration.
|
|
2
|
+
#
|
|
3
|
+
# Copy this file to one of:
|
|
4
|
+
# ~/.llm_cascade/routes.toml (default location)
|
|
5
|
+
# any path, then set LLM_CASCADE_CONFIG=/path/to/routes.toml
|
|
6
|
+
#
|
|
7
|
+
# Every section is optional. Anything you omit falls back to the built-in
|
|
8
|
+
# default shown in comments. Environment variables (see .env.example) always
|
|
9
|
+
# take precedence over this file for the settings they control (API keys,
|
|
10
|
+
# HTTP headers, the log path, and the free-cloud opt-in/kill-switch).
|
|
11
|
+
|
|
12
|
+
# Override which concrete model id backs each named tier.
|
|
13
|
+
[models]
|
|
14
|
+
opus = "claude-opus-4-8"
|
|
15
|
+
sonnet = "claude-sonnet-5"
|
|
16
|
+
haiku = "claude-haiku-4-5"
|
|
17
|
+
|
|
18
|
+
# USD price per 1M tokens, as [input, output]. Used only to compute the numbers
|
|
19
|
+
# shown by `llm-cascade --stats` -- it does not affect what you're actually
|
|
20
|
+
# billed by Anthropic.
|
|
21
|
+
[pricing]
|
|
22
|
+
claude-opus-4-8 = [5.0, 25.0]
|
|
23
|
+
claude-sonnet-5 = [2.0, 10.0]
|
|
24
|
+
claude-haiku-4-5 = [1.0, 5.0]
|
|
25
|
+
|
|
26
|
+
# The policy: which tier handles each task name. Tiers are "trivial" (local
|
|
27
|
+
# only, $0), "local" (local -> free cloud -> paid, safety net), "cheap" (free
|
|
28
|
+
# cloud -> paid, manual-only tier), "light" (paid Sonnet, free-cloud opt-in),
|
|
29
|
+
# and "heavy" (paid Opus, adaptive thinking). Add your own task names here --
|
|
30
|
+
# anything not listed falls back to `default_task_tier`.
|
|
31
|
+
[task_routes]
|
|
32
|
+
classify = "local"
|
|
33
|
+
extract = "local"
|
|
34
|
+
summarize = "local"
|
|
35
|
+
translate = "local"
|
|
36
|
+
rewrite = "light"
|
|
37
|
+
code = "heavy"
|
|
38
|
+
review = "heavy"
|
|
39
|
+
plan = "heavy"
|
|
40
|
+
creative = "heavy"
|
|
41
|
+
chat = "local"
|
|
42
|
+
# my_custom_task = "heavy"
|
|
43
|
+
|
|
44
|
+
# Tier used for any task name not found in [task_routes] above.
|
|
45
|
+
default_task_tier = "light"
|
|
46
|
+
|
|
47
|
+
# OpenRouter ":free" models tried in order for the free-cloud tier. Listed from
|
|
48
|
+
# different providers so rate limits are uncorrelated -- if one is saturated,
|
|
49
|
+
# the next is usually open.
|
|
50
|
+
openrouter_models = [
|
|
51
|
+
"meta-llama/llama-3.3-70b-instruct:free",
|
|
52
|
+
"qwen/qwen3-next-80b-a3b-instruct:free",
|
|
53
|
+
"google/gemma-4-31b-it:free",
|
|
54
|
+
"nvidia/nemotron-3-super-120b-a12b:free",
|
|
55
|
+
"nousresearch/hermes-3-llama-3.1-405b:free",
|
|
56
|
+
]
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Quickstart for llm-cascade.
|
|
3
|
+
|
|
4
|
+
Run it as-is with no configuration to see the local-only ("trivial") tier, or
|
|
5
|
+
set ANTHROPIC_API_KEY / OPENROUTER_API_KEY to unlock the higher tiers.
|
|
6
|
+
|
|
7
|
+
python examples/quickstart.py
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from llm_cascade import complete, complete_json, pick, route, stats
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main() -> None:
|
|
15
|
+
# route() is the main entrypoint: it returns a Completion with the text,
|
|
16
|
+
# the tier and model that served it, and the USD cost (0.0 for free tiers).
|
|
17
|
+
result = route("summarize", "Summarize: the quick brown fox jumps over the lazy dog.")
|
|
18
|
+
print(f"[{result.tier} -> {result.model}, ${result.cost_usd:.4f}]")
|
|
19
|
+
print(result.text)
|
|
20
|
+
print()
|
|
21
|
+
|
|
22
|
+
# complete() is a thin wrapper that returns just the text.
|
|
23
|
+
caption = complete("Write one short, upbeat product tagline.", task="creative")
|
|
24
|
+
print("Tagline:", caption)
|
|
25
|
+
print()
|
|
26
|
+
|
|
27
|
+
# complete_json() asks the model for structured output and parses it.
|
|
28
|
+
try:
|
|
29
|
+
data = complete_json(
|
|
30
|
+
'Extract as JSON: {"name": str, "age": int}. Text: "Alice is 30 years old."',
|
|
31
|
+
task="extract",
|
|
32
|
+
)
|
|
33
|
+
print("Extracted JSON:", data)
|
|
34
|
+
except Exception as e: # pragma: no cover - depends on which tier is available
|
|
35
|
+
print("complete_json skipped:", e)
|
|
36
|
+
print()
|
|
37
|
+
|
|
38
|
+
# pick() resolves a task name to its routing config without running anything.
|
|
39
|
+
print("Route for 'code':", pick("code"))
|
|
40
|
+
print()
|
|
41
|
+
|
|
42
|
+
# stats() reads the cumulative savings ledger (~/.llm_cascade/log.jsonl).
|
|
43
|
+
print("Savings ledger:", stats())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
main()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llm-cascade"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Free-first LLM routing for Python: local Ollama -> OpenRouter free models -> paid Anthropic API, with automatic fallback and a savings ledger."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "luandv92" },
|
|
14
|
+
]
|
|
15
|
+
keywords = [
|
|
16
|
+
"llm",
|
|
17
|
+
"router",
|
|
18
|
+
"anthropic",
|
|
19
|
+
"claude",
|
|
20
|
+
"ollama",
|
|
21
|
+
"openrouter",
|
|
22
|
+
"cost-optimization",
|
|
23
|
+
"ai",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3.10",
|
|
32
|
+
"Programming Language :: Python :: 3.11",
|
|
33
|
+
"Programming Language :: Python :: 3.12",
|
|
34
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
35
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
36
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
37
|
+
"Typing :: Typed",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
# Zero required third-party dependencies. `tomli` only backfills the stdlib
|
|
41
|
+
# `tomllib` parser (added in Python 3.11) for older interpreters.
|
|
42
|
+
dependencies = [
|
|
43
|
+
"tomli>=2.0.1; python_version < '3.11'",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.optional-dependencies]
|
|
47
|
+
anthropic = [
|
|
48
|
+
"anthropic>=0.40.0",
|
|
49
|
+
]
|
|
50
|
+
dev = [
|
|
51
|
+
"pytest>=8.0",
|
|
52
|
+
"ruff>=0.6",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[project.urls]
|
|
56
|
+
Homepage = "https://github.com/luandv92/llm-cascade"
|
|
57
|
+
Repository = "https://github.com/luandv92/llm-cascade"
|
|
58
|
+
Issues = "https://github.com/luandv92/llm-cascade/issues"
|
|
59
|
+
Changelog = "https://github.com/luandv92/llm-cascade/blob/main/CHANGELOG.md"
|
|
60
|
+
|
|
61
|
+
[project.scripts]
|
|
62
|
+
llm-cascade = "llm_cascade.cli:main"
|
|
63
|
+
|
|
64
|
+
[tool.hatch.build.targets.wheel]
|
|
65
|
+
packages = ["src/llm_cascade"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff]
|
|
68
|
+
line-length = 100
|
|
69
|
+
target-version = "py310"
|
|
70
|
+
|
|
71
|
+
[tool.ruff.lint]
|
|
72
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
73
|
+
|
|
74
|
+
[tool.pytest.ini_options]
|
|
75
|
+
testpaths = ["tests"]
|