refactor(stream): 将不完整流 token 估算逻辑收敛到 StreamContext

- 新增 has_partial_response / ensure_estimated_output_tokens / should_estimate_incomplete_tokens 方法
- CancelledError 路径在归因前即补充 output_tokens,确保日志包含估算值
- CLI Handler 和 Chat Handler 的兜底估算统一使用 should_estimate_incomplete_tokens
- 移除 cli_monitor_mixin 和 stream_telemetry 中重复的条件判断
- 新增对应单元测试
This commit is contained in:
fawney19
2026-03-17 03:37:06 +08:00
parent 40e0b82fa0
commit 438f16094f
6 changed files with 159 additions and 22 deletions

View File

@@ -170,6 +170,8 @@ class CliMonitorMixin:
# 也可能是服务端(重载/关停/内部取消)导致的协程取消。
# 这里尽量做一次"断连归因":仅当能确认客户端已断开时才记为 499 cancelled。
time_since_last_chunk = time_module.time() - last_chunk_time
if not ctx.has_completion:
ctx.ensure_estimated_output_tokens()
is_client_disconnected = False
disconnect_check_uncertain = False
@@ -287,6 +289,11 @@ class CliMonitorMixin:
bg_db, user, api_key, ctx.request_id, self.client_ip
)
if ctx.should_estimate_incomplete_tokens():
self._estimate_tokens_for_incomplete_stream(
ctx, ctx.provider_request_body or original_request_body
)
response_body = ctx.build_response_body(response_time_ms)
client_response_body = ctx.build_client_response_body(response_time_ms)
@@ -398,17 +405,6 @@ class CliMonitorMixin:
# 流未正常完成(如上游截断/连接中断)且无 token 数据时,
# 从已收集的文本和请求体估算 tokens避免 usage 记录为 0
if (
not ctx.has_completion
and ctx.data_count > 0
and ctx.input_tokens == 0
and ctx.output_tokens == 0
):
# 用实际发给 Provider 的请求体估算 token格式转换时与客户端请求体不同
self._estimate_tokens_for_incomplete_stream(
ctx, ctx.provider_request_body or original_request_body
)
# 流式成功时,返回给客户端的是提供商响应头 + SSE 必需头
client_response_headers = filter_proxy_response_headers(ctx.response_headers)
client_response_headers.update(

View File

@@ -331,6 +331,29 @@ class StreamContext:
"""检查是否因客户端断开连接而结束"""
return self.status_code == 499
def has_partial_response(self) -> bool:
"""是否已收到部分流式响应数据。"""
return self.data_count > 0 or self.chunk_count > 0 or self.collected_text_length > 0
def ensure_estimated_output_tokens(self) -> bool:
"""在缺少 usage 时,基于已收集文本补充输出 tokens。"""
if self.output_tokens > 0 or self.collected_text_length <= 0:
return False
self.output_tokens = max(1, self.collected_text_length // 4)
return True
def should_estimate_incomplete_tokens(self) -> bool:
"""流异常结束且尚无 usage 时,是否应做兜底 token 估算。
使用 or 而非 andCancelledError 路径中 ensure_estimated_output_tokens
可能已补了 output_tokens但 input_tokens 仍为 0此时仍需估算。
"""
return (
not self.has_completion
and (self.input_tokens == 0 or self.output_tokens == 0)
and self.has_partial_response()
)
def set_ttfb_ms(self, ms: int) -> None:
"""将首字节响应耗时TTFB注入到 proxy_info 中"""
if self.proxy_info is not None:

View File

@@ -100,16 +100,9 @@ class StreamTelemetryRecorder:
writer = await self._get_telemetry_writer(bg_db, ctx, response_time_ms)
if writer is None:
return
# 兜底估算:流未正常完成且 token 均为 0 时,从请求体粗略估算
# 覆盖 Chat Handler 路径CLI Handler 在更早的位置已做估算,
# 若已估算过则 token > 0此处条件不会触发
if (
ctx.is_success()
and not ctx.has_completion
and ctx.data_count > 0
and ctx.input_tokens == 0
and ctx.output_tokens == 0
):
# 兜底估算:流未正常完成且 token 均为 0 时,从请求体粗略估算
# 覆盖成功但缺少 completion以及已传出部分数据后被中断的场景。
if ctx.should_estimate_incomplete_tokens():
# 用实际发给 Provider 的请求体估算 token格式转换时与客户端请求体不同
self._estimate_tokens_for_incomplete_stream(
ctx, ctx.provider_request_body or original_request_body