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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user