160 lines
5.0 KiB
Python
160 lines
5.0 KiB
Python
"""pricing.py 价格表与 TelemetryEmitter 成本换算(M2 设计 §6)。
|
|
|
|
零内置单价(实验室走中转网关,内置表必然过时掩盖真实成本,P5);
|
|
价格由使用方经 JSON 文件或 dict 注入;查不到 → cost=None + 每 model
|
|
仅首次 warning,不阻塞调用;计算点收敛在 Emitter(单一 helper 铁律)。
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from polygateway.middleware.telemetry import TelemetryEmitter
|
|
from polygateway.pricing import ModelPrice, PricingTable
|
|
from polygateway.types import ChatRequest, LLMResponse, SourceConfig
|
|
|
|
_TABLE = PricingTable(
|
|
{
|
|
"qwen-max": ModelPrice(input_per_1m=2.4, output_per_1m=9.6),
|
|
"free-model": ModelPrice(input_per_1m=0.0, output_per_1m=0.0),
|
|
}
|
|
)
|
|
|
|
|
|
class TestPricingTable:
|
|
def test_cost_formula_per_million(self):
|
|
# 1M prompt + 0.5M completion: 2.4 + 4.8
|
|
assert _TABLE.cost("qwen-max", 1_000_000, 500_000) == pytest.approx(7.2)
|
|
assert _TABLE.cost("qwen-max", 1000, 2000) == pytest.approx(0.0216)
|
|
assert _TABLE.cost("free-model", 999, 999) == 0.0
|
|
|
|
def test_unknown_model_returns_none(self):
|
|
assert _TABLE.cost("mystery", 100, 100) is None
|
|
|
|
def test_from_file_roundtrip(self, tmp_path):
|
|
path = tmp_path / "prices.json"
|
|
path.write_text(
|
|
json.dumps({"m1": {"input_per_1m": 1.0, "output_per_1m": 2.0}}), encoding="utf-8"
|
|
)
|
|
table = PricingTable.from_file(path)
|
|
assert table.cost("m1", 1_000_000, 1_000_000) == pytest.approx(3.0)
|
|
|
|
def test_from_file_missing_or_bad_fails_loudly(self, tmp_path):
|
|
with pytest.raises(ValueError, match="价格表"):
|
|
PricingTable.from_file(tmp_path / "nope.json")
|
|
bad = tmp_path / "bad.json"
|
|
bad.write_text("not json", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="价格表"):
|
|
PricingTable.from_file(bad)
|
|
malformed = tmp_path / "malformed.json"
|
|
malformed.write_text(json.dumps({"m": {"input_per_1m": 1.0}}), encoding="utf-8")
|
|
with pytest.raises(ValueError, match="output_per_1m"):
|
|
PricingTable.from_file(malformed)
|
|
|
|
def test_negative_price_rejected(self):
|
|
with pytest.raises(ValueError):
|
|
ModelPrice(input_per_1m=-1.0, output_per_1m=0.0)
|
|
|
|
|
|
class _MemoryRecorder:
|
|
def __init__(self):
|
|
self.rows = []
|
|
|
|
async def record_llm_call(self, **fields):
|
|
self.rows.append(fields)
|
|
|
|
|
|
_REQ = ChatRequest(messages=[{"role": "user", "content": "hi"}])
|
|
|
|
|
|
def _resp(**overrides):
|
|
base = {
|
|
"content": "ok",
|
|
"thinking": "",
|
|
"model": "qwen-max",
|
|
"provider": "qwen",
|
|
"prompt_tokens": 1_000_000,
|
|
"completion_tokens": 500_000,
|
|
"latency_ms": 30,
|
|
"ttft_ms": None,
|
|
"max_inter_token_ms": None,
|
|
"cache_hit": False,
|
|
"call_id": "cid-1",
|
|
"source_name": "s1",
|
|
"usage_source": "measured",
|
|
}
|
|
base.update(overrides)
|
|
return LLMResponse(**base)
|
|
|
|
|
|
def _source(model="qwen-max"):
|
|
return SourceConfig(
|
|
name="s1",
|
|
provider="qwen",
|
|
base_url="https://gw.example/v1",
|
|
api_key="sk",
|
|
model=model,
|
|
timeout_s=10.0,
|
|
)
|
|
|
|
|
|
class TestEmitterCost:
|
|
async def test_success_row_costed(self):
|
|
rec = _MemoryRecorder()
|
|
emitter = TelemetryEmitter(rec, pricing=_TABLE)
|
|
await emitter.emit_attempt(
|
|
request=_REQ,
|
|
source=_source(),
|
|
call_id="c",
|
|
latency_ms=1,
|
|
response=_resp(),
|
|
error=None,
|
|
)
|
|
assert rec.rows[0]["cost"] == pytest.approx(7.2)
|
|
|
|
async def test_cache_hit_row_costs_zero(self):
|
|
rec = _MemoryRecorder()
|
|
emitter = TelemetryEmitter(rec, pricing=_TABLE)
|
|
await emitter.emit_cache_hit(request=_REQ, response=_resp(cache_hit=True))
|
|
assert rec.rows[0]["cost"] == 0.0
|
|
|
|
async def test_failure_row_cost_none(self):
|
|
rec = _MemoryRecorder()
|
|
emitter = TelemetryEmitter(rec, pricing=_TABLE)
|
|
await emitter.emit_attempt(
|
|
request=_REQ,
|
|
source=_source(),
|
|
call_id="c",
|
|
latency_ms=1,
|
|
response=None,
|
|
error="TransientError: boom",
|
|
)
|
|
assert rec.rows[0]["cost"] is None
|
|
|
|
async def test_unknown_model_none_without_blocking(self):
|
|
rec = _MemoryRecorder()
|
|
emitter = TelemetryEmitter(rec, pricing=_TABLE)
|
|
await emitter.emit_attempt(
|
|
request=_REQ,
|
|
source=_source(model="mystery"),
|
|
call_id="c",
|
|
latency_ms=1,
|
|
response=_resp(model="mystery"),
|
|
error=None,
|
|
)
|
|
assert rec.rows[0]["cost"] is None
|
|
|
|
async def test_no_pricing_keeps_none(self):
|
|
"""未注入价格表 = M1 现状: cost 恒 None(回归)。"""
|
|
rec = _MemoryRecorder()
|
|
emitter = TelemetryEmitter(rec)
|
|
await emitter.emit_attempt(
|
|
request=_REQ,
|
|
source=_source(),
|
|
call_id="c",
|
|
latency_ms=1,
|
|
response=_resp(),
|
|
error=None,
|
|
)
|
|
assert rec.rows[0]["cost"] is None
|