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

1"""LLM Routing Provider for Lexigram Framework dependency injection. 

2 

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""" 

7 

8from __future__ import annotations 

9 

10from typing import TYPE_CHECKING 

11 

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) 

23 

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 

31 

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 

41 

42logger = get_logger(__name__) 

43 

44__all__ = ["LLMRoutingProvider"] 

45 

46 

47@inject 

48class LLMRoutingProvider(Provider): 

49 """Provider that registers the multi-provider LLM router with the DI container. 

50 

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. 

54 

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. 

59 

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 """ 

71 

72 name = "llm_routing" 

73 priority = ProviderPriority.DOMAIN 

74 

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. 

82 

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 

97 

98 async def register(self, container: ContainerRegistrarProtocol) -> None: 

99 """Build and register the LLMRouter with the DI container. 

100 

101 Args: 

102 container: The Lexigram DI container registrar. 

103 """ 

104 logger.info("llm.routing: registering multi-provider router") 

105 

106 container.singleton(LLMConfig, self.config) 

107 clients = create_routing_clients(self.config) 

108 

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") 

116 

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") 

126 

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 

135 

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 ) 

143 

144 async def boot(self, container: ContainerResolverProtocol) -> None: 

145 """Boot phase — no-op for this provider. 

146 

147 Args: 

148 container: The DI container resolver. 

149 """ 

150 

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") 

160 

161 async def health_check(self, timeout: float = 5.0) -> HealthCheckResult: 

162 """Return basic health information for the router. 

163 

164 Args: 

165 timeout: Unused; retained for interface compatibility. 

166 

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 )