agentic-llm-router 0.2.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.
- agentic_llm_router-0.2.0/LICENSE +21 -0
- agentic_llm_router-0.2.0/PKG-INFO +277 -0
- agentic_llm_router-0.2.0/README.md +256 -0
- agentic_llm_router-0.2.0/pyproject.toml +30 -0
- agentic_llm_router-0.2.0/setup.cfg +4 -0
- agentic_llm_router-0.2.0/src/agentic_llm_router.egg-info/PKG-INFO +277 -0
- agentic_llm_router-0.2.0/src/agentic_llm_router.egg-info/SOURCES.txt +20 -0
- agentic_llm_router-0.2.0/src/agentic_llm_router.egg-info/dependency_links.txt +1 -0
- agentic_llm_router-0.2.0/src/agentic_llm_router.egg-info/requires.txt +8 -0
- agentic_llm_router-0.2.0/src/agentic_llm_router.egg-info/top_level.txt +1 -0
- agentic_llm_router-0.2.0/src/llm_router/__init__.py +55 -0
- agentic_llm_router-0.2.0/src/llm_router/budget.py +219 -0
- agentic_llm_router-0.2.0/src/llm_router/client.py +191 -0
- agentic_llm_router-0.2.0/src/llm_router/receipt.py +738 -0
- agentic_llm_router-0.2.0/tests/test_agent_contract.py +12 -0
- agentic_llm_router-0.2.0/tests/test_budget.py +104 -0
- agentic_llm_router-0.2.0/tests/test_client.py +312 -0
- agentic_llm_router-0.2.0/tests/test_ecosystem_component_contract.py +103 -0
- agentic_llm_router-0.2.0/tests/test_package_contract.py +23 -0
- agentic_llm_router-0.2.0/tests/test_receipt.py +282 -0
- agentic_llm_router-0.2.0/tests/test_receipt_contract_artifacts.py +35 -0
- agentic_llm_router-0.2.0/tests/test_security_portfolio_roadmap_contract.py +35 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 krivonosoff161
|
|
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,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentic-llm-router
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Cost-aware multi-provider LLM router with role tiers (cheap/mid/chief/audit).
|
|
5
|
+
Author: krivonosoff161
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/krivonosoff161/llm-router
|
|
8
|
+
Project-URL: Repository, https://github.com/krivonosoff161/llm-router
|
|
9
|
+
Keywords: llm,openai,router,agents,cost,qwen,yandex
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: aiohttp>=3.9
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: build>=1.2.2; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
17
|
+
Requires-Dist: ruff>=0.6; extra == "dev"
|
|
18
|
+
Requires-Dist: setuptools>=77; extra == "dev"
|
|
19
|
+
Requires-Dist: wheel>=0.44; extra == "dev"
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# llm-router
|
|
23
|
+
|
|
24
|
+
Ecosystem role and current integration status: [component roadmap](docs/component-roadmap.md).
|
|
25
|
+
The public cross-repository plan is owned by the
|
|
26
|
+
[Agentic Security Harness ecosystem roadmap](https://github.com/krivonosoff161/agentic-security-harness/blob/main/docs/ecosystem-roadmap.md).
|
|
27
|
+
|
|
28
|
+
[](https://github.com/krivonosoff161/llm-router/actions/workflows/tests.yml)
|
|
29
|
+
[](LICENSE)
|
|
30
|
+
[](https://www.python.org/)
|
|
31
|
+
|
|
32
|
+
**A tiny, dependency-light async LLM router with role tiers and per-call cost logging.**
|
|
33
|
+
One `call()` interface for the tested OpenAI-compatible request shape and a
|
|
34
|
+
separate Yandex AI Studio path. Provider compatibility depends on each endpoint's
|
|
35
|
+
current API contract and must be verified before use.
|
|
36
|
+
|
|
37
|
+
> The public library demonstrates a cheap-to-chief routing pattern with offline
|
|
38
|
+
> tests. It does not publish or verify a production deployment claim. No SDKs or
|
|
39
|
+
> models are hardcoded in the routing logic.
|
|
40
|
+
|
|
41
|
+
The repository now publishes a source-owned, offline invocation-receipt contract and is
|
|
42
|
+
therefore `contract_only` in the ecosystem. Its source tree builds the unique distribution
|
|
43
|
+
candidate `agentic-llm-router==0.2.0`, imported as `llm_router`. It is not yet published or
|
|
44
|
+
automatically activated by Harness.
|
|
45
|
+
|
|
46
|
+
> Supply-chain boundary: the generic PyPI name `llm-router` belongs to another project.
|
|
47
|
+
> Do not install or declare that coordinate for this repository. The only planned public
|
|
48
|
+
> distribution coordinate is `agentic-llm-router`.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Why
|
|
53
|
+
|
|
54
|
+
In agentic systems most LLM calls are cheap bulk work (extract, classify, filter) and a few are high-stakes (the final decision). Paying flagship prices for everything is wasteful; juggling provider SDKs is annoying. `llm-router` gives you:
|
|
55
|
+
|
|
56
|
+
- **Role tiers** — `cheap` / `mid` / `chief` / `audit`, each mapped to a model via env. Route volume to `cheap`, escalate only candidates to `chief`.
|
|
57
|
+
- **Provider flexibility** — a custom `OPENAI_BASE_URL` can target endpoints that
|
|
58
|
+
implement the tested request/response contract; Yandex AI Studio has a separate
|
|
59
|
+
path. Provider identity, terms, availability, and exact compatibility are external
|
|
60
|
+
gates.
|
|
61
|
+
- **Per-call cost** — every call returns token counts and cost in **USD + a configurable local currency** (set `LLM_FX` / `LLM_CCY`). Aggregate the dicts to a budget log.
|
|
62
|
+
- **Budget helpers** — aggregate usage records, check a daily cap, and estimate savings
|
|
63
|
+
versus sending the same tokens to the `chief` model.
|
|
64
|
+
- **Resilience** — retries on `429` / `5xx` with exponential backoff.
|
|
65
|
+
- **Canonical receipt contract** — strict, digest-bound attempt, usage, pricing, and FX
|
|
66
|
+
evidence without credentials, endpoints, prompts, output text, response bodies, or
|
|
67
|
+
exception messages.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Features
|
|
72
|
+
|
|
73
|
+
- Single `async call(role, system, user) -> (text | None, usage)` interface.
|
|
74
|
+
- Four configurable role tiers, models set per provider via env.
|
|
75
|
+
- OpenAI-compatible **and** Yandex AI Studio providers.
|
|
76
|
+
- `json_mode=True` → adds `response_format={"type":"json_object"}` (OpenAI-compatible).
|
|
77
|
+
- Cost estimation from an override-able price table (`LLM_PRICE_<MODEL>_IN/OUT_USD_PER_1M`).
|
|
78
|
+
- Budget helpers for logs you own: `summarize_usage`, `budget_status`, and
|
|
79
|
+
`build_savings_report`.
|
|
80
|
+
- Zero secrets cached at import — all config read live from env.
|
|
81
|
+
- ~150 LOC, one runtime dependency (`aiohttp`).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
git clone https://github.com/krivonosoff161/llm-router
|
|
89
|
+
cd llm-router
|
|
90
|
+
python -m build
|
|
91
|
+
python -m pip install dist/agentic_llm_router-0.2.0-py3-none-any.whl
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For editable development use `python -m pip install -e .[dev]`. Requires **Python 3.9+**.
|
|
95
|
+
CI builds and installs the exact wheel on Linux and Windows. Harness `main` declares a
|
|
96
|
+
source-only `router` extra using the unique `agentic-llm-router` distribution name, but
|
|
97
|
+
this package is not on PyPI and published Harness `v1.3.0` metadata does not contain that
|
|
98
|
+
extra. Public `pip install agentic-security-harness[router]` support is therefore
|
|
99
|
+
unavailable; package publication and newer Harness package metadata remain separate release gates.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Quickstart
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
import asyncio
|
|
107
|
+
from llm_router import call
|
|
108
|
+
|
|
109
|
+
async def main():
|
|
110
|
+
text, usage = await call("cheap", "You are concise.", "Name 3 primary colors.")
|
|
111
|
+
print(text)
|
|
112
|
+
print(usage) # {provider, model, role, input_tokens, output_tokens,
|
|
113
|
+
# total_tokens, cost_usd, cost_local, currency}
|
|
114
|
+
|
|
115
|
+
asyncio.run(main())
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Set at least a provider + key first (see **Configuration**). For OpenAI:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export OPENAI_API_KEY=sk-...
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Providers
|
|
127
|
+
|
|
128
|
+
| Provider | Set | Auth |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| **OpenAI** | `LLM_PROVIDER=openai` (default), `OPENAI_API_KEY` | `Bearer` |
|
|
131
|
+
| **Alibaba Qwen** | `OPENAI_BASE_URL=<dashscope compatible-mode/v1>` + `OPENAI_API_KEY` | `Bearer` |
|
|
132
|
+
| **OpenRouter / Together / Ollama / vLLM** | `OPENAI_BASE_URL=<their /v1>` + `OPENAI_API_KEY` | `Bearer` |
|
|
133
|
+
| **Yandex AI Studio** | `LLM_PROVIDER=yandex`, `YANDEX_API_KEY`, `YANDEX_FOLDER_ID` | `Api-Key` |
|
|
134
|
+
|
|
135
|
+
> The base URL must NOT include `/chat/completions` — the router appends it.
|
|
136
|
+
> **Yandex** requires `YANDEX_FOLDER_ID` (or an explicit `YANDEX_<ROLE>_MODEL`); otherwise `model_for` raises a clear configuration error (fail-fast) instead of sending an empty model.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Roles
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
from llm_router import call, model_for
|
|
144
|
+
|
|
145
|
+
model_for("cheap") # -> e.g. "gpt-4o-mini" (or your LLM_CHEAP_MODEL)
|
|
146
|
+
model_for("chief") # -> e.g. "gpt-4o"
|
|
147
|
+
|
|
148
|
+
# pattern: cheap for volume, chief only when it matters
|
|
149
|
+
facts, u1 = await call("cheap", EXTRACT_PROMPT, raw_text)
|
|
150
|
+
if looks_important(facts):
|
|
151
|
+
verdict, u2 = await call("chief", DECIDE_PROMPT, facts, json_mode=True)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Cost logging
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
text, usage = await call("cheap", sys, user)
|
|
160
|
+
# usage["cost_usd"] -> e.g. 0.0001
|
|
161
|
+
# usage["cost_local"] -> cost_usd * LLM_FX
|
|
162
|
+
# usage["currency"] -> LLM_CCY (e.g. "RUB")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Append each `usage` to a JSONL file and you have a per-call budget log. Prices come from a small built-in table and are **illustrative** — override per model:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
export LLM_PRICE_GPT_4O_MINI_IN_USD_PER_1M=0.15
|
|
169
|
+
export LLM_PRICE_GPT_4O_MINI_OUT_USD_PER_1M=0.60
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Summarize a batch of usage records:
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from llm_router import summarize_usage, budget_status, build_savings_report
|
|
176
|
+
|
|
177
|
+
usages = [u1, u2] # dicts returned by call()
|
|
178
|
+
print(summarize_usage(usages).as_dict())
|
|
179
|
+
print(budget_status(usages, limit_usd=1.00).as_dict())
|
|
180
|
+
print(build_savings_report(usages, counterfactual_role="chief").as_dict())
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`LLM_BUDGET_USD_DAY` can be used as a default budget cap for `budget_status(...)`.
|
|
184
|
+
The router stays stateless; you decide where the JSONL budget log lives.
|
|
185
|
+
|
|
186
|
+
## Canonical invocation receipts
|
|
187
|
+
|
|
188
|
+
[`router-invocation-receipt-v1.0`](docs/invocation-receipt.md) is a separate offline
|
|
189
|
+
interchange surface for already-observed sanitized values. It uses canonical UTF-8 JSON,
|
|
190
|
+
domain-separated content identities, contiguous attempt accounting, strict token totals,
|
|
191
|
+
and integer nano-unit cost arithmetic. Pricing and FX inputs bind caller-supplied source
|
|
192
|
+
artifact digests; those digests are evidence references, not authenticity proofs.
|
|
193
|
+
|
|
194
|
+
The receipt builder never calls a provider and the existing `call()` return value is
|
|
195
|
+
unchanged. A receipt contains digests of request, response, output, model, and producer
|
|
196
|
+
identity—not their raw bytes. It always declares `invoice_authoritative=false` and
|
|
197
|
+
`operational_authority=none`.
|
|
198
|
+
|
|
199
|
+
Deterministic hashes are content-minimizing, not anonymizing: they remain linkable and can
|
|
200
|
+
be guessed when the source space is small. A receipt is not automatically safe to publish.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from llm_router import InvocationAttemptV1
|
|
204
|
+
|
|
205
|
+
attempt = InvocationAttemptV1(
|
|
206
|
+
attempt_index=1,
|
|
207
|
+
outcome="network_error",
|
|
208
|
+
http_status=None,
|
|
209
|
+
reason_code="provider.network_error",
|
|
210
|
+
response_payload_sha256=None,
|
|
211
|
+
)
|
|
212
|
+
# Supply only already-observed sanitized values; see docs/invocation-receipt.md.
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Configuration (env)
|
|
218
|
+
|
|
219
|
+
| Variable | Default | Purpose |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| `LLM_PROVIDER` | `openai` | `openai` (compatible) or `yandex` |
|
|
222
|
+
| `OPENAI_API_KEY` | — | key for the OpenAI-compatible endpoint |
|
|
223
|
+
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | point at Alibaba/OpenRouter/Ollama/... |
|
|
224
|
+
| `LLM_CHEAP_MODEL` / `LLM_MID_MODEL` / `LLM_CHIEF_MODEL` / `LLM_AUDIT_MODEL` | gpt-4o-mini / gpt-4o-mini / gpt-4o / gpt-4o | role → model |
|
|
225
|
+
| `YANDEX_API_KEY`, `YANDEX_FOLDER_ID` | — | Yandex AI Studio |
|
|
226
|
+
| `YANDEX_<ROLE>_MODEL` | wraps `gpt://<folder>/<name>/latest` | override a Yandex role model URI |
|
|
227
|
+
| `LLM_FX` | `1.0` | USD → local currency multiplier |
|
|
228
|
+
| `LLM_CCY` | `USD` | local currency label |
|
|
229
|
+
| `LLM_DEFAULT_TIMEOUT` | `60` | per-call timeout (s) |
|
|
230
|
+
| `LLM_MAX_RETRIES` | `2` | retries on 429/5xx |
|
|
231
|
+
| `LLM_PRICE_<MODEL>_IN/OUT_USD_PER_1M` | from table | override price per model |
|
|
232
|
+
| `LLM_BUDGET_USD_DAY` | unset | optional cap used by budget helpers |
|
|
233
|
+
|
|
234
|
+
See [.env.example](.env.example).
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Examples & tests
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
python examples/basic.py # one call
|
|
242
|
+
python examples/role_tiers.py # cheap vs chief + cost
|
|
243
|
+
python -m pytest -q # offline unit tests (no network)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
What each example shows and what it does *not* prove: [examples/README.md](examples/README.md).
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Docs
|
|
251
|
+
|
|
252
|
+
- [Component roadmap](docs/component-roadmap.md) — source-owned ecosystem role,
|
|
253
|
+
platform evidence, historical projections, and integration gates.
|
|
254
|
+
- [Project map](docs/project-map.md) — modules, what exists today vs not included, reviewer checklist.
|
|
255
|
+
- [Use cases](docs/use-cases.md) — who this is for, practical workflows, limitations.
|
|
256
|
+
- [Operating model](docs/operating-model.md) — role budgets, usage records, escalation gates, and residual risk.
|
|
257
|
+
- [Invocation receipt V1](docs/invocation-receipt.md) — canonical codec, attempt state
|
|
258
|
+
machine, fixed-point arithmetic, privacy boundary, and non-claims.
|
|
259
|
+
- [Harness ecosystem roadmap](https://github.com/krivonosoff161/agentic-security-harness/blob/main/docs/ecosystem-roadmap.md)
|
|
260
|
+
— the canonical public ordering for cross-repository integration work.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Limitations / non-goals
|
|
265
|
+
|
|
266
|
+
- Chat completions only (no streaming, embeddings, tools/function-calling, vision — kept intentionally small).
|
|
267
|
+
- One system + one user message per call (no multi-turn history helper).
|
|
268
|
+
- The price table is illustrative; confirm real prices with your provider.
|
|
269
|
+
- Not a full framework — it's a focused routing + cost-logging utility you drop into your own agent loop.
|
|
270
|
+
- Not the portfolio flagship, policy authority, or security boundary. Larger
|
|
271
|
+
systems own their own validation, authorization, storage, and safety rules.
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# llm-router
|
|
2
|
+
|
|
3
|
+
Ecosystem role and current integration status: [component roadmap](docs/component-roadmap.md).
|
|
4
|
+
The public cross-repository plan is owned by the
|
|
5
|
+
[Agentic Security Harness ecosystem roadmap](https://github.com/krivonosoff161/agentic-security-harness/blob/main/docs/ecosystem-roadmap.md).
|
|
6
|
+
|
|
7
|
+
[](https://github.com/krivonosoff161/llm-router/actions/workflows/tests.yml)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[](https://www.python.org/)
|
|
10
|
+
|
|
11
|
+
**A tiny, dependency-light async LLM router with role tiers and per-call cost logging.**
|
|
12
|
+
One `call()` interface for the tested OpenAI-compatible request shape and a
|
|
13
|
+
separate Yandex AI Studio path. Provider compatibility depends on each endpoint's
|
|
14
|
+
current API contract and must be verified before use.
|
|
15
|
+
|
|
16
|
+
> The public library demonstrates a cheap-to-chief routing pattern with offline
|
|
17
|
+
> tests. It does not publish or verify a production deployment claim. No SDKs or
|
|
18
|
+
> models are hardcoded in the routing logic.
|
|
19
|
+
|
|
20
|
+
The repository now publishes a source-owned, offline invocation-receipt contract and is
|
|
21
|
+
therefore `contract_only` in the ecosystem. Its source tree builds the unique distribution
|
|
22
|
+
candidate `agentic-llm-router==0.2.0`, imported as `llm_router`. It is not yet published or
|
|
23
|
+
automatically activated by Harness.
|
|
24
|
+
|
|
25
|
+
> Supply-chain boundary: the generic PyPI name `llm-router` belongs to another project.
|
|
26
|
+
> Do not install or declare that coordinate for this repository. The only planned public
|
|
27
|
+
> distribution coordinate is `agentic-llm-router`.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Why
|
|
32
|
+
|
|
33
|
+
In agentic systems most LLM calls are cheap bulk work (extract, classify, filter) and a few are high-stakes (the final decision). Paying flagship prices for everything is wasteful; juggling provider SDKs is annoying. `llm-router` gives you:
|
|
34
|
+
|
|
35
|
+
- **Role tiers** — `cheap` / `mid` / `chief` / `audit`, each mapped to a model via env. Route volume to `cheap`, escalate only candidates to `chief`.
|
|
36
|
+
- **Provider flexibility** — a custom `OPENAI_BASE_URL` can target endpoints that
|
|
37
|
+
implement the tested request/response contract; Yandex AI Studio has a separate
|
|
38
|
+
path. Provider identity, terms, availability, and exact compatibility are external
|
|
39
|
+
gates.
|
|
40
|
+
- **Per-call cost** — every call returns token counts and cost in **USD + a configurable local currency** (set `LLM_FX` / `LLM_CCY`). Aggregate the dicts to a budget log.
|
|
41
|
+
- **Budget helpers** — aggregate usage records, check a daily cap, and estimate savings
|
|
42
|
+
versus sending the same tokens to the `chief` model.
|
|
43
|
+
- **Resilience** — retries on `429` / `5xx` with exponential backoff.
|
|
44
|
+
- **Canonical receipt contract** — strict, digest-bound attempt, usage, pricing, and FX
|
|
45
|
+
evidence without credentials, endpoints, prompts, output text, response bodies, or
|
|
46
|
+
exception messages.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- Single `async call(role, system, user) -> (text | None, usage)` interface.
|
|
53
|
+
- Four configurable role tiers, models set per provider via env.
|
|
54
|
+
- OpenAI-compatible **and** Yandex AI Studio providers.
|
|
55
|
+
- `json_mode=True` → adds `response_format={"type":"json_object"}` (OpenAI-compatible).
|
|
56
|
+
- Cost estimation from an override-able price table (`LLM_PRICE_<MODEL>_IN/OUT_USD_PER_1M`).
|
|
57
|
+
- Budget helpers for logs you own: `summarize_usage`, `budget_status`, and
|
|
58
|
+
`build_savings_report`.
|
|
59
|
+
- Zero secrets cached at import — all config read live from env.
|
|
60
|
+
- ~150 LOC, one runtime dependency (`aiohttp`).
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
git clone https://github.com/krivonosoff161/llm-router
|
|
68
|
+
cd llm-router
|
|
69
|
+
python -m build
|
|
70
|
+
python -m pip install dist/agentic_llm_router-0.2.0-py3-none-any.whl
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
For editable development use `python -m pip install -e .[dev]`. Requires **Python 3.9+**.
|
|
74
|
+
CI builds and installs the exact wheel on Linux and Windows. Harness `main` declares a
|
|
75
|
+
source-only `router` extra using the unique `agentic-llm-router` distribution name, but
|
|
76
|
+
this package is not on PyPI and published Harness `v1.3.0` metadata does not contain that
|
|
77
|
+
extra. Public `pip install agentic-security-harness[router]` support is therefore
|
|
78
|
+
unavailable; package publication and newer Harness package metadata remain separate release gates.
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Quickstart
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
import asyncio
|
|
86
|
+
from llm_router import call
|
|
87
|
+
|
|
88
|
+
async def main():
|
|
89
|
+
text, usage = await call("cheap", "You are concise.", "Name 3 primary colors.")
|
|
90
|
+
print(text)
|
|
91
|
+
print(usage) # {provider, model, role, input_tokens, output_tokens,
|
|
92
|
+
# total_tokens, cost_usd, cost_local, currency}
|
|
93
|
+
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Set at least a provider + key first (see **Configuration**). For OpenAI:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
export OPENAI_API_KEY=sk-...
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Providers
|
|
106
|
+
|
|
107
|
+
| Provider | Set | Auth |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| **OpenAI** | `LLM_PROVIDER=openai` (default), `OPENAI_API_KEY` | `Bearer` |
|
|
110
|
+
| **Alibaba Qwen** | `OPENAI_BASE_URL=<dashscope compatible-mode/v1>` + `OPENAI_API_KEY` | `Bearer` |
|
|
111
|
+
| **OpenRouter / Together / Ollama / vLLM** | `OPENAI_BASE_URL=<their /v1>` + `OPENAI_API_KEY` | `Bearer` |
|
|
112
|
+
| **Yandex AI Studio** | `LLM_PROVIDER=yandex`, `YANDEX_API_KEY`, `YANDEX_FOLDER_ID` | `Api-Key` |
|
|
113
|
+
|
|
114
|
+
> The base URL must NOT include `/chat/completions` — the router appends it.
|
|
115
|
+
> **Yandex** requires `YANDEX_FOLDER_ID` (or an explicit `YANDEX_<ROLE>_MODEL`); otherwise `model_for` raises a clear configuration error (fail-fast) instead of sending an empty model.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Roles
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from llm_router import call, model_for
|
|
123
|
+
|
|
124
|
+
model_for("cheap") # -> e.g. "gpt-4o-mini" (or your LLM_CHEAP_MODEL)
|
|
125
|
+
model_for("chief") # -> e.g. "gpt-4o"
|
|
126
|
+
|
|
127
|
+
# pattern: cheap for volume, chief only when it matters
|
|
128
|
+
facts, u1 = await call("cheap", EXTRACT_PROMPT, raw_text)
|
|
129
|
+
if looks_important(facts):
|
|
130
|
+
verdict, u2 = await call("chief", DECIDE_PROMPT, facts, json_mode=True)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Cost logging
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
text, usage = await call("cheap", sys, user)
|
|
139
|
+
# usage["cost_usd"] -> e.g. 0.0001
|
|
140
|
+
# usage["cost_local"] -> cost_usd * LLM_FX
|
|
141
|
+
# usage["currency"] -> LLM_CCY (e.g. "RUB")
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Append each `usage` to a JSONL file and you have a per-call budget log. Prices come from a small built-in table and are **illustrative** — override per model:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
export LLM_PRICE_GPT_4O_MINI_IN_USD_PER_1M=0.15
|
|
148
|
+
export LLM_PRICE_GPT_4O_MINI_OUT_USD_PER_1M=0.60
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Summarize a batch of usage records:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
from llm_router import summarize_usage, budget_status, build_savings_report
|
|
155
|
+
|
|
156
|
+
usages = [u1, u2] # dicts returned by call()
|
|
157
|
+
print(summarize_usage(usages).as_dict())
|
|
158
|
+
print(budget_status(usages, limit_usd=1.00).as_dict())
|
|
159
|
+
print(build_savings_report(usages, counterfactual_role="chief").as_dict())
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
`LLM_BUDGET_USD_DAY` can be used as a default budget cap for `budget_status(...)`.
|
|
163
|
+
The router stays stateless; you decide where the JSONL budget log lives.
|
|
164
|
+
|
|
165
|
+
## Canonical invocation receipts
|
|
166
|
+
|
|
167
|
+
[`router-invocation-receipt-v1.0`](docs/invocation-receipt.md) is a separate offline
|
|
168
|
+
interchange surface for already-observed sanitized values. It uses canonical UTF-8 JSON,
|
|
169
|
+
domain-separated content identities, contiguous attempt accounting, strict token totals,
|
|
170
|
+
and integer nano-unit cost arithmetic. Pricing and FX inputs bind caller-supplied source
|
|
171
|
+
artifact digests; those digests are evidence references, not authenticity proofs.
|
|
172
|
+
|
|
173
|
+
The receipt builder never calls a provider and the existing `call()` return value is
|
|
174
|
+
unchanged. A receipt contains digests of request, response, output, model, and producer
|
|
175
|
+
identity—not their raw bytes. It always declares `invoice_authoritative=false` and
|
|
176
|
+
`operational_authority=none`.
|
|
177
|
+
|
|
178
|
+
Deterministic hashes are content-minimizing, not anonymizing: they remain linkable and can
|
|
179
|
+
be guessed when the source space is small. A receipt is not automatically safe to publish.
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from llm_router import InvocationAttemptV1
|
|
183
|
+
|
|
184
|
+
attempt = InvocationAttemptV1(
|
|
185
|
+
attempt_index=1,
|
|
186
|
+
outcome="network_error",
|
|
187
|
+
http_status=None,
|
|
188
|
+
reason_code="provider.network_error",
|
|
189
|
+
response_payload_sha256=None,
|
|
190
|
+
)
|
|
191
|
+
# Supply only already-observed sanitized values; see docs/invocation-receipt.md.
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Configuration (env)
|
|
197
|
+
|
|
198
|
+
| Variable | Default | Purpose |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `LLM_PROVIDER` | `openai` | `openai` (compatible) or `yandex` |
|
|
201
|
+
| `OPENAI_API_KEY` | — | key for the OpenAI-compatible endpoint |
|
|
202
|
+
| `OPENAI_BASE_URL` | `https://api.openai.com/v1` | point at Alibaba/OpenRouter/Ollama/... |
|
|
203
|
+
| `LLM_CHEAP_MODEL` / `LLM_MID_MODEL` / `LLM_CHIEF_MODEL` / `LLM_AUDIT_MODEL` | gpt-4o-mini / gpt-4o-mini / gpt-4o / gpt-4o | role → model |
|
|
204
|
+
| `YANDEX_API_KEY`, `YANDEX_FOLDER_ID` | — | Yandex AI Studio |
|
|
205
|
+
| `YANDEX_<ROLE>_MODEL` | wraps `gpt://<folder>/<name>/latest` | override a Yandex role model URI |
|
|
206
|
+
| `LLM_FX` | `1.0` | USD → local currency multiplier |
|
|
207
|
+
| `LLM_CCY` | `USD` | local currency label |
|
|
208
|
+
| `LLM_DEFAULT_TIMEOUT` | `60` | per-call timeout (s) |
|
|
209
|
+
| `LLM_MAX_RETRIES` | `2` | retries on 429/5xx |
|
|
210
|
+
| `LLM_PRICE_<MODEL>_IN/OUT_USD_PER_1M` | from table | override price per model |
|
|
211
|
+
| `LLM_BUDGET_USD_DAY` | unset | optional cap used by budget helpers |
|
|
212
|
+
|
|
213
|
+
See [.env.example](.env.example).
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Examples & tests
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
python examples/basic.py # one call
|
|
221
|
+
python examples/role_tiers.py # cheap vs chief + cost
|
|
222
|
+
python -m pytest -q # offline unit tests (no network)
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
What each example shows and what it does *not* prove: [examples/README.md](examples/README.md).
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Docs
|
|
230
|
+
|
|
231
|
+
- [Component roadmap](docs/component-roadmap.md) — source-owned ecosystem role,
|
|
232
|
+
platform evidence, historical projections, and integration gates.
|
|
233
|
+
- [Project map](docs/project-map.md) — modules, what exists today vs not included, reviewer checklist.
|
|
234
|
+
- [Use cases](docs/use-cases.md) — who this is for, practical workflows, limitations.
|
|
235
|
+
- [Operating model](docs/operating-model.md) — role budgets, usage records, escalation gates, and residual risk.
|
|
236
|
+
- [Invocation receipt V1](docs/invocation-receipt.md) — canonical codec, attempt state
|
|
237
|
+
machine, fixed-point arithmetic, privacy boundary, and non-claims.
|
|
238
|
+
- [Harness ecosystem roadmap](https://github.com/krivonosoff161/agentic-security-harness/blob/main/docs/ecosystem-roadmap.md)
|
|
239
|
+
— the canonical public ordering for cross-repository integration work.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Limitations / non-goals
|
|
244
|
+
|
|
245
|
+
- Chat completions only (no streaming, embeddings, tools/function-calling, vision — kept intentionally small).
|
|
246
|
+
- One system + one user message per call (no multi-turn history helper).
|
|
247
|
+
- The price table is illustrative; confirm real prices with your provider.
|
|
248
|
+
- Not a full framework — it's a focused routing + cost-logging utility you drop into your own agent loop.
|
|
249
|
+
- Not the portfolio flagship, policy authority, or security boundary. Larger
|
|
250
|
+
systems own their own validation, authorization, storage, and safety rules.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "agentic-llm-router"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Cost-aware multi-provider LLM router with role tiers (cheap/mid/chief/audit)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "krivonosoff161" }]
|
|
13
|
+
keywords = ["llm", "openai", "router", "agents", "cost", "qwen", "yandex"]
|
|
14
|
+
dependencies = ["aiohttp>=3.9"]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = ["build>=1.2.2", "pytest>=7", "ruff>=0.6", "setuptools>=77", "wheel>=0.44"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Homepage = "https://github.com/krivonosoff161/llm-router"
|
|
21
|
+
Repository = "https://github.com/krivonosoff161/llm-router"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
25
|
+
|
|
26
|
+
[tool.pytest.ini_options]
|
|
27
|
+
testpaths = ["tests"]
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint]
|
|
30
|
+
select = ["E4", "E7", "E9", "F"]
|