Coverage for /home/admin/Documents/AI/applications/lexigram-dev/lexigram/experimental/ai/lexigram-ai-llm/src/lexigram/ai/llm/routing/di_factories.py: 28%

92 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-25 07:19 +0800

1"""Factory functions for constructing LLM routing clients. 

2 

3Used by ``LLMRoutingProvider`` to build a dict of provider-keyed 

4:class:`~lexigram.contracts.ai.LLMClientProtocol` instances from a 

5:class:`~lexigram.ai.llm.routing.config.LLMConfig`. 

6""" 

7 

8from __future__ import annotations 

9 

10from typing import TYPE_CHECKING 

11 

12from lexigram.ai.llm.config import ClientConfig 

13from lexigram.contracts.ai.thinking import ThinkingConfig 

14from lexigram.logging import ( 

15 get_logger, 

16) 

17 

18if TYPE_CHECKING: 

19 from lexigram.ai.llm.routing.config import ( 

20 LLMConfig, 

21 ProviderConfig, 

22 ) 

23 from lexigram.contracts.ai import LLMClientProtocol 

24 

25logger = get_logger(__name__) 

26 

27__all__ = ["create_routing_clients"] 

28 

29 

30def _thinking_from_provider(p: ProviderConfig) -> ThinkingConfig | None: 

31 """Build a ThinkingConfig for suppress-thinking providers. 

32 

33 Returns ``ThinkingConfig(suppress=True)`` when the provider is configured 

34 with ``suppress_thinking=True``, otherwise ``None`` (no thinking config). 

35 

36 Args: 

37 p: Routing provider configuration. 

38 

39 Returns: 

40 A :class:`ThinkingConfig` with ``suppress=True`` or ``None``. 

41 """ 

42 if p.suppress_thinking: 

43 return ThinkingConfig(suppress=True) 

44 return None 

45 

46 

47def _build_openai_client(p: ProviderConfig) -> LLMClientProtocol: 

48 from lexigram.ai.llm.clients.openai import OpenAIClient 

49 

50 config = ClientConfig( 

51 provider="openai", 

52 model=p.model, 

53 api_key=p.api_key, 

54 timeout=float(p.timeout), 

55 thinking=_thinking_from_provider(p), 

56 ) 

57 return OpenAIClient(config) # type: ignore[return-value] 

58 

59 

60def _build_anthropic_client(p: ProviderConfig) -> LLMClientProtocol: 

61 from lexigram.ai.llm.clients.anthropic import AnthropicClient 

62 

63 config = ClientConfig( 

64 provider="anthropic", 

65 model=p.model, 

66 api_key=p.api_key, 

67 timeout=float(p.timeout), 

68 thinking=_thinking_from_provider(p), 

69 ) 

70 return AnthropicClient(config) # type: ignore[return-value] 

71 

72 

73def _build_groq_client(p: ProviderConfig) -> LLMClientProtocol: 

74 from lexigram.ai.llm.clients.groq import GroqClient 

75 

76 config = ClientConfig( 

77 provider="groq", 

78 model=p.model, 

79 api_key=p.api_key, 

80 timeout=float(p.timeout), 

81 thinking=_thinking_from_provider(p), 

82 ) 

83 return GroqClient(config) # type: ignore[return-value] 

84 

85 

86def _build_gemini_client(p: ProviderConfig) -> LLMClientProtocol: 

87 from lexigram.ai.llm.clients.gemini import GeminiClient 

88 

89 config = ClientConfig( 

90 provider="gemini", 

91 model=p.model, 

92 api_key=p.api_key, 

93 timeout=float(p.timeout), 

94 thinking=_thinking_from_provider(p), 

95 ) 

96 return GeminiClient(config) # type: ignore[return-value] 

97 

98 

99def _build_mistral_client(p: ProviderConfig) -> LLMClientProtocol: 

100 from lexigram.ai.llm.clients.mistral import MistralClient 

101 

102 config = ClientConfig( 

103 provider="mistral", 

104 model=p.model, 

105 api_key=p.api_key, 

106 timeout=float(p.timeout), 

107 thinking=_thinking_from_provider(p), 

108 ) 

109 return MistralClient(config) # type: ignore[return-value] 

110 

111 

112def _build_cohere_client(p: ProviderConfig) -> LLMClientProtocol: 

113 from lexigram.ai.llm.clients.cohere import CohereClient 

114 

115 config = ClientConfig( 

116 provider="cohere", 

117 model=p.model, 

118 api_key=p.api_key, 

119 timeout=float(p.timeout), 

120 thinking=_thinking_from_provider(p), 

121 ) 

122 return CohereClient(config) # type: ignore[return-value] 

123 

124 

125def _build_openrouter_client(p: ProviderConfig) -> LLMClientProtocol: 

126 from lexigram.ai.llm.clients.openrouter import OpenRouterClient 

127 

128 config = ClientConfig( 

129 provider="openrouter", 

130 model=p.model, 

131 api_key=p.api_key, 

132 api_base=p.base_url or "https://openrouter.ai/api/v1", 

133 timeout=float(p.timeout), 

134 thinking=_thinking_from_provider(p), 

135 ) 

136 return OpenRouterClient(config) # type: ignore[return-value] 

137 

138 

139def _build_deepseek_client(p: ProviderConfig) -> LLMClientProtocol: 

140 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient 

141 

142 config = ClientConfig( 

143 provider="deepseek", 

144 model=p.model, 

145 api_key=p.api_key, 

146 api_base=p.base_url or "https://api.deepseek.com/v1", 

147 timeout=float(p.timeout), 

148 thinking=_thinking_from_provider(p), 

149 ) 

150 return OpenAICompatibleClient(config) # type: ignore[return-value] 

151 

152 

153def _build_together_client(p: ProviderConfig) -> LLMClientProtocol: 

154 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient 

155 

156 config = ClientConfig( 

157 provider="together", 

158 model=p.model, 

159 api_key=p.api_key, 

160 api_base=p.base_url or "https://api.together.xyz/v1", 

161 timeout=float(p.timeout), 

162 thinking=_thinking_from_provider(p), 

163 ) 

164 return OpenAICompatibleClient(config) # type: ignore[return-value] 

165 

166 

167def _build_fireworks_client(p: ProviderConfig) -> LLMClientProtocol: 

168 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient 

169 

170 config = ClientConfig( 

171 provider="fireworks", 

172 model=p.model, 

173 api_key=p.api_key, 

174 api_base=p.base_url or "https://api.fireworks.ai/inference/v1", 

175 timeout=float(p.timeout), 

176 thinking=_thinking_from_provider(p), 

177 ) 

178 return OpenAICompatibleClient(config) # type: ignore[return-value] 

179 

180 

181def _build_ollama_client(p: ProviderConfig) -> LLMClientProtocol: 

182 from lexigram.ai.llm.clients.ollama import OllamaClient 

183 

184 config = ClientConfig( 

185 provider="ollama", 

186 model=p.model, 

187 api_base=p.base_url, 

188 timeout=float(p.timeout), 

189 thinking=_thinking_from_provider(p), 

190 ) 

191 return OllamaClient(config) # type: ignore[return-value] 

192 

193 

194def _build_openai_compatible_client(p: ProviderConfig) -> LLMClientProtocol: 

195 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient 

196 

197 config = ClientConfig( 

198 provider="openai_compatible", 

199 model=p.model, 

200 api_key=p.api_key, 

201 api_base=p.base_url, 

202 timeout=float(p.timeout), 

203 thinking=_thinking_from_provider(p), 

204 ) 

205 return OpenAICompatibleClient(config) # type: ignore[return-value] 

206 

207 

208def _build_azure_client(p: ProviderConfig) -> LLMClientProtocol: 

209 from lexigram.ai.llm.clients.azure_openai import AzureOpenAIClient 

210 

211 config = ClientConfig( 

212 provider="azure", 

213 model=p.model, 

214 api_key=p.api_key, 

215 api_base=p.base_url, 

216 timeout=float(p.timeout), 

217 extra=p.extras, 

218 thinking=_thinking_from_provider(p), 

219 ) 

220 return AzureOpenAIClient(config) # type: ignore[return-value] 

221 

222 

223def _build_cloudflare_client(p: ProviderConfig) -> LLMClientProtocol: 

224 from lexigram.ai.llm.clients.cloudflare_workers import CloudflareWorkersClient 

225 

226 config = ClientConfig( 

227 provider="cloudflare", 

228 model=p.model, 

229 timeout=float(p.timeout), 

230 extra=p.extras, 

231 thinking=_thinking_from_provider(p), 

232 ) 

233 return CloudflareWorkersClient(config) # type: ignore[return-value] 

234 

235 

236def _build_bedrock_client(p: ProviderConfig) -> LLMClientProtocol: 

237 from lexigram.ai.llm.clients.aws_bedrock import BedrockClient 

238 

239 config = ClientConfig( 

240 provider="bedrock", 

241 model=p.model, 

242 timeout=float(p.timeout), 

243 extra=p.extras, 

244 thinking=_thinking_from_provider(p), 

245 ) 

246 return BedrockClient(config) # type: ignore[return-value] 

247 

248 

249def _build_vertex_client(p: ProviderConfig) -> LLMClientProtocol: 

250 from lexigram.ai.llm.clients.vertex_ai import VertexAIClient 

251 

252 config = ClientConfig( 

253 provider="vertex", 

254 model=p.model, 

255 timeout=float(p.timeout), 

256 extra=p.extras, 

257 thinking=_thinking_from_provider(p), 

258 ) 

259 return VertexAIClient(config) # type: ignore[return-value] 

260 

261 

262_CLIENT_FACTORIES: dict[str, object] = { 

263 "openai": _build_openai_client, 

264 "anthropic": _build_anthropic_client, 

265 "groq": _build_groq_client, 

266 "gemini": _build_gemini_client, 

267 "mistral": _build_mistral_client, 

268 "cohere": _build_cohere_client, 

269 "openrouter": _build_openrouter_client, 

270 "deepseek": _build_deepseek_client, 

271 "together": _build_together_client, 

272 "fireworks": _build_fireworks_client, 

273 "ollama": _build_ollama_client, 

274 "openai_compatible": _build_openai_compatible_client, 

275 "azure": _build_azure_client, 

276 "cloudflare": _build_cloudflare_client, 

277 "bedrock": _build_bedrock_client, 

278 "vertex": _build_vertex_client, 

279} 

280 

281 

282def create_routing_clients(config: LLMConfig) -> dict[str, LLMClientProtocol]: 

283 """Build a provider-keyed dict of :class:`~lexigram.contracts.ai.LLMClientProtocol` instances. 

284 

285 Only providers with :attr:`~ProviderConfig.enabled` set to ``True`` 

286 and a recognised factory are included. Unknown provider names emit a 

287 warning and are silently skipped. 

288 

289 Args: 

290 config: Root routing configuration. 

291 

292 Returns: 

293 Mapping of cascade-entry key (``ProviderConfig.key``, i.e. 

294 ``name:model``) → :class:`~lexigram.contracts.ai.LLMClientProtocol`. 

295 One client per entry, so per-entry timeout/thinking settings and 

296 circuit breakers stay isolated even when entries share a name. 

297 """ 

298 clients: dict[str, LLMClientProtocol] = {} 

299 

300 for provider_cfg in config.providers: 

301 if not provider_cfg.enabled: 

302 logger.info("llm.routing: skipping disabled provider %s", provider_cfg.name) 

303 continue 

304 

305 factory = _CLIENT_FACTORIES.get(provider_cfg.name) 

306 if factory is None: 

307 logger.warning( 

308 "llm.routing: no factory for provider %s — skipping", 

309 provider_cfg.name, 

310 ) 

311 continue 

312 

313 try: 

314 clients[provider_cfg.key] = factory(provider_cfg) # type: ignore[operator] 

315 logger.info( 

316 "llm.routing: built client for provider %s (model=%s)", 

317 provider_cfg.name, 

318 provider_cfg.model, 

319 ) 

320 except Exception as exc: 

321 logger.exception( 

322 "llm.routing: failed to build client for provider %s: %s", 

323 provider_cfg.name, 

324 exc, 

325 ) 

326 

327 return clients