tanglebrain 0.22.0__tar.gz → 0.24.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.
- {tanglebrain-0.22.0/tanglebrain.egg-info → tanglebrain-0.24.0}/PKG-INFO +13 -8
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/README.md +10 -5
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/pyproject.toml +36 -3
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/cli.py +4 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/server.py +6 -1
- tanglebrain-0.24.0/tanglebrain/gui/static/index.html +594 -0
- tanglebrain-0.24.0/tanglebrain/integrity.py +410 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/mcp_server.py +4 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/measurement.py +79 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/router.py +40 -10
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/serve/server.py +4 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/totals.py +161 -2
- {tanglebrain-0.22.0 → tanglebrain-0.24.0/tanglebrain.egg-info}/PKG-INFO +13 -8
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/SOURCES.txt +2 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/requires.txt +2 -2
- tanglebrain-0.24.0/tests/test_gui.py +917 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_gui_contrast.py +22 -4
- tanglebrain-0.24.0/tests/test_integrity.py +619 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_measurement.py +329 -0
- tanglebrain-0.24.0/tests/test_packaging.py +212 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_router.py +84 -3
- tanglebrain-0.22.0/tanglebrain/gui/static/index.html +0 -313
- tanglebrain-0.22.0/tests/test_gui.py +0 -492
- tanglebrain-0.22.0/tests/test_packaging.py +0 -116
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/LICENSE +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/setup.cfg +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/__init__.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/__init__.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/api.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/base.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/cli.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/openai_compat.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/atomic.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/classifier.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/config/pricing.yaml +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/config/roster.yaml +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/config/settings.yaml +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/delegate.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/__init__.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/static/logo.png +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/views.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/roster.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/roster_edit.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/selector.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/serve/__init__.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/serve/views.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/settings.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/dependency_links.txt +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/entry_points.txt +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/top_level.txt +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_api_adapter.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_atomic.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_bind_address.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_classifier.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_cli.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_cli_adapter.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_delegate.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_key_permissions.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_live.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_mcp_server.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_model_pin_delegation.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_openai_compat.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_plugin_manifest.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_roster.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_roster_edit.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_selector.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_serve.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_settings.py +0 -0
- {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tanglebrain
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.0
|
|
4
4
|
Summary: A local-first, config-driven LLM router across OpenAI-compatible backends you own.
|
|
5
5
|
Author: Jason Vaughan
|
|
6
6
|
License: MIT
|
|
@@ -30,8 +30,8 @@ Requires-Dist: PyYAML<7,>=6.0
|
|
|
30
30
|
Provides-Extra: delegate
|
|
31
31
|
Requires-Dist: mcp<3,>=2; extra == "delegate"
|
|
32
32
|
Provides-Extra: dev
|
|
33
|
-
Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
|
|
34
|
-
Requires-Dist: mypy<2.4,>=2.3; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff<0.17,>=0.16.6; extra == "dev"
|
|
34
|
+
Requires-Dist: mypy<2.4,>=2.3.1; extra == "dev"
|
|
35
35
|
Requires-Dist: types-PyYAML<7,>=6; extra == "dev"
|
|
36
36
|
Dynamic: license-file
|
|
37
37
|
|
|
@@ -295,8 +295,13 @@ A thin **localhost-only** web panel over the config — zero extra dependencies
|
|
|
295
295
|
.venv/bin/tanglebrain-gui --port 3260 # override the port if 3250 is busy
|
|
296
296
|
```
|
|
297
297
|
|
|
298
|
-
The panel
|
|
299
|
-
**run a prompt** through the router
|
|
298
|
+
The panel has two views, switched from the sidebar and addressable by fragment. **Chat**
|
|
299
|
+
(`#/chat`, where it opens) lets you **run a prompt** through the router, showing which tier/model
|
|
300
|
+
served it. **Settings** (`#/settings`) holds the knobs: the cost-avoided rollup, the roster, and the
|
|
301
|
+
pricing reference. A **status footer** sits beneath both, carrying the signals that qualify every
|
|
302
|
+
figure on the page — the placeholder-pricing caveat and any measurement-health finding — so they
|
|
303
|
+
do not depend on which view you are on. It is absent when there is nothing to report, and says so
|
|
304
|
+
plainly when the figures could not be read at all. The **pricing card is
|
|
300
305
|
editable** — change the rates / reference label / placeholder flag and Save; it writes the tracked
|
|
301
306
|
`tanglebrain/config/pricing.yaml` (strict validation, atomic write, a backup to the state dir, and
|
|
302
307
|
the methodology header preserved), so the edit is git-visible for you to commit. The **roster is
|
|
@@ -500,9 +505,9 @@ order and never paid-routes a roster that has no orchestrators to exhaust first.
|
|
|
500
505
|
`key_ref` = the env/file reference above, `enabled: true`, and `budget_usd_month: 25`
|
|
501
506
|
(display-only — match what you capped at the source).
|
|
502
507
|
4. **Flip the global gate**: set `api_billing_enabled: true` in `tanglebrain/config/settings.yaml`.
|
|
503
|
-
5. **Verify** in the knob panel (`tanglebrain-gui`):
|
|
504
|
-
|
|
505
|
-
explicit paid call. To pause spend without editing keys, set the entry's `enabled: false` (a
|
|
508
|
+
5. **Verify** in the knob panel (`tanglebrain-gui`): open **Settings** in the sidebar — the panel
|
|
509
|
+
opens on Chat — and the roster card there shows a **Paid-API billing: ON** banner and the entry's
|
|
510
|
+
`budget: $25.00/mo` note; or run `tanglebrain --model <id> "…"` for an explicit paid call. To pause spend without editing keys, set the entry's `enabled: false` (a
|
|
506
511
|
per-key kill-switch) or flip the global gate back to `false`.
|
|
507
512
|
|
|
508
513
|
## Develop
|
|
@@ -258,8 +258,13 @@ A thin **localhost-only** web panel over the config — zero extra dependencies
|
|
|
258
258
|
.venv/bin/tanglebrain-gui --port 3260 # override the port if 3250 is busy
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
The panel
|
|
262
|
-
**run a prompt** through the router
|
|
261
|
+
The panel has two views, switched from the sidebar and addressable by fragment. **Chat**
|
|
262
|
+
(`#/chat`, where it opens) lets you **run a prompt** through the router, showing which tier/model
|
|
263
|
+
served it. **Settings** (`#/settings`) holds the knobs: the cost-avoided rollup, the roster, and the
|
|
264
|
+
pricing reference. A **status footer** sits beneath both, carrying the signals that qualify every
|
|
265
|
+
figure on the page — the placeholder-pricing caveat and any measurement-health finding — so they
|
|
266
|
+
do not depend on which view you are on. It is absent when there is nothing to report, and says so
|
|
267
|
+
plainly when the figures could not be read at all. The **pricing card is
|
|
263
268
|
editable** — change the rates / reference label / placeholder flag and Save; it writes the tracked
|
|
264
269
|
`tanglebrain/config/pricing.yaml` (strict validation, atomic write, a backup to the state dir, and
|
|
265
270
|
the methodology header preserved), so the edit is git-visible for you to commit. The **roster is
|
|
@@ -463,9 +468,9 @@ order and never paid-routes a roster that has no orchestrators to exhaust first.
|
|
|
463
468
|
`key_ref` = the env/file reference above, `enabled: true`, and `budget_usd_month: 25`
|
|
464
469
|
(display-only — match what you capped at the source).
|
|
465
470
|
4. **Flip the global gate**: set `api_billing_enabled: true` in `tanglebrain/config/settings.yaml`.
|
|
466
|
-
5. **Verify** in the knob panel (`tanglebrain-gui`):
|
|
467
|
-
|
|
468
|
-
explicit paid call. To pause spend without editing keys, set the entry's `enabled: false` (a
|
|
471
|
+
5. **Verify** in the knob panel (`tanglebrain-gui`): open **Settings** in the sidebar — the panel
|
|
472
|
+
opens on Chat — and the roster card there shows a **Paid-API billing: ON** banner and the entry's
|
|
473
|
+
`budget: $25.00/mo` note; or run `tanglebrain --model <id> "…"` for an explicit paid call. To pause spend without editing keys, set the entry's `enabled: false` (a
|
|
469
474
|
per-key kill-switch) or flip the global gate back to `false`.
|
|
470
475
|
|
|
471
476
|
## Develop
|
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
|
|
2
|
+
# setuptools carries a floor because `>= 68` admits two advisories this project has no reason to
|
|
3
|
+
# keep accepting:
|
|
4
|
+
# * GHSA-5rjg-fvgr-3xxf / CVE-2025-47273 (HIGH) — path traversal in `PackageIndex.download`,
|
|
5
|
+
# fixed in 78.1.1.
|
|
6
|
+
# * GHSA-h35f-9h28-mq5c / CVE-2026-59890 (MODERATE) — a `MANIFEST.in` exclusion bypass when
|
|
7
|
+
# building an sdist, via a Unicode NFC/NFD normalization collision, fixed in 83.0.0. That one
|
|
8
|
+
# is a defect in the exact operation this requirement governs.
|
|
9
|
+
#
|
|
10
|
+
# This is a BUILD requirement, not a runtime one: it is resolved when building from an sdist, never
|
|
11
|
+
# by someone installing the wheel. So it owes no announcement under
|
|
12
|
+
# docs/design/deprecation-policy.md § Dependency floors, which governs what a *user* may install.
|
|
13
|
+
# That asymmetry is the rule, recorded in that document: a build requirement carries the ceiling and
|
|
14
|
+
# is exempt from the announcement, and it is why this floor could move while a runtime floor raise
|
|
15
|
+
# has to earn its way past an announcement first.
|
|
16
|
+
#
|
|
17
|
+
# WHY 84 AND NOT 83, since the change that justifies this is fixed in 83.0.0: 83 is the security
|
|
18
|
+
# minimum and `>= 83, < 85` would be equally safe. 84 is simply the current major, which keeps the
|
|
19
|
+
# floor and the ceiling one window apart. The security argument buys 83; the extra step to 84 is a
|
|
20
|
+
# convention choice and is not claimed as security.
|
|
21
|
+
#
|
|
22
|
+
# THE CEILING'S COST, stated because it is real: setuptools ships majors every few weeks (77
|
|
23
|
+
# through 84 inside a year), so `< 85` will produce a Dependabot pull request often. It is kept
|
|
24
|
+
# because the upper-bound rule has no exemption for build requirements. Note what does NOT justify
|
|
25
|
+
# it — the weekly CI canary cannot help here, because with this cap in force an isolated build only
|
|
26
|
+
# ever resolves 84.x, so a breaking *major* is precisely what the canary cannot see. What catches
|
|
27
|
+
# that is CI on the Dependabot pull request that raises the cap, which is the model
|
|
28
|
+
# docs/design/nonfunctional-requirements.md already describes.
|
|
29
|
+
#
|
|
30
|
+
# `wheel` is bounded rather than dropped. setuptools has declared it itself since v70, so at this
|
|
31
|
+
# floor it is redundant and could go — but removing a build requirement is a packaging change with
|
|
32
|
+
# its own blast radius, not a version bump, and this change is scoped to the floors. Tracked
|
|
33
|
+
# separately rather than left as a comment. The floor is the current minor and the ceiling the
|
|
34
|
+
# major boundary; `wheel` had no constraint at all before this.
|
|
35
|
+
requires = ["setuptools >= 84, < 85", "wheel >= 0.45, < 1"]
|
|
3
36
|
build-backend = "setuptools.build_meta"
|
|
4
37
|
|
|
5
38
|
[project]
|
|
6
39
|
name = "tanglebrain"
|
|
7
|
-
version = "0.
|
|
40
|
+
version = "0.24.0"
|
|
8
41
|
description = "A local-first, config-driven LLM router across OpenAI-compatible backends you own."
|
|
9
42
|
readme = "README.md"
|
|
10
43
|
requires-python = ">=3.10"
|
|
@@ -55,7 +88,7 @@ delegate = ["mcp >= 2, < 3"]
|
|
|
55
88
|
# an API break. The canary argument that justifies a wide range for a runtime dependency does not
|
|
56
89
|
# transfer here — no user resolves a dev tool, so an unannounced upgrade is pure noise. Bump these
|
|
57
90
|
# deliberately; a tool release that reds the gate is fixed, not un-gated.
|
|
58
|
-
dev = ["ruff >= 0.16, < 0.17", "mypy >= 2.3, < 2.4", "types-PyYAML >= 6, < 7"]
|
|
91
|
+
dev = ["ruff >= 0.16.6, < 0.17", "mypy >= 2.3.1, < 2.4", "types-PyYAML >= 6, < 7"]
|
|
59
92
|
|
|
60
93
|
[project.urls]
|
|
61
94
|
Homepage = "https://github.com/Jason-Vaughan/TangleBrain"
|
|
@@ -31,6 +31,7 @@ from tanglebrain.measurement import (
|
|
|
31
31
|
rollup,
|
|
32
32
|
)
|
|
33
33
|
from tanglebrain.roster import RosterEntry, RosterError, load_roster
|
|
34
|
+
from tanglebrain.integrity import warn_if_migration_incomplete
|
|
34
35
|
from tanglebrain.router import Router, RouterError, migrate_state_root
|
|
35
36
|
from tanglebrain.selector import SelectionError, build_adapter, select_by_id, select_local
|
|
36
37
|
from tanglebrain.settings import load_settings
|
|
@@ -445,6 +446,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
445
446
|
# Move a pre-0.21 cache-tier state root forward before anything reads it. Idempotent,
|
|
446
447
|
# never raises; the notice goes to stderr so a piped answer stays clean.
|
|
447
448
|
migrate_state_root()
|
|
449
|
+
# Then say so if that move — or the v0.21.0 one that predates the unique staging name —
|
|
450
|
+
# left the log short. Read-only: it reports, and never touches the operator's data.
|
|
451
|
+
warn_if_migration_incomplete()
|
|
448
452
|
parser = build_parser()
|
|
449
453
|
args = parser.parse_args(argv)
|
|
450
454
|
|
|
@@ -15,6 +15,7 @@ import json
|
|
|
15
15
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
16
16
|
from importlib import resources
|
|
17
17
|
|
|
18
|
+
from tanglebrain.integrity import warn_if_migration_incomplete
|
|
18
19
|
from tanglebrain.router import migrate_state_root
|
|
19
20
|
from tanglebrain.gui.views import (
|
|
20
21
|
DEFAULT_PORT,
|
|
@@ -159,9 +160,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
159
160
|
# Move a pre-0.21 cache-tier state root forward before anything reads it. Idempotent,
|
|
160
161
|
# never raises; the notice goes to stderr so a piped answer stays clean.
|
|
161
162
|
migrate_state_root()
|
|
163
|
+
# Then say so if that move — or the v0.21.0 one that predates the unique staging name —
|
|
164
|
+
# left the log short. Read-only: it reports, and never touches the operator's data.
|
|
165
|
+
warn_if_migration_incomplete()
|
|
162
166
|
parser = argparse.ArgumentParser(
|
|
163
167
|
prog="tanglebrain-gui",
|
|
164
|
-
description="Serve the TangleBrain knob panel
|
|
168
|
+
description="Serve the TangleBrain knob panel on localhost. Reads the roster, pricing and\n"
|
|
169
|
+
"spend-avoided rollup; can run prompts and edit pricing and roster fields.",
|
|
165
170
|
)
|
|
166
171
|
parser.add_argument(
|
|
167
172
|
"--port", type=int, default=DEFAULT_PORT,
|