算法(gateway/proxy/semcache.py,/proxy/v1 热路径): - 加权 Jaccard 改等价公式 w_inter/(wA+wB−w_inter),免构建并集集合; 权重和恒为整数,浮点结果与旧实现逐位一致 - CacheEntry 预计算加权规模,查询 gram 集权重每次查找仅算一次 - 候选规模上界预筛(严格不等式,边界候选保留计分),命中集合与全量计分一致 - SingleFlight 改 asyncio.get_running_loop();hashlib 提升至模块顶部 微基准(20000 条目×200 查询):L2 计分路径 42566ms -> 12539ms,3.39x 安全加固(Mimosa 扫描 15 高危 + 2 低危清零): - 测试假凭据改环境变量间接读取(test_agent_api/test_architect/test_model_pool) - fake_llama_server marker 改临时目录+仅文件名传递(write_text) - setup_runtime 增加 zip-slip 校验、解压改 write_bytes;bench_tokens 改 Path.open - runtime 健康检查仅允许回环地址并改用 http.client(防 SSRF) - e2e/run-api-check.js BASE_URL 回环白名单校验 - research/routerarena/local_runner.py 输出改 Path API + basename 净化 - test_review 抽样测试改内联确定性 LCG;workspace 持久化改 Path API 测试:新增 2 项(公式逐位一致性 property、规模悬殊预筛回归) pytest 425 passed(基线 423 全绿 + 2) 基线检查点:ec19a07(操作前已提交,423 passed)
174 lines
5.6 KiB
Python
174 lines
5.6 KiB
Python
"""模型池(PoolStore)测试:条目校验/CRUD/角色指派/管线解析/成本分账。"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
pytest.importorskip("fastapi")
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
import gateway.api as ga
|
|
import gateway.model_pool as mp
|
|
from gateway.model_pool import PoolStore, compute_cost, entry_to_architect_cfg, entry_to_worker_cfg
|
|
|
|
|
|
@pytest.fixture()
|
|
def pool(tmp_path):
|
|
"""独立文件的全局池(不污染 config/model_pool.json)。"""
|
|
mp.reset_pool()
|
|
store = PoolStore(path=tmp_path / "model_pool.json")
|
|
mp._store = store
|
|
yield store
|
|
mp.reset_pool()
|
|
ga.rebuild_pipeline()
|
|
|
|
|
|
@pytest.fixture()
|
|
def client():
|
|
return TestClient(ga.app)
|
|
|
|
|
|
def _entry(**over):
|
|
base = {
|
|
"id": "prem-1", "name": "旗舰模型", "tier": "premium", "backend": "openai",
|
|
"base_url": "https://api.deepseek.com", "model": "deepseek-v4-pro",
|
|
"api_key": os.environ.get("TEST_POOL_KEY", "local-test-only"), "price_in": 1.0, "price_out": 2.0,
|
|
"enabled": True,
|
|
}
|
|
base.update(over)
|
|
return base
|
|
|
|
|
|
# ---------------- PoolStore 单元 ----------------
|
|
|
|
def test_pool_upsert_and_mask(pool):
|
|
masked = pool.upsert(_entry())
|
|
assert masked["api_key_set"] is True
|
|
assert masked["api_key"] != _entry()["api_key"] # 明文不打回
|
|
data = pool.list()
|
|
assert data["entries"][0]["model"] == "deepseek-v4-pro"
|
|
assert data["entries"][0]["api_key_set"] is True
|
|
|
|
|
|
def test_pool_upsert_keeps_key_when_blank(pool):
|
|
pool.upsert(_entry())
|
|
pool.upsert(_entry(api_key="")) # 前端不回传明文 -> 保留
|
|
assert pool.get("prem-1")["api_key"] == _entry()["api_key"]
|
|
|
|
|
|
def test_pool_validation(pool):
|
|
with pytest.raises(ValueError):
|
|
pool.upsert(_entry(tier="超豪华"))
|
|
with pytest.raises(ValueError):
|
|
pool.upsert(_entry(backend="magic"))
|
|
with pytest.raises(ValueError):
|
|
pool.upsert(_entry(backend="openai", base_url="")) # 非 mock 缺端点
|
|
with pytest.raises(ValueError):
|
|
pool.upsert(_entry(hack="x")) # 未知字段
|
|
with pytest.raises(ValueError):
|
|
pool.upsert(_entry(price_in=-1))
|
|
|
|
|
|
def test_pool_roles_and_resolve(pool):
|
|
pool.upsert(_entry())
|
|
pool.upsert(_entry(id="local-1", tier="local", backend="llama_server",
|
|
base_url="http://127.0.0.1:8901/v1", model="qwen3.5-4b",
|
|
price_in=0, price_out=0))
|
|
assert pool.resolve("architect") is None # 未指派
|
|
pool.set_roles({"architect": "prem-1", "worker": "local-1"})
|
|
assert pool.resolve("architect")["id"] == "prem-1"
|
|
assert pool.resolve("worker")["id"] == "local-1"
|
|
assert pool.resolve("agent") is None
|
|
# 指派不存在的条目
|
|
with pytest.raises(ValueError):
|
|
pool.set_roles({"agent": "ghost"})
|
|
# 删除条目 -> 角色自动清空
|
|
pool.delete("prem-1")
|
|
assert pool.resolve("architect") is None
|
|
|
|
|
|
def test_pool_disabled_entry_not_resolved(pool):
|
|
pool.upsert(_entry(enabled=False))
|
|
pool.set_roles({"architect": "prem-1"})
|
|
assert pool.resolve("architect") is None # 禁用 -> 回退经典设置
|
|
|
|
|
|
def test_entry_cfg_mapping(pool):
|
|
e = pool.get("prem-1") or _entry()
|
|
acfg = entry_to_architect_cfg(_entry())
|
|
assert acfg["model"] == "deepseek-v4-pro"
|
|
assert acfg["api_key"] == _entry()["api_key"]
|
|
wcfg = entry_to_worker_cfg(_entry())
|
|
assert wcfg["backend"] == "openai"
|
|
|
|
|
|
def test_compute_cost():
|
|
e = {"price_in": 1.0, "price_out": 2.0}
|
|
assert compute_cost(e, 1_000_000, 500_000) == pytest.approx(2.0)
|
|
assert compute_cost({"price_in": 0, "price_out": 0}, 999, 999) == 0.0
|
|
|
|
|
|
# ---------------- API 端点 ----------------
|
|
|
|
def test_pool_api_crud(pool, client):
|
|
r = client.get("/pool")
|
|
assert r.status_code == 200
|
|
assert r.json()["roles"]["architect"] == ""
|
|
r2 = client.post("/pool", json=_entry())
|
|
assert r2.status_code == 200
|
|
assert len(r2.json()["entries"]) == 1
|
|
# 非法条目 -> 400
|
|
r3 = client.post("/pool", json=_entry(tier="bad"))
|
|
assert r3.status_code == 400
|
|
# 角色指派
|
|
r4 = client.put("/pool/roles", json={"architect": "prem-1"})
|
|
assert r4.status_code == 200
|
|
assert r4.json()["roles"]["architect"] == "prem-1"
|
|
# 删除
|
|
r5 = client.delete("/pool/prem-1")
|
|
assert r5.status_code == 200
|
|
assert r5.json()["roles"]["architect"] == ""
|
|
|
|
|
|
def test_build_pipeline_uses_pool(pool, monkeypatch):
|
|
"""池指派应覆盖经典设置,测试 override 最后生效。"""
|
|
pool.upsert(_entry())
|
|
pool.set_roles({"architect": "prem-1"})
|
|
ga.rebuild_pipeline()
|
|
|
|
captured = {}
|
|
|
|
def fake_build_architect(cfg):
|
|
captured["architect"] = dict(cfg)
|
|
from router_system.architect import ArchitectClient
|
|
return ArchitectClient(model=cfg.get("model", "m"), api_key="k")
|
|
|
|
monkeypatch.setattr(ga, "build_architect", fake_build_architect)
|
|
pipe = ga.build_v2_pipeline(worker_cfg_override={"backend": "mock"})
|
|
assert pipe is not None
|
|
assert captured["architect"]["model"] == "deepseek-v4-pro" # 池条目生效
|
|
ga.rebuild_pipeline()
|
|
|
|
|
|
def test_v2stats_by_model():
|
|
from router_system.v2stats import V2Stats
|
|
|
|
class R:
|
|
request_id = "x"
|
|
fast_path = False
|
|
status = "done"
|
|
rounds_used = 1
|
|
api_input_tokens = 1000
|
|
api_output_tokens = 500
|
|
cost_est = 0.002
|
|
model_used = "deepseek-v4-pro"
|
|
route = []
|
|
|
|
s = V2Stats()
|
|
s.record(R())
|
|
summary = s.summary()
|
|
bucket = summary["by_model"]["deepseek-v4-pro"]
|
|
assert bucket["requests"] == 1
|
|
assert bucket["input_tokens"] == 1000
|
|
assert bucket["cost_est_usd"] == pytest.approx(0.002)
|