tanglebrain 0.16.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.

Potentially problematic release.


This version of tanglebrain might be problematic. Click here for more details.

Files changed (52) hide show
  1. tanglebrain-0.16.0/LICENSE +21 -0
  2. tanglebrain-0.16.0/PKG-INFO +369 -0
  3. tanglebrain-0.16.0/README.md +352 -0
  4. tanglebrain-0.16.0/pyproject.toml +39 -0
  5. tanglebrain-0.16.0/setup.cfg +4 -0
  6. tanglebrain-0.16.0/tanglebrain/__init__.py +23 -0
  7. tanglebrain-0.16.0/tanglebrain/adapters/__init__.py +15 -0
  8. tanglebrain-0.16.0/tanglebrain/adapters/api.py +65 -0
  9. tanglebrain-0.16.0/tanglebrain/adapters/base.py +46 -0
  10. tanglebrain-0.16.0/tanglebrain/adapters/cli.py +341 -0
  11. tanglebrain-0.16.0/tanglebrain/adapters/openai_compat.py +197 -0
  12. tanglebrain-0.16.0/tanglebrain/classifier.py +99 -0
  13. tanglebrain-0.16.0/tanglebrain/cli.py +251 -0
  14. tanglebrain-0.16.0/tanglebrain/config/pricing.yaml +14 -0
  15. tanglebrain-0.16.0/tanglebrain/config/roster.yaml +129 -0
  16. tanglebrain-0.16.0/tanglebrain/config/settings.yaml +39 -0
  17. tanglebrain-0.16.0/tanglebrain/delegate.py +485 -0
  18. tanglebrain-0.16.0/tanglebrain/gui/__init__.py +10 -0
  19. tanglebrain-0.16.0/tanglebrain/gui/server.py +162 -0
  20. tanglebrain-0.16.0/tanglebrain/gui/static/index.html +295 -0
  21. tanglebrain-0.16.0/tanglebrain/gui/static/logo.png +0 -0
  22. tanglebrain-0.16.0/tanglebrain/gui/views.py +180 -0
  23. tanglebrain-0.16.0/tanglebrain/mcp_server.py +208 -0
  24. tanglebrain-0.16.0/tanglebrain/measurement.py +548 -0
  25. tanglebrain-0.16.0/tanglebrain/roster.py +415 -0
  26. tanglebrain-0.16.0/tanglebrain/roster_edit.py +201 -0
  27. tanglebrain-0.16.0/tanglebrain/router.py +232 -0
  28. tanglebrain-0.16.0/tanglebrain/selector.py +117 -0
  29. tanglebrain-0.16.0/tanglebrain/settings.py +132 -0
  30. tanglebrain-0.16.0/tanglebrain.egg-info/PKG-INFO +369 -0
  31. tanglebrain-0.16.0/tanglebrain.egg-info/SOURCES.txt +50 -0
  32. tanglebrain-0.16.0/tanglebrain.egg-info/dependency_links.txt +1 -0
  33. tanglebrain-0.16.0/tanglebrain.egg-info/entry_points.txt +4 -0
  34. tanglebrain-0.16.0/tanglebrain.egg-info/requires.txt +5 -0
  35. tanglebrain-0.16.0/tanglebrain.egg-info/top_level.txt +1 -0
  36. tanglebrain-0.16.0/tests/test_api_adapter.py +75 -0
  37. tanglebrain-0.16.0/tests/test_classifier.py +95 -0
  38. tanglebrain-0.16.0/tests/test_cli.py +414 -0
  39. tanglebrain-0.16.0/tests/test_cli_adapter.py +369 -0
  40. tanglebrain-0.16.0/tests/test_delegate.py +569 -0
  41. tanglebrain-0.16.0/tests/test_gui.py +337 -0
  42. tanglebrain-0.16.0/tests/test_live.py +259 -0
  43. tanglebrain-0.16.0/tests/test_mcp_server.py +154 -0
  44. tanglebrain-0.16.0/tests/test_measurement.py +464 -0
  45. tanglebrain-0.16.0/tests/test_openai_compat.py +194 -0
  46. tanglebrain-0.16.0/tests/test_plugin_manifest.py +77 -0
  47. tanglebrain-0.16.0/tests/test_roster.py +269 -0
  48. tanglebrain-0.16.0/tests/test_roster_edit.py +227 -0
  49. tanglebrain-0.16.0/tests/test_router.py +361 -0
  50. tanglebrain-0.16.0/tests/test_selector.py +103 -0
  51. tanglebrain-0.16.0/tests/test_settings.py +102 -0
  52. tanglebrain-0.16.0/tests/test_version.py +32 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jason Vaughan
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,369 @@
1
+ Metadata-Version: 2.4
2
+ Name: tanglebrain
3
+ Version: 0.16.0
4
+ Summary: A local-first, config-driven LLM router across OpenAI-compatible backends you own.
5
+ Author: Jason Vaughan
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Jason-Vaughan/TangleBrain
8
+ Keywords: llm,router,ollama,openai-compatible,litellm,local-llm
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: httpx>=0.27
13
+ Requires-Dist: PyYAML>=6.0
14
+ Provides-Extra: delegate
15
+ Requires-Dist: mcp>=1.0; extra == "delegate"
16
+ Dynamic: license-file
17
+
18
+ <p align="center">
19
+ <img src="https://raw.githubusercontent.com/Jason-Vaughan/project-assets/main/tanglebrain-logo-512.png" alt="TangleBrain logo" width="180">
20
+ </p>
21
+
22
+ # TangleBrain
23
+
24
+ [![CI](https://github.com/Jason-Vaughan/TangleBrain/actions/workflows/ci.yml/badge.svg)](https://github.com/Jason-Vaughan/TangleBrain/actions/workflows/ci.yml)
25
+
26
+ A **local-first, config-driven router across OpenAI-compatible backends you own.**
27
+
28
+ ## The Problem: Cloud-by-Default Routing
29
+
30
+ Most AI tooling sends **every** request to a paid cloud API by default — even the trivial ones, even
31
+ when you already run capable models on hardware you own. The spend accrues invisibly, you're coupled
32
+ to a single provider's endpoint, and the moment you want to blend a local model, an authenticated CLI
33
+ you already pay for, and a bring-your-own-key API, you end up hand-wiring glue and editing source just
34
+ to change *where* a request goes. There's no single, plain place to declare "here are the backends I
35
+ have — route across them, in this order," and no measurement of what you're actually spending versus
36
+ avoiding.
37
+
38
+ **This is routing debt: vendor lock-in, invisible spend, and routing logic that lives in code instead
39
+ of config.**
40
+
41
+ ## The Solution: A Local-First Router You Own
42
+
43
+ TangleBrain keeps the whole roster of backends in one editable YAML file and routes each request to
44
+ the backend you've configured — a free local model server by default. It favors credentials you
45
+ already hold: your **local models** and your **authenticated, OAuth-logged-in tools** come first,
46
+ while **raw API keys stay a separate, explicitly-gated opt-in** rather than the default (it never
47
+ injects a key into a CLI — your tool uses its own session). An optional classifier can **read each
48
+ request and route by its complexity** — sending the grunt work to your free local model where it's
49
+ cheap, and reserving heavier backends for what actually needs them. And every routed task is logged
50
+ with an **estimated cloud-equivalent cost**, so you can see what you're spending versus avoiding.
51
+ Adding or removing a backend is a config edit, not a code change.
52
+
53
+ ## Standalone, or part of the Tangle family
54
+
55
+ TangleBrain runs entirely on its own — clone it, point it at your backends, and go. It's MIT-licensed
56
+ and open to contributors: forks and pull requests are welcome. It's also designed to drop in
57
+ seamlessly alongside [TangleClaw](https://github.com/Jason-Vaughan/TangleClaw) and the wider **Tangle
58
+ family** of tools, so it works the same whether you run it solo or as part of that ecosystem.
59
+
60
+ **Status:** publicly released and actively developed — see the
61
+ [latest release](https://github.com/Jason-Vaughan/TangleBrain/releases) and [`CHANGELOG.md`](CHANGELOG.md).
62
+
63
+ ## What it does
64
+
65
+ - **Local-first routing** — ships pointing at a free local model server; nothing leaves your machine
66
+ unless you configure a backend that does.
67
+ - **OAuth- and local-first credentials** — prefers your local models and your authenticated
68
+ (OAuth-logged-in) tool sessions; it never injects an API key into a CLI. The raw-API-key tier is a
69
+ deliberate opt-in, off by default behind explicit gates.
70
+ - **Prompt-aware routing** — an optional classifier reads each request and sends trivial / grunt work
71
+ straight to your free local model where it's cheap, reserving heavier backends for what actually
72
+ needs them (off by default, fails safe).
73
+ - **Config-driven roster** — every routable backend is one entry in a plain YAML list; add, remove,
74
+ or reorganize backends by editing config.
75
+ - **Pluggable CLI-backed orchestration** — drive authenticated command-line tools as orchestrators,
76
+ with rotation and failover across them for resilience.
77
+ - **Multi-target sub-task delegation (scatter-gather)** — an orchestrator can decompose a task and
78
+ offload sub-tasks through MCP tools: to the free local backend, or to any configured backend **by id
79
+ or by capability** (a `good_at` tag), and **fan several out concurrently** in one call. Each
80
+ delegated sub-call is metered and linked back to the specific top-level task that spawned it, then
81
+ the orchestrator reviews and synthesises the results.
82
+ - **Cost measurement** — every routed task is logged with an estimated cloud-equivalent cost;
83
+ `tanglebrain --stats` rolls up what you've spent versus avoided.
84
+ - **Knob GUI** — a localhost panel to view the roster, pricing, and rollup, edit a focused set of
85
+ config knobs, and run a prompt.
86
+ - **Gated paid-API tier** — bring-your-own-key overflow, off by default behind two independent
87
+ switches.
88
+
89
+ See [`ARCHITECTURE.md`](ARCHITECTURE.md) for how the pieces fit together, [`CHANGELOG.md`](CHANGELOG.md)
90
+ for development history, and [`DISCLAIMER.md`](DISCLAIMER.md) for the opt-in / bring-your-own-key
91
+ posture.
92
+
93
+ ## Tiers
94
+
95
+ | Tier | Example | Default |
96
+ |---|---|---|
97
+ | **Free local** | a local model via Ollama / any OpenAI-compatible server you run | **active** |
98
+ | **Subscription / authenticated CLI** | command-line tools you've installed and logged in (e.g. `claude -p`, `codex exec`, `gemini -p`) | opt-in (commented) |
99
+ | **Paid API** | bring-your-own-key overflow (any OpenAI-compatible endpoint you hold a key for) | opt-in, off by default |
100
+
101
+ > **Opt-in adapters & your responsibility.** The subscription / authenticated-CLI tier and the
102
+ > paid-API tier are **opt-in** — you enable them by editing your own roster. Driving an authenticated
103
+ > CLI is your responsibility under that provider's Terms of Service, and the paid tier is
104
+ > bring-your-own-key. Read [`DISCLAIMER.md`](DISCLAIMER.md) before enabling either.
105
+
106
+ ## Install
107
+
108
+ Requires Python ≥ 3.10.
109
+
110
+ ```sh
111
+ make venv # create .venv and install -e . (dev deps included)
112
+ ```
113
+
114
+ ## Use
115
+
116
+ The roster of routable backends is a plain, editable YAML list — adding or removing a backend is a
117
+ config edit, not a code change. The shipped
118
+ [`tanglebrain/config/roster.yaml`](tanglebrain/config/roster.yaml) is only a **generic example** with
119
+ a single active entry (a local Ollama backend); keep your real roster **outside the repo** so updates
120
+ never clobber it. It's auto-discovered in order: `$TANGLEBRAIN_ROSTER` →
121
+ `~/.config/tanglebrain/roster.yaml` → the packaged example. Copy the example to
122
+ `~/.config/tanglebrain/roster.yaml` and edit it there (or pass `--roster <path>`).
123
+
124
+ ```sh
125
+ # Route to the free local backend directly — works out of the box once a local server is running:
126
+ .venv/bin/tanglebrain --local "Write a haiku about local inference."
127
+
128
+ # Show the cost-avoided rollup across every routed task so far:
129
+ .venv/bin/tanglebrain --stats
130
+ ```
131
+
132
+ The default `tanglebrain "…"` (no `--local`) uses the **orchestrator router**. Since the packaged
133
+ roster ships with no active orchestrators, that path needs at least one opt-in backend enabled first
134
+ — see below. Until then, use `--local` for the local backend.
135
+
136
+ ### Orchestrator routing (opt-in)
137
+
138
+ Enable one or more orchestrator backends by uncommenting an entry in your roster (subscription /
139
+ authenticated-CLI examples are provided, commented out, in the shipped roster) and reading
140
+ [`DISCLAIMER.md`](DISCLAIMER.md) first. With at least one orchestrator active:
141
+
142
+ ```sh
143
+ # Default: route through an orchestrator. Rotates across the configured orchestrators and fails over
144
+ # on error; an orchestrator can offload sub-tasks to a configured backend (see "Delegate (MCP)"):
145
+ .venv/bin/tanglebrain "Refactor this module and add tests."
146
+ .venv/bin/tanglebrain --task code "Refactor this function for clarity." # task-fit hint
147
+
148
+ # Force a specific roster entry (explicit override, bypasses the router):
149
+ .venv/bin/tanglebrain --model my-backend "Summarize this long document."
150
+
151
+ # Opt into the local classifier gate for this run (trivial → local backend, else router):
152
+ .venv/bin/tanglebrain --gate "What's the capital of France?"
153
+ ```
154
+
155
+ An orchestrator is any roster entry flagged `can_orchestrate: true`. The router prefers an
156
+ orchestrator whose `good_at` matches the `--task` hint, rotates across the eligible set for
157
+ resilience, and on an error fails over to the next; if all fail it reports each failure.
158
+
159
+ ### Classifier gate (optional, off by default)
160
+
161
+ By default every (non-`--local`) request goes through the router. You can put a **cheap local
162
+ classifier in front**: it rates each request's complexity on the local backend and sends **trivial**
163
+ work straight to the local backend, while **frontier** work falls through to the router. Enable it
164
+ persistently with `classifier_gate_enabled: true` in
165
+ [`tanglebrain/config/settings.yaml`](tanglebrain/config/settings.yaml), or per run with `--gate` /
166
+ `--no-gate`. It is **off by default** and **fails safe** — any classifier error or ambiguity routes
167
+ to frontier, so a hard task is never trapped on the local tier. (Fail-safe covers the
168
+ *classification*; a trivial-classified task that then fails to execute on local surfaces that error,
169
+ the same as `--local` — it doesn't silently re-route.) Gated runs show up as `gate-local` in
170
+ `--stats`. If the gate ever seems to route *everything* to frontier, the classify call is likely
171
+ truncating — raise its token budget.
172
+
173
+ ### Cost avoided (measurement)
174
+
175
+ Every routed task is logged as one JSON line in an append-only usage log
176
+ (`~/.cache/tanglebrain/usage.jsonl`, or under `TANGLEBRAIN_STATE_DIR`): path, tier, model,
177
+ estimated tokens, and the **cloud-equivalent cost it avoided** — what the work would have cost on a
178
+ paid frontier API. `tanglebrain --stats` rolls those records up into a single figure.
179
+
180
+ Tokens are *estimated* with a uniform `chars/4` heuristic over the visible prompt + response — the
181
+ authenticated CLIs expose no usable token counts, so one consistent (if approximate) methodology is
182
+ applied to every tier. The reference frontier price lives in
183
+ [`tanglebrain/config/pricing.yaml`](tanglebrain/config/pricing.yaml) — tune it to whatever frontier
184
+ model you want to compare against. A `placeholder` flag makes the rollup render a PLACEHOLDER caveat
185
+ when the rates are rough. Logging is best-effort and never affects the returned answer.
186
+
187
+ ### Knob panel (`tanglebrain-gui`)
188
+
189
+ A thin **localhost-only** web panel over the config — zero extra dependencies (stdlib `http.server`
190
+ + a single vanilla HTML/CSS/JS page):
191
+
192
+ ```sh
193
+ .venv/bin/tanglebrain-gui # serves http://127.0.0.1:3250/ (Ctrl-C to stop)
194
+ .venv/bin/tanglebrain-gui --port 3260 # override the port if 3250 is busy
195
+ ```
196
+
197
+ The panel **views** the roster, the pricing reference, and the cost-avoided rollup, and lets you
198
+ **run a prompt** through the router (showing which tier/model served it). The **pricing card is
199
+ editable** — change the rates / reference label / placeholder flag and Save; it writes the tracked
200
+ `tanglebrain/config/pricing.yaml` (strict validation, atomic write, a backup to the state dir, and
201
+ the methodology header preserved), so the edit is git-visible for you to commit. The **roster is
202
+ editable for a focused set of per-entry fields** — `enabled`, `can_orchestrate`, `budget_usd_month`,
203
+ and `good_at` (each row has its own Save). Edits are surgical and **comment-preserving**: only the
204
+ targeted value on the targeted line changes, so the curated inline comments and the nested `invoke`
205
+ block survive byte-for-byte (same validate → backup → atomic-write safety as pricing; the candidate
206
+ is re-parsed before any write). Adding/removing entries and editing the `invoke` block are still
207
+ hand-edits. The panel binds `127.0.0.1` only: running a prompt spends real backend quota and it reads
208
+ the roster, so it is never network-exposed. The roster view shows each entry's `key_ref` as the
209
+ reference string only — secrets are never resolved or sent to the browser.
210
+
211
+ ### Delegate (MCP) — let an orchestrator offload sub-tasks to a configured backend
212
+
213
+ `tanglebrain-delegate` is an MCP server that lets an orchestrator offload bulk sub-tasks instead of
214
+ running them itself, then review the results — a decompose → delegate → review loop that is emergent
215
+ from the orchestrator simply having the tool (no graph engine required). It reuses the same roster +
216
+ adapters as the CLI above, so endpoints and keys live in one place. It exposes four tools:
217
+
218
+ - **`delegate_local(prompt, max_tokens?)`** — route a sub-task to the free local tier (the $0
219
+ default).
220
+ - **`delegate(prompt, target?, task?, max_tokens?)`** — route a sub-task to a *configured* backend,
221
+ two ways (precedence: `target` > `task` > local):
222
+ - **`target`** — an explicit roster id flagged `can_delegate: true`. The orchestrator names the
223
+ exact backend.
224
+ - **`task`** — a capability tag (a `good_at` value, e.g. `code`). TangleBrain picks the **cheapest
225
+ `can_delegate` backend** good_at it (`local` before `sub`); the orchestrator just says *what kind
226
+ of work it is* and doesn't need to know ids. **Paid `api` backends are never auto-selected by
227
+ `task`** (reach one only by naming it as `target`). If nothing fits, the tool hands the sub-task
228
+ **back to the orchestrator to do itself** — not an error, just a signal that it's the most capable
229
+ backend available.
230
+
231
+ A target is invoked as a leaf (it never gets its own delegate tool — no recursion); `api` targets
232
+ named explicitly still obey the billing gate, so a paid target raises rather than spending while
233
+ billing is off.
234
+ - **`delegate_many(tasks, max_concurrency?)`** — fan **several sub-tasks out concurrently** in one
235
+ call and collect them, instead of delegating one at a time. Each item is `{prompt, target?, task?,
236
+ max_tokens?}` (same routing as `delegate`), so a batch can mix backends. Returns a JSON array, one
237
+ entry per task **in input order**, each `{index, status}` — `ok` (+`text`), `no_fit` (+`message`),
238
+ or `error` (+`error`); a failing sub-task never sinks the others. Concurrency is bounded
239
+ automatically from the host (`os.cpu_count()`), overridable by the operator
240
+ (`delegate_max_concurrency` in `settings.yaml` — pin it to your backend's real parallelism, e.g.
241
+ `OLLAMA_NUM_PARALLEL`) and lowerable per call. Dispatch + collect only — the orchestrator
242
+ synthesises the results.
243
+ - **`delegate_targets()`** — list the configured targets (`id`, `tier`, `good_at`, `cost`, `kind`)
244
+ so the orchestrator can decide based on what's available. The `delegate` tool's description also
245
+ enumerates them (built at server startup; the tool reflects the live roster).
246
+
247
+ Make a backend a delegate target by flagging its roster entry `can_delegate: true` (mirrors
248
+ `can_orchestrate`). The shipped example flags the local tier, so the menu is non-empty out of the
249
+ box. **Delegated sub-calls are now metered**: each is logged as a `kind: delegate` usage record, and
250
+ `tanglebrain --stats` (and the knob panel) show a "Delegated sub-tasks" breakdown by backend (count,
251
+ est tokens, informational cloud-equiv). These are kept **out of** the "spend avoided" headline so a
252
+ sub-call's saving is never double-counted against its parent task. Any non-local target is opt-in and
253
+ your responsibility under that provider's terms — see [DISCLAIMER.md](DISCLAIMER.md).
254
+
255
+ **Synthesising fan-out results.** The full pattern is decompose → fan out (`delegate_many`) →
256
+ **reduce** → answer. TangleBrain ships the dispatch primitives but deliberately does *not* own the
257
+ reduce step: the orchestrator gets the results array back and combines it itself, because it holds
258
+ the original task context that makes for good synthesis — something a fresh reduce backend lacks. If
259
+ the reduction is instead **mechanical and large** (concatenating generated files, merging many
260
+ summaries into one list — where the original intent doesn't matter), offload that stitch too with a
261
+ normal `delegate(prompt="Combine these results: …", task="summarization")` call, keeping the heavy
262
+ formatting off your frontier budget. No separate "reduce" tool is needed — the existing `delegate`
263
+ covers it.
264
+
265
+ It needs the optional `mcp` dependency:
266
+
267
+ ```sh
268
+ pip install -e ".[delegate]" # or: make venv (installs the extra)
269
+ tanglebrain-delegate # serve over stdio (for a manual smoke test)
270
+ ```
271
+
272
+ Register it with an orchestrator CLI (exact flags vary by CLI version — check `<cli> mcp --help`):
273
+
274
+ ```sh
275
+ # Claude Code:
276
+ claude mcp add tanglebrain-delegate -- tanglebrain-delegate
277
+ # Gemini CLI:
278
+ gemini mcp add tanglebrain-delegate tanglebrain-delegate
279
+ # Codex: add a stdio MCP server entry pointing at `tanglebrain-delegate` in its MCP config.
280
+ ```
281
+
282
+ Claude Code users can instead install it as a **plugin** — this repo is its own plugin marketplace
283
+ (see [`plugins/tanglebrain-delegate/`](plugins/tanglebrain-delegate/)):
284
+
285
+ ```
286
+ /plugin marketplace add Jason-Vaughan/TangleBrain
287
+ /plugin install tanglebrain-delegate@tanglebrain
288
+ ```
289
+
290
+ The plugin registers the server declaratively — it wires the `tanglebrain-delegate` command, it
291
+ doesn't vendor it — so the console script must be on `PATH`: from a clone, `pip install -e
292
+ ".[delegate]"` as above; without one, `pip install "tanglebrain[delegate] @
293
+ git+https://github.com/Jason-Vaughan/TangleBrain"` (TangleBrain is not on PyPI).
294
+
295
+ To point the server at a non-default roster, set `TANGLEBRAIN_ROSTER=/path/to/roster.yaml` in its
296
+ environment.
297
+
298
+ ### Paid-API tier (opt-in, off by default)
299
+
300
+ Paid API is the genuine last resort — it costs real money, so it is **disabled by default** and
301
+ gated by a single explicit switch. A `tier: api` roster entry parses and is inspectable at all
302
+ times, but it is **never routable** until you turn it on. See [`DISCLAIMER.md`](DISCLAIMER.md) for
303
+ the bring-your-own-key posture.
304
+
305
+ The durable rule: *no paid billing without the explicit toggle.* Two independent gates must both be
306
+ on for a paid entry to build:
307
+
308
+ 1. **Global gate** — `api_billing_enabled: true` in `tanglebrain/config/settings.yaml` (ships
309
+ `false`).
310
+ 2. **Per-entry switch** — `enabled: true` on the roster entry (a per-key kill-switch).
311
+
312
+ Custody is **by reference, never embedding**: TangleBrain never holds a raw key in config —
313
+ `key_ref` points at an env var (`env:OPENAI_API_KEY`) or a `0600` key file
314
+ (`file:~/.config/tanglebrain/keys/paid.key`). Prefer fronting paid APIs through a budget-capped
315
+ gateway or a scoped key so spend is bounded **at the source**. A paid entry also records
316
+ `budget_usd_month` for visibility — TangleBrain does **not** enforce spend; cap it at your
317
+ gateway/provider. A commented example entry is at the bottom of `tanglebrain/config/roster.yaml`.
318
+
319
+ Once both gates are on, a paid entry runs either when selected explicitly (`--model <id>`) or as the
320
+ router's **genuine last resort** — the default `tanglebrain "…"` router falls through to an enabled
321
+ `api` entry only after *every* orchestrator has failed/exhausted. It tries paid entries in roster
322
+ order and never paid-routes a roster that has no orchestrators to exhaust first.
323
+
324
+ > **Live status:** the paid tier is **hermetically tested but never run against a real paid
325
+ > endpoint** — by design (TangleBrain is deliberately bring-your-own-key; we don't mint billable keys
326
+ > just to test). The hooks are in place and the routing/gating/visibility are proven; the live
327
+ > `router → ApiAdapter → key → provider` round-trip is unverified until an operator wires a real key.
328
+ > See [#23](https://github.com/Jason-Vaughan/TangleBrain/issues/23). Treat it as hermetically correct
329
+ > but live-unproven, and file a fix if a live provider needs one.
330
+
331
+ #### Runbook — enabling a paid key
332
+
333
+ 1. **Get a key for any OpenAI-compatible endpoint you control** — a provider directly, OpenRouter, or
334
+ a self-hosted gateway (e.g. LiteLLM). Prefer a **budget-capped / scoped** key so spend is bounded
335
+ at the source; TangleBrain doesn't enforce spend itself.
336
+ 2. **Store it outside the repo.** Reference an env var (`key_ref: env:OPENAI_API_KEY`) or a `0600`
337
+ file (`*.key` is gitignored):
338
+ ```sh
339
+ install -m 600 /dev/stdin ~/.config/tanglebrain/keys/paid.key <<< 'sk-your-key'
340
+ ```
341
+ 3. **Add the roster entry** (uncomment/adapt the example at the bottom of your roster): `tier: api`,
342
+ `invoke.kind: api`, `base_url` = your endpoint, `model` = the model id it exposes,
343
+ `key_ref` = the env/file reference above, `enabled: true`, and `budget_usd_month: 25`
344
+ (display-only — match what you capped at the source).
345
+ 4. **Flip the global gate**: set `api_billing_enabled: true` in `tanglebrain/config/settings.yaml`.
346
+ 5. **Verify** in the knob panel (`tanglebrain-gui`): the roster card shows a **Paid-API billing: ON**
347
+ banner and the entry's `budget: $25.00/mo` note; or run `tanglebrain --model <id> "…"` for an
348
+ explicit paid call. To pause spend without editing keys, set the entry's `enabled: false` (a
349
+ per-key kill-switch) or flip the global gate back to `false`.
350
+
351
+ ## Develop
352
+
353
+ ```sh
354
+ make help # list targets
355
+ make lint # smoke-check every Python file parses
356
+ make test # lint + run the unit test suite (hermetic; HTTP is mocked)
357
+ make test-live # opt-in: hit the real local endpoint your roster points at, end-to-end
358
+ ```
359
+
360
+ ## Contributing
361
+
362
+ Contributions are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for dev setup, branch/PR
363
+ conventions, and good first contributions (adding a backend is usually a config edit, not a code
364
+ change). All participation is governed by our [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md), and the
365
+ opt-in / bring-your-own-key posture is in [`DISCLAIMER.md`](DISCLAIMER.md).
366
+
367
+ ## License
368
+
369
+ [MIT](LICENSE).