Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/di/routing_provider.py: 38%
53 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""LLM Routing Provider for Lexigram Framework dependency injection.
3Registers the multi-provider LLMRouter and its dependencies
4(quota backend, inference logger, and routing clients) with the
5DI container so the router is injectable throughout the application.
6"""
8from __future__ import annotations
10from typing import TYPE_CHECKING
12from lexigram.contracts.ai.routing import (
13 InferenceLoggerProtocol,
14 LLMRouterProtocol,
15 QuotaBackendProtocol,
16)
17from lexigram.contracts.core.health import HealthCheckResult, HealthStatus
18from lexigram.di.decorators import inject
19from lexigram.di.provider import Provider, ProviderPriority
20from lexigram.logging import (
21 get_logger,
22)
24if TYPE_CHECKING:
25 from lexigram.ai.llm.selection.core import ModelSelector
26 from lexigram.contracts.core.di import (
27 ContainerRegistrarProtocol,
28 ContainerResolverProtocol,
29 )
30 from lexigram.contracts.data import DatabaseProviderProtocol
32from lexigram.ai.llm.routing import (
33 DatabaseInferenceLogger,
34 DatabaseQuotaBackend,
35 InMemoryInferenceLogger,
36 InMemoryQuotaBackend,
37 LLMConfig,
38 LLMRouter,
39)
40from lexigram.ai.llm.routing.di_factories import create_routing_clients
42logger = get_logger(__name__)
44__all__ = ["LLMRoutingProvider"]
47@inject
48class LLMRoutingProvider(Provider):
49 """Provider that registers the multi-provider LLM router with the DI container.
51 Builds the :class:`~lexigram.ai.llm.routing.LLMRouter` from a
52 :class:`~lexigram.ai.llm.routing.LLMConfig`, chooses the appropriate
53 quota backend and inference logger, and registers everything as singletons.
55 Configuration is explicit-only: ``LLMConfig`` is built from ``LEX_AI_LLM__``
56 environment variables via ``LLMConfig.from_env()`` and is not bound to a
57 ``LexigramConfig`` section, so this provider declares no ``config_key``/
58 ``config_model`` attributes.
60 Example:
61 >>> from lexigram.ai.llm.module import LLMModule
62 >>> from lexigram.ai.llm.routing import LLMConfig
63 >>>
64 >>> app.use(LLMModule.configure(routing=LLMConfig.from_env()))
65 >>>
66 >>> # LLMRouterProtocol is now injectable:
67 >>> class MyService:
68 ... def __init__(self, router: LLMRouterProtocol) -> None:
69 ... self.router = router
70 """
72 name = "llm_routing"
73 priority = ProviderPriority.DOMAIN
75 def __init__(
76 self,
77 config: LLMConfig | None = None,
78 database_provider: DatabaseProviderProtocol | None = None,
79 model_selector: ModelSelector | None = None,
80 ) -> None:
81 """Initialise the LLM routing provider.
83 Args:
84 config: Routing configuration; defaults to ``LLMConfig.from_env()``.
85 database_provider: Injected DB provider used when ``quota.backend``
86 or ``logging.backend`` is ``database``.
87 model_selector: Optional model selector for capability-based
88 routing. When provided, ``required_capabilities`` in route
89 kwargs will filter providers whose models lack the requested
90 capabilities.
91 """
92 super().__init__(name="llm_routing")
93 self.config = config or LLMConfig.from_env()
94 self.database_provider = database_provider
95 self._model_selector = model_selector
96 self._router: LLMRouter | None = None
98 async def register(self, container: ContainerRegistrarProtocol) -> None:
99 """Build and register the LLMRouter with the DI container.
101 Args:
102 container: The Lexigram DI container registrar.
103 """
104 logger.info("llm.routing: registering multi-provider router")
106 container.singleton(LLMConfig, self.config)
107 clients = create_routing_clients(self.config)
109 quota_backend: QuotaBackendProtocol
110 if self.config.quota.backend == "database" and self.database_provider:
111 quota_backend = DatabaseQuotaBackend(db=self.database_provider)
112 logger.info("llm.routing: using database quota backend")
113 else:
114 quota_backend = InMemoryQuotaBackend()
115 logger.info("llm.routing: using in-memory quota backend")
117 inference_logger: InferenceLoggerProtocol
118 if self.config.logging.backend == "database" and self.database_provider:
119 inference_logger = DatabaseInferenceLogger(db=self.database_provider)
120 logger.info("llm.routing: using database inference logger")
121 else:
122 inference_logger = InMemoryInferenceLogger(
123 max_size=self.config.logging.max_entries,
124 )
125 logger.info("llm.routing: using in-memory inference logger")
127 router = LLMRouter(
128 config=self.config,
129 clients=clients,
130 quota_backend=quota_backend,
131 inference_logger=inference_logger,
132 model_selector=self._model_selector,
133 )
134 self._router = router
136 container.singleton(LLMRouterProtocol, lambda: router)
137 container.singleton("llm_router", lambda: router)
138 container.singleton(InferenceLoggerProtocol, lambda: inference_logger)
139 logger.info(
140 "llm.routing: router registered with %d providers",
141 len(self.config.providers),
142 )
144 async def boot(self, container: ContainerResolverProtocol) -> None:
145 """Boot phase — no-op for this provider.
147 Args:
148 container: The DI container resolver.
149 """
151 async def shutdown(self) -> None:
152 """Close all routing clients on application shutdown."""
153 if self._router is not None:
154 try:
155 await self._router.close()
156 except (ConnectionError, TimeoutError, OSError) as exc:
157 logger.warning("llm.routing: error during shutdown", error=str(exc))
158 self._router = None
159 logger.info("llm.routing: provider shutdown complete")
161 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult:
162 """Return basic health information for the router.
164 Args:
165 timeout: Unused; retained for interface compatibility.
167 Returns:
168 A dict with ``status`` and ``providers`` keys.
169 """
170 providers = [p.name for p in self.config.providers]
171 return HealthCheckResult(
172 component="llm_routing",
173 status=(
174 HealthStatus.HEALTHY
175 if self._router is not None
176 else HealthStatus.DEGRADED
177 ),
178 details={"providers": providers},
179 )