Files
projectAIpopular/tests/test_route_score.py
T
tzt aa9db718e6 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 项(拒绝理由/排序偏好/同分决胜/留痕)
2026-09-19 10:15:21 +08:00

63 lines
3.0 KiB
Python
Raw 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.
"""五维评分选模与拒绝理由留痕(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