Coverage for src/lilbee/providers/fleet/contract.py: 100%
26 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-14 11:46 +0000
1"""Decides whether a running engine can serve a lilbee's configuration.
3The contract is the per-role models plus the engine build pin. Planner-derived
4values (ctx, slots) are accepted from the running engine, never recomputed:
5they vary legitimately with GPU occupancy at plan time. The one exception is
6``chat_ctx_covers``: a live chat window smaller than what this process needs
7cannot be adopted, since no prompt fit can grow it.
8"""
10from __future__ import annotations
12from typing import TYPE_CHECKING
14from lilbee.providers.fleet.launch import InstanceLaunch
15from lilbee.providers.roles import WorkerRole
17if TYPE_CHECKING:
18 from collections.abc import Iterable
20 from lilbee.providers.fleet.swap_manager import SwapState
23def decoded_launches(state: SwapState) -> list[InstanceLaunch] | None:
24 """The engine's recorded launches, or ``None`` when the contract is undecodable.
26 The single decode site. Callers previously re-decoded launches bare, relying on
27 a preceding contract_matches call to have proven decodability, so reordering or
28 dropping that guard turned a non-match into an unhandled exception in the bind
29 ladder. Returning ``None`` makes "undecodable" a value every caller must handle.
30 """
31 try:
32 return [InstanceLaunch.from_state(item) for item in state.launches]
33 except (KeyError, TypeError, ValueError):
34 return None
37def served_pairs(state: SwapState) -> set[tuple[WorkerRole, str]] | None:
38 """The (role, model) pairs the engine behind *state* serves, or ``None``."""
39 launches = decoded_launches(state)
40 return None if launches is None else {(launch.role, launch.model) for launch in launches}
43def chat_ctx_covers(launches: Iterable[InstanceLaunch], demanded_ctx: int) -> bool:
44 """Whether the engine's per-slot chat window serves *demanded_ctx* tokens.
46 ``launches`` are the running engine's recorded launches. A zero demand, no
47 chat launch, or a record without a positive chat ctx never refuses: derived
48 values are adopted from the running engine. A window below the demand still
49 covers when the record's ``built_ctx_target`` reaches the demand: the same
50 planner aimed at least as high and achieved this window, so replacing the
51 engine would rebuild the same window in a loop. A refusal sends the ladder
52 to its replace-or-overflow decision.
53 """
54 chat = [launch for launch in launches if launch.role is WorkerRole.CHAT and launch.ctx > 0]
55 live = min((launch.ctx for launch in chat), default=0)
56 if demanded_ctx <= 0 or live <= 0 or demanded_ctx <= live:
57 return True
58 built_target = min((launch.built_ctx_target for launch in chat), default=0)
59 return built_target > 0 and demanded_ctx <= built_target
62def contract_matches(state: SwapState, wanted: Iterable[tuple[WorkerRole, str]], pin: str) -> bool:
63 """Whether the engine behind *state* serves every wanted (role, model) pair.
65 The engine may serve more roles than asked; the engine build pin must
66 equal *pin*, and an empty or undecodable served contract never matches.
67 """
68 if state.engine_pin != pin:
69 return False
70 served = served_pairs(state)
71 if not served:
72 return False
73 return all(pair in served for pair in wanted)