"""档位路由:tier -> 回退链 [(provider, model), ...](ARCH §4.3/§4.5)。 - `resolve_route(tier)`:单路由(M1 兼容;返回链首)。 - `resolve_chain(tier)`:回退链。默认实现只读全局 `config.tier_defaults`(单元素链)。 真正的多 provider 回退链由 apps/api 按 DB `tier_routing.fallback` 构造一个 `ChainResolver` 注入网关(§4.3 三级解析 + §4.5 回退链)。 """ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass from ww_config import get_settings from .types import Tier @dataclass(frozen=True) class Route: provider: str model: str # 解析器缝:tier -> 回退链。apps/api 注入据 DB tier_routing 构造的实现。 ChainResolver = Callable[[Tier], "list[Route]"] def _parse_spec(spec: str) -> Route: provider, sep, model = spec.partition(":") if not sep or not provider or not model: raise ValueError(f"invalid tier route {spec!r}; expected 'provider:model'") return Route(provider=provider, model=model) def resolve_route(tier: Tier) -> Route: spec = get_settings().tier_defaults.get(tier) if not spec: raise ValueError(f"no tier_defaults entry for tier={tier!r}") return _parse_spec(spec) def resolve_chain(tier: Tier) -> list[Route]: """默认链解析:仅全局默认(单元素)。多 provider 回退由注入的 `ChainResolver` 提供。""" return [resolve_route(tier)] def chain_from_routing(tier: Tier, primary: str, fallback: list[str]) -> list[Route]: """据 DB `tier_routing` 行(primary `provider:model` + fallback 列表)构造回退链。 供 apps/api 包成 `ChainResolver` 注入网关;放这里以便单测覆盖解析逻辑。 去重保序:同一 (provider, model) 只保留首次出现。 """ routes: list[Route] = [_parse_spec(primary)] routes.extend(_parse_spec(s) for s in fallback) seen: set[tuple[str, str]] = set() unique: list[Route] = [] for r in routes: key = (r.provider, r.model) if key not in seen: seen.add(key) unique.append(r) return unique