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.
Files changed (69) hide show
  1. {tanglebrain-0.22.0/tanglebrain.egg-info → tanglebrain-0.24.0}/PKG-INFO +13 -8
  2. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/README.md +10 -5
  3. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/pyproject.toml +36 -3
  4. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/cli.py +4 -0
  5. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/server.py +6 -1
  6. tanglebrain-0.24.0/tanglebrain/gui/static/index.html +594 -0
  7. tanglebrain-0.24.0/tanglebrain/integrity.py +410 -0
  8. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/mcp_server.py +4 -0
  9. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/measurement.py +79 -0
  10. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/router.py +40 -10
  11. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/serve/server.py +4 -0
  12. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/totals.py +161 -2
  13. {tanglebrain-0.22.0 → tanglebrain-0.24.0/tanglebrain.egg-info}/PKG-INFO +13 -8
  14. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/SOURCES.txt +2 -0
  15. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/requires.txt +2 -2
  16. tanglebrain-0.24.0/tests/test_gui.py +917 -0
  17. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_gui_contrast.py +22 -4
  18. tanglebrain-0.24.0/tests/test_integrity.py +619 -0
  19. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_measurement.py +329 -0
  20. tanglebrain-0.24.0/tests/test_packaging.py +212 -0
  21. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_router.py +84 -3
  22. tanglebrain-0.22.0/tanglebrain/gui/static/index.html +0 -313
  23. tanglebrain-0.22.0/tests/test_gui.py +0 -492
  24. tanglebrain-0.22.0/tests/test_packaging.py +0 -116
  25. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/LICENSE +0 -0
  26. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/setup.cfg +0 -0
  27. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/__init__.py +0 -0
  28. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/__init__.py +0 -0
  29. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/api.py +0 -0
  30. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/base.py +0 -0
  31. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/cli.py +0 -0
  32. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/adapters/openai_compat.py +0 -0
  33. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/atomic.py +0 -0
  34. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/classifier.py +0 -0
  35. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/config/pricing.yaml +0 -0
  36. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/config/roster.yaml +0 -0
  37. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/config/settings.yaml +0 -0
  38. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/delegate.py +0 -0
  39. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/__init__.py +0 -0
  40. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/static/logo.png +0 -0
  41. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/gui/views.py +0 -0
  42. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/roster.py +0 -0
  43. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/roster_edit.py +0 -0
  44. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/selector.py +0 -0
  45. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/serve/__init__.py +0 -0
  46. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/serve/views.py +0 -0
  47. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain/settings.py +0 -0
  48. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/dependency_links.txt +0 -0
  49. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/entry_points.txt +0 -0
  50. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tanglebrain.egg-info/top_level.txt +0 -0
  51. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_api_adapter.py +0 -0
  52. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_atomic.py +0 -0
  53. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_bind_address.py +0 -0
  54. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_classifier.py +0 -0
  55. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_cli.py +0 -0
  56. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_cli_adapter.py +0 -0
  57. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_delegate.py +0 -0
  58. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_key_permissions.py +0 -0
  59. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_live.py +0 -0
  60. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_mcp_server.py +0 -0
  61. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_model_pin_delegation.py +0 -0
  62. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_openai_compat.py +0 -0
  63. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_plugin_manifest.py +0 -0
  64. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_roster.py +0 -0
  65. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_roster_edit.py +0 -0
  66. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_selector.py +0 -0
  67. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_serve.py +0 -0
  68. {tanglebrain-0.22.0 → tanglebrain-0.24.0}/tests/test_settings.py +0 -0
  69. {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.22.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 **views** the roster, the pricing reference, and the cost-avoided rollup, and lets you
299
- **run a prompt** through the router (showing which tier/model served it). The **pricing card is
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`): the roster card shows a **Paid-API billing: ON**
504
- banner and the entry's `budget: $25.00/mo` note; or run `tanglebrain --model <id> "…"` for an
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 **views** the roster, the pricing reference, and the cost-avoided rollup, and lets you
262
- **run a prompt** through the router (showing which tier/model served it). The **pricing card is
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`): the roster card shows a **Paid-API billing: ON**
467
- banner and the entry's `budget: $25.00/mo` note; or run `tanglebrain --model <id> "…"` for an
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
- requires = ["setuptools>=68", "wheel"]
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.22.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 (read-only) on localhost.",
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,