forked from Jason-Vaughan/TangleBrain
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathselector.py
More file actions
117 lines (94 loc) · 4.72 KB
/
Copy pathselector.py
File metadata and controls
117 lines (94 loc) · 4.72 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
"""Direct selection — explicit entry / local-first resolution.
**This is NOT the router.** The router (:mod:`tanglebrain.router`) — frontier-first decompose,
task-fit orchestrator selection, rotation, failover — lives in its own module.
This module's job is narrow: resolve a single entry (an explicit ``--model`` id, or the free local
tier) and build its adapter. The logic here is deliberately minimal; do not grow it into the router
— that belongs in its own module so the two don't get conflated.
"""
from __future__ import annotations
from tanglebrain.adapters import ApiAdapter, AdapterError, CliAdapter, OpenAICompatAdapter
from tanglebrain.adapters.base import Adapter
from tanglebrain.roster import Roster, RosterEntry
from tanglebrain.settings import Settings, load_settings
class SelectionError(RuntimeError):
"""Raised when no suitable roster entry can be selected for a request."""
def select_local(roster: Roster) -> RosterEntry:
"""Select the free local entry to route to.
Returns the first ``local``-tier entry invoked via ``openai-compat`` — the free tier the
openai-compat adapter can actually call.
Args:
roster: The loaded roster.
Returns:
The selected local :class:`~tanglebrain.roster.RosterEntry`.
Raises:
SelectionError: If the roster has no invocable local entry.
"""
for entry in roster:
if entry.tier == "local" and entry.invoke.kind == "openai-compat":
return entry
raise SelectionError(
"no invocable local entry in roster (need tier=local, invoke.kind=openai-compat)"
)
def select_by_id(roster: Roster, entry_id: str) -> RosterEntry:
"""Select a roster entry by id (lets the CLI drive a named entry end-to-end).
This is **not** the router — it makes no routing decision, it just resolves an explicit id the
caller named. Orchestrator selection / rotation / failover lives in the router.
Args:
roster: The loaded roster.
entry_id: The id to select.
Returns:
The matching :class:`~tanglebrain.roster.RosterEntry`.
Raises:
SelectionError: If no entry has that id.
"""
try:
return roster.by_id(entry_id)
except KeyError:
known = ", ".join(e.id for e in roster) or "(empty roster)"
raise SelectionError(f"no roster entry with id {entry_id!r}; known ids: {known}")
def build_adapter(
entry: RosterEntry,
inject_delegate: bool = False,
settings: Settings | None = None,
) -> Adapter:
"""Build the adapter for a roster entry.
Supports the ``openai-compat`` (free local), ``cli`` (authenticated CLI), and ``api`` (paid,
gateway-fronted) adapters. The paid tier is **gated**: an ``api`` entry only builds when the
global ``api_billing_enabled`` flag is on *and* the entry's own ``enabled`` flag is on — the
durable "no paid billing without the explicit toggle" rule. Otherwise it raises clearly rather
than pretending the entry is routable, so a ``tier: api`` entry parses but stays inert by
default.
Args:
entry: The roster entry to build an adapter for.
inject_delegate: For ``cli`` entries, make the local-delegate tool available to the CLI as
an orchestrator — the router sets this so an orchestrator can offload sub-tasks to the
free local backend. Ignored for non-``cli`` kinds.
settings: Global settings carrying the billing gate. Loaded from the packaged
``config/settings.yaml`` when ``None`` (and only when an ``api`` entry is actually being
built, so non-paid builds never touch the file). Injectable for tests.
Returns:
An :class:`~tanglebrain.adapters.base.Adapter` for the entry.
Raises:
AdapterError: If an ``api`` entry is selected while billing is gated off or the entry is
disabled, or if the invoke kind is unknown.
"""
if entry.invoke.kind == "openai-compat":
return OpenAICompatAdapter.from_entry(entry)
if entry.invoke.kind == "cli":
return CliAdapter.from_entry(entry, inject_delegate=inject_delegate)
if entry.invoke.kind == "api":
if settings is None:
settings = load_settings()
if not settings.api_billing_enabled:
raise AdapterError(
f"entry {entry.id!r} is a paid-API tier but billing is disabled "
"(api_billing_enabled=false in config/settings.yaml); it is inert"
)
if not entry.enabled:
raise AdapterError(
f"paid-API entry {entry.id!r} is disabled (enabled=false); not routable"
)
return ApiAdapter.from_entry(entry)
raise AdapterError(
f"no adapter for invoke.kind {entry.invoke.kind!r} (entry {entry.id!r})"
)