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.
@@ -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
+ [![CI](https://github.com/luandv92/llm-cascade/actions/workflows/ci.yml/badge.svg)](https://github.com/luandv92/llm-cascade/actions/workflows/ci.yml)
37
+ [![PyPI](https://img.shields.io/pypi/v/llm-cascade.svg)](https://pypi.org/project/llm-cascade/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
39
+ [![Python versions](https://img.shields.io/pypi/pyversions/llm-cascade.svg)](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
+ [![CI](https://github.com/luandv92/llm-cascade/actions/workflows/ci.yml/badge.svg)](https://github.com/luandv92/llm-cascade/actions/workflows/ci.yml)
4
+ [![PyPI](https://img.shields.io/pypi/v/llm-cascade.svg)](https://pypi.org/project/llm-cascade/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+ [![Python versions](https://img.shields.io/pypi/pyversions/llm-cascade.svg)](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"]