Coverage for src / lexigram / contracts / ai / relay / operations.py: 24%
130 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-19 05:41 +0800
1"""Relay operational read and control contracts.
3Defines the health, capability, metric, report, and policy-control value
4types and service protocols that the gateway and governance admin
5surfaces consume. Value types are immutable, redaction-safe, and reject
6nonsense inputs at construction time; the service protocols are the only
7cross-package boundary for operational data.
8"""
10from __future__ import annotations
12from collections.abc import Mapping, Sequence
13from dataclasses import dataclass
14from datetime import datetime
15from typing import Literal, Protocol, runtime_checkable
17from lexigram.contracts.ai.relay.types import ConversionQuality, RelayFormat
19__all__ = [
20 "RelayActiveStream",
21 "RelayChannelHealth",
22 "RelayOperationsControlProtocol",
23 "RelayOperationsProtocol",
24 "RelayPolicyChange",
25 "RelayPolicySnapshot",
26 "RelayPolicyStoreProtocol",
27 "RelayRegistryDiagnostics",
28 "RelayRouteMetrics",
29 "TimeWindow",
30]
32_CHANNEL_HEALTH_STATUSES = frozenset({"healthy", "degraded", "unavailable", "failed"})
33"""Stable channel health status values."""
36@dataclass(frozen=True, slots=True)
37class TimeWindow:
38 """A closed, forward time range for metric aggregation.
40 Attributes:
41 start: Inclusive window start (UTC).
42 end: Inclusive window end (UTC), strictly after ``start``.
43 """
45 start: datetime
46 end: datetime
48 def __post_init__(self) -> None:
49 """Reject empty and inverted windows."""
50 if self.end <= self.start:
51 raise ValueError("window end must be after window start")
54@dataclass(frozen=True, slots=True)
55class RelayActiveStream:
56 """One in-flight upstream stream.
58 Attributes:
59 stream_id: Unique identifier of the stream session.
60 channel: Name of the channel serving the stream.
61 model: Outbound model alias of the stream.
62 request_id: Gateway request identifier the stream belongs to.
63 started_at: When the stream started (UTC).
64 """
66 stream_id: str
67 channel: str
68 model: str
69 request_id: str
70 started_at: datetime
72 def __post_init__(self) -> None:
73 """Reject empty identifiers."""
74 if not self.stream_id:
75 raise ValueError("stream_id must not be empty")
76 if not self.channel:
77 raise ValueError("channel must not be empty")
78 if not self.request_id:
79 raise ValueError("request_id must not be empty")
82@dataclass(frozen=True, slots=True)
83class RelayChannelHealth:
84 """One gateway channel's operational health snapshot.
86 Attributes:
87 channel: Channel name.
88 target: Target wire format the channel serves.
89 status: Stable status value.
90 model_count: Number of model aliases the channel serves.
91 latency_ms_p50: Median request latency, or ``None`` when unknown.
92 latency_ms_p95: P95 request latency, or ``None`` when unknown.
93 failure_count: Upstream failures in the window.
94 checked_at: When the snapshot was taken (UTC).
95 detail_code: Machine-readable reason for degraded/failed status.
96 """
98 channel: str
99 target: RelayFormat
100 status: Literal["healthy", "degraded", "unavailable", "failed"]
101 model_count: int
102 latency_ms_p50: float | None
103 latency_ms_p95: float | None
104 failure_count: int
105 checked_at: datetime
106 detail_code: str | None = None
108 def __post_init__(self) -> None:
109 """Validate the snapshot fields."""
110 if not self.channel:
111 raise ValueError("channel must not be empty")
112 if self.status not in _CHANNEL_HEALTH_STATUSES:
113 raise ValueError(f"unknown channel health status {self.status!r}")
114 if self.model_count < 0:
115 raise ValueError("model_count must not be negative")
116 if self.failure_count < 0:
117 raise ValueError("failure_count must not be negative")
118 if self.latency_ms_p50 is not None and self.latency_ms_p50 < 0:
119 raise ValueError("latency_ms_p50 must not be negative")
120 if self.latency_ms_p95 is not None and self.latency_ms_p95 < 0:
121 raise ValueError("latency_ms_p95 must not be negative")
124@dataclass(frozen=True, slots=True)
125class RelayRouteMetrics:
126 """Aggregated conversion metrics for one directed route and window.
128 Attributes:
129 source: Source wire format.
130 target: Target wire format.
131 quality: Static conversion quality of the route.
132 request_count: Completed requests on the route in the window.
133 loss_counts: Conversion loss codes and their counts.
134 unsupported_count: Requests dropped for unsupported features.
135 stream_failure_count: Streams that failed on the route.
136 converter_id: Route converter identifier, when known.
137 window_start: Aggregation window start (UTC).
138 window_end: Aggregation window end (UTC).
139 """
141 source: RelayFormat
142 target: RelayFormat
143 quality: ConversionQuality
144 request_count: int
145 loss_counts: Mapping[str, int]
146 unsupported_count: int
147 stream_failure_count: int
148 converter_id: str | None
149 window_start: datetime
150 window_end: datetime
152 def __post_init__(self) -> None:
153 """Validate the aggregated counters."""
154 if self.request_count < 0:
155 raise ValueError("request_count must not be negative")
156 if self.unsupported_count < 0:
157 raise ValueError("unsupported_count must not be negative")
158 if self.stream_failure_count < 0:
159 raise ValueError("stream_failure_count must not be negative")
160 if self.window_end <= self.window_start:
161 raise ValueError("window end must be after window start")
164@dataclass(frozen=True, slots=True)
165class RelayRegistryDiagnostics:
166 """Read-only registry state for operational diagnostics.
168 Attributes:
169 converter_id: Converter engine identifier.
170 converter_version: Converter engine version string.
171 mapper_ids: Registered mapper wire-format identifiers.
172 supported_routes: Directed route pairs served by the engine.
173 registration_errors: Errors observed at registration time.
174 """
176 converter_id: str
177 converter_version: str
178 mapper_ids: tuple[str, ...]
179 supported_routes: tuple[tuple[RelayFormat, RelayFormat], ...]
180 registration_errors: tuple[str, ...] = ()
183@dataclass(frozen=True, slots=True)
184class RelayPolicySnapshot:
185 """Current gateway routing policy.
187 Attributes:
188 enabled_channels: Channel name to enabled flag.
189 allowed_model_options: Channel name to allowed option names.
190 media_allowed_schemes: Media resolver URL schemes allowlist.
191 media_allowed_hosts: Media resolver URL hosts allowlist.
192 max_request_bytes: Maximum accepted request body size.
193 max_stream_seconds: Maximum streaming duration.
194 """
196 enabled_channels: Mapping[str, bool]
197 allowed_model_options: Mapping[str, frozenset[str]]
198 media_allowed_schemes: frozenset[str]
199 media_allowed_hosts: frozenset[str]
200 max_request_bytes: int
201 max_stream_seconds: float
203 def __post_init__(self) -> None:
204 """Validate the policy limits and allowlists."""
205 if "*" in self.media_allowed_hosts:
206 raise ValueError("media_allowed_hosts must not contain wildcards")
207 if self.max_request_bytes < 0:
208 raise ValueError("max_request_bytes must not be negative")
209 if self.max_stream_seconds <= 0:
210 raise ValueError("max_stream_seconds must be positive")
213@dataclass(frozen=True, slots=True)
214class RelayPolicyChange:
215 """A typed, partial policy mutation request.
217 Only the fields explicitly set are changed; ``None`` means unchanged.
218 """
220 channel: str | None = None
221 enabled: bool | None = None
222 allowed_model_options: frozenset[str] | None = None
223 media_allowed_schemes: frozenset[str] | None = None
224 media_allowed_hosts: frozenset[str] | None = None
225 max_request_bytes: int | None = None
226 max_stream_seconds: float | None = None
228 def __post_init__(self) -> None:
229 """Validate the mutation payload."""
230 if self.media_allowed_hosts is not None and "*" in self.media_allowed_hosts:
231 raise ValueError("media_allowed_hosts must not contain wildcards")
232 if self.max_request_bytes is not None and self.max_request_bytes < 0:
233 raise ValueError("max_request_bytes must not be negative")
234 if self.max_stream_seconds is not None and self.max_stream_seconds <= 0:
235 raise ValueError("max_stream_seconds must be positive")
238@runtime_checkable
239class RelayPolicyStoreProtocol(Protocol):
240 """Persistent backend for the runtime gateway routing policy.
242 The store is the source of truth for ``RelayPolicySnapshot`` between
243 control mutations: ``load`` returns the current snapshot and ``save``
244 persists a full replacement atomically.
245 """
247 async def load(self) -> RelayPolicySnapshot:
248 """Return the current policy snapshot.
250 Returns:
251 The current snapshot; a store that has never been written
252 returns the initial snapshot it was constructed with.
253 """
254 ...
256 async def save(self, snapshot: RelayPolicySnapshot) -> None:
257 """Atomically replace the stored snapshot with *snapshot*.
259 Args:
260 snapshot: The full replacement snapshot. Partial updates are
261 composed by the caller before saving.
262 """
263 ...
266@runtime_checkable
267class RelayOperationsProtocol(Protocol):
268 """Read-only operational queries for admin surfaces."""
270 async def channel_health(self) -> Sequence[RelayChannelHealth]:
271 """Return a health snapshot per gateway channel.
273 Returns:
274 One snapshot per configured channel. A dependency that is
275 not registered yields an ``unavailable`` snapshot, never a
276 fabricated healthy one.
277 """
278 ...
280 async def route_metrics(
281 self,
282 window: TimeWindow,
283 ) -> Sequence[RelayRouteMetrics]:
284 """Return aggregated conversion metrics inside *window*.
286 Args:
287 window: Bounded aggregation window; unbounded windows are
288 rejected by the caller.
290 Returns:
291 One aggregation per directed route that saw activity.
292 """
293 ...
295 async def registry_diagnostics(self) -> RelayRegistryDiagnostics:
296 """Return converter and mapper registry state.
298 Returns:
299 Engine identifier, version, mapper ids, and supported route
300 pairs. A missing converter is a failed dependency and is
301 reported by the caller as such.
302 """
303 ...
305 async def policy_snapshot(self) -> RelayPolicySnapshot:
306 """Return the current routing policy.
308 Returns:
309 The current enabled-channel, option, media, and limit
310 settings.
311 """
312 ...
314 async def active_streams(self) -> Sequence[RelayActiveStream]:
315 """Return the currently in-flight upstream streams.
317 Returns:
318 One row per active stream, oldest first; an empty sequence
319 when no stream is in flight.
320 """
321 ...
324@runtime_checkable
325class RelayOperationsControlProtocol(Protocol):
326 """Permissioned runtime control mutations."""
328 async def set_channel_state(
329 self,
330 channel: str,
331 enabled: bool,
332 actor_id: str,
333 ) -> None:
334 """Enable or drain *channel* for new requests.
336 Args:
337 channel: Channel name; unknown names are rejected.
338 enabled: ``False`` drains the channel for new requests while
339 existing streams finish.
340 actor_id: Operator identity recorded in the audit event.
342 Raises:
343 ValueError: The channel is unknown.
344 """
345 ...
347 async def update_policy(
348 self,
349 change: RelayPolicyChange,
350 actor_id: str,
351 ) -> None:
352 """Apply a typed policy change.
354 Args:
355 change: Partial policy mutation; only set fields change.
356 actor_id: Operator identity recorded in the audit event.
358 Raises:
359 ValueError: The change references unknown channels or options.
360 """
361 ...
363 async def policy_snapshot(self, actor_id: str) -> RelayPolicySnapshot:
364 """Return the current routing policy for *actor_id*.
366 Args:
367 actor_id: Operator identity; ``relay.read`` permission is
368 required.
370 Returns:
371 The current enabled-channel, option, media, and limit
372 settings.
374 Raises:
375 RelayGatewayError: With ``PERMISSION_DENIED`` when the actor
376 lacks ``relay.read``.
377 """
378 ...
380 async def force_cancel_stream(
381 self,
382 stream_id: str,
383 actor_id: str,
384 ) -> None:
385 """Force-cancel an in-flight upstream stream.
387 Args:
388 stream_id: Identifier of the stream to cancel. Unknown
389 streams are rejected.
390 actor_id: Operator identity recorded in the audit event;
391 ``relay.stream_control`` permission is required.
393 Raises:
394 ValueError: The stream identifier is unknown.
395 RelayGatewayError: With ``PERMISSION_DENIED`` when the actor
396 lacks ``relay.stream_control``.
397 """
398 ...