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