feat: collect provider cache tokens and reported model in transport

This commit is contained in:
2026-07-31 07:51:47 -04:00
parent 4841d901af
commit 0aa7202c87
2 changed files with 154 additions and 1 deletions
@@ -45,6 +45,9 @@ def _sse_delta(chunk: dict[str, Any], usage_sink: dict[str, Any]) -> tuple[bool,
"""从 chunk 提取增量: (True, content) 或 (False, reasoning);usage 帧旁路进 sink。"""
if chunk.get("usage"):
usage_sink["usage"] = chunk["usage"]
if "model" not in usage_sink and chunk.get("model") is not None:
# 首次写入即固定: 末帧的异常值不得覆盖首帧报的真实版本(issue #3)
usage_sink["model"] = chunk["model"]
choices = chunk.get("choices") or []
if not choices:
return None
@@ -152,6 +155,32 @@ def _resolve_usage(usage: dict[str, Any]) -> tuple[int, int, str]:
return 0, 0, "unavailable"
def _coerce_cached_tokens(usage: Any) -> int | None:
"""取 usage.prompt_tokens_details.cached_tokens(issue #3);形态异常一律 None。
`0` 与 `None` 必须可区分: 前者是"该源上报了一次真实零命中",后者是"该源
不报这个数",下游对两者的处置不同(后者不可做缓存成本校正)。故只把
**负数与非整数**归 None,`0` 如实保留。`bool` 显式排除——isinstance(True, int)
在 Python 里为真,放行会把 `True` 记成 1 个命中 token。
"""
if not isinstance(usage, dict):
return None
details = usage.get("prompt_tokens_details")
if not isinstance(details, dict):
return None
cached = details.get("cached_tokens")
if isinstance(cached, bool) or not isinstance(cached, int) or cached < 0:
return None
return cached
def _coerce_model_reported(value: Any) -> str | None:
"""取响应体的 model 字段(issue #3);非 str 或空白串一律 None。"""
if not isinstance(value, str) or not value.strip():
return None
return value
def _resolve_stream_usage(sink: dict[str, Any], salvaged: bool) -> tuple[int, int, str]:
"""流式用量口径: 打捞路径把 measured 降级为 estimated,unavailable 原样保留。
@@ -360,6 +389,8 @@ class OpenAICompatTransport:
ttft_ms=ttft_ms,
max_inter_token_ms=(max_gap if ttft_ms is not None else None),
raw={"usage": sink.get("usage")},
cached_prompt_tokens=_coerce_cached_tokens(sink.get("usage")),
model_reported=_coerce_model_reported(sink.get("model")),
)
def _check_done(
@@ -442,6 +473,8 @@ class OpenAICompatTransport:
ttft_ms=None,
max_inter_token_ms=None,
raw={"usage": body.get("usage")},
cached_prompt_tokens=_coerce_cached_tokens(body.get("usage")),
model_reported=_coerce_model_reported(body.get("model")),
)
async def aclose(self) -> None: