Coverage for src / lexigram / ai / relay / gateway / di / provider.py: 100%
79 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-08 23:08 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-08 23:08 +0800
1"""RelayGatewayProvider — registers the relay gateway behind its contract.
3The provider composes the gateway service from caller-owned dependencies.
4Configuration, the conversion engine, and the HTTP client are injected at
5construction; the registry, codec, upstream adapter, and service are built
6and registered against the container.
7"""
9from __future__ import annotations
11from typing import TYPE_CHECKING
13from lexigram.ai.relay.gateway.channels import RelayChannelRegistry
14from lexigram.ai.relay.gateway.codec import RelayPayloadCodec
15from lexigram.ai.relay.gateway.config import RelayGatewayConfig
16from lexigram.ai.relay.gateway.operations.auto_test import RelayChannelAutoTester
17from lexigram.ai.relay.gateway.operations.controls import (
18 InMemoryRelayPolicyStore,
19 RelayControlsService,
20)
21from lexigram.ai.relay.gateway.operations.health import (
22 RelayChannelCheckerProtocol,
23 RelayHealthService,
24)
25from lexigram.ai.relay.gateway.operations.metrics import (
26 RelayMetricsService,
27 RelayRouteEventSourceProtocol,
28)
29from lexigram.ai.relay.gateway.operations.streams import RelayStreamRegistry
30from lexigram.ai.relay.gateway.passthrough import PassthroughService
31from lexigram.ai.relay.gateway.service import RelayGatewayService
32from lexigram.ai.relay.gateway.upstream import HTTPUpstreamAdapter
33from lexigram.contracts.ai.governance import (
34 AIAuditStoreProtocol,
35 RelayBillingProtocol,
36)
37from lexigram.contracts.ai.relay import (
38 MediaResolverProtocol,
39 RelayConverterProtocol,
40 RelayGatewayProtocol,
41 RelayPolicyStoreProtocol,
42 RelayRegistryProtocol,
43)
44from lexigram.contracts.auth.guard import AuthorizerProtocol
45from lexigram.contracts.core.provider import ProviderPriority
46from lexigram.contracts.web import HTTPClientProtocol
47from lexigram.di.provider import Provider
48from lexigram.logging import get_logger
50if TYPE_CHECKING:
51 from lexigram.contracts.core.di import (
52 BootContainerProtocol,
53 ContainerRegistrarProtocol,
54 )
56logger = get_logger(__name__)
59class RelayGatewayProvider(Provider):
60 """Provider registering the relay gateway behind ``RelayGatewayProtocol``.
62 The caller owns the static configuration, the conversion engine, and
63 the HTTP client; the provider wires them into a ready-to-serve
64 :class:`RelayGatewayService`. Optional governance hooks (authorizer,
65 media resolver, billing) are forwarded to the service as-is.
67 Registers:
68 - ``RelayGatewayConfig`` — the injected configuration (always)
69 - ``RelayChannelRegistry`` — a registry built from the configuration
70 - ``RelayPolicyStoreProtocol`` — the runtime policy backend (always)
71 - ``RelayHealthService`` — channel health probing (always)
72 - ``RelayMetricsService`` — route metrics aggregation (always)
73 - ``RelayControlsService`` — permissioned control mutations (always)
74 - ``RelayChannelAutoTester`` — background channel auto-tester (only
75 when ``auto_test_channels`` is enabled in the configuration)
76 - ``RelayGatewayProtocol`` — the gateway service (only when both the
77 converter and an HTTP client are available)
78 - ``PassthroughService`` — passthrough endpoint dispatch (same
79 availability as the gateway service)
81 Args:
82 config: Gateway channel table and conversion metadata. Defaults
83 to an empty configuration when omitted.
84 converter: Conversion engine implementing
85 ``RelayConverterProtocol``. When ``None`` a startup
86 diagnostic is logged and the gateway binding is skipped.
87 http_client: HTTP client driving the upstream adapter. When
88 ``None`` a startup diagnostic is logged and the gateway
89 binding is skipped.
90 authorizer: Optional authorizer enforced before dispatch.
91 media_resolver: Optional media resolver placed on the conversion
92 context.
93 billing: Optional billing lifecycle; when ``None`` the gateway
94 runs without admission control or settlement.
95 converter_registry: Converter registry backing health and metrics
96 diagnostics and route quality. Optional; when ``None`` the
97 diagnostic surfaces are unavailable (``DEPENDENCY_UNAVAILABLE``).
98 channel_checker: Optional channel checker driving per-channel
99 probes; when ``None`` the health service reports unchecked
100 channels.
101 metrics_events: Optional route event source feeding metrics
102 aggregation; when ``None`` the metrics surface is unavailable.
103 policy_store: Optional runtime policy backend. ``None`` installs
104 an in-process ``InMemoryRelayPolicyStore`` seeded from the
105 configuration.
106 audit: Optional audit backend for control mutations. ``None``
107 disables audit emission.
108 """
110 name = "ai-relay-gateway"
111 priority = ProviderPriority.DOMAIN
113 def __init__(
114 self,
115 *,
116 config: RelayGatewayConfig | None = None,
117 converter: RelayConverterProtocol | None = None,
118 http_client: HTTPClientProtocol | None = None,
119 authorizer: AuthorizerProtocol | None = None,
120 media_resolver: MediaResolverProtocol | None = None,
121 billing: RelayBillingProtocol | None = None,
122 converter_registry: RelayRegistryProtocol | None = None,
123 channel_checker: RelayChannelCheckerProtocol | None = None,
124 metrics_events: RelayRouteEventSourceProtocol | None = None,
125 policy_store: RelayPolicyStoreProtocol | None = None,
126 audit: AIAuditStoreProtocol | None = None,
127 ) -> None:
128 super().__init__()
129 self._config = config if config is not None else RelayGatewayConfig()
130 self._converter = converter
131 self._http_client = http_client
132 self._authorizer = authorizer
133 self._media_resolver = media_resolver
134 self._billing = billing
135 self._converter_registry = converter_registry
136 self._channel_checker = channel_checker
137 self._metrics_events = metrics_events
138 self._policy_store = (
139 policy_store
140 if policy_store is not None
141 else InMemoryRelayPolicyStore.with_defaults(self._config)
142 )
143 self._audit = audit
144 self._auto_tester: RelayChannelAutoTester | None = None
146 async def register(self, container: ContainerRegistrarProtocol) -> None:
147 """Register the gateway configuration, registry, and service.
149 The configuration and registry are always bound. The gateway
150 service itself is only bound when the converter and HTTP client
151 are both present; otherwise a startup diagnostic is logged so the
152 missing dependency is discoverable.
154 Args:
155 container: The container registrar to bind into.
156 """
157 registry = RelayChannelRegistry(self._config)
158 streams = RelayStreamRegistry()
159 container.singleton(RelayGatewayConfig, self._config)
160 container.singleton(RelayChannelRegistry, registry)
161 container.singleton(RelayStreamRegistry, streams)
162 container.singleton(RelayPolicyStoreProtocol, self._policy_store)
163 health_service = RelayHealthService(
164 registry=registry,
165 checker=self._channel_checker,
166 converter=self._converter_registry,
167 policy=self._policy_store,
168 )
169 container.singleton(RelayHealthService, health_service)
170 if self._config.auto_test_channels:
171 self._auto_tester = RelayChannelAutoTester(
172 health=health_service,
173 registry=registry,
174 interval_seconds=self._config.auto_test_interval_seconds,
175 )
176 container.singleton(RelayChannelAutoTester, self._auto_tester)
177 metrics_service = RelayMetricsService(
178 events=self._metrics_events,
179 converter=self._converter_registry,
180 )
181 container.singleton(RelayMetricsService, metrics_service)
182 controls_service = RelayControlsService(
183 registry=registry,
184 store=self._policy_store,
185 authorizer=self._authorizer,
186 audit=self._audit,
187 streams=streams,
188 )
189 container.singleton(RelayControlsService, controls_service)
190 if self._converter is None:
191 logger.warning(
192 "relay_gateway_missing_dependency",
193 missing="RelayConverterProtocol",
194 )
195 return
196 if self._http_client is None:
197 logger.warning(
198 "relay_gateway_missing_dependency",
199 missing="HTTPClientProtocol",
200 )
201 return
202 service = RelayGatewayService(
203 converter=self._converter,
204 codec=RelayPayloadCodec(),
205 registry=registry,
206 upstream=HTTPUpstreamAdapter(self._http_client),
207 config=self._config,
208 authorizer=self._authorizer,
209 billing=self._billing,
210 media_resolver=self._media_resolver,
211 streams=streams,
212 )
213 container.singleton(RelayGatewayProtocol, service)
214 passthrough_service = PassthroughService(
215 registry=registry,
216 upstream=HTTPUpstreamAdapter(self._http_client),
217 config=self._config,
218 authorizer=self._authorizer,
219 billing=self._billing,
220 )
221 container.singleton(PassthroughService, passthrough_service)
222 logger.info("relay_gateway_provider_registered")
224 async def boot(self, container: BootContainerProtocol) -> None:
225 """Reconcile persisted policy drains into runtime selection.
227 Channels the policy store marks disabled (while the static
228 configuration still enables them) are drained in the runtime
229 registry so dispatch honors the persisted policy from the first
230 request onward. When auto-testing is enabled, the background
231 sweep is started after reconciliation.
233 Args:
234 container: The booted container used to resolve the policy
235 store and channel registry.
236 """
237 registry = await container.resolve(RelayChannelRegistry)
238 policy = await container.resolve(RelayPolicyStoreProtocol)
239 snapshot = await policy.load()
240 for channel in self._config.channels:
241 if channel.enabled and not snapshot.enabled_channels.get(
242 channel.name, True
243 ):
244 registry.set_runtime_enabled(channel.name, False)
245 if self._auto_tester is not None:
246 await self._auto_tester.start()
247 logger.info("relay_gateway_provider_booted")
249 async def shutdown(self) -> None:
250 """Stop the background auto-tester, if one was started."""
251 if self._auto_tester is not None:
252 await self._auto_tester.stop()
255__all__ = ["RelayGatewayProvider"]