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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-25 07:19 +0800
1"""Factory functions for constructing LLM routing clients.
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"""
8from __future__ import annotations
10from typing import TYPE_CHECKING
12from lexigram.ai.llm.config import ClientConfig
13from lexigram.contracts.ai.thinking import ThinkingConfig
14from lexigram.logging import (
15 get_logger,
16)
18if TYPE_CHECKING:
19 from lexigram.ai.llm.routing.config import (
20 LLMConfig,
21 ProviderConfig,
22 )
23 from lexigram.contracts.ai import LLMClientProtocol
25logger = get_logger(__name__)
27__all__ = ["create_routing_clients"]
30def _thinking_from_provider(p: ProviderConfig) -> ThinkingConfig | None:
31 """Build a ThinkingConfig for suppress-thinking providers.
33 Returns ``ThinkingConfig(suppress=True)`` when the provider is configured
34 with ``suppress_thinking=True``, otherwise ``None`` (no thinking config).
36 Args:
37 p: Routing provider configuration.
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
47def _build_openai_client(p: ProviderConfig) -> LLMClientProtocol:
48 from lexigram.ai.llm.clients.openai import OpenAIClient
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]
60def _build_anthropic_client(p: ProviderConfig) -> LLMClientProtocol:
61 from lexigram.ai.llm.clients.anthropic import AnthropicClient
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]
73def _build_groq_client(p: ProviderConfig) -> LLMClientProtocol:
74 from lexigram.ai.llm.clients.groq import GroqClient
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]
86def _build_gemini_client(p: ProviderConfig) -> LLMClientProtocol:
87 from lexigram.ai.llm.clients.gemini import GeminiClient
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]
99def _build_mistral_client(p: ProviderConfig) -> LLMClientProtocol:
100 from lexigram.ai.llm.clients.mistral import MistralClient
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]
112def _build_cohere_client(p: ProviderConfig) -> LLMClientProtocol:
113 from lexigram.ai.llm.clients.cohere import CohereClient
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]
125def _build_openrouter_client(p: ProviderConfig) -> LLMClientProtocol:
126 from lexigram.ai.llm.clients.openrouter import OpenRouterClient
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]
139def _build_deepseek_client(p: ProviderConfig) -> LLMClientProtocol:
140 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient
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]
153def _build_together_client(p: ProviderConfig) -> LLMClientProtocol:
154 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient
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]
167def _build_fireworks_client(p: ProviderConfig) -> LLMClientProtocol:
168 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient
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]
181def _build_ollama_client(p: ProviderConfig) -> LLMClientProtocol:
182 from lexigram.ai.llm.clients.ollama import OllamaClient
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]
194def _build_openai_compatible_client(p: ProviderConfig) -> LLMClientProtocol:
195 from lexigram.ai.llm.clients.openai_compatible import OpenAICompatibleClient
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]
208def _build_azure_client(p: ProviderConfig) -> LLMClientProtocol:
209 from lexigram.ai.llm.clients.azure_openai import AzureOpenAIClient
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]
223def _build_cloudflare_client(p: ProviderConfig) -> LLMClientProtocol:
224 from lexigram.ai.llm.clients.cloudflare_workers import CloudflareWorkersClient
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]
236def _build_bedrock_client(p: ProviderConfig) -> LLMClientProtocol:
237 from lexigram.ai.llm.clients.aws_bedrock import BedrockClient
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]
249def _build_vertex_client(p: ProviderConfig) -> LLMClientProtocol:
250 from lexigram.ai.llm.clients.vertex_ai import VertexAIClient
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]
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}
282def create_routing_clients(config: LLMConfig) -> dict[str, LLMClientProtocol]:
283 """Build a provider-keyed dict of :class:`~lexigram.contracts.ai.LLMClientProtocol` instances.
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.
289 Args:
290 config: Root routing configuration.
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] = {}
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
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
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 )
327 return clients