llm-sidecar 0.4.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_sidecar-0.4.0/.gitignore +29 -0
- llm_sidecar-0.4.0/LICENSE +21 -0
- llm_sidecar-0.4.0/PKG-INFO +535 -0
- llm_sidecar-0.4.0/README.md +488 -0
- llm_sidecar-0.4.0/install.bat +79 -0
- llm_sidecar-0.4.0/install.sh +101 -0
- llm_sidecar-0.4.0/llm_sidecar/__init__.py +455 -0
- llm_sidecar-0.4.0/llm_sidecar/__main__.py +12 -0
- llm_sidecar-0.4.0/llm_sidecar/answer.py +258 -0
- llm_sidecar-0.4.0/llm_sidecar/cache.py +205 -0
- llm_sidecar-0.4.0/llm_sidecar/catalogue.py +164 -0
- llm_sidecar-0.4.0/llm_sidecar/cli.py +312 -0
- llm_sidecar-0.4.0/llm_sidecar/client.py +291 -0
- llm_sidecar-0.4.0/llm_sidecar/config.py +199 -0
- llm_sidecar-0.4.0/llm_sidecar/daemon.py +599 -0
- llm_sidecar-0.4.0/llm_sidecar/deploy/searxng/docker-compose.yml +37 -0
- llm_sidecar-0.4.0/llm_sidecar/deploy/searxng/settings.yml +39 -0
- llm_sidecar-0.4.0/llm_sidecar/hardware.py +188 -0
- llm_sidecar-0.4.0/llm_sidecar/ledger.py +173 -0
- llm_sidecar-0.4.0/llm_sidecar/mcp_server.py +338 -0
- llm_sidecar-0.4.0/llm_sidecar/ops.py +180 -0
- llm_sidecar-0.4.0/llm_sidecar/picker.py +256 -0
- llm_sidecar-0.4.0/llm_sidecar/search/__init__.py +195 -0
- llm_sidecar-0.4.0/llm_sidecar/search/ddg.py +54 -0
- llm_sidecar-0.4.0/llm_sidecar/search/searxng.py +112 -0
- llm_sidecar-0.4.0/llm_sidecar/services.py +230 -0
- llm_sidecar-0.4.0/llm_sidecar/tests/__init__.py +0 -0
- llm_sidecar-0.4.0/llm_sidecar/tests/test_sidecar.py +2051 -0
- llm_sidecar-0.4.0/llm_sidecar/types.py +100 -0
- llm_sidecar-0.4.0/llm_sidecar/ui/index.html +998 -0
- llm_sidecar-0.4.0/llm_sidecar/verify.py +309 -0
- llm_sidecar-0.4.0/pyproject.toml +71 -0
- llm_sidecar-0.4.0/run.bat +18 -0
- llm_sidecar-0.4.0/run.sh +72 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# ===== Secrets — never commit =====
|
|
2
|
+
.env
|
|
3
|
+
*.key
|
|
4
|
+
*.pem
|
|
5
|
+
|
|
6
|
+
# ===== Python =====
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*$py.class
|
|
10
|
+
*.egg-info/
|
|
11
|
+
build/
|
|
12
|
+
dist/
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
|
|
16
|
+
# ===== Tooling =====
|
|
17
|
+
.pytest_cache/
|
|
18
|
+
.ruff_cache/
|
|
19
|
+
.mypy_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# ===== Editors / OS =====
|
|
24
|
+
.DS_Store
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
|
|
28
|
+
# ===== local config =====
|
|
29
|
+
.env
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 llm-sidecar contributors
|
|
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,535 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: llm-sidecar
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Local sidecar giving any tool grounded, cited, routed AI — Ollama or OpenRouter, no API key required.
|
|
5
|
+
Project-URL: Homepage, https://github.com/awaistechnologist/llm-sidecar
|
|
6
|
+
Project-URL: Repository, https://github.com/awaistechnologist/llm-sidecar
|
|
7
|
+
Project-URL: Issues, https://github.com/awaistechnologist/llm-sidecar/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/awaistechnologist/llm-sidecar/releases
|
|
9
|
+
Author: Muhammad Awais Tahir
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: citations,fact-checking,grounded-generation,llm,llm-router,local-first,mcp,model-context-protocol,ollama,openrouter,rag,sidecar
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Requires-Dist: httpx>=0.28.0
|
|
29
|
+
Provides-Extra: all
|
|
30
|
+
Requires-Dist: ddgs>=9.10.0; extra == 'all'
|
|
31
|
+
Requires-Dist: fastapi; extra == 'all'
|
|
32
|
+
Requires-Dist: mcp>=1.0.0; extra == 'all'
|
|
33
|
+
Requires-Dist: pydantic; extra == 'all'
|
|
34
|
+
Requires-Dist: uvicorn[standard]; extra == 'all'
|
|
35
|
+
Provides-Extra: daemon
|
|
36
|
+
Requires-Dist: fastapi; extra == 'daemon'
|
|
37
|
+
Requires-Dist: pydantic; extra == 'daemon'
|
|
38
|
+
Requires-Dist: uvicorn[standard]; extra == 'daemon'
|
|
39
|
+
Provides-Extra: dev
|
|
40
|
+
Requires-Dist: openai; extra == 'dev'
|
|
41
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
42
|
+
Provides-Extra: mcp
|
|
43
|
+
Requires-Dist: mcp>=1.0.0; extra == 'mcp'
|
|
44
|
+
Provides-Extra: search
|
|
45
|
+
Requires-Dist: ddgs>=9.10.0; extra == 'search'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# llm-sidecar
|
|
49
|
+
|
|
50
|
+
[](https://github.com/awaistechnologist/llm-sidecar/actions/workflows/test.yml)
|
|
51
|
+
[](https://pypi.org/project/llm-sidecar/)
|
|
52
|
+
[](https://pypi.org/project/llm-sidecar/)
|
|
53
|
+
[](LICENSE)
|
|
54
|
+
|
|
55
|
+
**A local sidecar that gives every tool on your machine grounded, cited, routed AI — and never asks you which model to use.**
|
|
56
|
+
|
|
57
|
+
One process. It picks a working model, searches the web, reads pages, checks
|
|
58
|
+
facts, and tells you what everything cost. Works with **no API key at all** if
|
|
59
|
+
you have [Ollama](https://ollama.com); works better with one.
|
|
60
|
+
|
|
61
|
+
```console
|
|
62
|
+
$ llm-sidecar answer "What is Iran's population?"
|
|
63
|
+
As of mid-2026, Iran's population is estimated at 93,168,497 (Worldometer,
|
|
64
|
+
based on UN 2024 Revision). The 2025 estimate from Wikipedia is 92,417,681.
|
|
65
|
+
|
|
66
|
+
Caveat: Sources differ by year and methodology: Worldometer (2026 mid-year)
|
|
67
|
+
gives 93.17 million; Wikipedia (2025 est.) gives 92.42 million.
|
|
68
|
+
|
|
69
|
+
Sources:
|
|
70
|
+
· https://en.wikipedia.org/wiki/Demographics_of_Iran
|
|
71
|
+
· https://www.worldometers.info/world-population/iran-population/
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
That came from the live web, not training data, and it says where it got it —
|
|
75
|
+
including that the sources disagree. Ask something the web can't settle and it
|
|
76
|
+
tells you, instead of guessing.
|
|
77
|
+
|
|
78
|
+
It is not instant: a grounded answer means a search, two or three page fetches
|
|
79
|
+
and a model call, so expect tens of seconds — more on free models, which are
|
|
80
|
+
slow and sometimes have to be rotated past.
|
|
81
|
+
|
|
82
|
+
> **Status: early (0.4.0).** The API may still move. Extracted from
|
|
83
|
+
> [Agora](https://github.com/awaistechnologist/agora), where the routing and
|
|
84
|
+
> verification were originally built and proven.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Sixty seconds
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install "llm-sidecar[all]"
|
|
92
|
+
llm-sidecar serve # dashboard at http://localhost:4001
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Or clone it, which also gets you the setup scripts:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/awaistechnologist/llm-sidecar
|
|
99
|
+
cd llm-sidecar
|
|
100
|
+
./install.sh # Windows: install.bat
|
|
101
|
+
./run.sh # Windows: run.bat
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`install.sh` builds a virtualenv, installs everything, then tells you what it
|
|
105
|
+
can actually reach — whether Ollama is running and how many models you have,
|
|
106
|
+
whether a key is set, whether Docker is available for SearXNG — and prints the
|
|
107
|
+
MCP config with your real paths filled in. Safe to re-run.
|
|
108
|
+
|
|
109
|
+
`run.sh` starts the daemon and opens the dashboard once it's answering. It
|
|
110
|
+
passes arguments through (`./run.sh --port 4100`, `./run.sh --no-ui`) and reads
|
|
111
|
+
a `.env` in the project directory if you keep your key there.
|
|
112
|
+
|
|
113
|
+
Open the dashboard: a chat window, every capability in a Tools tab, and a live
|
|
114
|
+
view of what is being chosen and what it costs. **No key required.**
|
|
115
|
+
|
|
116
|
+
Python 3.11+. **The core needs only `httpx`** — `llm-sidecar` on its own is a
|
|
117
|
+
seven-package install. The extras add what each door needs:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
pip install llm-sidecar # library only: complete, stream
|
|
121
|
+
pip install "llm-sidecar[search]" # + web search and read_url
|
|
122
|
+
pip install "llm-sidecar[daemon]" # + HTTP server and dashboard
|
|
123
|
+
pip install "llm-sidecar[mcp]" # + MCP server
|
|
124
|
+
pip install "llm-sidecar[all]"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## What it does
|
|
130
|
+
|
|
131
|
+
| | |
|
|
132
|
+
|---|---|
|
|
133
|
+
| **`answer`** | Ask a question → searches, reads the pages, answers **from those pages only**, with citations. Says "not in the sources" rather than guessing. |
|
|
134
|
+
| **`verify`** | Grade claims against live evidence: supported / contradicted / unverified, each cited. Unresolved claims are re-checked against full page text. |
|
|
135
|
+
| **`fact_check`** | Pull every claim out of a document and verify each one. |
|
|
136
|
+
| **`complete` / `stream`** | Routed inference. Local, free cloud, or paid — you don't name a model. |
|
|
137
|
+
| **`search` / `read_url`** | Keyless web search, and full page text with the navigation stripped. |
|
|
138
|
+
| **`summarise` / `classify` / `extract`** | Bounded structured work at temperature 0, so it's repeatable and cached. |
|
|
139
|
+
| **hardware advisor** | Which of your Ollama models actually fit in RAM, before one crawls in swap. |
|
|
140
|
+
| **usage ledger** | What you spent, on which model, over time. |
|
|
141
|
+
|
|
142
|
+
## Why it exists
|
|
143
|
+
|
|
144
|
+
Most "LLM router" projects answer *which provider should this call go to*.
|
|
145
|
+
That's plumbing. The point here is the layer above it: capabilities that are
|
|
146
|
+
hard to build well and that every tool re-implements badly — grounded search,
|
|
147
|
+
cited verification — sharing one routing, budget and cost substrate.
|
|
148
|
+
|
|
149
|
+
The routing does earn its keep in one specific way: **a catalogue entry is not
|
|
150
|
+
a working model.** Free tiers throttle, checkpoints get retired, endpoints
|
|
151
|
+
start returning empty completions. So before handing back a model, the picker
|
|
152
|
+
spends one tiny call proving it answers *right now*.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Four ways in
|
|
157
|
+
|
|
158
|
+
Same core, four doors, because the consumers can't use each other's interface.
|
|
159
|
+
|
|
160
|
+
### Library
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from llm_sidecar import Sidecar
|
|
164
|
+
sc = Sidecar()
|
|
165
|
+
|
|
166
|
+
a = sc.answer("What shipped in Python 3.14?")
|
|
167
|
+
if a.grounded: # False = the sources didn't settle it
|
|
168
|
+
print(a.text, a.sources)
|
|
169
|
+
|
|
170
|
+
sc.verify(["The Eiffel Tower is in Berlin"]) # → contradicted, cited
|
|
171
|
+
sc.fact_check(article) # extract claims, verify each
|
|
172
|
+
sc.summarise(text, style="bullets", focus="security")
|
|
173
|
+
sc.classify(tickets, labels=["bug", "feature", "question"])
|
|
174
|
+
sc.extract(invoice, {"total": "amount with currency", "due_date": "ISO date"})
|
|
175
|
+
|
|
176
|
+
sc.complete("Explain CRDTs") # routes itself
|
|
177
|
+
sc.complete(messages=[...]) # real multi-turn
|
|
178
|
+
sc.complete_many([p1, p2, p3]) # concurrent, ordered
|
|
179
|
+
await sc.acomplete("...")
|
|
180
|
+
|
|
181
|
+
sc.local_models() # your Ollama models, scored against this machine
|
|
182
|
+
sc.usage(days=30) # what you spent
|
|
183
|
+
sc.status() # everything at once
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### HTTP daemon — for any tool, in any language
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
llm-sidecar serve
|
|
190
|
+
export OPENAI_BASE_URL=http://localhost:4001/v1
|
|
191
|
+
export OPENAI_API_KEY=unused # the format demands the field
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
It speaks the chat-completions format every provider copied from OpenAI. That
|
|
195
|
+
format is a de facto standard, not a vendor tie — Ollama and OpenRouter accept
|
|
196
|
+
the identical request shape, which is why routing between them is a URL swap.
|
|
197
|
+
The variable is named after OpenAI because the `openai` SDK reads it; other
|
|
198
|
+
tools call the same setting `--openai-api-base`, `apiBase`, or "Base URL".
|
|
199
|
+
|
|
200
|
+
**The `model` field is a request, not an instruction.** A tool that hardcodes
|
|
201
|
+
`gpt-4o` gets a verified working model and never finds out:
|
|
202
|
+
|
|
203
|
+
```console
|
|
204
|
+
$ curl -s localhost:4001/v1/chat/completions -d '{"model":"gpt-4o", ...}'
|
|
205
|
+
{"model": "nvidia/nemotron-3-ultra-550b-a55b:free", ...,
|
|
206
|
+
"x_sidecar": {"cost_usd": 0.0, "local": false, "cached": false}}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Beyond the standard surface: `/v1/answer`, `/v1/verify`, `/ops/*` for each
|
|
210
|
+
capability, `/status`, `/usage`, `/resolve-preview`, `/config/*`. Swagger at
|
|
211
|
+
`/docs`.
|
|
212
|
+
|
|
213
|
+
### MCP — for agents
|
|
214
|
+
|
|
215
|
+
```json
|
|
216
|
+
{"mcpServers": {"llm-sidecar": {
|
|
217
|
+
"command": "/path/to/venv/bin/python", "args": ["-m", "llm_sidecar.mcp_server"]}}}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
`answer_question` · `search_web` · `read_url` · `verify_claims` ·
|
|
221
|
+
`extract_claims` · `fact_check_document` · `summarise` · `classify` ·
|
|
222
|
+
`extract_fields` · `delegate` · `usage_report` · `sidecar_status`
|
|
223
|
+
|
|
224
|
+
Note what's *not* there: a general "call an LLM" tool. An MCP client is
|
|
225
|
+
already a model, so exposing inference to it is close to a no-op. What it
|
|
226
|
+
can't do for itself is fetch live evidence and grade claims against it.
|
|
227
|
+
`delegate` is the exception — it's for offloading bulk work to something cheap.
|
|
228
|
+
|
|
229
|
+
### CLI
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
llm-sidecar answer "who currently runs the ECB?" # non-zero exit if ungrounded
|
|
233
|
+
llm-sidecar verify "the Great Wall is visible from space"
|
|
234
|
+
llm-sidecar sum report.md --style bullets
|
|
235
|
+
llm-sidecar models # local models vs your RAM
|
|
236
|
+
llm-sidecar usage --days 30
|
|
237
|
+
llm-sidecar searxng up # better search, one command
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## How a model gets chosen
|
|
243
|
+
|
|
244
|
+
Two questions, always. **Tier** = how capable. **Budget** = what it may cost.
|
|
245
|
+
They are independent.
|
|
246
|
+
|
|
247
|
+
### What each capability asks for
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
verify · fact_check · summarise · classify ┐
|
|
251
|
+
extract · extract_claims · delegate ├──► tier "fast"
|
|
252
|
+
┘ bulk work, cheap
|
|
253
|
+
|
|
254
|
+
answer · chat · complete() ───► tier "balanced"
|
|
255
|
+
|
|
256
|
+
budget is always your configured default unless you pass one.
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
There is no separate "chat model". One picker serves everything, so changing a
|
|
260
|
+
key or a budget changes every capability at once.
|
|
261
|
+
|
|
262
|
+
### The resolution order
|
|
263
|
+
|
|
264
|
+
Four checks, first match wins.
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
① model="ollama/qwen2.5:32b" given? ──yes──► use it. never rotated.
|
|
268
|
+
│ no
|
|
269
|
+
② is this tier LOCKED to a model? ──yes──► use it. budget ignored.
|
|
270
|
+
│ no
|
|
271
|
+
③ resolved this tier+budget <15m ago? ──yes──► reuse it.
|
|
272
|
+
│ no
|
|
273
|
+
④ resolve ▼
|
|
274
|
+
|
|
275
|
+
budget picks the candidate pool
|
|
276
|
+
┌──────────────────────────────────────────────┐
|
|
277
|
+
│ free + key free cloud models, Ollama next│
|
|
278
|
+
│ free + no key Ollama only │
|
|
279
|
+
│ cheap paid, under $1 per M tokens │
|
|
280
|
+
│ best paid, over $5 per M tokens │
|
|
281
|
+
└──────────────────────────────────────────────┘
|
|
282
|
+
│
|
|
283
|
+
probe 3 at once: "Reply OK"
|
|
284
|
+
│
|
|
285
|
+
┌───────────┴────────────┐
|
|
286
|
+
one answers all 3 fail
|
|
287
|
+
│ │
|
|
288
|
+
use it, cache 15m probe the next 3 …
|
|
289
|
+
│
|
|
290
|
+
nothing left → NoWorkingModel
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Probing concurrently changes only how *fast* a model is found — the
|
|
294
|
+
highest-priority success still wins, not whichever replied first. If a model
|
|
295
|
+
passes the probe and then fails the real call, it's marked dead for the
|
|
296
|
+
process and the next request routes elsewhere.
|
|
297
|
+
|
|
298
|
+
**Which free cloud model, specifically?** A curated list first, then the rest
|
|
299
|
+
of the pool by context length. Curated entries missing from the live catalogue
|
|
300
|
+
are skipped silently, which happens constantly — of nine curated free picks,
|
|
301
|
+
one survived to today. So in practice: the survivors, then the roomiest.
|
|
302
|
+
|
|
303
|
+
### When free OpenRouter models are used
|
|
304
|
+
|
|
305
|
+
Exactly one combination reaches them. **Tier is irrelevant to free-vs-paid.**
|
|
306
|
+
|
|
307
|
+
| budget | API key | picks from |
|
|
308
|
+
|---|---|---|
|
|
309
|
+
| `free` | **yes** | **free OpenRouter first**, Ollama as backup |
|
|
310
|
+
| `free` | no | Ollama only |
|
|
311
|
+
| `cheap` | yes | paid under $1/M — never free, never Ollama |
|
|
312
|
+
| `best` | yes | paid over $5/M |
|
|
313
|
+
| `cheap` / `best` | no | nothing eligible |
|
|
314
|
+
|
|
315
|
+
With a key and `budget=free`, the three tiers get three *different* free
|
|
316
|
+
models — pushing parallel work through one free endpoint is how you collect
|
|
317
|
+
429s.
|
|
318
|
+
|
|
319
|
+
### Locking a tier
|
|
320
|
+
|
|
321
|
+
Normally a tier chooses its own model and re-checks every 15 minutes.
|
|
322
|
+
**Locking** overrides that: always this model, no search, no probe, **no
|
|
323
|
+
budget** — a locked tier stays put even if you ask for `best`.
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
LLM_SIDECAR_MODEL_FAST=ollama/gemma3:27b # env
|
|
327
|
+
```
|
|
328
|
+
```
|
|
329
|
+
dashboard → Local models → click "fast" on a row
|
|
330
|
+
POST /config/tier {"tier": "fast", "model": "..."}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
Worth it for reproducibility and no probe latency. The cost: a locked tier
|
|
334
|
+
never rotates away, so if that model starts failing, your calls fail with it.
|
|
335
|
+
|
|
336
|
+
### `auto` is not a mode
|
|
337
|
+
|
|
338
|
+
`model` can only carry **one** axis:
|
|
339
|
+
|
|
340
|
+
| value | sets | leaves alone |
|
|
341
|
+
|---|---|---|
|
|
342
|
+
| `fast` `balanced` `powerful` | tier | budget |
|
|
343
|
+
| `free` `cheap` `best` | budget | tier |
|
|
344
|
+
| anything with a `/` | the exact model | overrides both |
|
|
345
|
+
| `auto`, `gpt-4o`, any other string | **nothing** | both stay default |
|
|
346
|
+
|
|
347
|
+
`auto` is an unrecognised string, and every unrecognised string means "use
|
|
348
|
+
both defaults" — which is exactly what lets a tool hardcoding `gpt-4o` work.
|
|
349
|
+
Because one field can't say "powerful **and** best", the daemon also takes a
|
|
350
|
+
separate `budget` field, and the dashboard has two selectors.
|
|
351
|
+
|
|
352
|
+
Not sure what you'd get? `GET /resolve-preview?tier=fast&budget=free` tells
|
|
353
|
+
you — current state, the candidate order, and why — without probing anything.
|
|
354
|
+
|
|
355
|
+
### What answered, and what it cost
|
|
356
|
+
|
|
357
|
+
| where | how |
|
|
358
|
+
|---|---|
|
|
359
|
+
| dashboard chat | `ollama/llama3.2:3b · 0.3s · 37 tok · free` under each reply |
|
|
360
|
+
| library | `.model` `.usage.cost_usd` `.cached` `.latency_s` |
|
|
361
|
+
| HTTP | `model`, plus an `x_sidecar` block |
|
|
362
|
+
| streaming | a final frame carrying usage and cost |
|
|
363
|
+
| CLI | a receipt on stderr |
|
|
364
|
+
| all of it | the ledger — `llm-sidecar usage` |
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Retrieval: who does the searching
|
|
369
|
+
|
|
370
|
+
| mode | cost | when |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| DuckDuckGo | free, no setup | the default |
|
|
373
|
+
| SearXNG | free, one command | more engines, no shared rate limit |
|
|
374
|
+
| OpenRouter | **billed per result** | when the free ones are being blocked |
|
|
375
|
+
|
|
376
|
+
### SearXNG
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
llm-sidecar searxng up
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Writes a compose file and settings to `~/.config/llm-sidecar/searxng/`,
|
|
383
|
+
generates a secret, starts the container, and waits until it really answers a
|
|
384
|
+
JSON query before reporting success. Then it's detected automatically.
|
|
385
|
+
|
|
386
|
+
Why it needs a command: SearXNG ships with its JSON API **disabled**, so the
|
|
387
|
+
stock image returns 403 to every request, which reads as "unavailable" and
|
|
388
|
+
falls back silently — the failure looks like nothing happening. The shipped
|
|
389
|
+
settings enable `formats: [html, json]` and turn off the bot limiter, which
|
|
390
|
+
protects public instances and here would only throttle you. Bound to
|
|
391
|
+
`127.0.0.1`: no auth, no limiter, so exposing it would hand anyone a free
|
|
392
|
+
search proxy on your address.
|
|
393
|
+
|
|
394
|
+
Already running one? Point `SEARXNG_URL` at it.
|
|
395
|
+
|
|
396
|
+
### OpenRouter
|
|
397
|
+
|
|
398
|
+
Not a search API. Its web plugin retrieves **inside** a chat completion — you
|
|
399
|
+
can't get results without paying for a completion too — which is why it's a
|
|
400
|
+
mode on the answer path rather than a search provider.
|
|
401
|
+
|
|
402
|
+
```python
|
|
403
|
+
sc.answer("...", via="openrouter") # billed, ~$4 per 1000 results
|
|
404
|
+
sc.complete("...", web=True)
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
Worth it when local search is being CAPTCHA'd: retrieval happens on their
|
|
408
|
+
side, against better sources. Never a default, never implicit, and never
|
|
409
|
+
cached — paying for retrieval and then serving a stored answer defeats the
|
|
410
|
+
point.
|
|
411
|
+
|
|
412
|
+
---
|
|
413
|
+
|
|
414
|
+
## Dashboard
|
|
415
|
+
|
|
416
|
+
`llm-sidecar serve` also serves a dashboard at **http://localhost:4001**.
|
|
417
|
+
|
|
418
|
+
**Dashboard** — API key and budget, status, which local models fit, spend over
|
|
419
|
+
30 days, cache size, whether SearXNG is really being used.
|
|
420
|
+
**Chat** — multi-turn, streaming, with the model and cost under every reply.
|
|
421
|
+
**Tools** — every capability, in one place, to try before you wire it in.
|
|
422
|
+
|
|
423
|
+
One HTML file. No build step, no dependencies, **no external requests** — no
|
|
424
|
+
CDN, no fonts, no analytics; there's a test asserting it. Settings apply to
|
|
425
|
+
the running daemon and are not written to disk unless you tick "remember".
|
|
426
|
+
|
|
427
|
+
Opt out entirely with `llm-sidecar serve --no-ui` (or `LLM_SIDECAR_NO_UI`);
|
|
428
|
+
`/` then 404s and the API is untouched. That controls *serving*, not
|
|
429
|
+
installing — the page is a ~40 KB file in the package either way.
|
|
430
|
+
|
|
431
|
+
---
|
|
432
|
+
|
|
433
|
+
## Configuration
|
|
434
|
+
|
|
435
|
+
Precedence: defaults < `~/.config/llm-sidecar/config.json` < environment <
|
|
436
|
+
keyword arguments to `Sidecar(...)`.
|
|
437
|
+
|
|
438
|
+
| Env var | Default | Meaning |
|
|
439
|
+
|---|---|---|
|
|
440
|
+
| `OPENROUTER_API_KEY` | — | Unset means local-only. Settable from the dashboard. |
|
|
441
|
+
| `OLLAMA_HOST` | `http://localhost:11434` | Local inference endpoint |
|
|
442
|
+
| `LLM_SIDECAR_BUDGET` | `free` | `free` \| `cheap` \| `best` |
|
|
443
|
+
| `LLM_SIDECAR_MODEL_{FAST,BALANCED,POWERFUL}` | — | Lock a tier |
|
|
444
|
+
| `LLM_SIDECAR_SEARCH_PROVIDER` | `auto` | `auto` \| `ddg` \| `searxng` |
|
|
445
|
+
| `SEARXNG_URL` | `http://localhost:8888` | Where to find SearXNG |
|
|
446
|
+
| `LLM_SIDECAR_HOST` / `_PORT` | `127.0.0.1` / `4001` | Daemon bind |
|
|
447
|
+
| `LLM_SIDECAR_TOKEN` | — | Require `Authorization: Bearer …` |
|
|
448
|
+
| `LLM_SIDECAR_NO_UI` | — | API only, no dashboard |
|
|
449
|
+
| `LLM_SIDECAR_NO_CACHE` / `_NO_LEDGER` | — | Turn those off |
|
|
450
|
+
|
|
451
|
+
The daemon binds loopback deliberately: it holds an API key and spends real
|
|
452
|
+
money on request. `config.save()` does **not** write the API key unless asked.
|
|
453
|
+
|
|
454
|
+
### Caching and cost
|
|
455
|
+
|
|
456
|
+
Deterministic requests (`temperature=0`) and searches are cached to disk,
|
|
457
|
+
which is why re-checking a document doesn't re-pay for the claims that didn't
|
|
458
|
+
change. Creative requests are deliberately **not** cached — a byte-identical
|
|
459
|
+
"random" answer is a surprise, not an optimisation. The cache is trimmed
|
|
460
|
+
oldest-first to 256 MiB; the ledger rotates at 8 MiB.
|
|
461
|
+
|
|
462
|
+
---
|
|
463
|
+
|
|
464
|
+
## Performance
|
|
465
|
+
|
|
466
|
+
Measured on an M3 Max, not estimated.
|
|
467
|
+
|
|
468
|
+
| | before | after |
|
|
469
|
+
|---|---|---|
|
|
470
|
+
| Evidence gathering, 8 claims | 11.3s sequential | 2.3s parallel |
|
|
471
|
+
| Model probing, 4 dead candidates ahead | ~10s | 4.0s |
|
|
472
|
+
| Batch of 5 completions | 2.6s | 0.3s |
|
|
473
|
+
| Repeat deterministic completion | 8.4s | cached |
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Honest limitations
|
|
478
|
+
|
|
479
|
+
- **Verification is only as good as retrieval.** The verifier can only grade
|
|
480
|
+
what came back; ambiguous evidence produces a wrong verdict. Unverified
|
|
481
|
+
claims are re-checked against full page text, and SearXNG covers more
|
|
482
|
+
engines — neither is a fix. When retrieval finds nothing useful you get
|
|
483
|
+
`unverified`, and that is the correct answer.
|
|
484
|
+
- `read_url` strips `<nav>`/`<header>`/`<footer>` and finds the content
|
|
485
|
+
region, but it's regex-based, not a readability port. Unusual markup
|
|
486
|
+
degrades to the whole page.
|
|
487
|
+
- Streaming bypasses the completion cache. Replaying stored tokens is a
|
|
488
|
+
different feature.
|
|
489
|
+
- The daemon has no request queue or admission control. Fine for one user,
|
|
490
|
+
wrong for anything shared.
|
|
491
|
+
- Parallel verification helps against cloud models, not against a single local
|
|
492
|
+
Ollama model — those queue server-side anyway.
|
|
493
|
+
- No embeddings, vector store, or cross-session memory. Different product.
|
|
494
|
+
- `searxng` drives Docker or Podman compose. Anything else: run the compose
|
|
495
|
+
file yourself and set `SEARXNG_URL`.
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Layout
|
|
500
|
+
|
|
501
|
+
| Module | Responsibility |
|
|
502
|
+
|---|---|
|
|
503
|
+
| `config.py` | `Config` dataclass, file + env loading |
|
|
504
|
+
| `catalogue.py` | OpenRouter list (disk-cached), local Ollama models |
|
|
505
|
+
| `picker.py` | Candidate ranking, live probing, `pick` / `pick_pool` |
|
|
506
|
+
| `client.py` | `complete` / `stream`, provider routing, backoff, web plugin |
|
|
507
|
+
| `answer.py` | Grounded question answering |
|
|
508
|
+
| `verify.py` | Claim extraction, evidence, grading, escalation |
|
|
509
|
+
| `ops.py` | `summarise` / `classify` / `extract` with pinned schemas |
|
|
510
|
+
| `search/` | Provider dispatch, DDG, SearXNG, `read_url` |
|
|
511
|
+
| `cache.py` · `ledger.py` · `hardware.py` | Cache, spend record, memory fit |
|
|
512
|
+
| `daemon.py` · `mcp_server.py` · `cli.py` · `ui/` | The four doors |
|
|
513
|
+
|
|
514
|
+
The core imports nothing but `httpx`. FastAPI and `mcp` are optional extras,
|
|
515
|
+
pulled in only by the doors that need them:
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
pip install -e ".[search]" # web search
|
|
519
|
+
pip install -e ".[daemon]" # HTTP server + dashboard
|
|
520
|
+
pip install -e ".[mcp]" # MCP server
|
|
521
|
+
pip install -e ".[all]"
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
## Tests
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
pytest
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
152 tests, fully offline — every network path is stubbed. Live-provider
|
|
531
|
+
behaviour is verified by hand.
|
|
532
|
+
|
|
533
|
+
## Licence
|
|
534
|
+
|
|
535
|
+
MIT.
|