Coverage for src/lilbee/providers/fleet/contract.py: 100%
33 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-09-28 17:20 +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. ``vision_slots_cover``
8applies the same rule to the vision role's slot ceiling.
9"""
11from __future__ import annotations
13from typing import TYPE_CHECKING
15from lilbee.providers.fleet.launch import InstanceLaunch
16from lilbee.providers.roles import WorkerRole
18if TYPE_CHECKING:
19 from collections.abc import Iterable
21 from lilbee.providers.fleet.swap_manager import SwapState
24def decoded_launches(state: SwapState) -> list[InstanceLaunch] | None:
25 """The engine's recorded launches, or ``None`` when the contract is undecodable.
27 The single decode site. Callers previously re-decoded launches bare, relying on
28 a preceding contract_matches call to have proven decodability, so reordering or
29 dropping that guard turned a non-match into an unhandled exception in the bind
30 ladder. Returning ``None`` makes "undecodable" a value every caller must handle.
31 """
32 try:
33 return [InstanceLaunch.from_state(item) for item in state.launches]
34 except (KeyError, TypeError, ValueError):
35 return None
38def served_pairs(state: SwapState) -> set[tuple[WorkerRole, str]] | None:
39 """The (role, model) pairs the engine behind *state* serves, or ``None``."""
40 launches = decoded_launches(state)
41 return None if launches is None else {(launch.role, launch.model) for launch in launches}
44def chat_ctx_covers(launches: Iterable[InstanceLaunch], demanded_ctx: int) -> bool:
45 """Whether the engine's per-slot chat window serves *demanded_ctx* tokens.
47 ``launches`` are the running engine's recorded launches. A zero demand, no
48 chat launch, or a record without a positive chat ctx never refuses: derived
49 values are adopted from the running engine. A window below the demand still
50 covers when the record's ``built_ctx_target`` reaches the demand: the same
51 planner aimed at least as high and achieved this window, so replacing the
52 engine would rebuild the same window in a loop. A refusal sends the ladder
53 to its replace-or-overflow decision.
54 """
55 chat = [launch for launch in launches if launch.role is WorkerRole.CHAT and launch.ctx > 0]
56 live = min((launch.ctx for launch in chat), default=0)
57 if demanded_ctx <= 0 or live <= 0 or demanded_ctx <= live:
58 return True
59 built_target = min((launch.built_ctx_target for launch in chat), default=0)
60 return built_target > 0 and demanded_ctx <= built_target
63def vision_slots_cover(launches: Iterable[InstanceLaunch], demanded_slots: int) -> bool:
64 """Whether the engine's vision slot ceiling serves *demanded_slots* at once.
66 A fitted count below the demand still covers when the builder's
67 ``built_slots_target`` reached it: the same ceiling produced this fit, so a
68 replacement would fit the same count in a loop.
69 """
70 vision = [
71 launch for launch in launches if launch.role is WorkerRole.VISION and launch.slots > 0
72 ]
73 live = min((launch.slots for launch in vision), default=0)
74 if demanded_slots <= 0 or live <= 0 or demanded_slots <= live:
75 return True
76 built_target = min((launch.built_slots_target for launch in vision), default=0)
77 return built_target > 0 and demanded_slots <= built_target
80def contract_matches(state: SwapState, wanted: Iterable[tuple[WorkerRole, str]], pin: str) -> bool:
81 """Whether the engine behind *state* serves every wanted (role, model) pair.
83 The engine may serve more roles than asked; the engine build pin must
84 equal *pin*, and an empty or undecodable served contract never matches.
85 """
86 if state.engine_pin != pin:
87 return False
88 served = served_pairs(state)
89 if not served:
90 return False
91 return all(pair in served for pair in wanted)