Coverage for src/lilbee/providers/roles.py: 100%
48 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"""Engine-neutral role identifiers shared across the provider stack.
3``WorkerRole`` names the four inference roles a local engine serves; the fleet
4maps each to one llama-server instance. ``OcrBackend`` names the PDF-OCR paths.
5``EngineBackend`` names the compute backend the engine serves those roles on.
6These outlive any particular engine, so they live here rather than inside an
7engine-specific module.
8"""
10from __future__ import annotations
12from dataclasses import dataclass
13from enum import StrEnum
14from typing import Literal
17class WorkerRole(StrEnum):
18 """Inference role identifier; addresses one llama-server in the fleet."""
20 EMBED = "embed"
21 RERANK = "rerank"
22 CHAT = "chat"
23 VISION = "vision"
26class EngineBackend(StrEnum):
27 """The compute backend the engine selected on this host.
29 ``CPU`` and ``UNKNOWN`` are separate answers on purpose. An empty device list
30 has two causes a client cannot tell apart: a host that has no usable GPU, and
31 a host whose device probe never answered. Only the first is CPU. Reporting
32 ``CPU`` for the second misidentifies a CUDA or Metal host in the diagnostics
33 a user pastes into a bug report.
34 """
36 CUDA = "cuda"
37 ROCM = "rocm"
38 METAL = "metal"
39 SYCL = "sycl"
40 VULKAN = "vulkan"
41 CPU = "cpu"
42 UNKNOWN = "unknown"
45class Phase(StrEnum):
46 """A run phase whose roles are loaded together on demand.
48 Ingest OCRs and embeds (vision + embed); a query embeds, reranks, and generates
49 (embed + rerank + chat). Roles sharing no phase are never co-resident, so on a
50 tight host they may share one swap group instead of both reserving VRAM.
51 """
53 INGEST = "ingest"
54 QUERY = "query"
57class RerankMode(StrEnum):
58 """Resolved reranker serving mode for one RERANK server.
60 ``CROSS_ENCODER`` serves an encoder GGUF with rank-pooling embeddings;
61 ``LLM`` serves a decoder GGUF generatively and scores yes/no logprobs.
62 """
64 CROSS_ENCODER = "cross_encoder"
65 LLM = "llm"
68@dataclass(frozen=True)
69class RoleInfo:
70 """The per-role knowledge the fleet needs to configure one llama-server.
72 One row per ``WorkerRole``, so adding a role is a single registry entry and the
73 scattered planning/placement/replica tuples all derive from here.
74 """
76 role: WorkerRole
77 config_field: str # the cfg ``*_model`` field whose value this role serves
78 replicated: bool # runs N data-parallel replicas (embed/vision)
79 replica_knob: str | None # cfg int field scaling replicas, None when not replicated
80 offload_all_layers: bool # loader offloads every layer, ignoring cfg.n_gpu_layers
81 flash_attn: bool # runs with flash attention (chat/vision)
82 pooled: bool # pooled single-slot search role (embed/cross-encoder rerank)
83 placement_rank: int # placement order; the elastic chat model is charged last
84 phases: frozenset[Phase] # run phases that load this role (co-residency model)
87ROLE_REGISTRY: dict[WorkerRole, RoleInfo] = {
88 WorkerRole.CHAT: RoleInfo(
89 role=WorkerRole.CHAT,
90 config_field="chat_model",
91 replicated=False,
92 replica_knob=None,
93 offload_all_layers=False,
94 flash_attn=True,
95 pooled=False,
96 placement_rank=2,
97 phases=frozenset({Phase.QUERY}),
98 ),
99 WorkerRole.EMBED: RoleInfo(
100 role=WorkerRole.EMBED,
101 config_field="embedding_model",
102 replicated=True,
103 replica_knob="embed_replicas",
104 offload_all_layers=True,
105 flash_attn=False,
106 pooled=True,
107 placement_rank=0,
108 phases=frozenset({Phase.INGEST, Phase.QUERY}),
109 ),
110 WorkerRole.RERANK: RoleInfo(
111 role=WorkerRole.RERANK,
112 config_field="reranker_model",
113 replicated=False,
114 replica_knob=None,
115 offload_all_layers=True,
116 flash_attn=False,
117 pooled=True,
118 placement_rank=0,
119 phases=frozenset({Phase.QUERY}),
120 ),
121 WorkerRole.VISION: RoleInfo(
122 role=WorkerRole.VISION,
123 config_field="vision_model",
124 replicated=True,
125 replica_knob="vision_replicas",
126 offload_all_layers=True,
127 flash_attn=True,
128 pooled=False,
129 placement_rank=1,
130 phases=frozenset({Phase.INGEST}),
131 ),
132}
133"""Single source of truth for per-role fleet configuration, ordered chat/embed/rerank/vision."""
136MODEL_FIELD_TO_ROLE: dict[str, WorkerRole] = {
137 info.config_field: role for role, info in ROLE_REGISTRY.items()
138}
139"""Config model-role field name -> the worker whose server serves it.
141A model-role setting change reloads just that role's server (off-thread) rather
142than dropping the whole fleet, so unrelated roles keep serving uninterrupted.
143"""
146MODEL_ROLE_FIELDS: frozenset[str] = frozenset(MODEL_FIELD_TO_ROLE)
147"""The cfg ``*_model`` field names, as a set (settings overlay + reload routing)."""
150ROLE_GATE_FIELD_TO_ROLE: dict[str, WorkerRole] = {"enable_ocr": WorkerRole.VISION}
151"""Non-model settings that decide whether a role is planned; a change reloads that role."""
154REPLICATED_ROLES: tuple[WorkerRole, ...] = tuple(
155 role for role, info in ROLE_REGISTRY.items() if info.replicated
156)
157"""Roles whose ``*_replicas`` knob scales data-parallel instances; others run one."""
160OcrBackend = Literal["vision"]
161"""PDF-OCR backends routed to the engine. Tesseract runs inline, not on a server."""
164def configured_model_message(role: WorkerRole, configured: str, requested: str) -> str:
165 """User-facing rejection for a per-call model that differs from the configured one."""
166 return (
167 f"This engine serves the configured {role} model ({configured}). "
168 f"To use {requested!r}, set it as the {role} model in lilbee settings "
169 f"(TUI /settings), then retry; the engine reloads automatically."
170 )