Files
projectAIpopular/tests/test_proxy_budget.py
T
tzt a043493548 feat(proxy): T-X2 上游有序降级链 + failover 三元组如实上报(采纳 cortiq tier 链)
- routes._fallback_chain:主条目之外的启用真实后端按(档位升序,单价和升序)
  构成有序候选链(≤3),排除 mock/停用/同模型,仅作 D-P4 首 token 前 failover
- _stream_response/_json_response 接入链式候选(原 [entry,entry] 同条目重试升级为真降级链)
- upstream.stream:failover 发生即记 sink[upstream_fallback] 与模块级统计;
  JSON 路径经 X-Upstream-Fallback/Original/Used/Reason 四头如实上报
  (流式路径头已发出不可追溯,由统计聚合暴露)
- /proxy/admin/stats 新增 upstream_failover 块(total + recent 20 条)

pytest 447 passed(T-X1 后 443 + 4)
2026-09-18 22:35:53 +08:00

135 lines
5.4 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-X1):budget_mode 黄金边界 + 降档选择器。
黄金用例锁死(整数基点 bp,1% = 100bp):
projected < 8000bp -> normal
8000bp <= projected -> optimize
9500bp <= projected -> cheap
projected > 10000bp -> blocktry_hold 硬拒兜底)
"""
import pytest
from gateway.model_pool import PoolStore
from gateway.proxy.billing import BUDGET_CHEAP_BP, BUDGET_OPTIMIZE_BP
from gateway.proxy.ledger import Ledger
from gateway.proxy.ledgerutil import _today
from gateway.proxy.routes import _budget_headers, _downgrade_entry
pytest.importorskip("fastapi")
_TS = 1789874000.0 # 固定时间戳(黄金用例确定性)
def _make_ledger(tmp_path, daily_cap_yuan: float = 10.0) -> Ledger:
led = Ledger.init_db(tmp_path / "budget.sqlite3")
led.upsert_student("张三", "软件2201", balance_yuan=100.0,
daily_cap_yuan=daily_cap_yuan)
return led
def _set_spent(led: Ledger, student_id: int, spent_milli: int,
spent_date: str = "") -> None:
"""直接写当日已花(白盒:budget_mode 的输入口径与 try_hold 一致)。"""
with led._lock, led._connect() as conn:
conn.execute(
"UPDATE students SET spent_today_milli = ?, spent_date = ? WHERE id = ?",
(spent_milli, spent_date, student_id))
def test_threshold_constants_golden():
"""阈值常量黄金锁定:80% / 95%。"""
assert BUDGET_OPTIMIZE_BP == 8000
assert BUDGET_CHEAP_BP == 9500
@pytest.mark.parametrize("spent,est,expected", [
(0, 7999, "normal"), # 7999bp
(0, 8000, "optimize"), # 恰好 8000bp(边界含头)
(0, 9499, "optimize"),
(0, 9500, "cheap"), # 恰好 9500bp(边界含头)
(0, 10000, "cheap"), # 恰好达上限 = cheap(不 block
(0, 10001, "block"), # 严格超出才 block
(5000, 3000, "optimize"), # 8000bpspent 与 est 合并计算
(5000, 4500, "cheap"), # 9500bp
(5000, 5001, "block"), # 10001bp
])
def test_budget_mode_golden_boundaries(tmp_path, spent, est, expected):
led = _make_ledger(tmp_path, daily_cap_yuan=10.0) # cap = 10000 毫元
_set_spent(led, 1, spent, spent_date=_today(_TS)) # 记账日 = 判定日(当日口径)
assert led.budget_mode(1, est, ts=_TS) == expected
def test_budget_mode_zero_cap_means_unlimited(tmp_path):
led = _make_ledger(tmp_path, daily_cap_yuan=0.0)
sid = 1
_set_spent(led, sid, 999999, spent_date=_today(_TS))
assert led.budget_mode(sid, 10**9, ts=_TS) == "normal"
def test_budget_mode_spent_resets_by_date(tmp_path):
"""spent_date 非今日 -> 当日已花按 0 计(与 try_hold 口径一致)。"""
led = _make_ledger(tmp_path, daily_cap_yuan=10.0)
_set_spent(led, 1, 9999, spent_date="2000-01-01") # 非今日
assert led.budget_mode(1, 1, ts=1789874000.0) == "normal"
def test_budget_mode_missing_student_normal(tmp_path):
led = Ledger.init_db(tmp_path / "b.sqlite3")
assert led.budget_mode(999, 100, ts=1789874000.0) == "normal"
def _make_pool(tmp_path) -> PoolStore:
import gateway.model_pool as mp
store = PoolStore(path=tmp_path / "pool.json") # 显式隔离,防污染真实池文件
mp.reset_pool()
store.upsert({"id": "p1", "name": "旗舰", "tier": "premium", "backend": "openai",
"base_url": "https://api.example.com", "model": "big-x",
"enabled": True})
store.upsert({"id": "b1", "name": "实惠", "tier": "budget", "backend": "openai",
"base_url": "https://api.example.com/v2", "model": "mid-y",
"enabled": True})
store.upsert({"id": "l1", "name": "本地", "tier": "local",
"backend": "llama_server", "base_url": "http://127.0.0.1:8901",
"model": "qwen-local", "enabled": True})
return store
def test_downgrade_optimize_one_tier(tmp_path):
pool = _make_pool(tmp_path)
premium = pool.find_by_model("big-x")
down = _downgrade_entry(pool, premium, "optimize")
assert down is not None and down["tier"] == "budget"
def test_downgrade_cheap_to_lowest(tmp_path):
pool = _make_pool(tmp_path)
premium = pool.find_by_model("big-x")
down = _downgrade_entry(pool, premium, "cheap")
assert down is not None and down["tier"] == "local"
def test_downgrade_stops_at_local(tmp_path):
"""已在最低档:cheap/optimize 均不再降(杜绝反向升档)。"""
pool = _make_pool(tmp_path)
local = pool.find_by_model("qwen-local")
assert _downgrade_entry(pool, local, "cheap") is None
assert _downgrade_entry(pool, local, "optimize") is None
def test_downgrade_skips_disabled_and_mock(tmp_path):
pool = _make_pool(tmp_path)
pool.upsert({"id": "b2", "name": "停用", "tier": "budget", "backend": "openai",
"base_url": "https://api.example.com/v3", "model": "mid-z",
"enabled": False})
pool.upsert({"id": "mk", "name": "假", "tier": "budget", "backend": "mock",
"model": "mock", "enabled": True})
premium = pool.find_by_model("big-x")
down = _downgrade_entry(pool, premium, "optimize")
assert down is not None and down["id"] == "b1" # 跳过停用与 mock
def test_budget_headers_only_when_abnormal():
assert _budget_headers("normal") == {}
assert _budget_headers("") == {}
assert _budget_headers("optimize") == {"X-Budget-Mode": "optimize"}
assert _budget_headers("cheap") == {"X-Budget-Mode": "cheap"}