fix: stop passing est_tokens off as measured usage

usage 帧缺失/非法时不再拿 est_tokens(最坏情形上界)当实测值,chat 与
embedding 两处兜底改记 0 并标 unavailable;打捞覆盖加 measured 前置条件,
避免 0/0 被洗成 estimated 而算出假的 0.0。embedding 全批合并扩三态(任一批
不可得 → 整体不可得),_total_cost 遇不可得批整体记 NULL。
This commit is contained in:
2026-07-30 10:39:32 -04:00
parent 42e429eb58
commit 195454d2e3
6 changed files with 252 additions and 25 deletions
+83 -10
View File
@@ -13,12 +13,14 @@ from polygateway.errors import (
SourceDeadError,
TransientError,
)
from polygateway.middleware.telemetry import TelemetryEmitter
from polygateway.pricing import ModelPrice, PricingTable
from polygateway.transports.openai_compat import (
OpenAICompatTransport,
_iter_sse_deltas,
_sse_data_payload,
)
from polygateway.types import SourceConfig
from polygateway.types import ChatRequest, LLMResponse, SourceConfig
def _source(**overrides):
@@ -71,6 +73,53 @@ async def _complete(transport, source, *, stream=True, overlay=None):
)
# 单价刻意取"输出贵于输入"的真实形态: est_tokens 兜底把整估值塞进 completion
# 时,虚高才显形(设计 §1 的 26 倍算例即此单价)。
_PRICING = PricingTable({"qwen-max": ModelPrice(input_per_1m=1.0, output_per_1m=8.0)})
class _MemoryRecorder:
def __init__(self):
self.rows = []
async def record_llm_call(self, **fields):
self.rows.append(fields)
async def _recorded_cost(result, source):
"""把 transport 产物走一遍真实计费路径,返回落库的 cost。
`unavailable` → cost=None 的判定在 `TelemetryEmitter` 里(设计 §3.2 #5),
直接调 `PricingTable.cost` 对 `0/0` 只会得到 `0.0`——那正是本组用例要防的
假金额,故断言必须穿过 emitter 而不是单测 pricing。
"""
recorder = _MemoryRecorder()
response = LLMResponse(
content=result.content,
thinking=result.thinking,
model=source.model,
provider=source.provider,
prompt_tokens=result.prompt_tokens,
completion_tokens=result.completion_tokens,
latency_ms=1,
ttft_ms=result.ttft_ms,
max_inter_token_ms=result.max_inter_token_ms,
cache_hit=False,
call_id="cid-1",
source_name=source.name,
usage_source=result.usage_source,
)
await TelemetryEmitter(recorder, pricing=_PRICING).emit_attempt(
request=ChatRequest(messages=[{"role": "user", "content": "hi"}]),
source=source,
call_id="cid-1",
latency_ms=1,
response=response,
error=None,
)
return recorder.rows[0]["cost"]
class TestSsePureFunctions:
def test_data_payload_filters_noise(self):
assert _sse_data_payload("") is None
@@ -129,13 +178,21 @@ class TestStreamHappyPath:
assert result.content == "answer"
assert result.thinking == "hmm"
async def test_usage_missing_falls_back_to_est(self):
async def test_usage_missing_is_unavailable_with_null_cost(self):
"""usage 帧缺失 → 0/0 + unavailable + cost NULL(设计 §3.2 #3)。
改前拿 `est_tokens` 当实测并整估值塞 completion,同一条调用记成
`0/4000` → cost 0.032(设计 §1 的 26 倍虚高)。
"""
def handler(request):
return _sse_stream(_chunk(content="ok"))
result = await _complete(_transport_for(handler), _source(tpm=1000, est_tokens=333))
assert result.usage_source == "estimated"
assert result.prompt_tokens == 0 and result.completion_tokens == 333
source = _source(tpm=1000, est_tokens=4000)
result = await _complete(_transport_for(handler), source)
assert result.usage_source == "unavailable"
assert result.prompt_tokens == 0 and result.completion_tokens == 0
assert await _recorded_cost(result, source) is None
class TestMissingDoneSemantics:
@@ -146,12 +203,28 @@ class TestMissingDoneSemantics:
with pytest.raises(TransientError, match="missing_done|truncated"):
await _complete(_transport_for(self._no_done_handler), _source())
async def test_salvage_policy_keeps_content_as_estimated(self):
result = await _complete(
_transport_for(self._no_done_handler), _source(missing_done="salvage")
)
async def test_salvage_with_usage_frame_degrades_to_estimated(self):
"""打捞且收到 usage 帧: 数字真实、可信度降级 → estimated 且照常计费。"""
source = _source(missing_done="salvage", tpm=1000, est_tokens=4000)
result = await _complete(_transport_for(self._no_done_handler), source)
assert result.content == "partial"
assert result.usage_source == "estimated" # 打捞路径强制 estimated
assert result.usage_source == "estimated"
assert result.prompt_tokens == 11 and result.completion_tokens == 7
assert await _recorded_cost(result, source) == pytest.approx(
11 / 1_000_000 * 1.0 + 7 / 1_000_000 * 8.0
)
async def test_salvage_without_usage_frame_stays_unavailable(self):
"""打捞且 usage 帧缺失: 0/0 不得被洗成 estimated,否则算出假的 0.0(设计 §3.2 #4)。"""
def handler(request):
return _sse_stream(_chunk(content="partial"), done=False)
source = _source(missing_done="salvage", tpm=1000, est_tokens=4000)
result = await _complete(_transport_for(handler), source)
assert result.content == "partial"
assert result.usage_source == "unavailable"
assert await _recorded_cost(result, source) is None
async def test_early_eof_always_transient_even_under_salvage(self):
def handler(request):