generated from coulomb/repo-seed
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>
90 lines
2.9 KiB
Python
90 lines
2.9 KiB
Python
"""
|
|
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"
|
|
)
|