feat(proxy): T-X13 采纳 ai-model-router 五维评分选模——能力过滤后确定性排序 + 拒绝理由留痕

- routes:能力位硬过滤(T-X6 语义不变)后对存活候选五维评分——
  cost_efficiency=1/(1+均价*2)、capability=上下文余量(不满足为 0)、
  speed/quality=档位秩比互补(local 快 / premium 优)、reliability 权重占位;
  重定向目标从'取首元素'改为'取最高分';降级链按评分降序稳定重排
- 被淘汰候选带人话拒绝理由(vision/tools/上下文窗口三类)写入评分留痕环
  (deque 上限 100),/admin/stats 新增 route_scored 段透出(管理面可观测)
- 硬过滤语义不变:评分只改变链内顺序,不改变谁能存活
- 新增 tests/test_route_score.py 4 项(拒绝理由/排序偏好/同分决胜/留痕)
This commit is contained in:
tzt
2026-09-19 10:15:21 +08:00
parent b2b7a64cb3
commit aa9db718e6
2 changed files with 166 additions and 5 deletions
+62
View File
@@ -0,0 +1,62 @@
"""五维评分选模与拒绝理由留痕(T-X13,采纳 ai-model-router scoring 设计)。"""
import gateway.proxy.routes as R
from gateway.proxy.routes import (_entry_reject_reason, _rank_candidates,
_record_route_score_event,
reset_route_score_events)
def _entry(model, tier="budget", price_in=1.0, price_out=2.0,
vision=True, tools=True, ctx=0):
return {"id": model, "model": model, "enabled": True,
"backend": "openai", "base_url": "http://127.0.0.1:9/v1",
"tier": tier, "price_in": price_in, "price_out": price_out,
"capabilities": {"vision": vision, "tools": tools,
"context_window": ctx}}
_NEEDS = {"vision": True, "tools": False, "min_context_tokens": 1000}
def test_reject_reasons_are_human_readable():
"""vision/tools/上下文三类不满足均有对应人话理由;满足则 None。"""
assert "vision" in (_entry_reject_reason(_entry("a", vision=False), _NEEDS) or "")
need_tools = {"vision": False, "tools": True, "min_context_tokens": 1000}
assert "tools" in (_entry_reject_reason(_entry("a", tools=False), need_tools) or "")
small = _entry("a", ctx=512)
assert "512" in (_entry_reject_reason(small, _NEEDS) or "")
assert _entry_reject_reason(_entry("a", ctx=8192), _NEEDS) is None
def test_rank_prefers_capable_and_cheap():
"""不满足需求者 capability=0 沉底;便宜者靠成本效率维胜出。"""
good_cheap = _entry("cheap", tier="budget", price_in=0.1, price_out=0.2, ctx=8192)
good_pricey = _entry("pricey", tier="premium", price_in=8.0, price_out=8.0, ctx=8192)
ranked = _rank_candidates([good_pricey, good_cheap], _NEEDS)
assert ranked[0]["model"] == "cheap"
assert ranked[0]["dims"]["capability"] > 0
assert ranked[0]["dims"]["cost_efficiency"] > ranked[1]["dims"]["cost_efficiency"]
# premium 的质量维更高(启发式)
assert ranked[1]["dims"]["quality"] > ranked[0]["dims"]["quality"]
def test_rank_tie_break_deterministic():
"""同配置同分按模型名字典序。"""
a, b = _entry("zeta"), _entry("alpha")
ranked = _rank_candidates([a, b], _NEEDS)
assert [s["model"] for s in ranked] == ["alpha", "zeta"]
def test_score_event_records_rejections():
"""评分留痕:入选排序 + 被淘汰候选的拒绝理由;reset 清空。"""
reset_route_score_events()
ranked = _rank_candidates([_entry("good", ctx=8192)], _NEEDS)
rejected = [{"model": "bad", "reason": "模型不支持视觉输入(vision"}]
_record_route_score_event("px-test", ranked, rejected)
_record_route_score_event("px-test-2", ranked, rejected)
assert len(R._route_score_events) == 2
ev = R._route_score_events[-1]
assert ev["request_id"] == "px-test-2"
assert ev["ranked"][0]["model"] == "good"
assert ev["rejected"][0]["reason"].startswith("模型不支持")
reset_route_score_events()
assert len(R._route_score_events) == 0