Coverage for src / lexigram / contracts / ai / routing.py: 0%
22 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-15 18:57 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-15 18:57 +0800
1"""Routing protocols for multi-provider LLM inference.
3Defines the protocol interfaces that decouple ``lexigram-ai-llm``'s routing
4implementation from consumers. All types that live in extension packages
5are represented as ``Any`` to satisfy the zero-dependency constraint of
6``lexigram-contracts``.
7"""
9from __future__ import annotations
11from typing import TYPE_CHECKING, Any, Protocol, runtime_checkable
13if TYPE_CHECKING:
14 from datetime import datetime
16__all__ = [
17 "InferenceLoggerProtocol",
18 "LLMRouterProtocol",
19 "QuotaBackendProtocol",
20 "RoutingStrategyProtocol",
21]
24@runtime_checkable
25class RoutingStrategyProtocol(Protocol):
26 """Protocol for routing strategies used by the LLM router."""
28 async def execute(
29 self,
30 *,
31 providers: list[Any],
32 clients: dict[str, Any],
33 quota: QuotaBackendProtocol,
34 config: Any,
35 messages: list[Any],
36 kwargs: dict[str, Any],
37 ) -> tuple[Any | None, list[str], int]:
38 """Execute routing strategy."""
39 ...
42@runtime_checkable
43class LLMRouterProtocol(Protocol):
44 """Protocol for multi-provider LLM routing.
46 Implementations cascade across a list of free providers (subject to
47 daily quota tracking) before optionally falling through to a paid
48 fallback. Returns a ``Result[InferenceLog, InferenceError]`` —
49 typed as ``Any`` here to avoid importing extension-package types.
51 Example:
52 ```python
53 result = await router.route(
54 messages=[{"role": "user", "content": "Hello!"}],
55 temperature=0.2,
56 )
57 if result.is_ok():
58 log = result.unwrap()
59 print(log.completion.content)
60 ```
61 """
63 async def route(
64 self,
65 messages: list[Any],
66 **kwargs: Any,
67 ) -> Any:
68 """Route a completion request across configured providers.
70 Args:
71 messages: OpenAI-compatible message list.
72 **kwargs: Provider-agnostic generation options (temperature,
73 max_tokens, model, etc.).
75 Returns:
76 ``Result[InferenceLog, InferenceError]`` — ``Ok`` on any
77 successful completion, ``Err`` when every provider (including
78 the paid fallback) is exhausted or fails.
79 """
80 ...
82 async def health_probe(self) -> Any:
83 """Probe configured providers without issuing a normal inference request.
85 Returns:
86 ``Result[InferenceLog, InferenceError]`` — ``Ok`` on the first
87 healthy configured provider, ``Err`` when no enabled provider can
88 satisfy a lightweight health check.
89 """
90 ...
93@runtime_checkable
94class QuotaBackendProtocol(Protocol):
95 """Protocol for per-provider daily quota tracking.
97 Implementations MUST be safe for concurrent async access. All
98 mutations (increment, mark_exhausted, record_error) are fire-and-forget
99 from the router's perspective; backends that persist to a database
100 should absorb their own errors rather than propagating them.
102 Example:
103 ```python
104 if await backend.is_exhausted("groq"):
105 # Skip this provider
106 ...
107 await backend.increment("groq")
108 ```
109 """
111 async def is_exhausted(self, provider: str) -> bool:
112 """Return ``True`` when the provider has exceeded its daily quota.
114 Args:
115 provider: Provider name (e.g. ``"groq"``).
117 Returns:
118 ``True`` when the provider should be skipped.
119 """
120 ...
122 async def increment(self, provider: str) -> None:
123 """Record one successful completion for *provider* today.
125 Args:
126 provider: Provider name.
127 """
128 ...
130 async def mark_exhausted(
131 self, provider: str, *, until: datetime | None = None
132 ) -> None:
133 """Mark *provider* exhausted.
135 Called when the provider returns HTTP 429 (transient throttle —
136 pass a short ``until`` cooldown) or 402 (account-level — leave
137 ``until`` unset).
139 Args:
140 provider: Cascade-entry key (``name:model``) or provider name.
141 until: Exhaustion expiry. ``None`` means the rest of the
142 current UTC day (legacy behavior).
143 """
144 ...
146 async def record_error(self, provider: str) -> None:
147 """Record a non-exhaustion error for *provider*.
149 Used for observability; does not affect quota state.
151 Args:
152 provider: Provider name.
153 """
154 ...
156 async def get_usage(self, provider: str) -> Any:
157 """Return today's usage record for *provider*.
159 Args:
160 provider: Provider name.
162 Returns:
163 ``ProviderUsage`` dataclass or ``None`` when no record exists.
164 """
165 ...
167 async def get_all_usage(self) -> Any:
168 """Return all today's usage records.
170 Returns:
171 ``list[ProviderUsage]`` for all tracked providers.
172 """
173 ...
176@runtime_checkable
177class InferenceLoggerProtocol(Protocol):
178 """Protocol for recording inference attempt logs.
180 Every routing run — successful or failed — should be logged for
181 observability. Implementations MUST NOT raise exceptions; logging
182 failures are absorbed silently so they never interrupt inference.
184 Example:
185 ```python
186 await logger.log(inference_log)
187 recent = await logger.get_recent(limit=20)
188 ```
189 """
191 async def log(self, entry: Any) -> None:
192 """Record one ``InferenceLog`` entry.
194 Args:
195 entry: ``InferenceLog`` dataclass instance.
196 """
197 ...
199 async def get_recent(self, limit: int = 100) -> list[Any]:
200 """Return the most recent *limit* log entries.
202 Args:
203 limit: Maximum number of entries (newest-first).
205 Returns:
206 List of ``InferenceLog`` dataclass instances.
207 """
208 ...