Coverage for src / lexigram / contracts / ai / relay / gateway.py: 15%
92 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"""Gateway contracts for the relay engine.
3Defines the channel configuration, gateway request/result value types,
4gateway error, and the gateway service protocol implemented by the
5``lexigram-ai-relay-gateway`` extension package.
6"""
8from __future__ import annotations
10from collections.abc import AsyncIterator, Mapping
11from dataclasses import dataclass, field
12from enum import StrEnum
13from typing import Protocol, runtime_checkable
15from lexigram.contracts.ai.relay.transport import RelayWireEvent
16from lexigram.contracts.ai.relay.types import ConversionQuality, JsonValue, RelayFormat
17from lexigram.contracts.core.result import Result
20@dataclass(frozen=True, slots=True)
21class RelayChannel:
22 """Configuration of one upstream endpoint a gateway can route to."""
24 name: str
25 upstream_base_url: str
26 target_format: RelayFormat
27 models: tuple[str, ...]
28 capabilities: frozenset[str] = frozenset()
29 endpoint_kinds: frozenset[str] = frozenset()
30 priority: int = 100
31 weight: int = 100
32 enabled: bool = True
33 timeout_seconds: float = 60.0
34 model_map: Mapping[str, str] = field(default_factory=dict)
36 def __post_init__(self) -> None:
37 """Validate the channel configuration."""
38 if not self.name:
39 raise ValueError("name must not be empty")
40 if not self.upstream_base_url:
41 raise ValueError("upstream_base_url must not be empty")
42 if not self.models:
43 raise ValueError("models must not be empty")
44 if self.timeout_seconds <= 0:
45 raise ValueError("timeout_seconds must be positive")
46 if self.weight < 0:
47 raise ValueError("weight must not be negative")
48 unknown = set(self.model_map) - set(self.models)
49 if unknown:
50 raise ValueError(
51 f"model_map keys must be listed in models; unknown: {sorted(unknown)}"
52 )
54 def resolve_model(self, alias: str) -> str:
55 """Resolve the upstream model name for *alias*.
57 The mapping wins when it carries *alias*; otherwise the alias is
58 used as its own upstream name.
60 Args:
61 alias: The client-visible model alias.
63 Returns:
64 The model name sent to the channel's upstream.
65 """
66 return self.model_map.get(alias, alias)
69@dataclass(frozen=True, slots=True)
70class RelayGatewayRequest:
71 """A request accepted by the relay gateway for dispatch.
73 The ``channel`` field is a preferred-channel hint and audit
74 snapshot: the service always re-selects the channel through the
75 registry and never routes on this snapshot alone.
76 """
78 request_id: str
79 tenant_id: str
80 source: RelayFormat
81 model: str
82 stream: bool
83 payload: Mapping[str, JsonValue]
84 headers: Mapping[str, str]
85 channel: RelayChannel | None = None
88@dataclass(frozen=True, slots=True)
89class RelayGatewayResult:
90 """The outcome of a relay gateway handle call."""
92 status_code: int
93 headers: Mapping[str, str]
94 payload: Mapping[str, JsonValue] | None = None
95 stream: AsyncIterator[RelayWireEvent] | None = None
96 metadata: RelayGatewayMetadata | None = None
99@dataclass(frozen=True, slots=True)
100class RelayGatewayMetadata:
101 """Conversion bookkeeping attached to a gateway result."""
103 converter_id: str
104 source: RelayFormat
105 target: RelayFormat
106 quality: ConversionQuality
107 loss_codes: tuple[str, ...] = ()
108 warnings: tuple[str, ...] = ()
111class RelayGatewayErrorCode(StrEnum):
112 """Stable machine-readable codes carried by :class:`RelayGatewayError`.
114 Existing callers may construct ``RelayGatewayError`` with the plain
115 string value (``"INVALID_REQUEST"``); the members compare equal to
116 those strings.
118 Attributes:
119 CHANNEL_DISABLED: No enabled channel exists.
120 TARGET_FORMAT_UNSUPPORTED: No channel transforms the source format.
121 CAPABILITY_UNAVAILABLE: No channel satisfies the capability filters.
122 MODEL_NOT_FOUND: No channel serves the requested model.
123 INVALID_REQUEST: The request payload is malformed.
124 UNSUPPORTED_FORMAT: Unknown wire format.
125 ENCODE_FAILED: Outbound payload serialization failed.
126 DECODE_FAILED: Inbound payload deserialization failed.
127 AUTH_DENIED: The caller is not authorized to invoke the model.
128 PERMISSION_DENIED: The operator is not authorized for the
129 requested control surface.
130 UPSTREAM_ERROR: The upstream responded with a non-2xx status.
131 UPSTREAM_TIMEOUT: The upstream request timed out.
132 UPSTREAM_CANCELLED: The upstream request was cancelled.
133 UPSTREAM_FAILED: Generic upstream transport failure.
134 UPSTREAM_MALFORMED: A 2xx upstream body was malformed.
135 CONVERSION_FAILED: Request/response conversion failed.
136 STREAM_ERROR: A streaming session failed.
137 QUOTA_EXCEEDED: Billing denied admission to the request.
138 BILLING_FAILED: The billing pipeline failed unexpectedly.
139 DEPENDENCY_UNAVAILABLE: An optional runtime dependency was not
140 registered (e.g. the converter registry).
141 """
143 CHANNEL_DISABLED = "CHANNEL_DISABLED"
144 TARGET_FORMAT_UNSUPPORTED = "TARGET_FORMAT_UNSUPPORTED"
145 CAPABILITY_UNAVAILABLE = "CAPABILITY_UNAVAILABLE"
146 MODEL_NOT_FOUND = "MODEL_NOT_FOUND"
147 INVALID_REQUEST = "INVALID_REQUEST"
148 UNSUPPORTED_FORMAT = "UNSUPPORTED_FORMAT"
149 ENCODE_FAILED = "ENCODE_FAILED"
150 DECODE_FAILED = "DECODE_FAILED"
151 AUTH_DENIED = "AUTH_DENIED"
152 PERMISSION_DENIED = "PERMISSION_DENIED"
153 UPSTREAM_ERROR = "UPSTREAM_ERROR"
154 UPSTREAM_TIMEOUT = "UPSTREAM_TIMEOUT"
155 UPSTREAM_CANCELLED = "UPSTREAM_CANCELLED"
156 UPSTREAM_FAILED = "UPSTREAM_FAILED"
157 UPSTREAM_MALFORMED = "UPSTREAM_MALFORMED"
158 CONVERSION_FAILED = "CONVERSION_FAILED"
159 STREAM_ERROR = "STREAM_ERROR"
160 QUOTA_EXCEEDED = "QUOTA_EXCEEDED"
161 BILLING_FAILED = "BILLING_FAILED"
162 DEPENDENCY_UNAVAILABLE = "DEPENDENCY_UNAVAILABLE"
165@dataclass(frozen=True, slots=True)
166class RelayGatewayError(Exception):
167 """A domain error returned (or raised) by the relay gateway."""
169 code: str
170 message: str
171 status_code: int
172 request_id: str
173 retryable: bool = False
176@runtime_checkable
177class RelayGatewayProtocol(Protocol):
178 """Service protocol implemented by the relay gateway."""
180 async def handle(
181 self, request: RelayGatewayRequest
182 ) -> Result[RelayGatewayResult, RelayGatewayError]: ...