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"
|
||
|
|
)
|