generated from coulomb/repo-seed
feat: WP-0003 — RoutingPolicy (FR-2) and HTTP serve mode (FR-1)
FR-2 RoutingPolicy: - RoutingPolicy + RoutingRule dataclasses in llm_connect/routing.py - resolve(task_type, estimated_cost_per_1k=None) with cost-cap fallback - Exported from llm_connect.__init__; contract doc at contracts/functional/routing-policy.md - 11 tests covering rule match, cost-cap, fallback, unknown type, no-match FR-1 HTTP serve mode: - LLMServer in llm_connect/server.py (stdlib http.server, zero extra deps) - POST /execute + GET /health; CLI via python -m llm_connect.server - [server] optional-dep group added to pyproject.toml - Contract doc at contracts/functional/server.md - 9 tests: health, round-trip, 400/404/500 errors, config forwarding - Added "mock" provider to factory for CLI default All 101 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
89
llm_connect/routing.py
Normal file
89
llm_connect/routing.py
Normal file
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
RoutingPolicy — task-type-aware adapter selection (FR-2).
|
||||
|
||||
Maps task types to preferred adapters with optional cost-cap fallback.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, List
|
||||
|
||||
from llm_connect.adapter import LLMAdapter
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoutingRule:
|
||||
"""Single routing rule binding a task type to an adapter.
|
||||
|
||||
Attributes:
|
||||
task_type: Logical task identifier (e.g. ``"triage"``, ``"summarise"``).
|
||||
prefer: Adapter to use when this rule matches.
|
||||
max_cost_per_1k: Optional cost ceiling (USD per 1 000 tokens). When the
|
||||
caller supplies ``estimated_cost_per_1k`` to :meth:`RoutingPolicy.resolve`
|
||||
and it exceeds this cap, *fallback* is returned instead of *prefer*.
|
||||
fallback: Adapter to use when the cost cap is breached.
|
||||
"""
|
||||
|
||||
task_type: str
|
||||
prefer: LLMAdapter
|
||||
max_cost_per_1k: Optional[float] = None
|
||||
fallback: Optional[LLMAdapter] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class RoutingPolicy:
|
||||
"""Route task types to LLM adapters.
|
||||
|
||||
Rules are evaluated in order; the first match wins. When no rule matches,
|
||||
*default* is returned. If *default* is also absent, ``LookupError`` is raised.
|
||||
|
||||
Example::
|
||||
|
||||
policy = RoutingPolicy(
|
||||
rules=[
|
||||
RoutingRule("triage", prefer=fast_adapter, max_cost_per_1k=0.5, fallback=cheap_adapter),
|
||||
RoutingRule("analysis", prefer=smart_adapter),
|
||||
],
|
||||
default=cheap_adapter,
|
||||
)
|
||||
adapter = policy.resolve("triage")
|
||||
"""
|
||||
|
||||
rules: List[RoutingRule] = field(default_factory=list)
|
||||
default: Optional[LLMAdapter] = None
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
task_type: str,
|
||||
estimated_cost_per_1k: Optional[float] = None,
|
||||
) -> LLMAdapter:
|
||||
"""Return the adapter for *task_type*.
|
||||
|
||||
Args:
|
||||
task_type: Logical task identifier.
|
||||
estimated_cost_per_1k: Caller-supplied cost estimate (USD / 1k tokens).
|
||||
When provided and a matching rule has ``max_cost_per_1k`` set, the
|
||||
rule's ``fallback`` is returned if the estimate exceeds the cap.
|
||||
|
||||
Returns:
|
||||
The selected :class:`~llm_connect.adapter.LLMAdapter`.
|
||||
|
||||
Raises:
|
||||
LookupError: No matching rule and no *default* configured.
|
||||
"""
|
||||
for rule in self.rules:
|
||||
if rule.task_type == task_type:
|
||||
if (
|
||||
estimated_cost_per_1k is not None
|
||||
and rule.max_cost_per_1k is not None
|
||||
and estimated_cost_per_1k > rule.max_cost_per_1k
|
||||
and rule.fallback is not None
|
||||
):
|
||||
return rule.fallback
|
||||
return rule.prefer
|
||||
|
||||
if self.default is not None:
|
||||
return self.default
|
||||
|
||||
raise LookupError(
|
||||
f"No routing rule for task_type={task_type!r} and no default configured"
|
||||
)
|
||||
Reference in New Issue
Block a user