Skip to content

Reference

llmbroker

llmbroker — a standalone, host-agnostic LLM-provider broker.

AsyncBroker

Façade over the LLM pool: route completions, inspect state, edit the catalog.

Source code in src/llmbroker/broker/broker.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
class AsyncBroker:
    """Façade over the LLM pool: route completions, inspect state, edit the catalog."""

    def __init__(  # noqa: PLR0913
        self,
        registry: RegistryProtocol | str | Path | None = None,
        *,
        secrets: SecretsProtocol | None = None,
        store: StoreProtocol | None = None,
        optimize: bool | Optimizer = True,
        scope: str | None = None,
    ) -> None:
        if scope == "":
            raise ValueError("scope must not be empty string; use None for unscoped")
        if registry is None:
            raise ValueError("AsyncBroker requires a `registry` source")

        source_secrets: SecretsProtocol | None = None
        source_store: StoreProtocol | None = None
        if isinstance(registry, (str, Path)):
            registry, source_secrets, source_store = resolve_source(registry)

        secrets = as_secrets(secrets) if secrets is not None else (source_secrets or Secrets())
        store = store if store is not None else (source_store or _default_store(registry))

        if isinstance(optimize, Optimizer):
            self._optimizer: Optimizer | None = optimize
        elif optimize:
            self._optimizer = Optimizer()
        else:
            self._optimizer = None

        self._registry = registry
        self._secrets = secrets
        self._base_store = store
        self._scope = scope

        pool = LLMPool(optimizer=self._optimizer)
        self._pool = pool
        self._catalog = Catalog(registry, secrets, pool, scope=scope)

        self._learning_hook: _LearningHook | None = None
        effective_store: StoreProtocol
        if self._optimizer is not None:
            self._learning_hook = _LearningHook(
                self._optimizer,
                store,
                pool,
                self._catalog.resync,
            )
            effective_store = self._learning_hook
        else:
            effective_store = store

        self._store = effective_store
        self._router = Router(pool, effective_store, scope=scope, optimizer=self._optimizer)
        self._pool_view = PoolView(pool, effective_store)

        self._provisioned = False
        self._provision_lock = asyncio.Lock()
        self._last_underprov_alert: float = float("-inf")
        self._underprov_alert_interval: float = 60.0

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    async def ensure_pool(self) -> None:
        """Lazy idempotent initializer — provisions the pool exactly once.

        Raises if the registry is empty — call ``sync(preset)`` first.
        """
        if self._provisioned:
            return
        async with self._provision_lock:
            if self._provisioned:
                return
            await self._catalog.provision()
            if self._learning_hook is not None:
                # warm start — provision() above already resynced the registry
                await self._learning_hook.maybe_rebuild(force=True, resync_registry=False)
            self._provisioned = True

    async def sync(self, preset: RegistryProtocol | str | Path) -> None:
        """Mirror ``preset`` into the registry: add new entries, update existing
        ones, delete entries absent from the preset. Explicit and idempotent —
        call it once to initialize a fresh DB, or again whenever the preset changes.
        If the pool is already provisioned, the change takes effect immediately.
        """
        if isinstance(preset, (str, Path)):
            preset = Registry(preset)
        await self._catalog.sync(preset)
        if isinstance(self._base_store, DisabledMapProtocol):
            configs = await self._registry.load()
            await self._base_store.seed_disabled([c.name for c in configs])
        if self._provisioned:
            await self._catalog.resync()

    async def aclose(self) -> None:
        await self._router.aclose()
        for port in (self._registry, self._secrets, self._store):
            if isinstance(port, AsyncResourceProtocol):
                await port.aclose()

    async def __aenter__(self) -> "AsyncBroker":
        await self.ensure_pool()
        return self

    async def __aexit__(self, *exc: object) -> None:
        await self.aclose()

    # ------------------------------------------------------------------
    # Routing
    # ------------------------------------------------------------------

    async def ask(
        self,
        prompt: str,
        *,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> AsyncResult:
        await self.ensure_pool()
        try:
            return await self._router.ask(prompt, operation=operation, trace_id=trace_id, wait=wait)
        except NoLLMAvailableError as exc:
            self._maybe_alert_underprov(exc)
            raise

    async def chat(
        self,
        messages: list[dict],
        *,
        tools: list[dict] | None = None,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> AsyncResult:
        await self.ensure_pool()
        try:
            return await self._router.chat(
                messages,
                tools=tools,
                operation=operation,
                trace_id=trace_id,
                wait=wait,
            )
        except NoLLMAvailableError as exc:
            self._maybe_alert_underprov(exc)
            raise

    # ------------------------------------------------------------------
    # Inspection
    # ------------------------------------------------------------------

    async def get(self, name: str) -> AsyncLLM:
        await self.ensure_pool()
        return self._pool_view.get(name)

    async def count(self) -> int:
        await self.ensure_pool()
        return self._pool_view.count()

    async def snapshot(self) -> Mapping[str, LLMSnapshot]:
        await self.ensure_pool()
        return await self._pool_view.snapshot()

    # ------------------------------------------------------------------
    # Manual disable — the one verdict that actually excludes
    # ------------------------------------------------------------------

    async def disable_llm(self, name: str) -> None:
        """Set the manual latch: withdraws the slot, survives preset rolls, covers
        every operation including future ones. Only ``enable_llm`` clears it."""
        await self.ensure_pool()
        self._pool.set_disabled(name)
        if isinstance(self._base_store, DisabledMapProtocol):
            await self._base_store.set_disabled(name, True)

    async def enable_llm(self, name: str) -> None:
        """Clear the manual latch — a re-enabled model rehabilitates through new
        ratings, no quality reset exists."""
        await self.ensure_pool()
        await self._pool.clear_disabled(name)
        if isinstance(self._base_store, DisabledMapProtocol):
            await self._base_store.set_disabled(name, False)

    # ------------------------------------------------------------------
    # Call journal
    # ------------------------------------------------------------------

    async def calls(self, *, limit: int) -> list[Call]:
        return await self._require_queryable().calls(limit=limit, scope=self._scope)

    def _maybe_alert_underprov(self, exc: NoLLMAvailableError) -> None:
        """Fire when zero keyed configs are routable — the genuine "no usable models" alarm.

        A keyless config is never enqueued/acquired/cooled (see the partial-key framing
        in architecture.md), so it must be excluded here: with even one keyless config
        present, an unfiltered check could never observe "all non-AVAILABLE", masking
        the real alarm even when every *keyed* config is COOLING. Only a ``"timeout"``
        reason means the pool is merely temporarily exhausted; every other reason
        (no keys, all disabled, empty pool) already logs its own actionable line.
        """
        if exc.reason != "timeout":
            return
        if self._optimizer is None:
            return
        if not self._pool.configs:
            return
        now = time.monotonic()
        if now - self._last_underprov_alert < self._underprov_alert_interval:
            return
        keyed_names = [name for name in self._pool.configs if self._pool.has_key(name)]
        all_offline = all(
            self._pool.state(name).phase is not LifecyclePhase.AVAILABLE for name in keyed_names
        )
        if all_offline:
            self._last_underprov_alert = now
            logger.warning(
                "pool under-provisioned: all LLMs are COOLING — add more LLMs to the registry",
            )

    def _require_queryable(self) -> QueryableStoreProtocol:
        if not isinstance(self._base_store, QueryableStoreProtocol):
            raise TypeError(
                "this store backend is not queryable — use a queryable backend"
                " (e.g. llmbroker.sqlite.Store) for calls()",
            )
        return cast(QueryableStoreProtocol, self._store)

disable_llm(name) async

Set the manual latch: withdraws the slot, survives preset rolls, covers every operation including future ones. Only enable_llm clears it.

Source code in src/llmbroker/broker/broker.py
239
240
241
242
243
244
245
async def disable_llm(self, name: str) -> None:
    """Set the manual latch: withdraws the slot, survives preset rolls, covers
    every operation including future ones. Only ``enable_llm`` clears it."""
    await self.ensure_pool()
    self._pool.set_disabled(name)
    if isinstance(self._base_store, DisabledMapProtocol):
        await self._base_store.set_disabled(name, True)

enable_llm(name) async

Clear the manual latch — a re-enabled model rehabilitates through new ratings, no quality reset exists.

Source code in src/llmbroker/broker/broker.py
247
248
249
250
251
252
253
async def enable_llm(self, name: str) -> None:
    """Clear the manual latch — a re-enabled model rehabilitates through new
    ratings, no quality reset exists."""
    await self.ensure_pool()
    await self._pool.clear_disabled(name)
    if isinstance(self._base_store, DisabledMapProtocol):
        await self._base_store.set_disabled(name, False)

ensure_pool() async

Lazy idempotent initializer — provisions the pool exactly once.

Raises if the registry is empty — call sync(preset) first.

Source code in src/llmbroker/broker/broker.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
async def ensure_pool(self) -> None:
    """Lazy idempotent initializer — provisions the pool exactly once.

    Raises if the registry is empty — call ``sync(preset)`` first.
    """
    if self._provisioned:
        return
    async with self._provision_lock:
        if self._provisioned:
            return
        await self._catalog.provision()
        if self._learning_hook is not None:
            # warm start — provision() above already resynced the registry
            await self._learning_hook.maybe_rebuild(force=True, resync_registry=False)
        self._provisioned = True

sync(preset) async

Mirror preset into the registry: add new entries, update existing ones, delete entries absent from the preset. Explicit and idempotent — call it once to initialize a fresh DB, or again whenever the preset changes. If the pool is already provisioned, the change takes effect immediately.

Source code in src/llmbroker/broker/broker.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
async def sync(self, preset: RegistryProtocol | str | Path) -> None:
    """Mirror ``preset`` into the registry: add new entries, update existing
    ones, delete entries absent from the preset. Explicit and idempotent —
    call it once to initialize a fresh DB, or again whenever the preset changes.
    If the pool is already provisioned, the change takes effect immediately.
    """
    if isinstance(preset, (str, Path)):
        preset = Registry(preset)
    await self._catalog.sync(preset)
    if isinstance(self._base_store, DisabledMapProtocol):
        configs = await self._registry.load()
        await self._base_store.seed_disabled([c.name for c in configs])
    if self._provisioned:
        await self._catalog.resync()

AsyncLLM

Handle returned by AsyncBroker.get(name) — live view into broker internals.

Source code in src/llmbroker/broker/result.py
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
class AsyncLLM:
    """Handle returned by ``AsyncBroker.get(name)`` — live view into broker internals."""

    def __init__(
        self,
        name: str,
        config: LLMConfig,
        pool: LLMPool,
        store: StoreProtocol,
    ) -> None:
        self._name = name
        self._config = config
        self._pool = pool
        self._store = store

    @property
    def config(self) -> LLMConfig:
        return self._config

    @property
    def disabled(self) -> bool:
        return self._pool.is_disabled(self._name)

    async def state(self) -> LLMState:
        return self._pool.state(self._name)

    async def metrics(self) -> LLMMetrics:
        all_metrics = await resolve_metrics_map(self._store)
        return all_metrics.get(self._name, LLMMetrics(0, None, None))

AsyncResult

Returned by AsyncBroker.ask()/chat().

Source code in src/llmbroker/broker/result.py
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
class AsyncResult:
    """Returned by AsyncBroker.ask()/chat()."""

    def __init__(  # noqa: PLR0913
        self,
        *,
        text: str,
        tool_calls: list[dict] | None,
        usage: Usage | None,
        call_id: str,
        llm_name: str,
        operation: str | None = None,
        store: StoreProtocol,
    ) -> None:
        self.text = text
        self.tool_calls = tool_calls
        self.usage = usage
        self._call_id = call_id
        self._llm_name = llm_name
        self._operation = operation
        self._store = store

    async def record_quality(self, score: float) -> None:
        await self._store.record_quality(
            self._llm_name,
            self._operation,
            score,
            call_id=self._call_id,
        )

Broker

Synchronous client over an AsyncBroker on a background loop thread.

Source code in src/llmbroker/sync.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Broker:
    """Synchronous client over an AsyncBroker on a background loop thread."""

    def __init__(  # noqa: PLR0913
        self,
        registry: RegistryProtocol | str | Path | None = None,
        *,
        secrets: SecretsProtocol | None = None,
        store: StoreProtocol | None = None,
        optimize: bool | Optimizer = True,
        scope: str | None = None,
    ) -> None:
        self._async = AsyncBroker(
            registry,
            secrets=secrets,
            store=store,
            optimize=optimize,
            scope=scope,
        )
        self._loop = asyncio.new_event_loop()
        self._thread = threading.Thread(
            target=_run_loop,
            args=(self._loop,),
            daemon=True,
            name="llmbroker-loop",
        )
        self._thread.start()
        # Backstop: if the caller never closes the Broker, stop the loop and
        # join the thread when the instance is garbage-collected. The callback
        # holds only loop + thread (never self), so it does not pin the Broker.
        self._finalizer = weakref.finalize(self, _shutdown, self._loop, self._thread)

    def _run(self, coro: Coroutine[Any, Any, Any]) -> Any:
        return asyncio.run_coroutine_threadsafe(coro, self._loop).result()

    def _ensure_pool(self) -> None:
        self._run(self._async.ensure_pool())

    # ── Accessors ──
    def get(self, name: str) -> LLM:
        return LLM(self._run, self._run(self._async.get(name)))

    def count(self) -> int:
        return self._run(self._async.count())

    # ── calls ──
    def ask(
        self,
        prompt: str,
        *,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> Result:
        return Result(
            self._run,
            self._run(self._async.ask(prompt, operation=operation, trace_id=trace_id, wait=wait)),
        )

    def chat(
        self,
        messages: list[dict],
        *,
        tools: list[dict] | None = None,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> Result:
        return Result(
            self._run,
            self._run(
                self._async.chat(
                    messages,
                    tools=tools,
                    operation=operation,
                    trace_id=trace_id,
                    wait=wait,
                ),
            ),
        )

    def snapshot(self) -> Mapping[str, LLMSnapshot]:
        return self._run(self._async.snapshot())

    def sync(self, preset: RegistryProtocol | str | Path) -> None:
        self._run(self._async.sync(preset))

    def disable_llm(self, name: str) -> None:
        self._run(self._async.disable_llm(name))

    def enable_llm(self, name: str) -> None:
        self._run(self._async.enable_llm(name))

    def calls(self, *, limit: int) -> list[Call]:
        return self._run(self._async.calls(limit=limit))

    # ── lifecycle ──
    def close(self) -> None:
        if not self._finalizer.alive:
            return
        self._run(self._async.aclose())
        # Run the same teardown the GC backstop would, and mark it done so the
        # finalizer does not repeat it later.
        self._finalizer()

    def __enter__(self) -> "Broker":
        self._ensure_pool()
        return self

    def __exit__(self, *exc: object) -> None:
        self.close()

DictSecrets

Read-only secrets resolver backed by an in-memory mapping (tests / preloaded keys).

Source code in src/llmbroker/standalone/secrets.py
25
26
27
28
29
30
31
32
33
34
class DictSecrets:
    """Read-only secrets resolver backed by an in-memory mapping (tests / preloaded keys)."""

    def __init__(self, mapping: dict[str, str]) -> None:
        self._mapping = dict(mapping)

    async def resolve(self, ref: str) -> str:
        if ref not in self._mapping:
            raise KeyError(f"DictSecrets: ref {ref!r} not found")
        return self._mapping[ref]

FileStore

Day-split JSONL call journal plus a YAML disabled-verdict map, under one directory.

Source code in src/llmbroker/standalone/store.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class FileStore:
    """Day-split JSONL call journal plus a YAML disabled-verdict map, under one directory."""

    def __init__(self, directory: str | Path, *, retention: timedelta = _DEFAULT_RETENTION) -> None:
        self._dir = Path(directory)
        self._calls_dir = self._dir / "calls"
        self._disabled_path = self._dir / "disabled.yml"
        self._retention = retention
        self._last_purge = float("-inf")

    def _day_path(self, ts: datetime) -> Path:
        return self._calls_dir / f"{ts.date().isoformat()}.jsonl"

    def _append(self, call: Call) -> None:
        ts = call.ts or datetime.now(UTC)
        path = self._day_path(ts)
        path.parent.mkdir(parents=True, exist_ok=True)
        line = json.dumps(_call_to_jsonable(call))
        with path.open("a", encoding="utf-8") as fh:
            fh.write(line + "\n")

    async def record(self, call: Call) -> None:
        await asyncio.to_thread(self._append, call)
        await self._maybe_purge()

    async def record_quality(
        self,
        llm_name: str,
        operation: str | None,
        score: float,
        *,
        call_id: str | None = None,
    ) -> None:
        await self.record(_new_quality_call(llm_name, operation, score, call_id))

    def _day_files_newest_first(self) -> list[Path]:
        if not self._calls_dir.exists():
            return []
        return sorted(self._calls_dir.glob("*.jsonl"), reverse=True)

    def _read_tail(self, limit: int, scope: str | None) -> list[Call]:
        result: list[Call] = []
        for path in self._day_files_newest_first():
            lines = path.read_text(encoding="utf-8").splitlines()
            for raw_line in reversed(lines):
                stripped = raw_line.strip()
                if not stripped:
                    continue
                call = _call_from_jsonable(json.loads(stripped))
                if scope is not None and call.scope != scope:
                    continue
                result.append(call)
                if len(result) >= limit:
                    return result
        return result

    async def calls(self, *, limit: int, scope: str | None = None) -> list[Call]:
        """Newest-first tail of the journal, both kinds interleaved — unfiltered by scope
        (learning is global); ``scope`` is accepted for the host-facing filter only."""
        return await asyncio.to_thread(self._read_tail, limit, scope)

    def _purge_old_day_files(self) -> None:
        cutoff = (datetime.now(UTC) - self._retention).date()
        for path in self._day_files_newest_first():
            try:
                file_date = date.fromisoformat(path.stem)
            except ValueError:
                continue
            if file_date < cutoff:
                path.unlink(missing_ok=True)

    async def _maybe_purge(self) -> None:
        now = time.monotonic()
        if now - self._last_purge < _PURGE_INTERVAL_SECONDS:
            return
        self._last_purge = now
        await asyncio.to_thread(self._purge_old_day_files)

    def _read_disabled(self) -> dict[str, bool]:
        if not self._disabled_path.exists():
            return {}
        data = yaml.safe_load(self._disabled_path.read_text(encoding="utf-8"))
        return dict(data) if data else {}

    def _write_disabled(self, data: dict[str, bool]) -> None:
        self._disabled_path.parent.mkdir(parents=True, exist_ok=True)
        body = yaml.safe_dump(data, sort_keys=True)
        self._disabled_path.write_text(_DISABLED_HEADER + body, encoding="utf-8")

    async def get_disabled(self, name: str) -> bool:
        data = await asyncio.to_thread(self._read_disabled)
        return bool(data.get(name, False))

    async def set_disabled(self, name: str, flag: bool) -> None:  # noqa: FBT001
        data = await asyncio.to_thread(self._read_disabled)
        data[name] = flag
        await asyncio.to_thread(self._write_disabled, data)

    async def seed_disabled(self, names: list[str]) -> None:
        data = await asyncio.to_thread(self._read_disabled)
        changed = False
        for name in names:
            if name not in data:
                data[name] = False
                changed = True
        if changed:
            await asyncio.to_thread(self._write_disabled, data)

    async def disabled_map(self) -> dict[str, bool]:
        return await asyncio.to_thread(self._read_disabled)

calls(*, limit, scope=None) async

Newest-first tail of the journal, both kinds interleaved — unfiltered by scope (learning is global); scope is accepted for the host-facing filter only.

Source code in src/llmbroker/standalone/store.py
179
180
181
182
async def calls(self, *, limit: int, scope: str | None = None) -> list[Call]:
    """Newest-first tail of the journal, both kinds interleaved — unfiltered by scope
    (learning is global); ``scope`` is accepted for the host-facing filter only."""
    return await asyncio.to_thread(self._read_tail, limit, scope)

InMemoryStore

Explicit in-memory opt-out — no persistence, session-scoped learning; disabled verdicts live only in process memory.

Source code in src/llmbroker/standalone/store.py
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
class InMemoryStore:
    """Explicit in-memory opt-out — no persistence, session-scoped learning;
    disabled verdicts live only in process memory."""

    def __init__(self) -> None:
        self._disabled: dict[str, bool] = {}

    async def record(self, _call: Call) -> None:
        return

    async def record_quality(
        self,
        _llm_name: str,
        _operation: str | None,
        _score: float,
        *,
        call_id: str | None = None,  # noqa: ARG002
    ) -> None:
        return

    async def get_disabled(self, name: str) -> bool:
        return self._disabled.get(name, False)

    async def set_disabled(self, name: str, flag: bool) -> None:  # noqa: FBT001
        self._disabled[name] = flag

    async def seed_disabled(self, names: list[str]) -> None:
        for name in names:
            self._disabled.setdefault(name, False)

    async def disabled_map(self) -> dict[str, bool]:
        return dict(self._disabled)

LLM

Synchronous analogue of AsyncLLM.

Source code in src/llmbroker/sync.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class LLM:
    """Synchronous analogue of AsyncLLM."""

    def __init__(self, run_fn: "Callable[[Any], Any]", async_llm: AsyncLLM) -> None:
        self._run = run_fn
        self._async = async_llm

    @property
    def config(self) -> LLMConfig:
        return self._async.config

    @property
    def disabled(self) -> bool:
        return self._async.disabled

    def state(self) -> LLMState:
        return self._run(self._async.state())

    def metrics(self) -> LLMMetrics:
        return self._run(self._async.metrics())

LLMRequestError

Bases: Exception

Base: this request could not be completed.

Source code in src/llmbroker/exceptions.py
6
7
class LLMRequestError(Exception):
    """Base: this request could not be completed."""

LifecyclePhase

Bases: Enum

The FSM label for one LLM's lifecycle, always derived from cooldown_until vs now.

Source code in src/llmbroker/models.py
14
15
16
17
18
class LifecyclePhase(Enum):
    """The FSM label for one LLM's lifecycle, always derived from cooldown_until vs now."""

    AVAILABLE = "available"
    COOLING = "cooling"

NoLLMAvailableError

Bases: LLMRequestError

No LLM slot was available for this request.

reason is one of:

  • "empty_pool" — the pool has zero slots.
  • "no_keys" — slots exist but none has a resolved key.
  • "all_disabled" — keyed slots exist but every one is admin-disabled.
  • "excluded" — every candidate was excluded for this request (internal).
  • "timeout" — the deadline expired (or nothing is free right now); retry_at carries the earliest known return time, when known.
Source code in src/llmbroker/exceptions.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class NoLLMAvailableError(LLMRequestError):
    """No LLM slot was available for this request.

    ``reason`` is one of:

    - ``"empty_pool"`` — the pool has zero slots.
    - ``"no_keys"`` — slots exist but none has a resolved key.
    - ``"all_disabled"`` — keyed slots exist but every one is admin-disabled.
    - ``"excluded"`` — every candidate was excluded for this request (internal).
    - ``"timeout"`` — the deadline expired (or nothing is free right now);
      ``retry_at`` carries the earliest known return time, when known.
    """

    def __init__(
        self,
        message: str,
        *,
        reason: str,
        retry_at: datetime | None = None,
    ) -> None:
        super().__init__(message)
        self.reason = reason
        self.retry_at = retry_at

Optimizer dataclass

Consecutive-failure counter for backoff, plus per-(model, operation) sliding windows of raw quality ratings backing the demoted-last selection order.

Source code in src/llmbroker/optimizer.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@dataclass
class Optimizer:
    """Consecutive-failure counter for backoff, plus per-(model, operation) sliding
    windows of raw quality ratings backing the demoted-last selection order."""

    max_delay: float = 3600.0
    backoff_factor: float = 2.0
    quality_floor: float = 0.3
    quality_confidence: float = 0.95  # z for the Wilson upper bound
    quality_window: int = 30  # ratings kept per (model, operation)
    quality_min_count: int = 10  # verdicts need at least this many

    _rl_fail_count: dict[str, int] = field(default_factory=dict, init=False, repr=False)
    _scores: dict[tuple[str, str | None], deque] = field(
        default_factory=dict,
        init=False,
        repr=False,
    )

    def rl_fail_count(self, llm_name: str) -> int:
        return self._rl_fail_count.get(llm_name, 0)

    def on_rate_limited(self, llm_name: str) -> None:
        """Increment the consecutive-failure count the router reads for its backoff exponent."""
        self._rl_fail_count[llm_name] = self._rl_fail_count.get(llm_name, 0) + 1

    def on_success(self, llm_name: str) -> None:
        self._rl_fail_count[llm_name] = 0

    # ------------------------------------------------------------------
    # Quality windows + derived per-operation demotion verdicts
    # ------------------------------------------------------------------

    def wilson_bound(self, llm_name: str, operation: str | None) -> float | None:
        """The Wilson-score upper bound backing ``is_demoted``, for diagnostics."""
        window = self._scores.get((llm_name, operation))
        if not window:
            return None
        return wilson_upper(list(window), _z_score(self.quality_confidence))

    def is_demoted(self, llm_name: str, operation: str | None) -> bool:
        """True iff the window holds at least ``quality_min_count`` ratings and their
        Wilson-score upper bound sits below ``quality_floor``."""
        window = self._scores.get((llm_name, operation))
        if window is None or len(window) < self.quality_min_count:
            return False
        bound = wilson_upper(list(window), _z_score(self.quality_confidence))
        return bound < self.quality_floor

    def demoted_operations(self, llm_name: str) -> frozenset[str | None]:
        return frozenset(
            operation
            for name, operation in self._scores
            if name == llm_name and self.is_demoted(name, operation)
        )

    def record_quality(self, llm_name: str, operation: str | None, score: float) -> None:
        """Fold one rating into the (model, operation) window, oldest evicted first."""
        before = self.is_demoted(llm_name, operation)
        window = self._scores.setdefault(
            (llm_name, operation),
            deque(maxlen=self.quality_window),
        )
        window.append(score)
        self._log_flip(llm_name, operation, before, self.is_demoted(llm_name, operation))

    def load_scores(self, scores: dict[tuple[str, str | None], list[float]]) -> None:
        """Replace every window wholesale — used by the journal rebuild."""
        keys = set(self._scores) | set(scores)
        before = {key: self.is_demoted(*key) for key in keys}
        self._scores = {
            key: deque(values[-self.quality_window :], maxlen=self.quality_window)
            for key, values in scores.items()
        }
        for key in keys:
            self._log_flip(key[0], key[1], before[key], self.is_demoted(*key))

    def _log_flip(
        self,
        llm_name: str,
        operation: str | None,
        before: bool,  # noqa: FBT001
        after: bool,  # noqa: FBT001
    ) -> None:
        if before == after:
            return
        bound = self.wilson_bound(llm_name, operation)
        if after:
            if bound is not None:
                logger.warning(
                    "%s: quality-demoted for operation=%r (wilson upper %.3f < floor %.2f)",
                    llm_name,
                    operation,
                    bound,
                    self.quality_floor,
                )
            else:
                logger.warning("%s: quality-demoted for operation=%r", llm_name, operation)
        elif bound is not None:
            logger.info(
                "%s: quality demotion cleared for operation=%r (wilson upper %.3f)",
                llm_name,
                operation,
                bound,
            )
        else:
            logger.info("%s: quality demotion cleared for operation=%r", llm_name, operation)

is_demoted(llm_name, operation)

True iff the window holds at least quality_min_count ratings and their Wilson-score upper bound sits below quality_floor.

Source code in src/llmbroker/optimizer.py
72
73
74
75
76
77
78
79
def is_demoted(self, llm_name: str, operation: str | None) -> bool:
    """True iff the window holds at least ``quality_min_count`` ratings and their
    Wilson-score upper bound sits below ``quality_floor``."""
    window = self._scores.get((llm_name, operation))
    if window is None or len(window) < self.quality_min_count:
        return False
    bound = wilson_upper(list(window), _z_score(self.quality_confidence))
    return bound < self.quality_floor

load_scores(scores)

Replace every window wholesale — used by the journal rebuild.

Source code in src/llmbroker/optimizer.py
 98
 99
100
101
102
103
104
105
106
107
def load_scores(self, scores: dict[tuple[str, str | None], list[float]]) -> None:
    """Replace every window wholesale — used by the journal rebuild."""
    keys = set(self._scores) | set(scores)
    before = {key: self.is_demoted(*key) for key in keys}
    self._scores = {
        key: deque(values[-self.quality_window :], maxlen=self.quality_window)
        for key, values in scores.items()
    }
    for key in keys:
        self._log_flip(key[0], key[1], before[key], self.is_demoted(*key))

on_rate_limited(llm_name)

Increment the consecutive-failure count the router reads for its backoff exponent.

Source code in src/llmbroker/optimizer.py
54
55
56
def on_rate_limited(self, llm_name: str) -> None:
    """Increment the consecutive-failure count the router reads for its backoff exponent."""
    self._rl_fail_count[llm_name] = self._rl_fail_count.get(llm_name, 0) + 1

record_quality(llm_name, operation, score)

Fold one rating into the (model, operation) window, oldest evicted first.

Source code in src/llmbroker/optimizer.py
88
89
90
91
92
93
94
95
96
def record_quality(self, llm_name: str, operation: str | None, score: float) -> None:
    """Fold one rating into the (model, operation) window, oldest evicted first."""
    before = self.is_demoted(llm_name, operation)
    window = self._scores.setdefault(
        (llm_name, operation),
        deque(maxlen=self.quality_window),
    )
    window.append(score)
    self._log_flip(llm_name, operation, before, self.is_demoted(llm_name, operation))

wilson_bound(llm_name, operation)

The Wilson-score upper bound backing is_demoted, for diagnostics.

Source code in src/llmbroker/optimizer.py
65
66
67
68
69
70
def wilson_bound(self, llm_name: str, operation: str | None) -> float | None:
    """The Wilson-score upper bound backing ``is_demoted``, for diagnostics."""
    window = self._scores.get((llm_name, operation))
    if not window:
        return None
    return wilson_upper(list(window), _z_score(self.quality_confidence))

Registry

File-backed read-only registry — .toml / .json by extension.

Source code in src/llmbroker/standalone/registry.py
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
class Registry:
    """File-backed read-only registry — ``.toml`` / ``.json`` by extension."""

    def __init__(self, path: str | Path) -> None:
        self._path = Path(path)

    @property
    def path(self) -> Path:
        return self._path

    async def load(self) -> list[LLMConfig]:
        data = _read_data(self._path)
        result: list[LLMConfig] = []
        for entry in data.get("llms", []):
            cfg = _config_from_entry(entry)
            if cfg is not None:
                result.append(cfg)
        return result

    async def key_info(self) -> dict[str, KeyInfo]:
        """Per-provider onboarding metadata from the ``[keys]`` table, keyed by ``api_key_ref``."""
        raw = _read_data(self._path).get("keys", {})
        if not isinstance(raw, dict):
            return {}
        return {str(ref): key_info_from_entry(str(ref), val) for ref, val in raw.items()}

key_info() async

Per-provider onboarding metadata from the [keys] table, keyed by api_key_ref.

Source code in src/llmbroker/standalone/registry.py
77
78
79
80
81
82
async def key_info(self) -> dict[str, KeyInfo]:
    """Per-provider onboarding metadata from the ``[keys]`` table, keyed by ``api_key_ref``."""
    raw = _read_data(self._path).get("keys", {})
    if not isinstance(raw, dict):
        return {}
    return {str(ref): key_info_from_entry(str(ref), val) for ref, val in raw.items()}

Result

Synchronous analogue of AsyncResult.

Source code in src/llmbroker/sync.py
43
44
45
46
47
48
49
50
51
52
53
54
class Result:
    """Synchronous analogue of AsyncResult."""

    def __init__(self, run_fn: "Callable[[Any], Any]", async_result: AsyncResult) -> None:
        self._run = run_fn
        self._async = async_result
        self.text = async_result.text
        self.tool_calls = async_result.tool_calls
        self.usage = async_result.usage

    def record_quality(self, score: float) -> None:
        self._run(self._async.record_quality(score))

Secrets

Read-only env-backed secrets resolver (the default battery).

Source code in src/llmbroker/standalone/secrets.py
15
16
17
18
19
20
21
22
class Secrets:
    """Read-only env-backed secrets resolver (the default battery)."""

    async def resolve(self, ref: str) -> str:
        value = os.environ.get(ref)
        if value is None:
            raise KeyError(f"Secrets: env var {ref!r} is not set")
        return value

arun_tool_loop(llms, messages, *, tools=None, dispatch=None, max_steps=8, **chat_kwargs) async

Drive broker.chat until a tool-call-free reply; execute tools via dispatch.

Source code in src/llmbroker/chat.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
async def arun_tool_loop(
    llms,  # noqa: ANN001 - AsyncBroker (avoid import cycle)
    messages: list[dict],
    *,
    tools: list[dict] | None = None,
    dispatch: Mapping[str, Callable[..., object]] | None = None,
    max_steps: int = 8,
    **chat_kwargs,
) -> str:
    """Drive ``broker.chat`` until a tool-call-free reply; execute tools via dispatch."""
    convo = list(messages)
    dispatch = dispatch or {}
    for _ in range(max_steps):
        result = await llms.chat(convo, tools=tools, **chat_kwargs)
        text = _advance_tool_loop(convo, result, dispatch)
        if text is not None:
            return text
    return ""

run_tool_loop(llms, messages, *, tools=None, dispatch=None, max_steps=8, **chat_kwargs)

Synchronous tool loop over a sync Broker.

Mirrors arun_tool_loop but calls the blocking Broker.chat; it does not use the async engine directly so it is safe to call from any thread.

Source code in src/llmbroker/chat.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def run_tool_loop(
    llms,  # noqa: ANN001 - sync Broker
    messages: list[dict],
    *,
    tools: list[dict] | None = None,
    dispatch: Mapping[str, Callable[..., object]] | None = None,
    max_steps: int = 8,
    **chat_kwargs,
) -> str:
    """Synchronous tool loop over a sync ``Broker``.

    Mirrors ``arun_tool_loop`` but calls the blocking ``Broker.chat``; it does
    not use the async engine directly so it is safe to call from any thread.
    """
    convo = list(messages)
    dispatch = dispatch or {}
    for _ in range(max_steps):
        result = llms.chat(convo, tools=tools, **chat_kwargs)
        text = _advance_tool_loop(convo, result, dispatch)
        if text is not None:
            return text
    return ""

__main__

python -m llmbroker entry point.

aws

AWS Secrets Manager backend for llmbroker.

Needs the aioboto3 driver (llmbroker[aws]); importing this package is how a host declares that dependency, so a bare import llmbroker stays driver-free.

Secrets

AWS Secrets Manager-backed mutable secrets store.

A secretsmanager client is opened as an async context manager per call; there is no shared client state, so aclose is a no-op. endpoint_url targets a non-default endpoint (e.g. LocalStack or a VPC endpoint).

Source code in src/llmbroker/aws/secrets.py
 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
class Secrets:
    """AWS Secrets Manager-backed mutable secrets store.

    A secretsmanager client is opened as an async context manager per call;
    there is no shared client state, so ``aclose`` is a no-op. ``endpoint_url``
    targets a non-default endpoint (e.g. LocalStack or a VPC endpoint).
    """

    def __init__(
        self,
        *,
        region_name: str | None = None,
        endpoint_url: str | None = None,
        prefix: str = "llmbroker/",
    ) -> None:
        self._session = aioboto3.Session()
        self._region_name = region_name
        self._endpoint_url = endpoint_url
        self._prefix = prefix

    def _name(self, ref: str) -> str:
        return f"{self._prefix}{ref}"

    def _client(self):
        return self._session.client(
            "secretsmanager",
            region_name=self._region_name,
            endpoint_url=self._endpoint_url,
        )

    async def resolve(self, ref: str) -> str:
        async with self._client() as client:
            try:
                response = await client.get_secret_value(SecretId=self._name(ref))
            except ClientError as exc:
                if exc.response["Error"]["Code"] != "ResourceNotFoundException":
                    raise
                raise KeyError(f"aws.Secrets: ref {ref!r} not found") from exc
            return response["SecretString"]

    async def set(self, ref: str, value: str) -> None:
        name = self._name(ref)
        async with self._client() as client:
            try:
                await client.put_secret_value(SecretId=name, SecretString=value)
            except ClientError as exc:
                if exc.response["Error"]["Code"] != "ResourceNotFoundException":
                    raise
                await client.create_secret(
                    Name=name,
                    SecretString=value,
                    Tags=[{"Key": "llmbroker", "Value": "1"}],
                )

    async def aclose(self) -> None:
        return

secrets

AWS Secrets Manager-backed mutable secrets store.

Secrets

AWS Secrets Manager-backed mutable secrets store.

A secretsmanager client is opened as an async context manager per call; there is no shared client state, so aclose is a no-op. endpoint_url targets a non-default endpoint (e.g. LocalStack or a VPC endpoint).

Source code in src/llmbroker/aws/secrets.py
 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
class Secrets:
    """AWS Secrets Manager-backed mutable secrets store.

    A secretsmanager client is opened as an async context manager per call;
    there is no shared client state, so ``aclose`` is a no-op. ``endpoint_url``
    targets a non-default endpoint (e.g. LocalStack or a VPC endpoint).
    """

    def __init__(
        self,
        *,
        region_name: str | None = None,
        endpoint_url: str | None = None,
        prefix: str = "llmbroker/",
    ) -> None:
        self._session = aioboto3.Session()
        self._region_name = region_name
        self._endpoint_url = endpoint_url
        self._prefix = prefix

    def _name(self, ref: str) -> str:
        return f"{self._prefix}{ref}"

    def _client(self):
        return self._session.client(
            "secretsmanager",
            region_name=self._region_name,
            endpoint_url=self._endpoint_url,
        )

    async def resolve(self, ref: str) -> str:
        async with self._client() as client:
            try:
                response = await client.get_secret_value(SecretId=self._name(ref))
            except ClientError as exc:
                if exc.response["Error"]["Code"] != "ResourceNotFoundException":
                    raise
                raise KeyError(f"aws.Secrets: ref {ref!r} not found") from exc
            return response["SecretString"]

    async def set(self, ref: str, value: str) -> None:
        name = self._name(ref)
        async with self._client() as client:
            try:
                await client.put_secret_value(SecretId=name, SecretString=value)
            except ClientError as exc:
                if exc.response["Error"]["Code"] != "ResourceNotFoundException":
                    raise
                await client.create_secret(
                    Name=name,
                    SecretString=value,
                    Tags=[{"Key": "llmbroker", "Value": "1"}],
                )

    async def aclose(self) -> None:
        return

backends

Zero-dependency storage core: declarative table spec, the Driver protocol, generic ports written once against any Driver, and an in-memory reference driver.

Importable by a bare import llmbroker — no driver package required. Each DB backend package (sqlite, postgres, mongodb) supplies one Driver implementation and wraps the generic ports in its facade classes.

driver

The per-DB storage contract: record-shaped, not domain-shaped.

A driver method body is the one statement that is genuinely DB-specific; the generic layer (backends.ports) owns lazy ensure_schema gating, JSON⇄dataclass translation, and KeyError semantics.

Driver

Bases: Protocol

Source code in src/llmbroker/backends/driver.py
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
class Driver(Protocol):
    async def ensure_schema(self) -> None: ...

    # ------------------------------------------------------------------
    # Keyed records (registry, disabled, secrets) — every key column is
    # non-null text, so key matching is plain equality.
    # ------------------------------------------------------------------

    async def fetch(self, table: str) -> list[Row]:
        """All rows, ordered by key columns (registry load order feeds selection priority)."""
        ...

    async def get(self, table: str, key: Key) -> Row | None: ...

    async def upsert(self, table: str, key: Key, row: Row) -> None: ...

    async def delete(self, table: str, key: Key) -> bool: ...

    # ------------------------------------------------------------------
    # Journal ops (llmbroker_calls) — strictly append-only: no update op
    # exists; quality is its own appended record.
    # ------------------------------------------------------------------

    async def append(self, table: str, row: Row) -> None: ...

    async def recent(self, table: str, limit: int, match: Row | None = None) -> list[Row]:
        """Newest-first tail; ``match`` is an optional equality filter."""
        ...

    async def purge(self, table: str, before: datetime) -> int:
        """Delete rows older than ``before``; returns the count removed."""
        ...

    async def aclose(self) -> None: ...
fetch(table) async

All rows, ordered by key columns (registry load order feeds selection priority).

Source code in src/llmbroker/backends/driver.py
23
24
25
async def fetch(self, table: str) -> list[Row]:
    """All rows, ordered by key columns (registry load order feeds selection priority)."""
    ...
purge(table, before) async

Delete rows older than before; returns the count removed.

Source code in src/llmbroker/backends/driver.py
44
45
46
async def purge(self, table: str, before: datetime) -> int:
    """Delete rows older than ``before``; returns the count removed."""
    ...
recent(table, limit, match=None) async

Newest-first tail; match is an optional equality filter.

Source code in src/llmbroker/backends/driver.py
40
41
42
async def recent(self, table: str, limit: int, match: Row | None = None) -> list[Row]:
    """Newest-first tail; ``match`` is an optional equality filter."""
    ...

inmemory

Trivial dict-based Driver — a test double, and a dependency-free storage option for hosts that want the full port surface without a database.

InMemoryDriver

In-process Driver implementation. Not persisted, not process-shared.

Source code in src/llmbroker/backends/inmemory.py
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
class InMemoryDriver:
    """In-process ``Driver`` implementation. Not persisted, not process-shared."""

    def __init__(self) -> None:
        self._tables: dict[str, dict[Key, Row]] = {}
        self._journals: dict[str, list[Row]] = {}

    async def ensure_schema(self) -> None:
        return

    async def fetch(self, table: str) -> list[Row]:
        return [dict(row) for _, row in sorted(self._tables.get(table, {}).items())]

    async def get(self, table: str, key: Key) -> Row | None:
        row = self._tables.get(table, {}).get(key)
        return dict(row) if row is not None else None

    async def upsert(self, table: str, key: Key, row: Row) -> None:
        self._tables.setdefault(table, {})[key] = dict(row)

    async def delete(self, table: str, key: Key) -> bool:
        return self._tables.get(table, {}).pop(key, None) is not None

    async def append(self, table: str, row: Row) -> None:
        self._journals.setdefault(table, []).append(dict(row))

    async def recent(self, table: str, limit: int, match: Row | None = None) -> list[Row]:
        rows = self._journals.get(table, [])
        if match:
            rows = [r for r in rows if all(r.get(k) == v for k, v in match.items())]
        ordered = sorted(rows, key=lambda r: r.get("called_at") or _EPOCH, reverse=True)
        return [dict(r) for r in ordered[:limit]]

    async def purge(self, table: str, before: datetime) -> int:
        rows = self._journals.get(table, [])
        keep = [r for r in rows if (r.get("called_at") or before) >= before]
        removed = len(rows) - len(keep)
        self._journals[table] = keep
        return removed

    async def aclose(self) -> None:
        return

ports

Generic ports: the domain protocols implemented once over any Driver.

Registry and secrets are global — no user scope exists anywhere in this layer (the broker turns an opaque scope string into a secret-ref prefix and a journal attribution field instead).

DriverRegistry

Registry over any Driver — a pure preset mirror, globally scoped.

Source code in src/llmbroker/backends/ports.py
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
class DriverRegistry:
    """Registry over any ``Driver`` — a pure preset mirror, globally scoped."""

    def __init__(self, driver: Driver) -> None:
        self._driver = driver

    async def load(self) -> list[LLMConfig]:
        rows = await self._driver.fetch("registry")
        return [
            LLMConfig.from_metadata(
                name=str(row["name"]),
                base_url=str(row["base_url"]),
                model=str(row["model"]),
                api_key_ref=str(row["api_key_ref"]),
                metadata=row.get("metadata"),  # type: ignore[arg-type]
            )
            for row in rows
        ]

    async def mirror(self, configs: list[LLMConfig]) -> None:
        """Total mirror: add new, update existing, delete stored entries absent
        from ``configs`` — the only registry write path."""
        source_names = {c.name for c in configs}
        existing = {str(row["name"]) for row in await self._driver.fetch("registry")}
        for name in existing - source_names:
            await self._driver.delete("registry", (name,))
        for cfg in configs:
            await self._driver.upsert(
                "registry",
                (cfg.name,),
                {
                    "name": cfg.name,
                    "base_url": cfg.base_url,
                    "model": cfg.model,
                    "api_key_ref": cfg.api_key_ref,
                    "metadata": cfg.to_metadata(),
                },
            )

    async def aclose(self) -> None:
        await self._driver.aclose()
mirror(configs) async

Total mirror: add new, update existing, delete stored entries absent from configs — the only registry write path.

Source code in src/llmbroker/backends/ports.py
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
async def mirror(self, configs: list[LLMConfig]) -> None:
    """Total mirror: add new, update existing, delete stored entries absent
    from ``configs`` — the only registry write path."""
    source_names = {c.name for c in configs}
    existing = {str(row["name"]) for row in await self._driver.fetch("registry")}
    for name in existing - source_names:
        await self._driver.delete("registry", (name,))
    for cfg in configs:
        await self._driver.upsert(
            "registry",
            (cfg.name,),
            {
                "name": cfg.name,
                "base_url": cfg.base_url,
                "model": cfg.model,
                "api_key_ref": cfg.api_key_ref,
                "metadata": cfg.to_metadata(),
            },
        )
DriverSecrets

Flat ref -> value secrets store over any Driver. Exact-match lookups only — the own→shared prefix fallback lives in the broker.

Source code in src/llmbroker/backends/ports.py
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class DriverSecrets:
    """Flat ``ref -> value`` secrets store over any ``Driver``. Exact-match lookups
    only — the own→shared prefix fallback lives in the broker."""

    def __init__(self, driver: Driver) -> None:
        self._driver = driver

    async def resolve(self, ref: str) -> str:
        row = await self._driver.get("secrets", (ref,))
        if row is None:
            raise KeyError(f"{type(self).__name__}: ref {ref!r} not found")
        return str(row["value"])

    async def set(self, ref: str, value: str) -> None:
        await self._driver.upsert("secrets", (ref,), {"ref": ref, "value": value})

    async def aclose(self) -> None:
        await self._driver.aclose()
DriverStore

Journal (append/recent/purge) + admin disabled-map, over any Driver.

Self-purges call rows older than retention, checked at most once per hour on write activity.

Source code in src/llmbroker/backends/ports.py
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
class DriverStore:
    """Journal (append/recent/purge) + admin disabled-map, over any ``Driver``.

    Self-purges call rows older than ``retention``, checked at most once per
    hour on write activity.
    """

    def __init__(self, driver: Driver, *, retention: timedelta = _DEFAULT_RETENTION) -> None:
        self._driver = driver
        self._retention = retention
        self._last_purge = float("-inf")

    async def record(self, call: Call) -> None:
        await self._driver.append("calls", _call_to_row(call))
        await self._maybe_purge()

    async def record_quality(
        self,
        llm_name: str,
        operation: str | None,
        score: float,
        *,
        call_id: str | None = None,
    ) -> None:
        """Append a self-contained quality record — never updates the call row."""
        await self.record(
            Call(
                id=str(uuid.uuid4()),
                llm_name=llm_name,
                operation=operation,
                trace_id=None,
                status=None,
                kind="quality",
                ts=datetime.now(UTC),
                quality_score=score,
                call_id=call_id,
            ),
        )

    async def calls(self, *, limit: int, scope: str | None = None) -> list[Call]:
        """Newest-first tail of the journal, both kinds interleaved — unfiltered by scope
        (learning is global); ``scope`` is accepted for the host-facing filter only."""
        match = {"scope": scope} if scope is not None else None
        rows = await self._driver.recent("calls", limit, match)
        return [_row_to_call(r) for r in rows]

    async def _purge_old_calls(self) -> None:
        cutoff = datetime.now(UTC) - self._retention
        await self._driver.purge("calls", cutoff)

    async def _maybe_purge(self) -> None:
        now = time.monotonic()
        if now - self._last_purge < _PURGE_INTERVAL_SECONDS:
            return
        self._last_purge = now
        await self._purge_old_calls()

    # ------------------------------------------------------------------
    # Admin disabled-verdict map
    # ------------------------------------------------------------------

    async def get_disabled(self, name: str) -> bool:
        row = await self._driver.get("disabled", (name,))
        return bool(row["disabled"]) if row else False

    async def set_disabled(self, name: str, flag: bool) -> None:  # noqa: FBT001
        await self._driver.upsert("disabled", (name,), {"name": name, "disabled": int(flag)})

    async def seed_disabled(self, names: list[str]) -> None:
        """Insert-if-absent every name with ``disabled=False`` — never touches existing values."""
        existing = {str(row["name"]) for row in await self._driver.fetch("disabled")}
        for name in names:
            if name not in existing:
                await self._driver.upsert("disabled", (name,), {"name": name, "disabled": 0})

    async def disabled_map(self) -> dict[str, bool]:
        rows = await self._driver.fetch("disabled")
        return {str(r["name"]): bool(r["disabled"]) for r in rows}

    async def aclose(self) -> None:
        await self._driver.aclose()
calls(*, limit, scope=None) async

Newest-first tail of the journal, both kinds interleaved — unfiltered by scope (learning is global); scope is accepted for the host-facing filter only.

Source code in src/llmbroker/backends/ports.py
171
172
173
174
175
176
async def calls(self, *, limit: int, scope: str | None = None) -> list[Call]:
    """Newest-first tail of the journal, both kinds interleaved — unfiltered by scope
    (learning is global); ``scope`` is accepted for the host-facing filter only."""
    match = {"scope": scope} if scope is not None else None
    rows = await self._driver.recent("calls", limit, match)
    return [_row_to_call(r) for r in rows]
record_quality(llm_name, operation, score, *, call_id=None) async

Append a self-contained quality record — never updates the call row.

Source code in src/llmbroker/backends/ports.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
async def record_quality(
    self,
    llm_name: str,
    operation: str | None,
    score: float,
    *,
    call_id: str | None = None,
) -> None:
    """Append a self-contained quality record — never updates the call row."""
    await self.record(
        Call(
            id=str(uuid.uuid4()),
            llm_name=llm_name,
            operation=operation,
            trace_id=None,
            status=None,
            kind="quality",
            ts=datetime.now(UTC),
            quality_score=score,
            call_id=call_id,
        ),
    )
seed_disabled(names) async

Insert-if-absent every name with disabled=False — never touches existing values.

Source code in src/llmbroker/backends/ports.py
200
201
202
203
204
205
async def seed_disabled(self, names: list[str]) -> None:
    """Insert-if-absent every name with ``disabled=False`` — never touches existing values."""
    existing = {str(row["name"]) for row in await self._driver.fetch("disabled")}
    for name in names:
        if name not in existing:
            await self._driver.upsert("disabled", (name,), {"name": name, "disabled": 0})

spec

One declarative description of the stores, consumed by every driver's ensure_schema. Column types are portable strings — each driver maps them to its own native DDL/wire representation.

TableSpec dataclass

One store's shape: name, identity columns, and portable column types.

Every key column is a single non-null text column: registry (name,), disabled (name,), secrets (ref,) — the user scope rides inside the ref string, so no scope columns exist anywhere. The calls journal has no keyed access (append-only); its id column is listed for completeness and uniqueness only.

Source code in src/llmbroker/backends/spec.py
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@dataclass(frozen=True)
class TableSpec:
    """One store's shape: name, identity columns, and portable column types.

    Every key column is a single non-null text column: registry (``name``,),
    disabled (``name``,), secrets (``ref``,) — the user scope rides inside the
    ref string, so no scope columns exist anywhere. The calls journal has no
    keyed access (append-only); its ``id`` column is listed for completeness
    and uniqueness only.
    """

    name: str
    key: tuple[str, ...]
    columns: dict[str, str] = field(default_factory=dict)
    indexes: tuple[tuple[str, ...], ...] = ()

broker

The async broker engine: the AsyncBroker façade plus its live-pool collaborators.

Implementation lives in focused sibling modules — broker / catalog / router / pool_view / pool / result / state. Request exceptions live in llmbroker.exceptions and the optimizer knob in llmbroker.optimizer.

broker

The AsyncBroker façade over the LLM pool and its collaborators.

AsyncBroker owns the external ports (registry, secrets, store), lazily provisions the live LLMPool once, and delegates each operation to the collaborator that owns it:

  • Catalog — pool membership in sync with the registry; sync(preset) mirrors a preset into it (the only registry write path)
  • Router — routing a completion over the pool with failover
  • PoolView — read-only views of current pool state
  • _LearningHook — quality windows, dead-key drops, and the debounced journal rebuild feeding shared cooldowns, snapshot metrics, and the admin disabled-verdict map (only wired when optimize is truthy)

The call journal (calls) is a thin pass-through to a queryable store backend; each backend self-purges records past its retention horizon. There is no alerts API: the few human-actionable events (dead key, demotion flip, under-provisioned pool) are log lines; hosts poll snapshot() for current raw state or hook the llmbroker logger.

AsyncBroker

Façade over the LLM pool: route completions, inspect state, edit the catalog.

Source code in src/llmbroker/broker/broker.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
class AsyncBroker:
    """Façade over the LLM pool: route completions, inspect state, edit the catalog."""

    def __init__(  # noqa: PLR0913
        self,
        registry: RegistryProtocol | str | Path | None = None,
        *,
        secrets: SecretsProtocol | None = None,
        store: StoreProtocol | None = None,
        optimize: bool | Optimizer = True,
        scope: str | None = None,
    ) -> None:
        if scope == "":
            raise ValueError("scope must not be empty string; use None for unscoped")
        if registry is None:
            raise ValueError("AsyncBroker requires a `registry` source")

        source_secrets: SecretsProtocol | None = None
        source_store: StoreProtocol | None = None
        if isinstance(registry, (str, Path)):
            registry, source_secrets, source_store = resolve_source(registry)

        secrets = as_secrets(secrets) if secrets is not None else (source_secrets or Secrets())
        store = store if store is not None else (source_store or _default_store(registry))

        if isinstance(optimize, Optimizer):
            self._optimizer: Optimizer | None = optimize
        elif optimize:
            self._optimizer = Optimizer()
        else:
            self._optimizer = None

        self._registry = registry
        self._secrets = secrets
        self._base_store = store
        self._scope = scope

        pool = LLMPool(optimizer=self._optimizer)
        self._pool = pool
        self._catalog = Catalog(registry, secrets, pool, scope=scope)

        self._learning_hook: _LearningHook | None = None
        effective_store: StoreProtocol
        if self._optimizer is not None:
            self._learning_hook = _LearningHook(
                self._optimizer,
                store,
                pool,
                self._catalog.resync,
            )
            effective_store = self._learning_hook
        else:
            effective_store = store

        self._store = effective_store
        self._router = Router(pool, effective_store, scope=scope, optimizer=self._optimizer)
        self._pool_view = PoolView(pool, effective_store)

        self._provisioned = False
        self._provision_lock = asyncio.Lock()
        self._last_underprov_alert: float = float("-inf")
        self._underprov_alert_interval: float = 60.0

    # ------------------------------------------------------------------
    # Lifecycle
    # ------------------------------------------------------------------

    async def ensure_pool(self) -> None:
        """Lazy idempotent initializer — provisions the pool exactly once.

        Raises if the registry is empty — call ``sync(preset)`` first.
        """
        if self._provisioned:
            return
        async with self._provision_lock:
            if self._provisioned:
                return
            await self._catalog.provision()
            if self._learning_hook is not None:
                # warm start — provision() above already resynced the registry
                await self._learning_hook.maybe_rebuild(force=True, resync_registry=False)
            self._provisioned = True

    async def sync(self, preset: RegistryProtocol | str | Path) -> None:
        """Mirror ``preset`` into the registry: add new entries, update existing
        ones, delete entries absent from the preset. Explicit and idempotent —
        call it once to initialize a fresh DB, or again whenever the preset changes.
        If the pool is already provisioned, the change takes effect immediately.
        """
        if isinstance(preset, (str, Path)):
            preset = Registry(preset)
        await self._catalog.sync(preset)
        if isinstance(self._base_store, DisabledMapProtocol):
            configs = await self._registry.load()
            await self._base_store.seed_disabled([c.name for c in configs])
        if self._provisioned:
            await self._catalog.resync()

    async def aclose(self) -> None:
        await self._router.aclose()
        for port in (self._registry, self._secrets, self._store):
            if isinstance(port, AsyncResourceProtocol):
                await port.aclose()

    async def __aenter__(self) -> "AsyncBroker":
        await self.ensure_pool()
        return self

    async def __aexit__(self, *exc: object) -> None:
        await self.aclose()

    # ------------------------------------------------------------------
    # Routing
    # ------------------------------------------------------------------

    async def ask(
        self,
        prompt: str,
        *,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> AsyncResult:
        await self.ensure_pool()
        try:
            return await self._router.ask(prompt, operation=operation, trace_id=trace_id, wait=wait)
        except NoLLMAvailableError as exc:
            self._maybe_alert_underprov(exc)
            raise

    async def chat(
        self,
        messages: list[dict],
        *,
        tools: list[dict] | None = None,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> AsyncResult:
        await self.ensure_pool()
        try:
            return await self._router.chat(
                messages,
                tools=tools,
                operation=operation,
                trace_id=trace_id,
                wait=wait,
            )
        except NoLLMAvailableError as exc:
            self._maybe_alert_underprov(exc)
            raise

    # ------------------------------------------------------------------
    # Inspection
    # ------------------------------------------------------------------

    async def get(self, name: str) -> AsyncLLM:
        await self.ensure_pool()
        return self._pool_view.get(name)

    async def count(self) -> int:
        await self.ensure_pool()
        return self._pool_view.count()

    async def snapshot(self) -> Mapping[str, LLMSnapshot]:
        await self.ensure_pool()
        return await self._pool_view.snapshot()

    # ------------------------------------------------------------------
    # Manual disable — the one verdict that actually excludes
    # ------------------------------------------------------------------

    async def disable_llm(self, name: str) -> None:
        """Set the manual latch: withdraws the slot, survives preset rolls, covers
        every operation including future ones. Only ``enable_llm`` clears it."""
        await self.ensure_pool()
        self._pool.set_disabled(name)
        if isinstance(self._base_store, DisabledMapProtocol):
            await self._base_store.set_disabled(name, True)

    async def enable_llm(self, name: str) -> None:
        """Clear the manual latch — a re-enabled model rehabilitates through new
        ratings, no quality reset exists."""
        await self.ensure_pool()
        await self._pool.clear_disabled(name)
        if isinstance(self._base_store, DisabledMapProtocol):
            await self._base_store.set_disabled(name, False)

    # ------------------------------------------------------------------
    # Call journal
    # ------------------------------------------------------------------

    async def calls(self, *, limit: int) -> list[Call]:
        return await self._require_queryable().calls(limit=limit, scope=self._scope)

    def _maybe_alert_underprov(self, exc: NoLLMAvailableError) -> None:
        """Fire when zero keyed configs are routable — the genuine "no usable models" alarm.

        A keyless config is never enqueued/acquired/cooled (see the partial-key framing
        in architecture.md), so it must be excluded here: with even one keyless config
        present, an unfiltered check could never observe "all non-AVAILABLE", masking
        the real alarm even when every *keyed* config is COOLING. Only a ``"timeout"``
        reason means the pool is merely temporarily exhausted; every other reason
        (no keys, all disabled, empty pool) already logs its own actionable line.
        """
        if exc.reason != "timeout":
            return
        if self._optimizer is None:
            return
        if not self._pool.configs:
            return
        now = time.monotonic()
        if now - self._last_underprov_alert < self._underprov_alert_interval:
            return
        keyed_names = [name for name in self._pool.configs if self._pool.has_key(name)]
        all_offline = all(
            self._pool.state(name).phase is not LifecyclePhase.AVAILABLE for name in keyed_names
        )
        if all_offline:
            self._last_underprov_alert = now
            logger.warning(
                "pool under-provisioned: all LLMs are COOLING — add more LLMs to the registry",
            )

    def _require_queryable(self) -> QueryableStoreProtocol:
        if not isinstance(self._base_store, QueryableStoreProtocol):
            raise TypeError(
                "this store backend is not queryable — use a queryable backend"
                " (e.g. llmbroker.sqlite.Store) for calls()",
            )
        return cast(QueryableStoreProtocol, self._store)
disable_llm(name) async

Set the manual latch: withdraws the slot, survives preset rolls, covers every operation including future ones. Only enable_llm clears it.

Source code in src/llmbroker/broker/broker.py
239
240
241
242
243
244
245
async def disable_llm(self, name: str) -> None:
    """Set the manual latch: withdraws the slot, survives preset rolls, covers
    every operation including future ones. Only ``enable_llm`` clears it."""
    await self.ensure_pool()
    self._pool.set_disabled(name)
    if isinstance(self._base_store, DisabledMapProtocol):
        await self._base_store.set_disabled(name, True)
enable_llm(name) async

Clear the manual latch — a re-enabled model rehabilitates through new ratings, no quality reset exists.

Source code in src/llmbroker/broker/broker.py
247
248
249
250
251
252
253
async def enable_llm(self, name: str) -> None:
    """Clear the manual latch — a re-enabled model rehabilitates through new
    ratings, no quality reset exists."""
    await self.ensure_pool()
    await self._pool.clear_disabled(name)
    if isinstance(self._base_store, DisabledMapProtocol):
        await self._base_store.set_disabled(name, False)
ensure_pool() async

Lazy idempotent initializer — provisions the pool exactly once.

Raises if the registry is empty — call sync(preset) first.

Source code in src/llmbroker/broker/broker.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
async def ensure_pool(self) -> None:
    """Lazy idempotent initializer — provisions the pool exactly once.

    Raises if the registry is empty — call ``sync(preset)`` first.
    """
    if self._provisioned:
        return
    async with self._provision_lock:
        if self._provisioned:
            return
        await self._catalog.provision()
        if self._learning_hook is not None:
            # warm start — provision() above already resynced the registry
            await self._learning_hook.maybe_rebuild(force=True, resync_registry=False)
        self._provisioned = True
sync(preset) async

Mirror preset into the registry: add new entries, update existing ones, delete entries absent from the preset. Explicit and idempotent — call it once to initialize a fresh DB, or again whenever the preset changes. If the pool is already provisioned, the change takes effect immediately.

Source code in src/llmbroker/broker/broker.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
async def sync(self, preset: RegistryProtocol | str | Path) -> None:
    """Mirror ``preset`` into the registry: add new entries, update existing
    ones, delete entries absent from the preset. Explicit and idempotent —
    call it once to initialize a fresh DB, or again whenever the preset changes.
    If the pool is already provisioned, the change takes effect immediately.
    """
    if isinstance(preset, (str, Path)):
        preset = Registry(preset)
    await self._catalog.sync(preset)
    if isinstance(self._base_store, DisabledMapProtocol):
        configs = await self._registry.load()
        await self._base_store.seed_disabled([c.name for c in configs])
    if self._provisioned:
        await self._catalog.resync()

catalog

Catalog: keep the live pool's membership in sync with the registry.

Loads configs from the registry, resolves their API keys via the secrets backend, and reflects every change into the LLMPool. The registry is a pure mirror of a preset (see sync) — nothing else writes it.

Catalog

Reconciles the persistent registry into the live pool, and mirrors presets into it.

Source code in src/llmbroker/broker/catalog.py
 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
118
119
120
121
122
123
124
125
class Catalog:
    """Reconciles the persistent registry into the live pool, and mirrors presets into it."""

    def __init__(
        self,
        registry: RegistryProtocol,
        secrets: SecretsProtocol,
        pool: LLMPool,
        *,
        scope: str | None,
    ) -> None:
        self._registry = registry
        self._secrets = secrets
        self._pool = pool
        self._scope = scope

    async def provision(self) -> None:
        """Reconcile the pool with the registry. The caller serializes this
        one-time init; it is not re-entrant. Raises if the registry is empty."""
        configs = await self._registry.load()
        if not configs:
            raise RuntimeError(
                "registry is empty — call sync(preset) to mirror a preset into it before"
                " provisioning (e.g. `await broker.sync(preset)` or `python -m llmbroker sync`)",
            )
        await self._reconcile(configs)

    async def resync(self) -> None:
        """Re-read the registry and reconcile pool membership — no emptiness check.

        Called by the debounced journal rebuild so registry edits and key changes
        from other processes/nodes take effect on a running broker.
        """
        configs = await self._registry.load()
        await self._reconcile(configs)

    async def _reconcile(self, configs: list[LLMConfig]) -> None:
        names = {c.name for c in configs}
        for name in list(self._pool.configs):
            if name not in names:
                await self._pool.drop(name)
        for order, cfg in enumerate(configs):
            await self._pool.add(cfg, await self._resolve_key(cfg), order=order)

    async def sync(self, preset: RegistryProtocol) -> None:
        """Mirror ``preset`` into the registry: add new entries, update existing
        ones, delete entries absent from the preset — the total mirror, nothing to
        preserve. Refuses (raises) a ``model`` identity change under an existing
        name, since that binds learned stats to the model name; a model bump must
        land under a new entry name instead.
        """
        registry = self._require_mutable_registry()
        source_configs = await preset.load()
        existing = {c.name: c for c in await registry.load()}
        for cfg in source_configs:
            current = existing.get(cfg.name)
            if current is not None and current.model != cfg.model:
                raise ValueError(
                    f"sync: refusing to change model for {cfg.name!r}"
                    f" (stored {current.model!r} vs preset {cfg.model!r}) — a model bump"
                    " must be a new entry name",
                )
        await registry.mirror(source_configs)
        await self._seed_secrets(source_configs)

    async def _resolve_key(self, cfg: LLMConfig) -> str | None:
        """Try the scope-prefixed (own) ref first, falling back to the shared ref."""
        if self._scope is not None:
            try:
                return await self._secrets.resolve(f"{self._scope}/{cfg.api_key_ref}")
            except KeyError:
                pass
        try:
            return await self._secrets.resolve(cfg.api_key_ref)
        except KeyError:
            logger.info(
                "LLM %s: api_key_ref %r not resolved — inactive until the env var /"
                " secret is set; this is normal, the pool routes over whatever keys are present",
                cfg.name,
                cfg.api_key_ref,
            )
            return None

    async def _seed_secrets(self, configs: list[LLMConfig]) -> None:
        """Copy any env-resolvable keys into a mutable secrets backend, preserving existing."""
        if not isinstance(self._secrets, MutableSecretsProtocol):
            return
        bootstrap = Secrets()
        for cfg in configs:
            try:
                await self._secrets.resolve(cfg.api_key_ref)
                continue  # already resolvable — preserve
            except KeyError:
                pass
            try:
                value = await bootstrap.resolve(cfg.api_key_ref)
            except KeyError:
                continue
            await self._secrets.set(cfg.api_key_ref, value)

    def _require_mutable_registry(self) -> MutableRegistryProtocol:
        if not isinstance(self._registry, MutableRegistryProtocol):
            raise TypeError(
                f"{type(self._registry).__name__} does not support mutations"
                " (sync requires a mutable registry such as llmbroker.sqlite.Registry)",
            )
        return self._registry
provision() async

Reconcile the pool with the registry. The caller serializes this one-time init; it is not re-entrant. Raises if the registry is empty.

Source code in src/llmbroker/broker/catalog.py
35
36
37
38
39
40
41
42
43
44
async def provision(self) -> None:
    """Reconcile the pool with the registry. The caller serializes this
    one-time init; it is not re-entrant. Raises if the registry is empty."""
    configs = await self._registry.load()
    if not configs:
        raise RuntimeError(
            "registry is empty — call sync(preset) to mirror a preset into it before"
            " provisioning (e.g. `await broker.sync(preset)` or `python -m llmbroker sync`)",
        )
    await self._reconcile(configs)
resync() async

Re-read the registry and reconcile pool membership — no emptiness check.

Called by the debounced journal rebuild so registry edits and key changes from other processes/nodes take effect on a running broker.

Source code in src/llmbroker/broker/catalog.py
46
47
48
49
50
51
52
53
async def resync(self) -> None:
    """Re-read the registry and reconcile pool membership — no emptiness check.

    Called by the debounced journal rebuild so registry edits and key changes
    from other processes/nodes take effect on a running broker.
    """
    configs = await self._registry.load()
    await self._reconcile(configs)
sync(preset) async

Mirror preset into the registry: add new entries, update existing ones, delete entries absent from the preset — the total mirror, nothing to preserve. Refuses (raises) a model identity change under an existing name, since that binds learned stats to the model name; a model bump must land under a new entry name instead.

Source code in src/llmbroker/broker/catalog.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
async def sync(self, preset: RegistryProtocol) -> None:
    """Mirror ``preset`` into the registry: add new entries, update existing
    ones, delete entries absent from the preset — the total mirror, nothing to
    preserve. Refuses (raises) a ``model`` identity change under an existing
    name, since that binds learned stats to the model name; a model bump must
    land under a new entry name instead.
    """
    registry = self._require_mutable_registry()
    source_configs = await preset.load()
    existing = {c.name: c for c in await registry.load()}
    for cfg in source_configs:
        current = existing.get(cfg.name)
        if current is not None and current.model != cfg.model:
            raise ValueError(
                f"sync: refusing to change model for {cfg.name!r}"
                f" (stored {current.model!r} vs preset {cfg.model!r}) — a model bump"
                " must be a new entry name",
            )
    await registry.mirror(source_configs)
    await self._seed_secrets(source_configs)

learning

_LearningHook: the wrapper around a store backend.

Drives Optimizer bookkeeping (backoff counters, quality windows) from the live event stream, and periodically rebuilds derived state — quality-window verdicts, shared cooldowns, snapshot metrics, registry membership, and the admin disabled-verdict map — from one cached read of the journal tail. No second storage subsystem: everything llmbroker learns beyond config is re-derived from the append-only journal plus the tiny disabled map.

metrics_from_calls(rows)

rows newest-first: the first call row per model is its most recent.

Source code in src/llmbroker/broker/learning.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
def metrics_from_calls(rows: list[Call]) -> dict[str, LLMMetrics]:
    """rows newest-first: the first call row per model is its most recent."""
    metrics: dict[str, LLMMetrics] = {}
    for row in rows:
        if row.kind != "call":
            continue
        existing = metrics.get(row.llm_name)
        if existing is None:
            metrics[row.llm_name] = LLMMetrics(
                call_count=1,
                last_status=row.status,
                last_at=row.ts,
            )
        else:
            metrics[row.llm_name] = LLMMetrics(
                call_count=existing.call_count + 1,
                last_status=existing.last_status,
                last_at=existing.last_at,
            )
    return metrics
resolve_metrics_map(store) async

Derive a per-LLM metrics map from whatever the store can offer: a _LearningHook's cache, a queryable store's tail, or nothing.

Source code in src/llmbroker/broker/learning.py
55
56
57
58
59
60
61
62
63
async def resolve_metrics_map(store: StoreProtocol) -> dict[str, LLMMetrics]:
    """Derive a per-LLM metrics map from whatever the store can offer:
    a ``_LearningHook``'s cache, a queryable store's tail, or nothing."""
    if isinstance(store, _LearningHook):
        return store.metrics_cache
    if isinstance(store, QueryableStoreProtocol):
        rows = await store.calls(limit=_DEFAULT_QUALITY_REBUILD_LIMIT)
        return metrics_from_calls(rows)
    return {}

pool

LLMPool: live per-LLM slot state (config, key, cooldown, quality) backing routing.

LLMPool

The pool of LLM slots and their live cooldown / quality state.

Source code in src/llmbroker/broker/pool.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
class LLMPool:
    """The pool of LLM slots and their live cooldown / quality state."""

    def __init__(self, *, optimizer: Optimizer | None = None) -> None:
        self._slots: dict[str, _Slot] = {}
        self._cond = asyncio.Condition()
        self._next_order = 0
        self._optimizer = optimizer

    # ------------------------------------------------------------------
    # Membership / lookup
    # ------------------------------------------------------------------

    def __contains__(self, name: str) -> bool:
        return name in self._slots

    def __len__(self) -> int:
        return len(self._slots)

    @property
    def configs(self) -> dict[str, LLMConfig]:
        return {name: slot.config for name, slot in self._slots.items()}

    def config(self, name: str) -> LLMConfig:
        return self._slots[name].config

    def has_key(self, name: str) -> bool:
        slot = self._slots.get(name)
        return slot is not None and slot.key is not None

    def resolved_key(self, name: str) -> str:
        slot = self._slots[name]
        if slot.key is None:
            raise KeyError(name)
        return slot.key

    # ------------------------------------------------------------------
    # Membership mutation
    # ------------------------------------------------------------------

    async def add(self, cfg: LLMConfig, key: str | None, order: int | None = None) -> None:
        """Register/refresh a config. A ``None`` key leaves any prior key intact.

        Upserts in place so an existing slot's live state (cooldown, fail count,
        in-flight, disabled) survives a config refresh. ``order`` defaults to
        insertion order when the caller has no curated position to assert.
        """
        async with self._cond:
            resolved_order = order if order is not None else self._next_order
            self._next_order = max(self._next_order, resolved_order + 1)
            slot = self._slots.get(cfg.name)
            if slot is None:
                self._slots[cfg.name] = _Slot(config=cfg, key=key, order=resolved_order)
            else:
                slot.config = cfg
                slot.order = resolved_order
                if key is not None:
                    slot.key = key
            self._cond.notify_all()

    async def drop(self, name: str) -> None:
        """Remove a slot entirely, so a later re-add under the same name starts clean."""
        async with self._cond:
            self._slots.pop(name, None)
            self._cond.notify_all()

    # ------------------------------------------------------------------
    # Manual disable (hard exclusion)
    # ------------------------------------------------------------------

    def set_disabled(self, name: str) -> None:
        """Withdraw the slot. An in-flight call finishes normally; the flag excludes
        the slot from every acquisition afterward."""
        slot = self._slots.get(name)
        if slot is not None:
            slot.disabled = True

    async def clear_disabled(self, name: str) -> None:
        async with self._cond:
            slot = self._slots.get(name)
            if slot is not None:
                slot.disabled = False
            self._cond.notify_all()

    def is_disabled(self, name: str) -> bool:
        slot = self._slots.get(name)
        return slot is not None and slot.disabled

    # ------------------------------------------------------------------
    # Slot acquisition
    # ------------------------------------------------------------------

    def _is_demoted(self, name: str, operation: str | None) -> bool:
        return self._optimizer is not None and self._optimizer.is_demoted(name, operation)

    def demoted_operations(self, name: str) -> frozenset[str | None]:
        return (
            self._optimizer.demoted_operations(name) if self._optimizer is not None else frozenset()
        )

    def _wake_timeout(
        self,
        now: datetime,
        deadline: float | None,
        candidates: list[_Slot],
    ) -> float | None:
        """Seconds until the nearest event that could make a candidate available, or
        ``None`` when nothing is scheduled (wait solely on notification)."""
        wakeups: list[float] = []
        for slot in candidates:
            cap = slot.config.parallel
            if cap is not None and slot.in_flight >= cap:
                continue
            if slot.cooldown_until is not None and slot.cooldown_until > now:
                wakeups.append((slot.cooldown_until - now).total_seconds())
        if deadline is not None:
            wakeups.append(deadline - time.monotonic())
        return min(wakeups) if wakeups else None

    def _exhaustion_reason(self, exclude: frozenset[str]) -> str:
        if exclude & self._slots.keys():
            return "excluded"
        if not self._slots:
            return "empty_pool"
        if not any(slot.key is not None for slot in self._slots.values()):
            return "no_keys"
        return "all_disabled"

    def _raise_exhausted(self, exclude: frozenset[str]) -> None:
        reason = self._exhaustion_reason(exclude)
        message = {
            "excluded": "every candidate model was excluded for this request",
            "empty_pool": "the LLM pool has no slots",
            "no_keys": (
                "no LLM has a resolved api_key_ref — set at least one env var or configure"
                " a secrets backend"
            ),
            "all_disabled": "every LLM is administratively disabled",
        }[reason]
        raise NoLLMAvailableError(message, reason=reason)

    async def acquire(
        self,
        deadline: float | None,
        *,
        operation: str | None = None,
        exclude: frozenset[str] = frozenset(),
    ) -> LLMConfig:
        async with self._cond:
            while True:
                now = datetime.now(UTC)
                candidates = [
                    s
                    for s in self._slots.values()
                    if s.key is not None and not s.disabled and s.config.name not in exclude
                ]
                avail = [
                    s
                    for s in candidates
                    if (s.config.parallel is None or s.in_flight < s.config.parallel)
                    and (s.cooldown_until is None or s.cooldown_until <= now)
                ]
                if avail:
                    slot = min(
                        avail,
                        key=lambda s: (self._is_demoted(s.config.name, operation), s.order),
                    )
                    slot.in_flight += 1
                    return slot.config
                if not candidates:
                    self._raise_exhausted(exclude)
                if deadline is not None and time.monotonic() >= deadline:
                    cooling = [s.cooldown_until for s in candidates if s.cooldown_until is not None]
                    retry_at = min(cooling) if cooling else None
                    raise NoLLMAvailableError(
                        "no LLM slot came free within wait",
                        reason="timeout",
                        retry_at=retry_at,
                    )
                timeout = self._wake_timeout(now, deadline, candidates)
                try:
                    await asyncio.wait_for(self._cond.wait(), timeout)
                except TimeoutError:
                    # Re-check: a cooldown may have expired, or the deadline hit (next loop raises).
                    continue

    async def release(self, config: LLMConfig) -> None:
        """A missing name is legal (removed mid-flight) — no-op."""
        async with self._cond:
            slot = self._slots.get(config.name)
            if slot is not None:
                slot.in_flight = max(0, slot.in_flight - 1)
            self._cond.notify_all()

    # ------------------------------------------------------------------
    # Cooldown / state
    # ------------------------------------------------------------------

    def clear_cooling(self, name: str) -> None:
        slot = self._slots.get(name)
        if slot is not None:
            slot.cooldown_until = None

    async def cool_down(self, config: LLMConfig, delay: float) -> None:
        """Withdraw the slot for ``delay`` seconds."""
        cooldown_until = datetime.now(UTC) + timedelta(seconds=delay)
        async with self._cond:
            slot = self._slots.get(config.name)
            if slot is not None:
                slot.cooldown_until = cooldown_until
                slot.fail_count += 1
                slot.in_flight = max(0, slot.in_flight - 1)
            self._cond.notify_all()
        logger.warning("LLM %s cooling for %ds", config.name, delay)

    def state(self, name: str) -> LLMState:
        slot = self._slots.get(name)
        if slot is None:
            return LLMState()
        now = datetime.now(UTC)
        if slot.cooldown_until is not None and slot.cooldown_until > now:
            return LLMState(
                phase=LifecyclePhase.COOLING,
                cooldown_until=slot.cooldown_until,
                fail_count=slot.fail_count,
            )
        return LLMState(
            phase=LifecyclePhase.AVAILABLE,
            cooldown_until=None,
            fail_count=slot.fail_count,
        )

    async def apply_peer_cooldowns(
        self,
        cooldowns: dict[str, datetime],
        fail_counts: dict[str, int] | None = None,
    ) -> None:
        """Raise each named slot's ``cooldown_until`` to at least the given value.

        Called from the debounced journal rebuild — never lowers an already-later
        local cooldown, and never touches ``in_flight`` (nothing was acquired in
        this code path). The peer fail-streak folds in as ``max(local, peer)``.
        """
        fail_counts = fail_counts or {}
        async with self._cond:
            changed = False
            for name, until in cooldowns.items():
                slot = self._slots.get(name)
                if slot is None:
                    continue
                if slot.cooldown_until is None or until > slot.cooldown_until:
                    slot.cooldown_until = until
                    changed = True
            for name, count in fail_counts.items():
                slot = self._slots.get(name)
                if slot is not None and count > slot.fail_count:
                    slot.fail_count = count
            if changed:
                self._cond.notify_all()
add(cfg, key, order=None) async

Register/refresh a config. A None key leaves any prior key intact.

Upserts in place so an existing slot's live state (cooldown, fail count, in-flight, disabled) survives a config refresh. order defaults to insertion order when the caller has no curated position to assert.

Source code in src/llmbroker/broker/pool.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
async def add(self, cfg: LLMConfig, key: str | None, order: int | None = None) -> None:
    """Register/refresh a config. A ``None`` key leaves any prior key intact.

    Upserts in place so an existing slot's live state (cooldown, fail count,
    in-flight, disabled) survives a config refresh. ``order`` defaults to
    insertion order when the caller has no curated position to assert.
    """
    async with self._cond:
        resolved_order = order if order is not None else self._next_order
        self._next_order = max(self._next_order, resolved_order + 1)
        slot = self._slots.get(cfg.name)
        if slot is None:
            self._slots[cfg.name] = _Slot(config=cfg, key=key, order=resolved_order)
        else:
            slot.config = cfg
            slot.order = resolved_order
            if key is not None:
                slot.key = key
        self._cond.notify_all()
apply_peer_cooldowns(cooldowns, fail_counts=None) async

Raise each named slot's cooldown_until to at least the given value.

Called from the debounced journal rebuild — never lowers an already-later local cooldown, and never touches in_flight (nothing was acquired in this code path). The peer fail-streak folds in as max(local, peer).

Source code in src/llmbroker/broker/pool.py
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
async def apply_peer_cooldowns(
    self,
    cooldowns: dict[str, datetime],
    fail_counts: dict[str, int] | None = None,
) -> None:
    """Raise each named slot's ``cooldown_until`` to at least the given value.

    Called from the debounced journal rebuild — never lowers an already-later
    local cooldown, and never touches ``in_flight`` (nothing was acquired in
    this code path). The peer fail-streak folds in as ``max(local, peer)``.
    """
    fail_counts = fail_counts or {}
    async with self._cond:
        changed = False
        for name, until in cooldowns.items():
            slot = self._slots.get(name)
            if slot is None:
                continue
            if slot.cooldown_until is None or until > slot.cooldown_until:
                slot.cooldown_until = until
                changed = True
        for name, count in fail_counts.items():
            slot = self._slots.get(name)
            if slot is not None and count > slot.fail_count:
                slot.fail_count = count
        if changed:
            self._cond.notify_all()
cool_down(config, delay) async

Withdraw the slot for delay seconds.

Source code in src/llmbroker/broker/pool.py
232
233
234
235
236
237
238
239
240
241
242
async def cool_down(self, config: LLMConfig, delay: float) -> None:
    """Withdraw the slot for ``delay`` seconds."""
    cooldown_until = datetime.now(UTC) + timedelta(seconds=delay)
    async with self._cond:
        slot = self._slots.get(config.name)
        if slot is not None:
            slot.cooldown_until = cooldown_until
            slot.fail_count += 1
            slot.in_flight = max(0, slot.in_flight - 1)
        self._cond.notify_all()
    logger.warning("LLM %s cooling for %ds", config.name, delay)
drop(name) async

Remove a slot entirely, so a later re-add under the same name starts clean.

Source code in src/llmbroker/broker/pool.py
89
90
91
92
93
async def drop(self, name: str) -> None:
    """Remove a slot entirely, so a later re-add under the same name starts clean."""
    async with self._cond:
        self._slots.pop(name, None)
        self._cond.notify_all()
release(config) async

A missing name is legal (removed mid-flight) — no-op.

Source code in src/llmbroker/broker/pool.py
215
216
217
218
219
220
221
async def release(self, config: LLMConfig) -> None:
    """A missing name is legal (removed mid-flight) — no-op."""
    async with self._cond:
        slot = self._slots.get(config.name)
        if slot is not None:
            slot.in_flight = max(0, slot.in_flight - 1)
        self._cond.notify_all()
set_disabled(name)

Withdraw the slot. An in-flight call finishes normally; the flag excludes the slot from every acquisition afterward.

Source code in src/llmbroker/broker/pool.py
 99
100
101
102
103
104
def set_disabled(self, name: str) -> None:
    """Withdraw the slot. An in-flight call finishes normally; the flag excludes
    the slot from every acquisition afterward."""
    slot = self._slots.get(name)
    if slot is not None:
        slot.disabled = True

pool_view

PoolView: read-only views of the broker's current pool state.

PoolView

Live views over the pool: a single LLM handle, the count, a full snapshot.

Source code in src/llmbroker/broker/pool_view.py
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
class PoolView:
    """Live views over the pool: a single LLM handle, the count, a full snapshot."""

    def __init__(self, pool: LLMPool, store: StoreProtocol) -> None:
        self._pool = pool
        self._store = store

    def get(self, name: str) -> AsyncLLM:
        if name not in self._pool:
            raise KeyError(name)
        return AsyncLLM(name, self._pool.config(name), self._pool, self._store)

    def count(self) -> int:
        return len(self._pool)

    async def snapshot(self) -> Mapping[str, LLMSnapshot]:
        metrics_map = await resolve_metrics_map(self._store)
        result: dict[str, LLMSnapshot] = {}
        for name, cfg in self._pool.configs.items():
            result[name] = LLMSnapshot(
                config=cfg,
                disabled=self._pool.is_disabled(name),
                has_key=self._pool.has_key(name),
                cooldown_until=self._pool.state(name).cooldown_until,
                demoted_operations=tuple(self._pool.demoted_operations(name)),
                metrics=metrics_map.get(name),
            )
        return result

result

Per-call result handle and the live per-LLM view returned by the broker.

AsyncLLM

Handle returned by AsyncBroker.get(name) — live view into broker internals.

Source code in src/llmbroker/broker/result.py
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
class AsyncLLM:
    """Handle returned by ``AsyncBroker.get(name)`` — live view into broker internals."""

    def __init__(
        self,
        name: str,
        config: LLMConfig,
        pool: LLMPool,
        store: StoreProtocol,
    ) -> None:
        self._name = name
        self._config = config
        self._pool = pool
        self._store = store

    @property
    def config(self) -> LLMConfig:
        return self._config

    @property
    def disabled(self) -> bool:
        return self._pool.is_disabled(self._name)

    async def state(self) -> LLMState:
        return self._pool.state(self._name)

    async def metrics(self) -> LLMMetrics:
        all_metrics = await resolve_metrics_map(self._store)
        return all_metrics.get(self._name, LLMMetrics(0, None, None))
AsyncResult

Returned by AsyncBroker.ask()/chat().

Source code in src/llmbroker/broker/result.py
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
class AsyncResult:
    """Returned by AsyncBroker.ask()/chat()."""

    def __init__(  # noqa: PLR0913
        self,
        *,
        text: str,
        tool_calls: list[dict] | None,
        usage: Usage | None,
        call_id: str,
        llm_name: str,
        operation: str | None = None,
        store: StoreProtocol,
    ) -> None:
        self.text = text
        self.tool_calls = tool_calls
        self.usage = usage
        self._call_id = call_id
        self._llm_name = llm_name
        self._operation = operation
        self._store = store

    async def record_quality(self, score: float) -> None:
        await self._store.record_quality(
            self._llm_name,
            self._operation,
            score,
            call_id=self._call_id,
        )

router

Router: route one completion over the pool with per-LLM failover.

Acquires a free slot, calls the provider, and on a 429/503/401/403/5xx cools that LLM down (or drops it) and tries the next free one; a client-side 4xx (any other status in [400, 500)) fails over without cooling and, if every candidate is exhausted this way, surfaces the provider error to the caller. Every attempt is recorded to the journal.

Router

Routes a completion request over the pool, failing over between LLMs.

Source code in src/llmbroker/broker/router.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
class Router:
    """Routes a completion request over the pool, failing over between LLMs."""

    def __init__(
        self,
        pool: LLMPool,
        store: StoreProtocol,
        *,
        scope: str | None,
        optimizer: Optimizer | None = None,
    ) -> None:
        self._pool = pool
        self._store = store
        self._scope = scope
        self._optimizer = optimizer
        self._http: httpx.AsyncClient | None = None

    async def ask(
        self,
        prompt: str,
        *,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> AsyncResult:
        return await self.chat(
            [{"role": "user", "content": prompt}],
            operation=operation,
            trace_id=trace_id,
            wait=wait,
        )

    async def chat(
        self,
        messages: list[dict],
        *,
        tools: list[dict] | None = None,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> AsyncResult:
        deadline = None if wait is None else time.monotonic() + wait
        client_failed: set[str] = set()
        last_client_error: httpx.HTTPStatusError | None = None
        while True:
            try:
                config = await self._pool.acquire(
                    deadline,
                    operation=operation,
                    exclude=frozenset(client_failed),
                )
            except NoLLMAvailableError as exc:
                if exc.reason == "excluded" and last_client_error is not None:
                    raise last_client_error from None
                raise

            outcome = await self._attempt(
                config,
                messages,
                tools,
                operation=operation,
                trace_id=trace_id,
            )
            if isinstance(outcome, AsyncResult):
                return outcome
            if isinstance(outcome, _Failed):
                client_failed.add(config.name)
                if outcome.error is not None:
                    last_client_error = outcome.error
            # None or _Failed with no error ⇒ loop to the next free LLM.

    def _capped_wait(self, base: float, backoff: float) -> float:
        cap = self._optimizer.max_delay if self._optimizer else base
        return min(base * backoff, cap)

    async def _attempt(
        self,
        config: LLMConfig,
        messages: list[dict],
        tools: list[dict] | None,
        *,
        operation: str | None,
        trace_id: str | None,
    ) -> AsyncResult | _Failed | None:
        """Run one LLM. Return its result, ``None`` to try the next LLM after a
        cooldown, or ``_Failed`` to try the next LLM without cooling this one."""
        call_id = str(uuid.uuid4())
        t0 = time.monotonic()
        resolved_key = self._pool.resolved_key(config.name)

        async def record(  # noqa: PLR0913
            status: CallStatus,
            *,
            http_status: int | None = None,
            error_detail: str | None = None,
            usage: Usage | None = None,
            cooldown_delay: float | None = None,
        ) -> None:
            cooldown_until = (
                datetime.now(UTC) + timedelta(seconds=cooldown_delay)
                if cooldown_delay is not None
                else None
            )
            await self._log_call(
                Call(
                    id=call_id,
                    llm_name=config.name,
                    operation=operation,
                    trace_id=trace_id,
                    status=status,
                    ts=datetime.now(UTC),
                    http_status=http_status,
                    latency_ms=int((time.monotonic() - t0) * 1000),
                    error_detail=error_detail,
                    usage=usage,
                    scope=self._scope,
                    cooldown_until=cooldown_until,
                    key_hash=key_hash(resolved_key) if cooldown_delay is not None else None,
                ),
            )

        # Read before record() is awaited (which increments rl_fail_count via
        # the learning hook), so the first failure in a streak always sees exponent 0.
        fails_before = self._optimizer.rl_fail_count(config.name) if self._optimizer else 0
        backoff = self._optimizer.backoff_factor**fails_before if self._optimizer else 1.0

        if self._http is None:
            self._http = chat.make_client()

        try:
            content, tool_calls, usage = await call_provider(
                config,
                resolved_key,
                messages,
                tools,
                client=self._http,
            )
        except httpx.HTTPStatusError as exc:
            code = exc.response.status_code
            detail = exc.response.text[:300]
            if is_rate_limit(code):
                status = CallStatus.RATE_LIMITED if code == HTTP_429 else CallStatus.UNAVAILABLE
                base = retry_after_seconds(exc.response.headers, _DEFAULT_RATE_LIMIT_SEC)
                delay = self._capped_wait(base, backoff)
                await self._pool.cool_down(config, delay)
                await record(status, http_status=code, error_detail=detail, cooldown_delay=delay)
                return None
            if code in (HTTP_401, HTTP_403):
                delay = self._capped_wait(_DEFAULT_RATE_LIMIT_SEC, backoff)
                await self._pool.cool_down(config, delay)
                await record(
                    CallStatus.ERROR,
                    http_status=code,
                    error_detail=detail,
                    cooldown_delay=delay,
                )
                return _Failed(error=None)
            if _is_client_error(code):
                await self._pool.release(config)
                await record(CallStatus.ERROR, http_status=code, error_detail=detail)
                return _Failed(error=exc)
            delay = self._capped_wait(_DEFAULT_RATE_LIMIT_SEC, backoff)
            await self._pool.cool_down(config, delay)
            await record(
                CallStatus.ERROR,
                http_status=code,
                error_detail=detail,
                cooldown_delay=delay,
            )
            return None
        except (httpx.TimeoutException, httpx.ConnectError, OSError) as exc:
            delay = self._capped_wait(_DEFAULT_RATE_LIMIT_SEC, backoff)
            await self._pool.cool_down(config, delay)
            await record(CallStatus.ERROR, error_detail=type(exc).__name__, cooldown_delay=delay)
            return None

        await self._pool.release(config)
        self._pool.clear_cooling(config.name)
        await record(CallStatus.OK, http_status=200, usage=usage)
        return AsyncResult(
            text=content,
            tool_calls=tool_calls,
            usage=usage,
            call_id=call_id,
            llm_name=config.name,
            operation=operation,
            store=self._store,
        )

    async def _log_call(self, call: Call) -> None:
        try:
            await self._store.record(call)
        except Exception:  # noqa: BLE001
            logger.exception("llmbroker: store.record failed")

    async def aclose(self) -> None:
        if self._http is not None:
            await self._http.aclose()
            self._http = None

source

Dispatch a plain string/Path source to a registry/secrets/store triple.

Dispatch is dumb and explicit: .toml/.json -> file registry + env secrets (AsyncBroker falls back to the store/ sibling default); sqlite:// / .db / .sqlite -> sqlite, one file backing all three ports; postgresql:// / mongodb:// -> by scheme, one driver shared by all three ports. Anything else raises a clear error naming the accepted forms.

Each backend package is imported lazily here (never at module load) so a bare import llmbroker never pulls in a driver package.

resolve_source(source)

Returns (registry, secrets, store); a None store means "use the caller's own default" (the file-registry store/ sibling).

Source code in src/llmbroker/broker/source.py
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
def resolve_source(
    source: str | Path,
) -> tuple[RegistryProtocol, SecretsProtocol, StoreProtocol | None]:
    """Returns ``(registry, secrets, store)``; a ``None`` store means
    "use the caller's own default" (the file-registry ``store/`` sibling)."""
    source = str(source)
    if source.endswith(_FILE_SUFFIXES):
        return FileRegistry(source), EnvSecrets(), None

    sqlite_path = source.removeprefix("sqlite://")
    if source.startswith("sqlite://") or source.endswith(_SQLITE_SUFFIXES):
        try:
            from llmbroker.sqlite.driver import SqliteDriver  # noqa: PLC0415
        except ImportError as exc:
            raise ImportError(
                f"sqlite source {source!r} requires: pip install llmbroker[sqlite]",
            ) from exc
        driver = SqliteDriver(sqlite_path)
        return DriverRegistry(driver), DriverSecrets(driver), DriverStore(driver)

    if source.startswith("postgresql://"):
        try:
            from llmbroker.postgres.driver import PostgresDriver  # noqa: PLC0415
        except ImportError as exc:
            raise ImportError(
                f"postgres source {source!r} requires: pip install llmbroker[postgres]",
            ) from exc
        driver = PostgresDriver(dsn=source)
        return DriverRegistry(driver), DriverSecrets(driver), DriverStore(driver)

    if source.startswith("mongodb://"):
        try:
            from motor.motor_asyncio import AsyncIOMotorClient  # noqa: PLC0415
        except ImportError as exc:
            raise ImportError(
                f"mongodb source {source!r} requires: pip install llmbroker[mongodb]",
            ) from exc
        from llmbroker.mongodb.driver import MongoDriver  # noqa: PLC0415

        client = AsyncIOMotorClient(source)
        driver = MongoDriver(client.get_default_database(), client=client)
        return DriverRegistry(driver), DriverSecrets(driver), DriverStore(driver)

    raise ValueError(
        f"unrecognized registry source {source!r} — expected a .toml/.json file path,"
        " a sqlite path/URL (.db, .sqlite, sqlite://...), or a postgresql://... /"
        " mongodb://... URL",
    )

chat

OpenAI-compatible chat primitives and the host-agnostic tool-loop helpers.

Request building, response parsing and retry parsing live here once, adapted to the new LLMConfig (the resolved key is passed in, never read off the config). arun_tool_loop drives broker.chat() back-and-forth until a tool-call-free reply; run_tool_loop is its sync wrapper.

arun_tool_loop(llms, messages, *, tools=None, dispatch=None, max_steps=8, **chat_kwargs) async

Drive broker.chat until a tool-call-free reply; execute tools via dispatch.

Source code in src/llmbroker/chat.py
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
async def arun_tool_loop(
    llms,  # noqa: ANN001 - AsyncBroker (avoid import cycle)
    messages: list[dict],
    *,
    tools: list[dict] | None = None,
    dispatch: Mapping[str, Callable[..., object]] | None = None,
    max_steps: int = 8,
    **chat_kwargs,
) -> str:
    """Drive ``broker.chat`` until a tool-call-free reply; execute tools via dispatch."""
    convo = list(messages)
    dispatch = dispatch or {}
    for _ in range(max_steps):
        result = await llms.chat(convo, tools=tools, **chat_kwargs)
        text = _advance_tool_loop(convo, result, dispatch)
        if text is not None:
            return text
    return ""

build_chat_request(config, api_key, messages, tools=None)

Return (url, headers, json_body) for an OpenAI-compatible chat completion.

Source code in src/llmbroker/chat.py
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
def build_chat_request(
    config: LLMConfig,
    api_key: str,
    messages: list[dict],
    tools: list[dict] | None = None,
) -> tuple[str, dict[str, str], dict[str, Any]]:
    """Return (url, headers, json_body) for an OpenAI-compatible chat completion."""
    body: dict[str, Any] = {"model": config.model, "messages": messages}
    if tools:
        body["tools"] = tools
        body["tool_choice"] = "auto"
    return (
        f"{config.base_url}{_CHAT_PATH}",
        {"Authorization": f"Bearer {api_key}"},
        body,
    )

call_provider(config, api_key, messages, tools, *, client=None) async

POST an OpenAI-compatible completion and return (content, tool_calls, usage).

With client passed, it is reused as-is (never closed here — the caller owns its lifetime); with None, an ephemeral client is opened and closed for this single call.

Source code in src/llmbroker/chat.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
async def call_provider(
    config: LLMConfig,
    api_key: str,
    messages: list[dict],
    tools: list[dict] | None,
    *,
    client: httpx.AsyncClient | None = None,
) -> tuple[str, list[dict] | None, Usage | None]:
    """POST an OpenAI-compatible completion and return (content, tool_calls, usage).

    With ``client`` passed, it is reused as-is (never closed here — the caller
    owns its lifetime); with ``None``, an ephemeral client is opened and closed
    for this single call.
    """
    url, headers, body = build_chat_request(config, api_key, messages, tools)
    async with _resolve_client(client) as active:
        resp = await active.post(url, headers=headers, json=body)
        resp.raise_for_status()
        data = resp.json()
    message = message_from_response(data)
    content = str(message.get("content") or "")
    return content, parse_tool_calls(message), parse_usage(data)

execute_tool_calls(tool_calls, dispatch)

Run each tool call via dispatch; return the tool-result messages to append.

Source code in src/llmbroker/chat.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
def execute_tool_calls(
    tool_calls: list[dict],
    dispatch: Mapping[str, Callable[..., object]],
) -> list[dict]:
    """Run each tool call via dispatch; return the tool-result messages to append."""
    results: list[dict] = []
    for call in tool_calls:
        name = call["function"]["name"]
        try:
            args = json.loads(call["function"].get("arguments") or "{}")
        except json.JSONDecodeError:
            args = {}
        if not isinstance(args, dict):
            args = {}
        fn = dispatch.get(name)
        if fn is None:
            output: object = f"Unknown tool {name}"
        else:
            try:
                output = fn(**args)
            except Exception as exc:  # noqa: BLE001 - report back to the model so it can retry
                output = f"Tool {name} failed: {exc}"
        results.append({"role": "tool", "tool_call_id": call.get("id"), "content": str(output)})
    return results

message_from_response(data)

Extract the assistant message object from a chat-completion response body.

Source code in src/llmbroker/chat.py
64
65
66
def message_from_response(data: dict) -> dict:
    """Extract the assistant message object from a chat-completion response body."""
    return data["choices"][0]["message"]

parse_tool_calls(message)

Extract the raw tool_calls list from an assistant message, verbatim.

Source code in src/llmbroker/chat.py
128
129
130
131
132
133
def parse_tool_calls(message: dict) -> list[dict] | None:
    """Extract the raw ``tool_calls`` list from an assistant message, verbatim."""
    tool_calls = message.get("tool_calls")
    if not tool_calls:
        return None
    return tool_calls

parse_usage(data)

Extract token counts from a chat-completion response body, if present.

Source code in src/llmbroker/chat.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def parse_usage(data: dict) -> Usage | None:
    """Extract token counts from a chat-completion response body, if present."""
    raw = data.get("usage")
    if not isinstance(raw, dict):
        return None
    known = {"prompt_tokens", "completion_tokens", "total_tokens"}
    extra = {
        k: int(v)
        for k, v in raw.items()
        if k not in known and isinstance(v, (int, float)) and float(v) == int(v)
    }
    return Usage(
        prompt_tokens=raw.get("prompt_tokens"),
        completion_tokens=raw.get("completion_tokens"),
        total_tokens=raw.get("total_tokens"),
        extra=extra or None,
    )

retry_after_seconds(headers, default_sec)

Parse Retry-After as either delay-seconds or an HTTP-date, per RFC 9110.

Source code in src/llmbroker/chat.py
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
def retry_after_seconds(headers: Mapping[str, str], default_sec: int) -> int:
    """Parse ``Retry-After`` as either delay-seconds or an HTTP-date, per RFC 9110."""
    raw = headers.get("Retry-After")
    if raw is None:
        return default_sec
    try:
        return int(raw)
    except ValueError:
        pass
    try:
        when = parsedate_to_datetime(raw)
    except (TypeError, ValueError):
        return default_sec
    if when.tzinfo is None:
        when = when.replace(tzinfo=UTC)
    return max(0, int((when - datetime.now(UTC)).total_seconds()))

run_tool_loop(llms, messages, *, tools=None, dispatch=None, max_steps=8, **chat_kwargs)

Synchronous tool loop over a sync Broker.

Mirrors arun_tool_loop but calls the blocking Broker.chat; it does not use the async engine directly so it is safe to call from any thread.

Source code in src/llmbroker/chat.py
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def run_tool_loop(
    llms,  # noqa: ANN001 - sync Broker
    messages: list[dict],
    *,
    tools: list[dict] | None = None,
    dispatch: Mapping[str, Callable[..., object]] | None = None,
    max_steps: int = 8,
    **chat_kwargs,
) -> str:
    """Synchronous tool loop over a sync ``Broker``.

    Mirrors ``arun_tool_loop`` but calls the blocking ``Broker.chat``; it does
    not use the async engine directly so it is safe to call from any thread.
    """
    convo = list(messages)
    dispatch = dispatch or {}
    for _ in range(max_steps):
        result = llms.chat(convo, tools=tools, **chat_kwargs)
        text = _advance_tool_loop(convo, result, dispatch)
        if text is not None:
            return text
    return ""

cli

python -m llmbroker .

Subcommands: env (emit .env skeleton), preset (download curated preset TOML), sync (mirror a preset TOML into a DB registry — DB-init workflow).

exceptions

All exceptions a caller of llmbroker may catch, in one place.

LLMRequestError

Bases: Exception

Base: this request could not be completed.

Source code in src/llmbroker/exceptions.py
6
7
class LLMRequestError(Exception):
    """Base: this request could not be completed."""

NoLLMAvailableError

Bases: LLMRequestError

No LLM slot was available for this request.

reason is one of:

  • "empty_pool" — the pool has zero slots.
  • "no_keys" — slots exist but none has a resolved key.
  • "all_disabled" — keyed slots exist but every one is admin-disabled.
  • "excluded" — every candidate was excluded for this request (internal).
  • "timeout" — the deadline expired (or nothing is free right now); retry_at carries the earliest known return time, when known.
Source code in src/llmbroker/exceptions.py
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class NoLLMAvailableError(LLMRequestError):
    """No LLM slot was available for this request.

    ``reason`` is one of:

    - ``"empty_pool"`` — the pool has zero slots.
    - ``"no_keys"`` — slots exist but none has a resolved key.
    - ``"all_disabled"`` — keyed slots exist but every one is admin-disabled.
    - ``"excluded"`` — every candidate was excluded for this request (internal).
    - ``"timeout"`` — the deadline expired (or nothing is free right now);
      ``retry_at`` carries the earliest known return time, when known.
    """

    def __init__(
        self,
        message: str,
        *,
        reason: str,
        retry_at: datetime | None = None,
    ) -> None:
        super().__init__(message)
        self.reason = reason
        self.retry_at = retry_at

integrations

Host-side hooks for coexisting with third-party tools (e.g. Alembic).

These modules carry no backend logic and import no drivers; they are thin shims a host wires into another tool's configuration.

alembic

Alembic coexistence hook — autogenerate ignores every llmbroker_* object.

Wire into alembic/env.py::

import llmbroker.integrations.alembic
context.configure(
    ...,
    include_object=llmbroker.integrations.alembic.include_object,
)

Imports nothing from Alembic — it only inspects the object name.

models

DTOs, enums, and the shared resource-lifecycle protocol for llmbroker.

Pure data and the one cross-cutting capability protocol. No I/O, no driver imports — safe to import from anywhere in the package.

AsyncResourceProtocol

Bases: Protocol

Lifecycle capability for any backend that holds an open resource.

Orthogonal to a backend's data contract. aclose() is idempotent.

Source code in src/llmbroker/models.py
173
174
175
176
177
178
179
180
@runtime_checkable
class AsyncResourceProtocol(Protocol):
    """Lifecycle capability for any backend that holds an open resource.

    Orthogonal to a backend's data contract. ``aclose()`` is idempotent.
    """

    async def aclose(self) -> None: ...

Call dataclass

One append-only journal record: a call attempt (kind="call") or a self-contained quality rating (kind="quality"), interleaved in one stream.

A quality record fills only llm_name, operation, quality_score, ts, and optionally call_id (an opaque host-UI passthrough — never joined against the call row it rates); status is None exactly on quality records.

Source code in src/llmbroker/models.py
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
@dataclass(frozen=True, slots=True)
class Call:
    """One append-only journal record: a call attempt (``kind="call"``) or a
    self-contained quality rating (``kind="quality"``), interleaved in one stream.

    A quality record fills only ``llm_name``, ``operation``, ``quality_score``,
    ``ts``, and optionally ``call_id`` (an opaque host-UI passthrough — never
    joined against the call row it rates); ``status`` is ``None`` exactly on
    quality records.
    """

    id: str
    llm_name: str
    operation: str | None
    trace_id: str | None
    status: CallStatus | None
    kind: str = "call"
    ts: datetime | None = None
    http_status: int | None = None
    latency_ms: int | None = None
    error_detail: str | None = None
    usage: Usage | None = None
    quality_score: float | None = None
    call_id: str | None = None
    scope: str | None = None
    cooldown_until: datetime | None = None
    key_hash: str | None = None

KeyInfo dataclass

Per-provider onboarding metadata for one api_key_ref: a help blurb plus a free-form passthrough of whatever else the TOML [keys.REF] section holds — llmbroker has no taxonomy opinion on it.

Source code in src/llmbroker/models.py
30
31
32
33
34
35
36
37
38
@dataclass(frozen=True, slots=True)
class KeyInfo:
    """Per-provider onboarding metadata for one ``api_key_ref``: a help blurb plus
    a free-form passthrough of whatever else the TOML ``[keys.REF]`` section holds —
    llmbroker has no taxonomy opinion on it."""

    api_key_ref: str
    help: str
    extra: dict[str, str]

LLMConfig dataclass

Pure stored config for one LLM — no secret, safe to expose.

The registry is a pure mirror of the preset (see sync): nothing else writes it, so there is no provenance/curation marker to carry here.

Source code in src/llmbroker/models.py
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
@dataclass(frozen=True, slots=True)
class LLMConfig:
    """Pure stored config for one LLM — no secret, safe to expose.

    The registry is a pure mirror of the preset (see ``sync``): nothing else
    writes it, so there is no provenance/curation marker to carry here.
    """

    name: str
    base_url: str
    model: str
    api_key_ref: str
    parallel: int | None = None

    def to_metadata(self) -> dict[str, object]:
        """Structured optional config, serialized for the registry's JSON column.

        >>> LLMConfig(name="g", base_url="https://x/v1", model="m", api_key_ref="K").to_metadata()
        {}
        """
        metadata: dict[str, object] = {}
        if self.parallel is not None:
            metadata["parallel"] = self.parallel
        return metadata

    @classmethod
    def from_metadata(
        cls,
        *,
        name: str,
        base_url: str,
        model: str,
        api_key_ref: str,
        metadata: dict[str, object] | None,
    ) -> "LLMConfig":
        """Reconstruct from the core columns plus the JSON ``metadata`` blob."""
        metadata = metadata or {}
        raw_parallel = metadata.get("parallel")
        parallel = raw_parallel if isinstance(raw_parallel, int) else None
        return cls(
            name=name,
            base_url=base_url,
            model=model,
            api_key_ref=api_key_ref,
            parallel=parallel,
        )
from_metadata(*, name, base_url, model, api_key_ref, metadata) classmethod

Reconstruct from the core columns plus the JSON metadata blob.

Source code in src/llmbroker/models.py
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
@classmethod
def from_metadata(
    cls,
    *,
    name: str,
    base_url: str,
    model: str,
    api_key_ref: str,
    metadata: dict[str, object] | None,
) -> "LLMConfig":
    """Reconstruct from the core columns plus the JSON ``metadata`` blob."""
    metadata = metadata or {}
    raw_parallel = metadata.get("parallel")
    parallel = raw_parallel if isinstance(raw_parallel, int) else None
    return cls(
        name=name,
        base_url=base_url,
        model=model,
        api_key_ref=api_key_ref,
        parallel=parallel,
    )
to_metadata()

Structured optional config, serialized for the registry's JSON column.

LLMConfig(name="g", base_url="https://x/v1", model="m", api_key_ref="K").to_metadata() {}

Source code in src/llmbroker/models.py
55
56
57
58
59
60
61
62
63
64
def to_metadata(self) -> dict[str, object]:
    """Structured optional config, serialized for the registry's JSON column.

    >>> LLMConfig(name="g", base_url="https://x/v1", model="m", api_key_ref="K").to_metadata()
    {}
    """
    metadata: dict[str, object] = {}
    if self.parallel is not None:
        metadata["parallel"] = self.parallel
    return metadata

LLMMetrics dataclass

Per-LLM admin read-model derived from Call rows.

Source code in src/llmbroker/models.py
147
148
149
150
151
152
153
@dataclass(frozen=True, slots=True)
class LLMMetrics:
    """Per-LLM admin read-model derived from Call rows."""

    call_count: int
    last_status: CallStatus | None
    last_at: datetime | None

LLMSnapshot dataclass

Frozen point-in-time materialization of one LLM: raw facts, no status enum or precedence rule — the host derives whatever presentation it wants.

demoted_operations may contain None: the bucket for calls made without an operation= label.

Source code in src/llmbroker/models.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
@dataclass(frozen=True, slots=True)
class LLMSnapshot:
    """Frozen point-in-time materialization of one LLM: raw facts, no status enum
    or precedence rule — the host derives whatever presentation it wants.

    ``demoted_operations`` may contain ``None``: the bucket for calls made without
    an ``operation=`` label.
    """

    config: LLMConfig
    disabled: bool
    has_key: bool
    cooldown_until: datetime | None
    demoted_operations: tuple[str | None, ...]
    metrics: LLMMetrics | None

LLMState dataclass

Snapshot of one LLM's live runtime state, built fresh on each read.

Source code in src/llmbroker/models.py
21
22
23
24
25
26
27
@dataclass(frozen=True, slots=True)
class LLMState:
    """Snapshot of one LLM's live runtime state, built fresh on each read."""

    phase: LifecyclePhase = LifecyclePhase.AVAILABLE
    cooldown_until: datetime | None = None
    fail_count: int = 0

LifecyclePhase

Bases: Enum

The FSM label for one LLM's lifecycle, always derived from cooldown_until vs now.

Source code in src/llmbroker/models.py
14
15
16
17
18
class LifecyclePhase(Enum):
    """The FSM label for one LLM's lifecycle, always derived from cooldown_until vs now."""

    AVAILABLE = "available"
    COOLING = "cooling"

Usage dataclass

Resource use the provider reported for one call.

Source code in src/llmbroker/models.py
 96
 97
 98
 99
100
101
102
103
@dataclass(frozen=True, slots=True)
class Usage:
    """Resource use the provider reported for one call."""

    prompt_tokens: int | None = None
    completion_tokens: int | None = None
    total_tokens: int | None = None
    extra: dict[str, int] | None = None

key_hash(secret)

Short digest of a resolved key value — the quota-scope identity for shared cooldowns and dead-key drops (never the key itself).

key_hash("sk-abc") == key_hash("sk-abc") True len(key_hash("sk-abc")) 12

Source code in src/llmbroker/models.py
135
136
137
138
139
140
141
142
143
144
def key_hash(secret: str) -> str:
    """Short digest of a resolved key value — the quota-scope identity for shared
    cooldowns and dead-key drops (never the key itself).

    >>> key_hash("sk-abc") == key_hash("sk-abc")
    True
    >>> len(key_hash("sk-abc"))
    12
    """
    return hashlib.sha256(secret.encode()).hexdigest()[:12]

mongodb

MongoDB backend: registry, store, and secrets.

Needs the motor driver (llmbroker[mongodb]); importing this package is how a host declares that dependency, so a bare import llmbroker stays driver-free. All collections are llmbroker_-prefixed and owned by ensure_schema.

Registry

Bases: DriverRegistry

MongoDB-backed mutable registry over llmbroker_registry — a pure preset mirror.

Source code in src/llmbroker/mongodb/registry.py
 9
10
11
12
13
class Registry(DriverRegistry):
    """MongoDB-backed mutable registry over ``llmbroker_registry`` — a pure preset mirror."""

    def __init__(self, db: AsyncIOMotorDatabase) -> None:
        super().__init__(MongoDriver(db))

Secrets

Bases: DriverSecrets

MongoDB-backed mutable secrets store over llmbroker_secrets.

Source code in src/llmbroker/mongodb/secrets.py
 9
10
11
12
13
class Secrets(DriverSecrets):
    """MongoDB-backed mutable secrets store over ``llmbroker_secrets``."""

    def __init__(self, db: AsyncIOMotorDatabase) -> None:
        super().__init__(MongoDriver(db))

Store

Bases: DriverStore

MongoDB-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Source code in src/llmbroker/mongodb/store.py
14
15
16
17
18
19
20
21
22
23
24
class Store(DriverStore):
    """MongoDB-backed queryable store over ``llmbroker_calls`` + the
    ``llmbroker_disabled`` admin verdict map."""

    def __init__(
        self,
        db: AsyncIOMotorDatabase,
        *,
        retention: timedelta = _DEFAULT_RETENTION,
    ) -> None:
        super().__init__(MongoDriver(db), retention=retention)

driver

MongoDB Driver: renders indexes from backends.spec.TABLES.

One known installation, upgraded manually — on a version-marker mismatch ensure_schema fails fast instead of attempting an in-place migration.

MongoDriver

One Mongo database backing every llmbroker_* collection.

Source code in src/llmbroker/mongodb/driver.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
class MongoDriver:
    """One Mongo database backing every ``llmbroker_*`` collection."""

    def __init__(
        self,
        db: AsyncIOMotorDatabase,
        *,
        client: AsyncIOMotorClient | None = None,
    ) -> None:
        """Pass ``client=`` only when the driver should own (and close) it — the
        explicit-``db`` facade use case does not own the caller's client."""
        self._db = db
        self._client = client
        self._schema_ready = False
        self._schema_lock = asyncio.Lock()

    async def ensure_schema(self) -> None:
        """A plain per-instance flag: one driver per stack is app-lifetime, so this
        gives one check per process under intended usage. Not ``id(db)``-keyed —
        after GC an id can be reused by a different database and falsely skip creation."""
        if self._schema_ready:
            return
        async with self._schema_lock:
            if self._schema_ready:
                return
            version_doc = await self._db["llmbroker_schema_version"].find_one({})
            current = int(version_doc["version"]) if version_doc else 0
            if current not in (0, SCHEMA_VERSION):
                raise RuntimeError(
                    f"llmbroker schema version {current} found, this release expects"
                    f" {SCHEMA_VERSION} — drop the llmbroker_* collections and restart"
                    " (export registry/secrets/calls first if you need them)",
                )
            for spec in TABLES.values():
                if spec.key:
                    await self._db[spec.name].create_index(
                        [(k, 1) for k in spec.key],
                        unique=True,
                        name=f"{spec.name}_unique",
                    )
                for idx_cols in spec.indexes:
                    await self._db[spec.name].create_index(
                        [(k, 1) for k in idx_cols],
                        name=f"{spec.name}_idx_{'_'.join(idx_cols)}",
                    )
            if current == 0:
                await self._db["llmbroker_schema_version"].replace_one(
                    {},
                    {"version": SCHEMA_VERSION},
                    upsert=True,
                )
            self._schema_ready = True

    async def fetch(self, table: str) -> list[Row]:
        spec = TABLES[table]
        await self.ensure_schema()
        sort = [(k, 1) for k in spec.key] if spec.key else None
        cursor = self._db[spec.name].find({})
        if sort:
            cursor = cursor.sort(sort)
        docs = await cursor.to_list(length=None)
        return [_decode_doc(d, spec) for d in docs]

    async def get(self, table: str, key: Key) -> Row | None:
        spec = TABLES[table]
        await self.ensure_schema()
        doc = await self._db[spec.name].find_one(_key_filter(spec, key))
        return _decode_doc(doc, spec) if doc is not None else None

    async def upsert(self, table: str, key: Key, row: Row) -> None:
        spec = TABLES[table]
        await self.ensure_schema()
        await self._db[spec.name].update_one(
            _key_filter(spec, key),
            {"$set": row},
            upsert=True,
        )

    async def delete(self, table: str, key: Key) -> bool:
        spec = TABLES[table]
        await self.ensure_schema()
        result = await self._db[spec.name].delete_one(_key_filter(spec, key))
        return result.deleted_count > 0

    async def append(self, table: str, row: Row) -> None:
        spec = TABLES[table]
        await self.ensure_schema()
        await self._db[spec.name].insert_one(dict(row))

    async def recent(self, table: str, limit: int, match: Row | None = None) -> list[Row]:
        spec = TABLES[table]
        await self.ensure_schema()
        query: dict[str, object] = dict(match) if match else {}
        cursor = self._db[spec.name].find(query).sort("called_at", -1).limit(limit)
        docs = await cursor.to_list(length=None)
        return [_decode_doc(d, spec) for d in docs]

    async def purge(self, table: str, before: datetime) -> int:
        spec = TABLES[table]
        await self.ensure_schema()
        result = await self._db[spec.name].delete_many({"called_at": {"$lt": before}})
        return int(result.deleted_count)

    async def aclose(self) -> None:
        if self._client is not None:
            client, self._client = self._client, None
            client.close()
__init__(db, *, client=None)

Pass client= only when the driver should own (and close) it — the explicit-db facade use case does not own the caller's client.

Source code in src/llmbroker/mongodb/driver.py
41
42
43
44
45
46
47
48
49
50
51
52
def __init__(
    self,
    db: AsyncIOMotorDatabase,
    *,
    client: AsyncIOMotorClient | None = None,
) -> None:
    """Pass ``client=`` only when the driver should own (and close) it — the
    explicit-``db`` facade use case does not own the caller's client."""
    self._db = db
    self._client = client
    self._schema_ready = False
    self._schema_lock = asyncio.Lock()
ensure_schema() async

A plain per-instance flag: one driver per stack is app-lifetime, so this gives one check per process under intended usage. Not id(db)-keyed — after GC an id can be reused by a different database and falsely skip creation.

Source code in src/llmbroker/mongodb/driver.py
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
async def ensure_schema(self) -> None:
    """A plain per-instance flag: one driver per stack is app-lifetime, so this
    gives one check per process under intended usage. Not ``id(db)``-keyed —
    after GC an id can be reused by a different database and falsely skip creation."""
    if self._schema_ready:
        return
    async with self._schema_lock:
        if self._schema_ready:
            return
        version_doc = await self._db["llmbroker_schema_version"].find_one({})
        current = int(version_doc["version"]) if version_doc else 0
        if current not in (0, SCHEMA_VERSION):
            raise RuntimeError(
                f"llmbroker schema version {current} found, this release expects"
                f" {SCHEMA_VERSION} — drop the llmbroker_* collections and restart"
                " (export registry/secrets/calls first if you need them)",
            )
        for spec in TABLES.values():
            if spec.key:
                await self._db[spec.name].create_index(
                    [(k, 1) for k in spec.key],
                    unique=True,
                    name=f"{spec.name}_unique",
                )
            for idx_cols in spec.indexes:
                await self._db[spec.name].create_index(
                    [(k, 1) for k in idx_cols],
                    name=f"{spec.name}_idx_{'_'.join(idx_cols)}",
                )
        if current == 0:
            await self._db["llmbroker_schema_version"].replace_one(
                {},
                {"version": SCHEMA_VERSION},
                upsert=True,
            )
        self._schema_ready = True

registry

MongoDB-backed mutable registry over llmbroker_registry — a pure preset mirror.

Registry

Bases: DriverRegistry

MongoDB-backed mutable registry over llmbroker_registry — a pure preset mirror.

Source code in src/llmbroker/mongodb/registry.py
 9
10
11
12
13
class Registry(DriverRegistry):
    """MongoDB-backed mutable registry over ``llmbroker_registry`` — a pure preset mirror."""

    def __init__(self, db: AsyncIOMotorDatabase) -> None:
        super().__init__(MongoDriver(db))

secrets

MongoDB-backed mutable secrets store over llmbroker_secrets.

Secrets

Bases: DriverSecrets

MongoDB-backed mutable secrets store over llmbroker_secrets.

Source code in src/llmbroker/mongodb/secrets.py
 9
10
11
12
13
class Secrets(DriverSecrets):
    """MongoDB-backed mutable secrets store over ``llmbroker_secrets``."""

    def __init__(self, db: AsyncIOMotorDatabase) -> None:
        super().__init__(MongoDriver(db))

store

MongoDB-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Store

Bases: DriverStore

MongoDB-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Source code in src/llmbroker/mongodb/store.py
14
15
16
17
18
19
20
21
22
23
24
class Store(DriverStore):
    """MongoDB-backed queryable store over ``llmbroker_calls`` + the
    ``llmbroker_disabled`` admin verdict map."""

    def __init__(
        self,
        db: AsyncIOMotorDatabase,
        *,
        retention: timedelta = _DEFAULT_RETENTION,
    ) -> None:
        super().__init__(MongoDriver(db), retention=retention)

optimizer

The Optimizer knob — per-LLM failure bookkeeping, and per-(model, operation) quality-window demotion verdicts feeding the pool's demoted-last selection order.

Optimizer dataclass

Consecutive-failure counter for backoff, plus per-(model, operation) sliding windows of raw quality ratings backing the demoted-last selection order.

Source code in src/llmbroker/optimizer.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
@dataclass
class Optimizer:
    """Consecutive-failure counter for backoff, plus per-(model, operation) sliding
    windows of raw quality ratings backing the demoted-last selection order."""

    max_delay: float = 3600.0
    backoff_factor: float = 2.0
    quality_floor: float = 0.3
    quality_confidence: float = 0.95  # z for the Wilson upper bound
    quality_window: int = 30  # ratings kept per (model, operation)
    quality_min_count: int = 10  # verdicts need at least this many

    _rl_fail_count: dict[str, int] = field(default_factory=dict, init=False, repr=False)
    _scores: dict[tuple[str, str | None], deque] = field(
        default_factory=dict,
        init=False,
        repr=False,
    )

    def rl_fail_count(self, llm_name: str) -> int:
        return self._rl_fail_count.get(llm_name, 0)

    def on_rate_limited(self, llm_name: str) -> None:
        """Increment the consecutive-failure count the router reads for its backoff exponent."""
        self._rl_fail_count[llm_name] = self._rl_fail_count.get(llm_name, 0) + 1

    def on_success(self, llm_name: str) -> None:
        self._rl_fail_count[llm_name] = 0

    # ------------------------------------------------------------------
    # Quality windows + derived per-operation demotion verdicts
    # ------------------------------------------------------------------

    def wilson_bound(self, llm_name: str, operation: str | None) -> float | None:
        """The Wilson-score upper bound backing ``is_demoted``, for diagnostics."""
        window = self._scores.get((llm_name, operation))
        if not window:
            return None
        return wilson_upper(list(window), _z_score(self.quality_confidence))

    def is_demoted(self, llm_name: str, operation: str | None) -> bool:
        """True iff the window holds at least ``quality_min_count`` ratings and their
        Wilson-score upper bound sits below ``quality_floor``."""
        window = self._scores.get((llm_name, operation))
        if window is None or len(window) < self.quality_min_count:
            return False
        bound = wilson_upper(list(window), _z_score(self.quality_confidence))
        return bound < self.quality_floor

    def demoted_operations(self, llm_name: str) -> frozenset[str | None]:
        return frozenset(
            operation
            for name, operation in self._scores
            if name == llm_name and self.is_demoted(name, operation)
        )

    def record_quality(self, llm_name: str, operation: str | None, score: float) -> None:
        """Fold one rating into the (model, operation) window, oldest evicted first."""
        before = self.is_demoted(llm_name, operation)
        window = self._scores.setdefault(
            (llm_name, operation),
            deque(maxlen=self.quality_window),
        )
        window.append(score)
        self._log_flip(llm_name, operation, before, self.is_demoted(llm_name, operation))

    def load_scores(self, scores: dict[tuple[str, str | None], list[float]]) -> None:
        """Replace every window wholesale — used by the journal rebuild."""
        keys = set(self._scores) | set(scores)
        before = {key: self.is_demoted(*key) for key in keys}
        self._scores = {
            key: deque(values[-self.quality_window :], maxlen=self.quality_window)
            for key, values in scores.items()
        }
        for key in keys:
            self._log_flip(key[0], key[1], before[key], self.is_demoted(*key))

    def _log_flip(
        self,
        llm_name: str,
        operation: str | None,
        before: bool,  # noqa: FBT001
        after: bool,  # noqa: FBT001
    ) -> None:
        if before == after:
            return
        bound = self.wilson_bound(llm_name, operation)
        if after:
            if bound is not None:
                logger.warning(
                    "%s: quality-demoted for operation=%r (wilson upper %.3f < floor %.2f)",
                    llm_name,
                    operation,
                    bound,
                    self.quality_floor,
                )
            else:
                logger.warning("%s: quality-demoted for operation=%r", llm_name, operation)
        elif bound is not None:
            logger.info(
                "%s: quality demotion cleared for operation=%r (wilson upper %.3f)",
                llm_name,
                operation,
                bound,
            )
        else:
            logger.info("%s: quality demotion cleared for operation=%r", llm_name, operation)
is_demoted(llm_name, operation)

True iff the window holds at least quality_min_count ratings and their Wilson-score upper bound sits below quality_floor.

Source code in src/llmbroker/optimizer.py
72
73
74
75
76
77
78
79
def is_demoted(self, llm_name: str, operation: str | None) -> bool:
    """True iff the window holds at least ``quality_min_count`` ratings and their
    Wilson-score upper bound sits below ``quality_floor``."""
    window = self._scores.get((llm_name, operation))
    if window is None or len(window) < self.quality_min_count:
        return False
    bound = wilson_upper(list(window), _z_score(self.quality_confidence))
    return bound < self.quality_floor
load_scores(scores)

Replace every window wholesale — used by the journal rebuild.

Source code in src/llmbroker/optimizer.py
 98
 99
100
101
102
103
104
105
106
107
def load_scores(self, scores: dict[tuple[str, str | None], list[float]]) -> None:
    """Replace every window wholesale — used by the journal rebuild."""
    keys = set(self._scores) | set(scores)
    before = {key: self.is_demoted(*key) for key in keys}
    self._scores = {
        key: deque(values[-self.quality_window :], maxlen=self.quality_window)
        for key, values in scores.items()
    }
    for key in keys:
        self._log_flip(key[0], key[1], before[key], self.is_demoted(*key))
on_rate_limited(llm_name)

Increment the consecutive-failure count the router reads for its backoff exponent.

Source code in src/llmbroker/optimizer.py
54
55
56
def on_rate_limited(self, llm_name: str) -> None:
    """Increment the consecutive-failure count the router reads for its backoff exponent."""
    self._rl_fail_count[llm_name] = self._rl_fail_count.get(llm_name, 0) + 1
record_quality(llm_name, operation, score)

Fold one rating into the (model, operation) window, oldest evicted first.

Source code in src/llmbroker/optimizer.py
88
89
90
91
92
93
94
95
96
def record_quality(self, llm_name: str, operation: str | None, score: float) -> None:
    """Fold one rating into the (model, operation) window, oldest evicted first."""
    before = self.is_demoted(llm_name, operation)
    window = self._scores.setdefault(
        (llm_name, operation),
        deque(maxlen=self.quality_window),
    )
    window.append(score)
    self._log_flip(llm_name, operation, before, self.is_demoted(llm_name, operation))
wilson_bound(llm_name, operation)

The Wilson-score upper bound backing is_demoted, for diagnostics.

Source code in src/llmbroker/optimizer.py
65
66
67
68
69
70
def wilson_bound(self, llm_name: str, operation: str | None) -> float | None:
    """The Wilson-score upper bound backing ``is_demoted``, for diagnostics."""
    window = self._scores.get((llm_name, operation))
    if not window:
        return None
    return wilson_upper(list(window), _z_score(self.quality_confidence))

wilson_upper(scores, z)

Wilson-score upper bound of the mean of scores at confidence z.

round(wilson_upper([1.0] * 25 + [0.0] * 5, 1.96), 4) 0.9266

Source code in src/llmbroker/optimizer.py
17
18
19
20
21
22
23
24
25
26
27
28
29
def wilson_upper(scores: list[float], z: float) -> float:
    """Wilson-score upper bound of the mean of ``scores`` at confidence ``z``.

    >>> round(wilson_upper([1.0] * 25 + [0.0] * 5, 1.96), 4)
    0.9266
    """
    n = len(scores)
    p = sum(scores) / n
    z2 = z * z
    center = p + z2 / (2 * n)
    margin = z * ((p * (1 - p) / n) + z2 / (4 * n * n)) ** 0.5
    denom = 1 + z2 / n
    return (center + margin) / denom

postgres

Postgres backend: registry, store, and secrets.

Needs the asyncpg driver (llmbroker[postgres]); importing this package is how a host declares that dependency, so a bare import llmbroker stays driver-free. All tables are llmbroker_-prefixed and owned by ensure_schema.

Registry

Bases: DriverRegistry

Postgres-backed mutable registry over llmbroker_registry — a pure preset mirror.

Source code in src/llmbroker/postgres/registry.py
 9
10
11
12
13
class Registry(DriverRegistry):
    """Postgres-backed mutable registry over ``llmbroker_registry`` — a pure preset mirror."""

    def __init__(self, pool: asyncpg.Pool) -> None:
        super().__init__(PostgresDriver(pool))

Secrets

Bases: DriverSecrets

Postgres-backed mutable secrets store over llmbroker_secrets.

Source code in src/llmbroker/postgres/secrets.py
 9
10
11
12
13
class Secrets(DriverSecrets):
    """Postgres-backed mutable secrets store over ``llmbroker_secrets``."""

    def __init__(self, pool: asyncpg.Pool) -> None:
        super().__init__(PostgresDriver(pool))

Store

Bases: DriverStore

Postgres-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Source code in src/llmbroker/postgres/store.py
14
15
16
17
18
19
class Store(DriverStore):
    """Postgres-backed queryable store over ``llmbroker_calls`` + the
    ``llmbroker_disabled`` admin verdict map."""

    def __init__(self, pool: asyncpg.Pool, *, retention: timedelta = _DEFAULT_RETENTION) -> None:
        super().__init__(PostgresDriver(pool), retention=retention)

driver

Postgres Driver: renders DDL and DML from backends.spec.TABLES.

The caller owns the pool (pool = await asyncpg.create_pool(dsn)); aclose() is a no-op. One known installation, upgraded manually — on a fresh database the schema is created and stamped; on a version-marker mismatch ensure_schema fails fast instead of attempting an in-place migration.

PostgresDriver

One asyncpg pool backing every llmbroker_* table.

Pass a pre-built pool when the caller owns it (aclose() stays a no-op, unchanged contract). Pass dsn= instead to let the driver create and own its own pool lazily on first use (needed because pool creation is async but this constructor is not) — aclose() then closes it.

Source code in src/llmbroker/postgres/driver.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
class PostgresDriver:
    """One asyncpg pool backing every ``llmbroker_*`` table.

    Pass a pre-built ``pool`` when the caller owns it (``aclose()`` stays a
    no-op, unchanged contract). Pass ``dsn=`` instead to let the driver create
    and own its own pool lazily on first use (needed because pool creation is
    async but this constructor is not) — ``aclose()`` then closes it.
    """

    def __init__(self, pool: asyncpg.Pool | None = None, *, dsn: str | None = None) -> None:
        if pool is None and dsn is None:
            raise ValueError("PostgresDriver requires either pool= or dsn=")
        self._pool = pool
        self._dsn = dsn
        self._owns_pool = pool is None
        self._schema_ready = False
        self._schema_lock = asyncio.Lock()

    async def ensure_schema(self) -> None:
        """A plain per-instance flag: one driver per stack is app-lifetime, so this
        gives one check per process under intended usage. Not ``id(pool)``-keyed —
        after GC an id can be reused by a different pool and falsely skip creation."""
        if self._schema_ready:
            return
        async with self._schema_lock:
            if self._schema_ready:
                return
            if self._pool is None:
                self._pool = await asyncpg.create_pool(self._dsn)
            async with self._pool.acquire() as conn, conn.transaction():
                await _apply_ddl(conn)
            self._schema_ready = True

    async def fetch(self, table: str) -> list[Row]:
        spec = TABLES[table]
        await self.ensure_schema()
        cols = ", ".join(spec.columns)
        order = ", ".join(spec.key) if spec.key else cols
        async with self._pool.acquire() as conn:
            records = await conn.fetch(f"SELECT {cols} FROM {spec.name} ORDER BY {order}")  # noqa: S608
        return [_decode_row(r, spec) for r in records]

    async def get(self, table: str, key: Key) -> Row | None:
        spec = TABLES[table]
        await self.ensure_schema()
        cols = ", ".join(spec.columns)
        where = " AND ".join(f"{k} = ${i}" for i, k in enumerate(spec.key, start=1))
        async with self._pool.acquire() as conn:
            record = await conn.fetchrow(f"SELECT {cols} FROM {spec.name} WHERE {where}", *key)  # noqa: S608
        return _decode_row(record, spec) if record is not None else None

    async def upsert(self, table: str, key: Key, row: Row) -> None:  # noqa: ARG002
        """``key`` is part of the Driver contract but unused here: the row already
        carries its own key-column values, which ON CONFLICT matches against."""
        spec = TABLES[table]
        await self.ensure_schema()
        encoded = _encode_row(row, spec)
        cols = list(spec.columns)
        placeholders = ", ".join(_placeholder(i, spec.columns[c]) for i, c in enumerate(cols, 1))
        conflict = ", ".join(spec.key)
        updates = ", ".join(f"{c} = EXCLUDED.{c}" for c in cols if c not in spec.key)
        # Identifiers below come from the fixed internal TableSpec, not user input.
        query = (
            f"INSERT INTO {spec.name} ({', '.join(cols)}) VALUES ({placeholders})"  # noqa: S608
            f" ON CONFLICT ({conflict}) DO UPDATE SET {updates}"
        )
        async with self._pool.acquire() as conn:
            await conn.execute(query, *(encoded[c] for c in cols))

    async def delete(self, table: str, key: Key) -> bool:
        spec = TABLES[table]
        await self.ensure_schema()
        where = " AND ".join(f"{k} = ${i}" for i, k in enumerate(spec.key, start=1))
        async with self._pool.acquire() as conn:
            result = await conn.execute(f"DELETE FROM {spec.name} WHERE {where}", *key)  # noqa: S608
        return result.split()[-1] != "0"

    async def append(self, table: str, row: Row) -> None:
        spec = TABLES[table]
        await self.ensure_schema()
        encoded = _encode_row(row, spec)
        cols = list(spec.columns)
        placeholders = ", ".join(_placeholder(i, spec.columns[c]) for i, c in enumerate(cols, 1))
        query = f"INSERT INTO {spec.name} ({', '.join(cols)}) VALUES ({placeholders})"  # noqa: S608
        async with self._pool.acquire() as conn:
            await conn.execute(query, *(encoded[c] for c in cols))

    async def recent(self, table: str, limit: int, match: Row | None = None) -> list[Row]:
        spec = TABLES[table]
        await self.ensure_schema()
        cols = ", ".join(spec.columns)
        params: list[object] = []
        where = ""
        if match:
            conditions = []
            for k, v in match.items():
                if v is None:
                    conditions.append(f"{k} IS NULL")
                else:
                    params.append(v)
                    conditions.append(f"{k} = ${len(params)}")
            where = " WHERE " + " AND ".join(conditions)
        params.append(limit)
        async with self._pool.acquire() as conn:
            records = await conn.fetch(
                f"SELECT {cols} FROM {spec.name}{where}"  # noqa: S608
                f" ORDER BY called_at DESC LIMIT ${len(params)}",
                *params,
            )
        return [_decode_row(r, spec) for r in records]

    async def purge(self, table: str, before: datetime) -> int:
        spec = TABLES[table]
        await self.ensure_schema()
        async with self._pool.acquire() as conn:
            result = await conn.execute(f"DELETE FROM {spec.name} WHERE called_at < $1", before)  # noqa: S608
        return int(result.split()[-1])

    async def aclose(self) -> None:
        if self._owns_pool and self._pool is not None:
            pool, self._pool = self._pool, None
            await pool.close()
ensure_schema() async

A plain per-instance flag: one driver per stack is app-lifetime, so this gives one check per process under intended usage. Not id(pool)-keyed — after GC an id can be reused by a different pool and falsely skip creation.

Source code in src/llmbroker/postgres/driver.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
async def ensure_schema(self) -> None:
    """A plain per-instance flag: one driver per stack is app-lifetime, so this
    gives one check per process under intended usage. Not ``id(pool)``-keyed —
    after GC an id can be reused by a different pool and falsely skip creation."""
    if self._schema_ready:
        return
    async with self._schema_lock:
        if self._schema_ready:
            return
        if self._pool is None:
            self._pool = await asyncpg.create_pool(self._dsn)
        async with self._pool.acquire() as conn, conn.transaction():
            await _apply_ddl(conn)
        self._schema_ready = True
upsert(table, key, row) async

key is part of the Driver contract but unused here: the row already carries its own key-column values, which ON CONFLICT matches against.

Source code in src/llmbroker/postgres/driver.py
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
async def upsert(self, table: str, key: Key, row: Row) -> None:  # noqa: ARG002
    """``key`` is part of the Driver contract but unused here: the row already
    carries its own key-column values, which ON CONFLICT matches against."""
    spec = TABLES[table]
    await self.ensure_schema()
    encoded = _encode_row(row, spec)
    cols = list(spec.columns)
    placeholders = ", ".join(_placeholder(i, spec.columns[c]) for i, c in enumerate(cols, 1))
    conflict = ", ".join(spec.key)
    updates = ", ".join(f"{c} = EXCLUDED.{c}" for c in cols if c not in spec.key)
    # Identifiers below come from the fixed internal TableSpec, not user input.
    query = (
        f"INSERT INTO {spec.name} ({', '.join(cols)}) VALUES ({placeholders})"  # noqa: S608
        f" ON CONFLICT ({conflict}) DO UPDATE SET {updates}"
    )
    async with self._pool.acquire() as conn:
        await conn.execute(query, *(encoded[c] for c in cols))

registry

Postgres-backed mutable registry over llmbroker_registry — a pure preset mirror.

Registry

Bases: DriverRegistry

Postgres-backed mutable registry over llmbroker_registry — a pure preset mirror.

Source code in src/llmbroker/postgres/registry.py
 9
10
11
12
13
class Registry(DriverRegistry):
    """Postgres-backed mutable registry over ``llmbroker_registry`` — a pure preset mirror."""

    def __init__(self, pool: asyncpg.Pool) -> None:
        super().__init__(PostgresDriver(pool))

secrets

Postgres-backed mutable secrets store over llmbroker_secrets.

Secrets

Bases: DriverSecrets

Postgres-backed mutable secrets store over llmbroker_secrets.

Source code in src/llmbroker/postgres/secrets.py
 9
10
11
12
13
class Secrets(DriverSecrets):
    """Postgres-backed mutable secrets store over ``llmbroker_secrets``."""

    def __init__(self, pool: asyncpg.Pool) -> None:
        super().__init__(PostgresDriver(pool))

store

Postgres-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Store

Bases: DriverStore

Postgres-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Source code in src/llmbroker/postgres/store.py
14
15
16
17
18
19
class Store(DriverStore):
    """Postgres-backed queryable store over ``llmbroker_calls`` + the
    ``llmbroker_disabled`` admin verdict map."""

    def __init__(self, pool: asyncpg.Pool, *, retention: timedelta = _DEFAULT_RETENTION) -> None:
        super().__init__(PostgresDriver(pool), retention=retention)

protocols

Backend contracts: what a registry / secrets / store must do.

Implement these to add a custom backend. The zero-dependency implementations ship in llmbroker.standalone; dependency-carrying ones in per-driver packages (llmbroker.sqlite, …).

registry

Registry contract: load LLM configs; mutable backends also mirror a preset into them.

KeyInfoProtocol

Bases: Protocol

Optional capability: per-key onboarding metadata (help text plus a free-form extra passthrough).

Maps each api_key_ref to a KeyInfo. Keyed by the env-var name because one key is usually shared by several LLMs. A source without such metadata simply does not implement this protocol; callers probe with isinstance(reg, KeyInfoProtocol). It is independent of the broker — hosts query whichever registry they built.

Source code in src/llmbroker/protocols/registry.py
20
21
22
23
24
25
26
27
28
29
30
31
32
@runtime_checkable
class KeyInfoProtocol(Protocol):
    """Optional capability: per-key onboarding metadata (help text plus a
    free-form ``extra`` passthrough).

    Maps each ``api_key_ref`` to a ``KeyInfo``. Keyed by the env-var name because
    one key is usually shared by several LLMs. A source without such metadata
    simply does not implement this protocol; callers probe with
    ``isinstance(reg, KeyInfoProtocol)``. It is independent of the broker — hosts
    query whichever registry they built.
    """

    async def key_info(self) -> dict[str, KeyInfo]: ...
MutableRegistryProtocol

Bases: RegistryProtocol, Protocol

Source code in src/llmbroker/protocols/registry.py
12
13
14
15
16
17
@runtime_checkable
class MutableRegistryProtocol(RegistryProtocol, Protocol):
    async def mirror(self, configs: list[LLMConfig]) -> None:
        """Total mirror: add entries absent from the store, update existing ones,
        delete stored entries absent from ``configs``. The only registry write path."""
        ...
mirror(configs) async

Total mirror: add entries absent from the store, update existing ones, delete stored entries absent from configs. The only registry write path.

Source code in src/llmbroker/protocols/registry.py
14
15
16
17
async def mirror(self, configs: list[LLMConfig]) -> None:
    """Total mirror: add entries absent from the store, update existing ones,
    delete stored entries absent from ``configs``. The only registry write path."""
    ...

secrets

Secrets contract: resolve api_key_ref to a key; mutable backends also set.

store

Store contract: record calls; queryable backends also read the journal.

DisabledMapProtocol is the optional admin-verdict half: a tiny mutable name -> disabled document a backend may additionally implement.

DisabledMapProtocol

Bases: Protocol

Optional capability: the admin disabled-verdict map (name -> bool).

Source code in src/llmbroker/protocols/store.py
29
30
31
32
33
34
35
36
@runtime_checkable
class DisabledMapProtocol(Protocol):
    """Optional capability: the admin disabled-verdict map (``name -> bool``)."""

    async def get_disabled(self, name: str) -> bool: ...
    async def set_disabled(self, name: str, flag: bool) -> None: ...  # noqa: FBT001
    async def seed_disabled(self, names: list[str]) -> None: ...
    async def disabled_map(self) -> dict[str, bool]: ...

sqlite

SQLite backend: registry, store, and secrets over one DB file.

Needs the aiosqlite driver (llmbroker[sqlite]); importing this package is how a host declares that dependency, so a bare import llmbroker stays driver-free. All tables are llmbroker_-prefixed and owned by ensure_schema.

Registry

Bases: DriverRegistry

SQLite-backed mutable registry over llmbroker_registry — a pure preset mirror.

Source code in src/llmbroker/sqlite/registry.py
 9
10
11
12
13
class Registry(DriverRegistry):
    """SQLite-backed mutable registry over ``llmbroker_registry`` — a pure preset mirror."""

    def __init__(self, db_path: str | Path) -> None:
        super().__init__(SqliteDriver(db_path))

Secrets

Bases: DriverSecrets

SQLite-backed mutable secrets store over llmbroker_secrets.

Source code in src/llmbroker/sqlite/secrets.py
 9
10
11
12
13
class Secrets(DriverSecrets):
    """SQLite-backed mutable secrets store over ``llmbroker_secrets``."""

    def __init__(self, db_path: str | Path) -> None:
        super().__init__(SqliteDriver(db_path))

Store

Bases: DriverStore

SQLite-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Source code in src/llmbroker/sqlite/store.py
13
14
15
16
17
18
class Store(DriverStore):
    """SQLite-backed queryable store over ``llmbroker_calls`` + the
    ``llmbroker_disabled`` admin verdict map."""

    def __init__(self, db_path: str | Path, *, retention: timedelta = _DEFAULT_RETENTION) -> None:
        super().__init__(SqliteDriver(db_path), retention=retention)

driver

SQLite Driver: renders DDL and DML from backends.spec.TABLES.

One known installation, upgraded manually by its operator — on a fresh database the schema is created and stamped; on a version-marker mismatch ensure_schema fails fast instead of attempting an in-place migration.

SqliteDriver

One SQLite file (or one process-local :memory: database) backing every llmbroker_* table.

Source code in src/llmbroker/sqlite/driver.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
class SqliteDriver:
    """One SQLite file (or one process-local ``:memory:`` database) backing
    every ``llmbroker_*`` table."""

    def __init__(self, db_path: str | Path) -> None:
        self._db_path = str(db_path)
        # A ":memory:" database only exists for the lifetime of one connection —
        # opening a fresh connection per call (the file-path pattern) would give
        # each call its own empty database. Hold one connection open instead.
        self._memory_conn: aiosqlite.Connection | None = None

    @asynccontextmanager
    async def _connection(self) -> AsyncIterator[aiosqlite.Connection]:
        if self._db_path == ":memory:":
            if self._memory_conn is None:
                self._memory_conn = await aiosqlite.connect(":memory:")
            yield self._memory_conn
        else:
            async with aiosqlite.connect(self._db_path) as db:
                yield db

    async def ensure_schema(self) -> None:
        """A real file path migrates under its own ``isolation_level=None`` connection
        and a ``BEGIN IMMEDIATE`` transaction, so concurrent OS processes serialize on
        sqlite's file lock instead of racing. ``:memory:`` has no cross-process concern."""
        if self._db_path and self._db_path != ":memory:":
            if _schema_ready.get(self._db_path) == SCHEMA_VERSION:
                return
            async with aiosqlite.connect(self._db_path, isolation_level=None) as db:
                await db.execute("BEGIN IMMEDIATE")
                try:
                    await _apply_ddl(db)
                    await db.execute("COMMIT")
                except BaseException:
                    await db.execute("ROLLBACK")
                    raise
            _schema_ready[self._db_path] = SCHEMA_VERSION
        else:
            async with self._connection() as db:
                await _apply_ddl(db)
                await db.commit()

    async def fetch(self, table: str) -> list[Row]:
        spec = TABLES[table]
        await self.ensure_schema()
        cols = ", ".join(spec.columns)
        order = ", ".join(spec.key) if spec.key else cols
        query = f"SELECT {cols} FROM {spec.name} ORDER BY {order}"  # noqa: S608
        async with self._connection() as db:
            rows = await (await db.execute(query)).fetchall()
        return [_decode_row(r, spec) for r in rows]

    async def get(self, table: str, key: Key) -> Row | None:
        spec = TABLES[table]
        await self.ensure_schema()
        cols = ", ".join(spec.columns)
        where = " AND ".join(f"{k} = ?" for k in spec.key)
        async with self._connection() as db:
            row = await (
                await db.execute(f"SELECT {cols} FROM {spec.name} WHERE {where}", key)  # noqa: S608
            ).fetchone()
        return _decode_row(row, spec) if row is not None else None

    async def upsert(self, table: str, key: Key, row: Row) -> None:  # noqa: ARG002
        """``key`` is part of the Driver contract but unused here: the row already
        carries its own key-column values, which ON CONFLICT matches against."""
        spec = TABLES[table]
        await self.ensure_schema()
        encoded = _encode_row(row, spec)
        cols = list(spec.columns)
        placeholders = ", ".join("?" for _ in cols)
        conflict = ", ".join(spec.key)
        updates = ", ".join(f"{c} = excluded.{c}" for c in cols if c not in spec.key)
        # Identifiers below come from the fixed internal TableSpec, not user input.
        query = (
            f"INSERT INTO {spec.name} ({', '.join(cols)}) VALUES ({placeholders})"  # noqa: S608
            f" ON CONFLICT({conflict}) DO UPDATE SET {updates}"
        )
        async with self._connection() as db:
            await db.execute(query, [encoded[c] for c in cols])
            await db.commit()

    async def delete(self, table: str, key: Key) -> bool:
        spec = TABLES[table]
        await self.ensure_schema()
        where = " AND ".join(f"{k} = ?" for k in spec.key)
        async with self._connection() as db:
            cursor = await db.execute(f"DELETE FROM {spec.name} WHERE {where}", key)  # noqa: S608
            await db.commit()
            return cursor.rowcount > 0

    async def append(self, table: str, row: Row) -> None:
        spec = TABLES[table]
        await self.ensure_schema()
        encoded = _encode_row(row, spec)
        cols = list(spec.columns)
        placeholders = ", ".join("?" for _ in cols)
        query = f"INSERT INTO {spec.name} ({', '.join(cols)}) VALUES ({placeholders})"  # noqa: S608
        async with self._connection() as db:
            await db.execute(query, [encoded[c] for c in cols])
            await db.commit()

    async def recent(self, table: str, limit: int, match: Row | None = None) -> list[Row]:
        spec = TABLES[table]
        await self.ensure_schema()
        cols = ", ".join(spec.columns)
        params: list[object] = []
        where = ""
        if match:
            # "=" never matches NULL in SQL; a None filter value means "IS NULL".
            conditions = []
            for k, v in match.items():
                if v is None:
                    conditions.append(f"{k} IS NULL")
                else:
                    conditions.append(f"{k} = ?")
                    params.append(v)
            where = " WHERE " + " AND ".join(conditions)
        params.append(limit)
        async with self._connection() as db:
            rows = await (
                await db.execute(
                    f"SELECT {cols} FROM {spec.name}{where} ORDER BY called_at DESC LIMIT ?",  # noqa: S608
                    params,
                )
            ).fetchall()
        return [_decode_row(r, spec) for r in rows]

    async def purge(self, table: str, before: datetime) -> int:
        spec = TABLES[table]
        await self.ensure_schema()
        async with self._connection() as db:
            cursor = await db.execute(
                f"DELETE FROM {spec.name} WHERE called_at < ?",  # noqa: S608
                [before.isoformat()],
            )
            await db.commit()
            return cursor.rowcount

    async def aclose(self) -> None:
        if self._memory_conn is not None:
            conn, self._memory_conn = self._memory_conn, None
            await conn.close()
ensure_schema() async

A real file path migrates under its own isolation_level=None connection and a BEGIN IMMEDIATE transaction, so concurrent OS processes serialize on sqlite's file lock instead of racing. :memory: has no cross-process concern.

Source code in src/llmbroker/sqlite/driver.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
async def ensure_schema(self) -> None:
    """A real file path migrates under its own ``isolation_level=None`` connection
    and a ``BEGIN IMMEDIATE`` transaction, so concurrent OS processes serialize on
    sqlite's file lock instead of racing. ``:memory:`` has no cross-process concern."""
    if self._db_path and self._db_path != ":memory:":
        if _schema_ready.get(self._db_path) == SCHEMA_VERSION:
            return
        async with aiosqlite.connect(self._db_path, isolation_level=None) as db:
            await db.execute("BEGIN IMMEDIATE")
            try:
                await _apply_ddl(db)
                await db.execute("COMMIT")
            except BaseException:
                await db.execute("ROLLBACK")
                raise
        _schema_ready[self._db_path] = SCHEMA_VERSION
    else:
        async with self._connection() as db:
            await _apply_ddl(db)
            await db.commit()
upsert(table, key, row) async

key is part of the Driver contract but unused here: the row already carries its own key-column values, which ON CONFLICT matches against.

Source code in src/llmbroker/sqlite/driver.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
async def upsert(self, table: str, key: Key, row: Row) -> None:  # noqa: ARG002
    """``key`` is part of the Driver contract but unused here: the row already
    carries its own key-column values, which ON CONFLICT matches against."""
    spec = TABLES[table]
    await self.ensure_schema()
    encoded = _encode_row(row, spec)
    cols = list(spec.columns)
    placeholders = ", ".join("?" for _ in cols)
    conflict = ", ".join(spec.key)
    updates = ", ".join(f"{c} = excluded.{c}" for c in cols if c not in spec.key)
    # Identifiers below come from the fixed internal TableSpec, not user input.
    query = (
        f"INSERT INTO {spec.name} ({', '.join(cols)}) VALUES ({placeholders})"  # noqa: S608
        f" ON CONFLICT({conflict}) DO UPDATE SET {updates}"
    )
    async with self._connection() as db:
        await db.execute(query, [encoded[c] for c in cols])
        await db.commit()

registry

SQLite-backed mutable registry over llmbroker_registry — a pure preset mirror.

Registry

Bases: DriverRegistry

SQLite-backed mutable registry over llmbroker_registry — a pure preset mirror.

Source code in src/llmbroker/sqlite/registry.py
 9
10
11
12
13
class Registry(DriverRegistry):
    """SQLite-backed mutable registry over ``llmbroker_registry`` — a pure preset mirror."""

    def __init__(self, db_path: str | Path) -> None:
        super().__init__(SqliteDriver(db_path))

secrets

SQLite-backed mutable secrets store over llmbroker_secrets.

Secrets

Bases: DriverSecrets

SQLite-backed mutable secrets store over llmbroker_secrets.

Source code in src/llmbroker/sqlite/secrets.py
 9
10
11
12
13
class Secrets(DriverSecrets):
    """SQLite-backed mutable secrets store over ``llmbroker_secrets``."""

    def __init__(self, db_path: str | Path) -> None:
        super().__init__(SqliteDriver(db_path))

store

SQLite-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Store

Bases: DriverStore

SQLite-backed queryable store over llmbroker_calls + the llmbroker_disabled admin verdict map.

Source code in src/llmbroker/sqlite/store.py
13
14
15
16
17
18
class Store(DriverStore):
    """SQLite-backed queryable store over ``llmbroker_calls`` + the
    ``llmbroker_disabled`` admin verdict map."""

    def __init__(self, db_path: str | Path, *, retention: timedelta = _DEFAULT_RETENTION) -> None:
        super().__init__(SqliteDriver(db_path), retention=retention)

standalone

Zero-dependency implementations that work without any external backend.

The simplest way to use llmbroker (the path the README advertises): a config file, env-var secrets, a file-backed store — no database, no integration code. Available with a bare import llmbroker (re-exported as llmbroker.Registry etc.). Dependency-carrying backends live in per-driver packages instead.

registry

File-backed registry: .toml / .json of [[llms]] rows, no secrets.

Registry

File-backed read-only registry — .toml / .json by extension.

Source code in src/llmbroker/standalone/registry.py
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
class Registry:
    """File-backed read-only registry — ``.toml`` / ``.json`` by extension."""

    def __init__(self, path: str | Path) -> None:
        self._path = Path(path)

    @property
    def path(self) -> Path:
        return self._path

    async def load(self) -> list[LLMConfig]:
        data = _read_data(self._path)
        result: list[LLMConfig] = []
        for entry in data.get("llms", []):
            cfg = _config_from_entry(entry)
            if cfg is not None:
                result.append(cfg)
        return result

    async def key_info(self) -> dict[str, KeyInfo]:
        """Per-provider onboarding metadata from the ``[keys]`` table, keyed by ``api_key_ref``."""
        raw = _read_data(self._path).get("keys", {})
        if not isinstance(raw, dict):
            return {}
        return {str(ref): key_info_from_entry(str(ref), val) for ref, val in raw.items()}
key_info() async

Per-provider onboarding metadata from the [keys] table, keyed by api_key_ref.

Source code in src/llmbroker/standalone/registry.py
77
78
79
80
81
82
async def key_info(self) -> dict[str, KeyInfo]:
    """Per-provider onboarding metadata from the ``[keys]`` table, keyed by ``api_key_ref``."""
    raw = _read_data(self._path).get("keys", {})
    if not isinstance(raw, dict):
        return {}
    return {str(ref): key_info_from_entry(str(ref), val) for ref, val in raw.items()}
key_info_from_entry(ref, raw)

Parse one [keys.REF] entry; a bare string is the help-only form.

Source code in src/llmbroker/standalone/registry.py
28
29
30
31
32
33
34
35
36
37
38
39
40
def key_info_from_entry(ref: str, raw: object) -> KeyInfo:
    """Parse one ``[keys.REF]`` entry; a bare string is the help-only form."""
    if isinstance(raw, str):
        return KeyInfo(api_key_ref=ref, help=raw, extra={})
    if not isinstance(raw, dict):
        return KeyInfo(api_key_ref=ref, help="", extra={})
    help_text = raw.get("help")
    extra = {str(k): str(v) for k, v in raw.items() if k != "help"}
    return KeyInfo(
        api_key_ref=ref,
        help=help_text if isinstance(help_text, str) else "",
        extra=extra,
    )

secrets

Env-var and in-memory secrets resolvers — read-only, no external backend.

Secrets() resolves api_key_ref from os.environ; DictSecrets from a mapping. Both are read-only. A plain callable is accepted and adapted.

DictSecrets

Read-only secrets resolver backed by an in-memory mapping (tests / preloaded keys).

Source code in src/llmbroker/standalone/secrets.py
25
26
27
28
29
30
31
32
33
34
class DictSecrets:
    """Read-only secrets resolver backed by an in-memory mapping (tests / preloaded keys)."""

    def __init__(self, mapping: dict[str, str]) -> None:
        self._mapping = dict(mapping)

    async def resolve(self, ref: str) -> str:
        if ref not in self._mapping:
            raise KeyError(f"DictSecrets: ref {ref!r} not found")
        return self._mapping[ref]
Secrets

Read-only env-backed secrets resolver (the default battery).

Source code in src/llmbroker/standalone/secrets.py
15
16
17
18
19
20
21
22
class Secrets:
    """Read-only env-backed secrets resolver (the default battery)."""

    async def resolve(self, ref: str) -> str:
        value = os.environ.get(ref)
        if value is None:
            raise KeyError(f"Secrets: env var {ref!r} is not set")
        return value
as_secrets(secrets)

Return a SecretsProtocol, wrapping a bare callable if needed.

Source code in src/llmbroker/standalone/secrets.py
50
51
52
53
54
55
56
57
58
def as_secrets(secrets: object) -> SecretsProtocol:
    """Return a SecretsProtocol, wrapping a bare callable if needed."""
    if secrets is None:
        return Secrets()
    if isinstance(secrets, SecretsProtocol):
        return secrets
    if callable(secrets):
        return _CallableSecrets(cast(Callable[[str], str | Awaitable[str]], secrets))
    raise TypeError(f"secrets must be a SecretsProtocol or callable, got {type(secrets)!r}")

store

File-backed and in-memory stores — no external backend.

InMemoryStore() implements only the minimal contract and keeps its disabled-verdict map in process memory (session-scoped learning). It is llmbroker's internal subsystem, not application logging — a host that wants logs uses logging itself.

FileStore(directory) is the default persistent store: a day-split JSON-lines call journal (<directory>/calls/YYYY-MM-DD.jsonl, chosen by each record's UTC date — pure storage layout, not aggregation, since rebuild needs raw per-record scores and a quality record can rate a call from an earlier day) plus a YAML admin disabled-verdict map (<directory>/disabled.yml, meant for hand-editing). It self-purges call records older than retention by unlinking whole expired day files — no rewrite, no race with concurrent appends — checked at most once per hour on write activity. The disabled map is never purged.

FileStore

Day-split JSONL call journal plus a YAML disabled-verdict map, under one directory.

Source code in src/llmbroker/standalone/store.py
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
class FileStore:
    """Day-split JSONL call journal plus a YAML disabled-verdict map, under one directory."""

    def __init__(self, directory: str | Path, *, retention: timedelta = _DEFAULT_RETENTION) -> None:
        self._dir = Path(directory)
        self._calls_dir = self._dir / "calls"
        self._disabled_path = self._dir / "disabled.yml"
        self._retention = retention
        self._last_purge = float("-inf")

    def _day_path(self, ts: datetime) -> Path:
        return self._calls_dir / f"{ts.date().isoformat()}.jsonl"

    def _append(self, call: Call) -> None:
        ts = call.ts or datetime.now(UTC)
        path = self._day_path(ts)
        path.parent.mkdir(parents=True, exist_ok=True)
        line = json.dumps(_call_to_jsonable(call))
        with path.open("a", encoding="utf-8") as fh:
            fh.write(line + "\n")

    async def record(self, call: Call) -> None:
        await asyncio.to_thread(self._append, call)
        await self._maybe_purge()

    async def record_quality(
        self,
        llm_name: str,
        operation: str | None,
        score: float,
        *,
        call_id: str | None = None,
    ) -> None:
        await self.record(_new_quality_call(llm_name, operation, score, call_id))

    def _day_files_newest_first(self) -> list[Path]:
        if not self._calls_dir.exists():
            return []
        return sorted(self._calls_dir.glob("*.jsonl"), reverse=True)

    def _read_tail(self, limit: int, scope: str | None) -> list[Call]:
        result: list[Call] = []
        for path in self._day_files_newest_first():
            lines = path.read_text(encoding="utf-8").splitlines()
            for raw_line in reversed(lines):
                stripped = raw_line.strip()
                if not stripped:
                    continue
                call = _call_from_jsonable(json.loads(stripped))
                if scope is not None and call.scope != scope:
                    continue
                result.append(call)
                if len(result) >= limit:
                    return result
        return result

    async def calls(self, *, limit: int, scope: str | None = None) -> list[Call]:
        """Newest-first tail of the journal, both kinds interleaved — unfiltered by scope
        (learning is global); ``scope`` is accepted for the host-facing filter only."""
        return await asyncio.to_thread(self._read_tail, limit, scope)

    def _purge_old_day_files(self) -> None:
        cutoff = (datetime.now(UTC) - self._retention).date()
        for path in self._day_files_newest_first():
            try:
                file_date = date.fromisoformat(path.stem)
            except ValueError:
                continue
            if file_date < cutoff:
                path.unlink(missing_ok=True)

    async def _maybe_purge(self) -> None:
        now = time.monotonic()
        if now - self._last_purge < _PURGE_INTERVAL_SECONDS:
            return
        self._last_purge = now
        await asyncio.to_thread(self._purge_old_day_files)

    def _read_disabled(self) -> dict[str, bool]:
        if not self._disabled_path.exists():
            return {}
        data = yaml.safe_load(self._disabled_path.read_text(encoding="utf-8"))
        return dict(data) if data else {}

    def _write_disabled(self, data: dict[str, bool]) -> None:
        self._disabled_path.parent.mkdir(parents=True, exist_ok=True)
        body = yaml.safe_dump(data, sort_keys=True)
        self._disabled_path.write_text(_DISABLED_HEADER + body, encoding="utf-8")

    async def get_disabled(self, name: str) -> bool:
        data = await asyncio.to_thread(self._read_disabled)
        return bool(data.get(name, False))

    async def set_disabled(self, name: str, flag: bool) -> None:  # noqa: FBT001
        data = await asyncio.to_thread(self._read_disabled)
        data[name] = flag
        await asyncio.to_thread(self._write_disabled, data)

    async def seed_disabled(self, names: list[str]) -> None:
        data = await asyncio.to_thread(self._read_disabled)
        changed = False
        for name in names:
            if name not in data:
                data[name] = False
                changed = True
        if changed:
            await asyncio.to_thread(self._write_disabled, data)

    async def disabled_map(self) -> dict[str, bool]:
        return await asyncio.to_thread(self._read_disabled)
calls(*, limit, scope=None) async

Newest-first tail of the journal, both kinds interleaved — unfiltered by scope (learning is global); scope is accepted for the host-facing filter only.

Source code in src/llmbroker/standalone/store.py
179
180
181
182
async def calls(self, *, limit: int, scope: str | None = None) -> list[Call]:
    """Newest-first tail of the journal, both kinds interleaved — unfiltered by scope
    (learning is global); ``scope`` is accepted for the host-facing filter only."""
    return await asyncio.to_thread(self._read_tail, limit, scope)
InMemoryStore

Explicit in-memory opt-out — no persistence, session-scoped learning; disabled verdicts live only in process memory.

Source code in src/llmbroker/standalone/store.py
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
class InMemoryStore:
    """Explicit in-memory opt-out — no persistence, session-scoped learning;
    disabled verdicts live only in process memory."""

    def __init__(self) -> None:
        self._disabled: dict[str, bool] = {}

    async def record(self, _call: Call) -> None:
        return

    async def record_quality(
        self,
        _llm_name: str,
        _operation: str | None,
        _score: float,
        *,
        call_id: str | None = None,  # noqa: ARG002
    ) -> None:
        return

    async def get_disabled(self, name: str) -> bool:
        return self._disabled.get(name, False)

    async def set_disabled(self, name: str, flag: bool) -> None:  # noqa: FBT001
        self._disabled[name] = flag

    async def seed_disabled(self, names: list[str]) -> None:
        for name in names:
            self._disabled.setdefault(name, False)

    async def disabled_map(self) -> dict[str, bool]:
        return dict(self._disabled)

sync

Synchronous Broker / LLM / Result — blocking proxies over AsyncBroker.

Broker runs an AsyncBroker on a dedicated background event-loop thread; its blocking methods submit coroutines to that loop and wait. The pool's concurrency persists across calls.

Broker

Synchronous client over an AsyncBroker on a background loop thread.

Source code in src/llmbroker/sync.py
 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class Broker:
    """Synchronous client over an AsyncBroker on a background loop thread."""

    def __init__(  # noqa: PLR0913
        self,
        registry: RegistryProtocol | str | Path | None = None,
        *,
        secrets: SecretsProtocol | None = None,
        store: StoreProtocol | None = None,
        optimize: bool | Optimizer = True,
        scope: str | None = None,
    ) -> None:
        self._async = AsyncBroker(
            registry,
            secrets=secrets,
            store=store,
            optimize=optimize,
            scope=scope,
        )
        self._loop = asyncio.new_event_loop()
        self._thread = threading.Thread(
            target=_run_loop,
            args=(self._loop,),
            daemon=True,
            name="llmbroker-loop",
        )
        self._thread.start()
        # Backstop: if the caller never closes the Broker, stop the loop and
        # join the thread when the instance is garbage-collected. The callback
        # holds only loop + thread (never self), so it does not pin the Broker.
        self._finalizer = weakref.finalize(self, _shutdown, self._loop, self._thread)

    def _run(self, coro: Coroutine[Any, Any, Any]) -> Any:
        return asyncio.run_coroutine_threadsafe(coro, self._loop).result()

    def _ensure_pool(self) -> None:
        self._run(self._async.ensure_pool())

    # ── Accessors ──
    def get(self, name: str) -> LLM:
        return LLM(self._run, self._run(self._async.get(name)))

    def count(self) -> int:
        return self._run(self._async.count())

    # ── calls ──
    def ask(
        self,
        prompt: str,
        *,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> Result:
        return Result(
            self._run,
            self._run(self._async.ask(prompt, operation=operation, trace_id=trace_id, wait=wait)),
        )

    def chat(
        self,
        messages: list[dict],
        *,
        tools: list[dict] | None = None,
        operation: str | None = None,
        trace_id: str | None = None,
        wait: float | None = None,
    ) -> Result:
        return Result(
            self._run,
            self._run(
                self._async.chat(
                    messages,
                    tools=tools,
                    operation=operation,
                    trace_id=trace_id,
                    wait=wait,
                ),
            ),
        )

    def snapshot(self) -> Mapping[str, LLMSnapshot]:
        return self._run(self._async.snapshot())

    def sync(self, preset: RegistryProtocol | str | Path) -> None:
        self._run(self._async.sync(preset))

    def disable_llm(self, name: str) -> None:
        self._run(self._async.disable_llm(name))

    def enable_llm(self, name: str) -> None:
        self._run(self._async.enable_llm(name))

    def calls(self, *, limit: int) -> list[Call]:
        return self._run(self._async.calls(limit=limit))

    # ── lifecycle ──
    def close(self) -> None:
        if not self._finalizer.alive:
            return
        self._run(self._async.aclose())
        # Run the same teardown the GC backstop would, and mark it done so the
        # finalizer does not repeat it later.
        self._finalizer()

    def __enter__(self) -> "Broker":
        self._ensure_pool()
        return self

    def __exit__(self, *exc: object) -> None:
        self.close()

LLM

Synchronous analogue of AsyncLLM.

Source code in src/llmbroker/sync.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class LLM:
    """Synchronous analogue of AsyncLLM."""

    def __init__(self, run_fn: "Callable[[Any], Any]", async_llm: AsyncLLM) -> None:
        self._run = run_fn
        self._async = async_llm

    @property
    def config(self) -> LLMConfig:
        return self._async.config

    @property
    def disabled(self) -> bool:
        return self._async.disabled

    def state(self) -> LLMState:
        return self._run(self._async.state())

    def metrics(self) -> LLMMetrics:
        return self._run(self._async.metrics())

Result

Synchronous analogue of AsyncResult.

Source code in src/llmbroker/sync.py
43
44
45
46
47
48
49
50
51
52
53
54
class Result:
    """Synchronous analogue of AsyncResult."""

    def __init__(self, run_fn: "Callable[[Any], Any]", async_result: AsyncResult) -> None:
        self._run = run_fn
        self._async = async_result
        self.text = async_result.text
        self.tool_calls = async_result.tool_calls
        self.usage = async_result.usage

    def record_quality(self, score: float) -> None:
        self._run(self._async.record_quality(score))

vault

HashiCorp Vault backend for llmbroker.

Needs the hvac client (llmbroker[vault]); importing this package is how a host declares that dependency, so a bare import llmbroker stays driver-free. Uses KV v2.

Secrets

HashiCorp Vault KV v2-backed mutable secrets store.

hvac is sync-only; all calls run inside asyncio.to_thread. aclose is a no-op.

Source code in src/llmbroker/vault/secrets.py
 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
class Secrets:
    """HashiCorp Vault KV v2-backed mutable secrets store.

    ``hvac`` is sync-only; all calls run inside ``asyncio.to_thread``.
    ``aclose`` is a no-op.
    """

    def __init__(self, url: str, token: str, *, mount_point: str = "secret") -> None:
        self._client = hvac.Client(url=url, token=token)
        self._mount_point = mount_point

    def _path(self, ref: str) -> str:
        return f"llmbroker/{ref}"

    async def resolve(self, ref: str) -> str:
        try:
            response = await asyncio.to_thread(
                self._client.secrets.kv.v2.read_secret_version,
                path=self._path(ref),
                mount_point=self._mount_point,
            )
            return response["data"]["data"]["value"]
        except hvac.exceptions.InvalidPath as exc:
            raise KeyError(f"vault.Secrets: ref {ref!r} not found") from exc

    async def set(self, ref: str, value: str) -> None:
        await asyncio.to_thread(
            self._client.secrets.kv.v2.create_or_update_secret,
            path=self._path(ref),
            secret={"value": value},
            mount_point=self._mount_point,
        )

    async def aclose(self) -> None:
        return

secrets

HashiCorp Vault KV v2-backed mutable secrets store.

Secrets

HashiCorp Vault KV v2-backed mutable secrets store.

hvac is sync-only; all calls run inside asyncio.to_thread. aclose is a no-op.

Source code in src/llmbroker/vault/secrets.py
 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
class Secrets:
    """HashiCorp Vault KV v2-backed mutable secrets store.

    ``hvac`` is sync-only; all calls run inside ``asyncio.to_thread``.
    ``aclose`` is a no-op.
    """

    def __init__(self, url: str, token: str, *, mount_point: str = "secret") -> None:
        self._client = hvac.Client(url=url, token=token)
        self._mount_point = mount_point

    def _path(self, ref: str) -> str:
        return f"llmbroker/{ref}"

    async def resolve(self, ref: str) -> str:
        try:
            response = await asyncio.to_thread(
                self._client.secrets.kv.v2.read_secret_version,
                path=self._path(ref),
                mount_point=self._mount_point,
            )
            return response["data"]["data"]["value"]
        except hvac.exceptions.InvalidPath as exc:
            raise KeyError(f"vault.Secrets: ref {ref!r} not found") from exc

    async def set(self, ref: str, value: str) -> None:
        await asyncio.to_thread(
            self._client.secrets.kv.v2.create_or_update_secret,
            path=self._path(ref),
            secret={"value": value},
            mount_point=self._mount_point,
        )

    async def aclose(self) -> None:
        return