feat(proxy): T-P3 计价(峰谷窗口/毫元整数/差异化售价/黄金用例)
- pricing.py:is_offpeak(含跨午夜窗口、非法回落、±1min 边界语义); compute 全整数毫元——上游成本叠加 offpeak_factor(空闲半价), 学生售价按差异化折扣(in 0.5 / out 0.8,不叠加热闲系数,全天一口价), margin = charged - cost 恒等式 - 未配置价格模型三值零(调用方可拦截);配置换算加载期唯一 - 测试 +9(窗口边界/跨午夜/回落/黄金手算 #1#2/恒等式 10 组扫/零用量/ 稳定性/未知模型/换算不漂移),全量 354 passed
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
"""计价测试(T-P3):峰谷边界(假时钟 ±1 分钟)/ 毫元取整不漂移 / margin 恒等式。"""
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from gateway.proxy.config import ModelPrice, ProxyConfig, build_proxy_config
|
||||
from gateway.proxy.pricing import compute, is_offpeak
|
||||
from gateway.settings import DEFAULTS
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def cfg():
|
||||
return build_proxy_config(DEFAULTS)
|
||||
|
||||
|
||||
# ---------------- 峰谷窗口 ----------------
|
||||
|
||||
def _ts(y, mo, d, h, mi):
|
||||
return time.mktime((y, mo, d, h, mi, 0, 0, 0, -1))
|
||||
|
||||
|
||||
def test_offpeak_boundaries_plus_minus_one_minute():
|
||||
"""窗口 [08:30, 23:59]:边界 ±1 分钟翻转。"""
|
||||
base = _ts(2026, 9, 1, 8, 29)
|
||||
assert is_offpeak(base, "08:30", "23:59") is True # 08:29 空闲
|
||||
assert is_offpeak(base + 60, "08:30", "23:59") is False # 08:30 高峰
|
||||
base2 = _ts(2026, 9, 1, 23, 58)
|
||||
assert is_offpeak(base2, "08:30", "23:59") is False # 23:58 高峰
|
||||
assert is_offpeak(base2 + 120, "08:30", "23:59") is True # 00:00 次日空闲
|
||||
|
||||
|
||||
def test_offpeak_overnight_window():
|
||||
"""跨午夜窗口 [23:00, 06:00]:中午空闲、深夜高峰。"""
|
||||
noon = _ts(2026, 9, 1, 12, 0)
|
||||
deep = _ts(2026, 9, 1, 23, 30)
|
||||
early = _ts(2026, 9, 1, 5, 59)
|
||||
assert is_offpeak(noon, "23:00", "06:00") is True
|
||||
assert is_offpeak(deep, "23:00", "06:00") is False
|
||||
assert is_offpeak(early, "23:00", "06:00") is False
|
||||
|
||||
|
||||
def test_offpeak_invalid_window_falls_back():
|
||||
"""非法窗口回落默认 08:30-23:59。"""
|
||||
noon = _ts(2026, 9, 1, 12, 0)
|
||||
assert is_offpeak(noon, "garbage", "!!") is False
|
||||
assert is_offpeak(_ts(2026, 9, 1, 7, 0), "garbage", "!!") is True
|
||||
|
||||
|
||||
# ---------------- 计算与取整 ----------------
|
||||
|
||||
USAGE = {"in_miss": 3_000_000, "in_hit": 1_000_000, "out": 800_000}
|
||||
|
||||
|
||||
def test_compute_peak_math_exact(cfg):
|
||||
"""高峰:成本/售价逐项可手算(黄金用例 #1)。
|
||||
|
||||
deepseek-chat:in_miss=3000, in_hit=100, out=9000(毫元/1M)
|
||||
成本 = 3M×3000/1M + 1M×100/1M + 0.8M×9000/1M = 9000+100+7200 = 16300 毫元
|
||||
售价 = 9000×0.5 + 100×0.5 + 7200×0.8 = 4500+50+5760 = 10310 毫元
|
||||
"""
|
||||
ts = _ts(2026, 9, 1, 12, 0) # 高峰
|
||||
r = compute(USAGE, "deepseek-chat", ts, cfg)
|
||||
assert r["upstream_cost_milli"] == 16300
|
||||
assert r["charged_milli"] == 10310
|
||||
assert r["margin_milli"] == r["charged_milli"] - r["upstream_cost_milli"]
|
||||
|
||||
|
||||
def test_compute_offpeak_half_cost(cfg):
|
||||
"""空闲:上游成本半价(offpeak_factor=0.5),售价不变(黄金用例 #2)。"""
|
||||
ts = _ts(2026, 9, 2, 3, 0) # 空闲
|
||||
r = compute(USAGE, "deepseek-chat", ts, cfg)
|
||||
assert r["upstream_cost_milli"] == 8150 # 16300 × 0.5
|
||||
assert r["charged_milli"] == 10310
|
||||
assert r["margin_milli"] == 2160
|
||||
|
||||
|
||||
def test_margin_identity_holds_for_golden_cases(cfg):
|
||||
"""margin = charged - cost 恒等式(10 组黄金用例,量级/零/大数扫)。"""
|
||||
ts = _ts(2026, 9, 1, 12, 0)
|
||||
cases = [
|
||||
{"in_miss": 0, "in_hit": 0, "out": 0},
|
||||
{"in_miss": 1, "in_hit": 0, "out": 0},
|
||||
{"in_miss": 0, "in_hit": 1, "out": 0},
|
||||
{"in_miss": 0, "in_hit": 0, "out": 1},
|
||||
{"in_miss": 1000, "in_hit": 0, "out": 0},
|
||||
{"in_miss": 0, "in_hit": 1000, "out": 0},
|
||||
{"in_miss": 0, "in_hit": 0, "out": 4096},
|
||||
{"in_miss": 3500, "in_hit": 1500, "out": 800},
|
||||
{"in_miss": 50_000, "in_hit": 10_000, "out": 10_000}, # 智能体任务
|
||||
{"in_miss": 1_000_000, "in_hit": 1_000_000, "out": 100_000},
|
||||
]
|
||||
for usage in cases:
|
||||
r = compute(usage, "deepseek-chat", ts, cfg)
|
||||
assert r["margin_milli"] == r["charged_milli"] - r["upstream_cost_milli"]
|
||||
assert r["upstream_cost_milli"] >= 0 and r["charged_milli"] >= 0
|
||||
# 零用量 -> 三值全零
|
||||
assert compute(cases[0], "deepseek-chat", ts, cfg) == {
|
||||
"upstream_cost_milli": 0, "charged_milli": 0, "margin_milli": 0}
|
||||
|
||||
|
||||
def test_no_drift_integer_only(cfg):
|
||||
"""取整不漂移:分项 round 后求和,重复计算结果稳定一致。"""
|
||||
ts = _ts(2026, 9, 1, 12, 0)
|
||||
usage = {"in_miss": 123_456, "in_hit": 65_432, "out": 7_777}
|
||||
r1 = compute(usage, "deepseek-chat", ts, cfg)
|
||||
r2 = compute(usage, "deepseek-chat", ts, cfg)
|
||||
assert r1 == r2
|
||||
for v in r1.values():
|
||||
assert isinstance(v, int)
|
||||
|
||||
|
||||
def test_unknown_model_free(cfg):
|
||||
"""未配置价格的模型 -> 三值零(调用方可拦截)。"""
|
||||
ts = _ts(2026, 9, 1, 12, 0)
|
||||
assert compute(USAGE, "no-such-model", ts, cfg) == {
|
||||
"upstream_cost_milli": 0, "charged_milli": 0, "margin_milli": 0}
|
||||
|
||||
|
||||
def test_config_milli_conversion_no_drift():
|
||||
"""配置换算唯一性:元/1M 浮点 -> 毫元/1M 整数只发生一次。"""
|
||||
c = build_proxy_config({"proxy": {"pricing": {
|
||||
"m1": {"in_miss": 0.003, "in_hit": 0.0001, "out": 0.009},
|
||||
"m2": {"in_miss": 12.3456, "out": 1.5}}}})
|
||||
assert c.price("m1").in_miss == 3
|
||||
assert c.price("m1").in_hit == 1 # 0.0001 元 -> 0.1 毫元 -> 取整 0 -> 触发缺省 max(1, miss//30)
|
||||
# in_hit 缺省 = in_miss//30(D-P3):0.003 元 -> 3 毫元 -> 0(太小),显式补齐后重验
|
||||
c2 = build_proxy_config({"proxy": {"pricing": {
|
||||
"m1": {"in_miss": 3.0, "in_hit": 0.1, "out": 9.0},
|
||||
"m2": {"in_miss": 12.3456, "out": 1.5}}}})
|
||||
assert c2.price("m2").in_miss == 12346
|
||||
assert c2.price("m2").in_hit == 12346 // 30 # 411
|
||||
Reference in New Issue
Block a user