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.
Files changed (34) hide show
  1. llm_sidecar-0.4.0/.gitignore +29 -0
  2. llm_sidecar-0.4.0/LICENSE +21 -0
  3. llm_sidecar-0.4.0/PKG-INFO +535 -0
  4. llm_sidecar-0.4.0/README.md +488 -0
  5. llm_sidecar-0.4.0/install.bat +79 -0
  6. llm_sidecar-0.4.0/install.sh +101 -0
  7. llm_sidecar-0.4.0/llm_sidecar/__init__.py +455 -0
  8. llm_sidecar-0.4.0/llm_sidecar/__main__.py +12 -0
  9. llm_sidecar-0.4.0/llm_sidecar/answer.py +258 -0
  10. llm_sidecar-0.4.0/llm_sidecar/cache.py +205 -0
  11. llm_sidecar-0.4.0/llm_sidecar/catalogue.py +164 -0
  12. llm_sidecar-0.4.0/llm_sidecar/cli.py +312 -0
  13. llm_sidecar-0.4.0/llm_sidecar/client.py +291 -0
  14. llm_sidecar-0.4.0/llm_sidecar/config.py +199 -0
  15. llm_sidecar-0.4.0/llm_sidecar/daemon.py +599 -0
  16. llm_sidecar-0.4.0/llm_sidecar/deploy/searxng/docker-compose.yml +37 -0
  17. llm_sidecar-0.4.0/llm_sidecar/deploy/searxng/settings.yml +39 -0
  18. llm_sidecar-0.4.0/llm_sidecar/hardware.py +188 -0
  19. llm_sidecar-0.4.0/llm_sidecar/ledger.py +173 -0
  20. llm_sidecar-0.4.0/llm_sidecar/mcp_server.py +338 -0
  21. llm_sidecar-0.4.0/llm_sidecar/ops.py +180 -0
  22. llm_sidecar-0.4.0/llm_sidecar/picker.py +256 -0
  23. llm_sidecar-0.4.0/llm_sidecar/search/__init__.py +195 -0
  24. llm_sidecar-0.4.0/llm_sidecar/search/ddg.py +54 -0
  25. llm_sidecar-0.4.0/llm_sidecar/search/searxng.py +112 -0
  26. llm_sidecar-0.4.0/llm_sidecar/services.py +230 -0
  27. llm_sidecar-0.4.0/llm_sidecar/tests/__init__.py +0 -0
  28. llm_sidecar-0.4.0/llm_sidecar/tests/test_sidecar.py +2051 -0
  29. llm_sidecar-0.4.0/llm_sidecar/types.py +100 -0
  30. llm_sidecar-0.4.0/llm_sidecar/ui/index.html +998 -0
  31. llm_sidecar-0.4.0/llm_sidecar/verify.py +309 -0
  32. llm_sidecar-0.4.0/pyproject.toml +71 -0
  33. llm_sidecar-0.4.0/run.bat +18 -0
  34. 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
+ [![tests](https://github.com/awaistechnologist/llm-sidecar/actions/workflows/test.yml/badge.svg)](https://github.com/awaistechnologist/llm-sidecar/actions/workflows/test.yml)
51
+ [![PyPI](https://img.shields.io/pypi/v/llm-sidecar)](https://pypi.org/project/llm-sidecar/)
52
+ [![Python](https://img.shields.io/pypi/pyversions/llm-sidecar)](https://pypi.org/project/llm-sidecar/)
53
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](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.