Files
Yaojia Wang 765dbdfbd4 feat: M4 文风 + M5 生成/多provider/Skill + Kimi Code 订阅接入 + 本地联调修复
M4(文风): style-auditor 双轨(提取指纹/漂移第四审)+ jobs 长任务框架(zombie reaper) + 回炉 refine + GET /style read-back。
M5(生成+扩展): worldbuilder/character-gen(入库 continuity 409 gate + partition_writes 白名单 + schema→JSONB 形变);
  网关多 provider 回退链/熔断/能力降级(Anthropic/Gemini 适配器);Skill registry + 表权限沙箱 + 规则;
  前端 角色生成器/世界观/Codex/规则页/技能库/⌘K 命令面板。
K1(Kimi Code 订阅接入): OAuth device-flow(kimi-code)+ 静态 Console key(kimi-code-key)两路径;
  coding 端点 KimiCLI 伪造头(实测 UA allow-list 门禁,缺则 403)+ JSON 模式结构化(thinking ⊥ tool_choice)。
本地联调修复: CORS 中间件;assemble 注入 premise+「写第N章」指令(修空 prompt 400);
  GET /outline·/draft read-back + 大纲/工作台/审稿页重载;写页 client/server 常量边界 + notFound 健壮化;
  字数 toLocaleString locale 水合;审稿页终稿从已存草稿 seed(修 accept 422)。
门禁: backend ruff/mypy(157)/alembic 无漂移/pytest 451 · frontend lint/tsc/vitest/build。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 10:39:58 +02:00

64 lines
2.1 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""档位路由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