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
+100 -3
View File
@@ -9,20 +9,28 @@
telemetry 三层,放进最内层内核的类型测试会让它反向依赖具体实现。
覆盖的生产点(设计 §3.2 逐处改动表的字面量产出方):
`_resolve_usage`、`_resolve_embedding_usage`、`EmbeddingClient._merge`
`TelemetryEmitter.emit_attempt/emit_cache_hit/emit_terminal_failure`。
`_resolve_usage`、`_resolve_embedding_usage`、`_resolve_stream_usage`(打捞覆盖)
`EmbeddingClient._merge`、`EmbeddingClient.embed` 空输入短路、
`OcrClient._emit`、`TelemetryEmitter.emit_attempt/emit_cache_hit/emit_terminal_failure`。
"""
import itertools
import json
import httpx
import pytest
from polygateway.backends.memory.breaker import InMemoryGate
from polygateway.backends.memory.limiter import InMemoryLimiter
from polygateway.embedding import EmbeddingClient, _BatchOutcome
from polygateway.middleware.telemetry import TelemetryEmitter
from polygateway.ocr import OcrClient
from polygateway.sources import RoundRobinSelector
from polygateway.transports.openai_compat import _resolve_embedding_usage, _resolve_usage
from polygateway.transports.openai_compat import (
OpenAICompatTransport,
_resolve_embedding_usage,
_resolve_usage,
)
from polygateway.types import (
USAGE_SOURCES,
BackpressurePolicy,
@@ -31,6 +39,7 @@ from polygateway.types import (
EmbeddingTransportResult,
GlobalLimits,
LLMResponse,
OcrTextTransportResult,
RetryPolicy,
SourceConfig,
)
@@ -89,6 +98,94 @@ def test_resolve_embedding_usage_stays_in_domain(data):
assert _resolve_embedding_usage(data, _src())[1] in USAGE_SOURCES
def _sse(*frames, done):
"""构造 SSE 响应;done=False 触发打捞路径(`_complete_stream` 的覆盖分支)。"""
text = "".join(f"data: {json.dumps(f)}\n\n" for f in frames) + (
"data: [DONE]\n\n" if done else ""
)
return httpx.Response(200, content=text.encode(), headers={"content-type": "text/event-stream"})
@pytest.mark.parametrize("usage", [{"prompt_tokens": 11, "completion_tokens": 7}, None])
async def test_salvage_override_stays_in_domain(usage):
"""打捞覆盖(`openai_compat._complete_stream`)是第三个字面量产出方。"""
frames = [{"choices": [{"delta": {"content": "partial"}}]}]
if usage is not None:
frames.append({"choices": [], "usage": usage})
transport = OpenAICompatTransport(
client_factory=lambda src: httpx.AsyncClient(
base_url=src.base_url,
transport=httpx.MockTransport(lambda request: _sse(*frames, done=False)),
)
)
source = SourceConfig(
name="s1",
provider="qwen",
base_url="https://gw.example/v1",
api_key="sk",
model="m",
timeout_s=10.0,
est_tokens=4000,
missing_done="salvage",
)
result = await transport.complete(
messages=[{"role": "user", "content": "hi"}],
source=source,
stream=True,
overlay={},
call_id="cid",
)
assert result.usage_source in USAGE_SOURCES
class _ScriptedOcrTransport:
async def recognize_text(self, *, image, source, call_id):
return OcrTextTransportResult(text="LINE-1", raw={"task_type": "text"})
async def parse_layout(self, *, image, source, call_id):
raise NotImplementedError
async def check_health(self, *, source):
raise NotImplementedError
async def test_ocr_emit_stays_in_domain():
"""`OcrClient._emit` 的字面量(ocr.py:411)同样纳入封闭性断言。
值取 `measured` 是设计 §3.3 的裁决(OCR 的 0 token 属事实);此处只断言
落在三态内,精确取值的防回归钉在 `test_ocr_client.py`。
"""
source = SourceConfig(
name="m1",
provider="monkey",
base_url="http://gw.example",
api_key="none",
model="monkey-ocr",
timeout_s=10.0,
)
recorder = _MemoryRecorder()
client = OcrClient(
scope="ocr",
sources=[source],
selector=RoundRobinSelector(),
limiter=InMemoryLimiter(
scope="ocr",
sources={source.name: source},
global_limits=GlobalLimits(max_concurrency=0, rpm=0, tpm=0),
lease_ttl_s=100.0,
),
breaker=InMemoryGate(
config=BreakerConfig(fail_threshold=3, cooldown_s=60.0, probe_ttl_s=120.0)
),
transport=_ScriptedOcrTransport(),
retry=RetryPolicy(max_attempts=1, backoff_base_s=0.001, backoff_max_s=0.01),
backpressure=BackpressurePolicy(stall_window_s=300.0, poll_interval_s=0.001),
telemetry=recorder,
)
await client.recognize_text(b"jpg")
assert recorder.rows[0]["usage_source"] in USAGE_SOURCES
def _merge_client():
"""构造仅用于调用 `_merge` 的最小 EmbeddingClient(不发起任何调用)。"""
source = _src()