2025-12-12 15:42:45 +08:00
|
|
|
|
"""
|
|
|
|
|
|
流式处理上下文 - 类型安全的数据类替代 dict
|
|
|
|
|
|
|
|
|
|
|
|
提供流式请求处理过程中的状态跟踪,包括:
|
|
|
|
|
|
- Provider/Endpoint/Key 信息
|
|
|
|
|
|
- Token 统计
|
|
|
|
|
|
- 响应状态
|
|
|
|
|
|
- 请求/响应数据
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-30 12:43:08 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2026-02-11 11:30:29 +08:00
|
|
|
|
import json
|
2025-12-16 02:39:03 +08:00
|
|
|
|
import time
|
2026-03-18 23:38:26 +08:00
|
|
|
|
from contextlib import contextmanager
|
2025-12-12 15:42:45 +08:00
|
|
|
|
from dataclasses import dataclass, field
|
2026-03-18 23:38:26 +08:00
|
|
|
|
from typing import TYPE_CHECKING, Any, Iterator
|
2026-01-21 18:07:10 +08:00
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
2026-01-27 02:17:18 +08:00
|
|
|
|
from src.core.api_format.conversion.stream_state import StreamState
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-11 11:30:29 +08:00
|
|
|
|
def extract_proxy_timing(proxy_info: dict[str, Any] | None, headers: dict[str, str]) -> None:
|
|
|
|
|
|
"""从响应头中提取代理分阶段耗时(X-Proxy-Timing)写入 proxy_info"""
|
|
|
|
|
|
if proxy_info is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
timing_raw = headers.get("x-proxy-timing")
|
|
|
|
|
|
if not timing_raw:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
timing = json.loads(timing_raw)
|
|
|
|
|
|
if isinstance(timing, dict):
|
|
|
|
|
|
proxy_info["timing"] = timing
|
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-07 02:59:02 +08:00
|
|
|
|
def is_format_converted(
|
|
|
|
|
|
provider_api_format: str | None,
|
|
|
|
|
|
client_api_format: str | None,
|
|
|
|
|
|
) -> bool:
|
|
|
|
|
|
"""client 与 provider 的 api_format 是否真正不同(用于 usage 展示层)"""
|
|
|
|
|
|
return bool(
|
|
|
|
|
|
provider_api_format
|
|
|
|
|
|
and client_api_format
|
|
|
|
|
|
and provider_api_format.strip().lower() != client_api_format.strip().lower()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-10 15:33:46 +08:00
|
|
|
|
_MAX_COLLECTED_TEXT_CHARS = 16 * 1024
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 23:38:26 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class RecordedStreamBodies:
|
|
|
|
|
|
"""统一封装 telemetry/usage 使用的流式响应体引用。"""
|
|
|
|
|
|
|
|
|
|
|
|
response_body: dict[str, Any] | None
|
|
|
|
|
|
client_response_body: dict[str, Any] | None
|
|
|
|
|
|
|
|
|
|
|
|
def ensure_populated(self, ctx: StreamContext, response_time_ms: int) -> None:
|
|
|
|
|
|
"""在 fallback 到需要 body 的路径时按需补建响应体。"""
|
|
|
|
|
|
if self.response_body is None:
|
|
|
|
|
|
self.response_body = ctx.build_response_body(response_time_ms)
|
|
|
|
|
|
if self.client_response_body is None:
|
|
|
|
|
|
self.client_response_body = ctx.build_client_response_body(response_time_ms)
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-12 15:42:45 +08:00
|
|
|
|
@dataclass
|
|
|
|
|
|
class StreamContext:
|
|
|
|
|
|
"""
|
|
|
|
|
|
流式处理上下文
|
|
|
|
|
|
|
|
|
|
|
|
用于在流式请求处理过程中跟踪状态,替代原有的 ctx dict。
|
|
|
|
|
|
所有字段都有类型注解,提供更好的 IDE 支持和运行时类型安全。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# 请求基本信息
|
|
|
|
|
|
model: str
|
|
|
|
|
|
api_format: str
|
2026-02-21 13:17:11 +08:00
|
|
|
|
api_family: str | None = None # 协议族(从 Adapter 层透传)
|
|
|
|
|
|
endpoint_kind: str | None = None # 端点类型(从 Adapter 层透传)
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
2025-12-16 02:39:03 +08:00
|
|
|
|
# 请求标识信息(CLI handler 需要)
|
|
|
|
|
|
request_id: str = ""
|
|
|
|
|
|
user_id: int = 0
|
|
|
|
|
|
api_key_id: int = 0
|
|
|
|
|
|
|
2025-12-12 15:42:45 +08:00
|
|
|
|
# Provider 信息(在请求执行时填充)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
provider_name: str | None = None
|
|
|
|
|
|
provider_id: str | None = None
|
2026-02-04 23:59:45 +08:00
|
|
|
|
provider_type: str | None = None # Provider 类型(如 codex),用于元数据采集
|
2026-02-05 15:57:52 +08:00
|
|
|
|
# Transport 层选中的 base_url(用于 URL 可用性更新/故障转移等场景)
|
|
|
|
|
|
selected_base_url: str | None = None
|
2026-01-30 03:10:21 +08:00
|
|
|
|
endpoint_id: str | None = None
|
|
|
|
|
|
key_id: str | None = None
|
|
|
|
|
|
attempt_id: str | None = None
|
2025-12-16 02:39:03 +08:00
|
|
|
|
attempt_synced: bool = False
|
2026-01-30 03:10:21 +08:00
|
|
|
|
provider_api_format: str | None = None # Provider 的响应格式
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
# 模型映射
|
2026-01-30 03:10:21 +08:00
|
|
|
|
mapped_model: str | None = None
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
# Token 统计
|
|
|
|
|
|
input_tokens: int = 0
|
|
|
|
|
|
output_tokens: int = 0
|
|
|
|
|
|
cached_tokens: int = 0
|
|
|
|
|
|
cache_creation_tokens: int = 0
|
2026-02-28 11:44:08 +08:00
|
|
|
|
cache_creation_tokens_5m: int = 0 # 5min TTL 缓存创建
|
|
|
|
|
|
cache_creation_tokens_1h: int = 0 # 1h TTL 缓存创建
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
# 响应内容
|
2026-01-30 03:10:21 +08:00
|
|
|
|
_collected_text_parts: list[str] = field(default_factory=list, repr=False)
|
2026-03-10 15:33:46 +08:00
|
|
|
|
_collected_text_chars: int = field(default=0, repr=False)
|
|
|
|
|
|
_stored_collected_text_chars: int = field(default=0, repr=False)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
response_id: str | None = None
|
|
|
|
|
|
final_usage: dict[str, Any] | None = None
|
|
|
|
|
|
final_response: dict[str, Any] | None = None
|
2025-12-16 02:39:03 +08:00
|
|
|
|
|
|
|
|
|
|
# 时间指标
|
2026-01-30 03:10:21 +08:00
|
|
|
|
first_byte_time_ms: int | None = None # 首字时间 (TTFB - Time To First Byte)
|
2025-12-16 02:39:03 +08:00
|
|
|
|
start_time: float = field(default_factory=time.time)
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
# 响应状态
|
|
|
|
|
|
status_code: int = 200
|
2026-01-30 03:10:21 +08:00
|
|
|
|
error_message: str | None = None # 客户端友好的错误消息
|
|
|
|
|
|
upstream_response: str | None = None # 原始 Provider 响应(用于请求链路追踪)
|
2025-12-12 15:42:45 +08:00
|
|
|
|
has_completion: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
# 请求/响应数据
|
2026-01-30 03:10:21 +08:00
|
|
|
|
response_headers: dict[str, str] = field(default_factory=dict) # 提供商响应头
|
|
|
|
|
|
client_response_headers: dict[str, str] = field(default_factory=dict) # 返回给客户端的响应头
|
|
|
|
|
|
provider_request_headers: dict[str, str] = field(default_factory=dict)
|
|
|
|
|
|
provider_request_body: dict[str, Any] | None = None
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
2025-12-16 02:39:03 +08:00
|
|
|
|
# 格式转换信息(CLI handler 需要)
|
|
|
|
|
|
client_api_format: str = ""
|
2026-01-22 01:48:56 +08:00
|
|
|
|
needs_conversion: bool = False # 是否需要跨格式转换(由 handler 层设置)
|
2025-12-16 02:39:03 +08:00
|
|
|
|
|
|
|
|
|
|
# Provider 响应元数据(CLI handler 需要)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
response_metadata: dict[str, Any] = field(default_factory=dict)
|
2025-12-16 02:39:03 +08:00
|
|
|
|
|
2026-01-22 14:17:59 +08:00
|
|
|
|
# 整流标记(Thinking Rectifier)
|
|
|
|
|
|
rectified: bool = False # 请求是否经过整流(移除 thinking 块后重试)
|
|
|
|
|
|
|
2025-12-12 15:42:45 +08:00
|
|
|
|
# 流式处理统计
|
|
|
|
|
|
data_count: int = 0
|
|
|
|
|
|
chunk_count: int = 0
|
2026-01-30 03:10:21 +08:00
|
|
|
|
parsed_chunks: list[dict[str, Any]] = field(default_factory=list)
|
2026-02-21 01:56:28 +08:00
|
|
|
|
# 格式转换时保留提供商原始 chunks(转换前的数据)
|
|
|
|
|
|
provider_parsed_chunks: list[dict[str, Any]] = field(default_factory=list)
|
2026-02-03 21:32:53 +08:00
|
|
|
|
# 是否记录 parsed_chunks(可用于降低高并发/长流式响应的内存占用)
|
|
|
|
|
|
record_parsed_chunks: bool = True
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
2026-02-05 14:22:11 +08:00
|
|
|
|
# 性能采集(可选)
|
|
|
|
|
|
perf_sampled: bool = False
|
|
|
|
|
|
perf_metrics: dict[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
|
2026-02-10 17:37:53 +08:00
|
|
|
|
# 代理信息(用于 usage 记录和日志,含 ttfb_ms)
|
2026-02-08 00:49:43 +08:00
|
|
|
|
proxy_info: dict[str, Any] | None = None
|
|
|
|
|
|
|
2026-02-27 13:54:46 +08:00
|
|
|
|
# 号池调度摘要(来自 ExecutionResult.pool_summary)
|
|
|
|
|
|
pool_summary: dict[str, Any] | None = None
|
2026-03-03 09:22:20 +08:00
|
|
|
|
# 候选轨迹(来自 ExecutionResult.candidate_keys,写入 usage metadata)
|
|
|
|
|
|
candidate_keys: list[dict[str, Any]] = field(default_factory=list)
|
|
|
|
|
|
# 内部调度审计摘要(重试/故障转移/账号使用轨迹)
|
|
|
|
|
|
scheduling_audit: dict[str, Any] | None = None
|
2026-02-27 13:54:46 +08:00
|
|
|
|
|
2026-01-21 18:07:10 +08:00
|
|
|
|
# 流式格式转换状态(跨 chunk 追踪)
|
2026-01-30 03:10:21 +08:00
|
|
|
|
stream_conversion_state: StreamState | None = None
|
2026-02-20 22:04:46 +08:00
|
|
|
|
stream_conversion_event_count: int = 0 # 流式转换成功的 event 计数
|
2026-01-21 18:07:10 +08:00
|
|
|
|
|
2025-12-12 15:42:45 +08:00
|
|
|
|
def reset_for_retry(self) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
重试时重置状态
|
|
|
|
|
|
|
|
|
|
|
|
在故障转移重试时调用,清除之前的数据避免累积。
|
|
|
|
|
|
保留 model 和 api_format,重置其他所有状态。
|
|
|
|
|
|
"""
|
2026-03-18 23:38:26 +08:00
|
|
|
|
self.release_recorded_chunks()
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.chunk_count = 0
|
|
|
|
|
|
self.data_count = 0
|
|
|
|
|
|
self.has_completion = False
|
2025-12-16 02:39:03 +08:00
|
|
|
|
self._collected_text_parts = []
|
2026-03-10 15:33:46 +08:00
|
|
|
|
self._collected_text_chars = 0
|
|
|
|
|
|
self._stored_collected_text_chars = 0
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.input_tokens = 0
|
|
|
|
|
|
self.output_tokens = 0
|
|
|
|
|
|
self.cached_tokens = 0
|
|
|
|
|
|
self.cache_creation_tokens = 0
|
2026-02-28 11:44:08 +08:00
|
|
|
|
self.cache_creation_tokens_5m = 0
|
|
|
|
|
|
self.cache_creation_tokens_1h = 0
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.error_message = None
|
2026-01-10 18:43:53 +08:00
|
|
|
|
self.upstream_response = None
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.status_code = 200
|
2025-12-16 02:39:03 +08:00
|
|
|
|
self.first_byte_time_ms = None
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.response_headers = {}
|
2026-01-10 18:43:53 +08:00
|
|
|
|
self.client_response_headers = {}
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.provider_request_headers = {}
|
|
|
|
|
|
self.provider_request_body = None
|
2025-12-16 02:39:03 +08:00
|
|
|
|
self.response_id = None
|
|
|
|
|
|
self.final_usage = None
|
|
|
|
|
|
self.final_response = None
|
2026-02-08 00:49:43 +08:00
|
|
|
|
self.proxy_info = None
|
2026-03-03 09:22:20 +08:00
|
|
|
|
self.pool_summary = None
|
|
|
|
|
|
self.candidate_keys = []
|
|
|
|
|
|
self.scheduling_audit = None
|
2026-01-21 18:07:10 +08:00
|
|
|
|
self.stream_conversion_state = None
|
2026-02-20 22:04:46 +08:00
|
|
|
|
self.stream_conversion_event_count = 0
|
2026-01-22 01:48:56 +08:00
|
|
|
|
self.needs_conversion = False
|
2026-02-05 15:57:52 +08:00
|
|
|
|
self.selected_base_url = None
|
2025-12-16 02:39:03 +08:00
|
|
|
|
|
2026-03-18 23:38:26 +08:00
|
|
|
|
def release_recorded_chunks(self) -> None:
|
|
|
|
|
|
"""释放 telemetry/usage 已消费完的 chunk 列表,避免后台任务继续持有大对象。"""
|
|
|
|
|
|
self.parsed_chunks = []
|
|
|
|
|
|
self.provider_parsed_chunks = []
|
|
|
|
|
|
|
|
|
|
|
|
@contextmanager
|
|
|
|
|
|
def managed_recorded_bodies(
|
|
|
|
|
|
self,
|
|
|
|
|
|
response_time_ms: int,
|
|
|
|
|
|
*,
|
|
|
|
|
|
include_bodies: bool = True,
|
|
|
|
|
|
) -> Iterator[RecordedStreamBodies]:
|
|
|
|
|
|
"""统一管理响应体构建与 chunk 释放,避免 telemetry 路径重复写 finally。"""
|
|
|
|
|
|
recorded_bodies = RecordedStreamBodies(
|
|
|
|
|
|
response_body=self.build_response_body(response_time_ms) if include_bodies else None,
|
|
|
|
|
|
client_response_body=(
|
|
|
|
|
|
self.build_client_response_body(response_time_ms) if include_bodies else None
|
|
|
|
|
|
),
|
|
|
|
|
|
)
|
|
|
|
|
|
try:
|
|
|
|
|
|
yield recorded_bodies
|
|
|
|
|
|
finally:
|
|
|
|
|
|
self.release_recorded_chunks()
|
|
|
|
|
|
recorded_bodies.response_body = None
|
|
|
|
|
|
recorded_bodies.client_response_body = None
|
|
|
|
|
|
|
2025-12-16 02:39:03 +08:00
|
|
|
|
@property
|
|
|
|
|
|
def collected_text(self) -> str:
|
|
|
|
|
|
"""已收集的文本内容(按需拼接,避免在流式过程中频繁做字符串拷贝)"""
|
|
|
|
|
|
return "".join(self._collected_text_parts)
|
|
|
|
|
|
|
2026-03-10 15:33:46 +08:00
|
|
|
|
@property
|
|
|
|
|
|
def collected_text_length(self) -> int:
|
|
|
|
|
|
"""已收集文本的总字符数(包含未保留到内存的截断部分)"""
|
|
|
|
|
|
return self._collected_text_chars
|
|
|
|
|
|
|
2025-12-16 02:39:03 +08:00
|
|
|
|
def append_text(self, text: str) -> None:
|
2026-03-10 15:33:46 +08:00
|
|
|
|
"""追加文本内容(仅保留有限前缀,避免长流导致内存增长)"""
|
|
|
|
|
|
if not text:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
text_len = len(text)
|
|
|
|
|
|
self._collected_text_chars += text_len
|
|
|
|
|
|
|
|
|
|
|
|
remaining = _MAX_COLLECTED_TEXT_CHARS - self._stored_collected_text_chars
|
|
|
|
|
|
if remaining <= 0:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
if text_len <= remaining:
|
2025-12-16 02:39:03 +08:00
|
|
|
|
self._collected_text_parts.append(text)
|
2026-03-10 15:33:46 +08:00
|
|
|
|
self._stored_collected_text_chars += text_len
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
self._collected_text_parts.append(text[:remaining])
|
|
|
|
|
|
self._stored_collected_text_chars += remaining
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
|
|
|
|
|
def update_provider_info(
|
|
|
|
|
|
self,
|
|
|
|
|
|
provider_name: str,
|
|
|
|
|
|
provider_id: str,
|
|
|
|
|
|
endpoint_id: str,
|
|
|
|
|
|
key_id: str,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
provider_api_format: str | None = None,
|
2025-12-12 15:42:45 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
"""更新 Provider 信息"""
|
|
|
|
|
|
self.provider_name = provider_name
|
|
|
|
|
|
self.provider_id = provider_id
|
|
|
|
|
|
self.endpoint_id = endpoint_id
|
|
|
|
|
|
self.key_id = key_id
|
|
|
|
|
|
self.provider_api_format = provider_api_format
|
|
|
|
|
|
|
|
|
|
|
|
def update_usage(
|
|
|
|
|
|
self,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
input_tokens: int | None = None,
|
|
|
|
|
|
output_tokens: int | None = None,
|
|
|
|
|
|
cached_tokens: int | None = None,
|
|
|
|
|
|
cache_creation_tokens: int | None = None,
|
2025-12-12 15:42:45 +08:00
|
|
|
|
) -> None:
|
2025-12-16 00:02:49 +08:00
|
|
|
|
"""
|
|
|
|
|
|
更新 Token 使用统计
|
|
|
|
|
|
|
|
|
|
|
|
采用防御性更新策略:只有当新值 > 0 或当前值为 0 时才更新,避免用 0 覆盖已有的正确值。
|
|
|
|
|
|
|
|
|
|
|
|
设计原理:
|
|
|
|
|
|
- 在流式响应中,某些事件可能不包含完整的 usage 信息(字段为 0 或不存在)
|
|
|
|
|
|
- 后续事件可能会提供完整的统计数据
|
|
|
|
|
|
- 通过这种策略,确保一旦获得非零值就保留它,不会被后续的 0 值覆盖
|
|
|
|
|
|
|
|
|
|
|
|
示例场景:
|
|
|
|
|
|
- message_start 事件:input_tokens=100, output_tokens=0
|
|
|
|
|
|
- message_delta 事件:input_tokens=0, output_tokens=50
|
|
|
|
|
|
- 最终结果:input_tokens=100, output_tokens=50
|
|
|
|
|
|
|
|
|
|
|
|
注意事项:
|
|
|
|
|
|
- 此策略假设初始值为 0 是正确的默认状态
|
|
|
|
|
|
- 如果需要将已有值重置为 0,请直接修改实例属性(不使用此方法)
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
input_tokens: 输入 tokens 数量
|
|
|
|
|
|
output_tokens: 输出 tokens 数量
|
|
|
|
|
|
cached_tokens: 缓存命中 tokens 数量
|
|
|
|
|
|
cache_creation_tokens: 缓存创建 tokens 数量
|
|
|
|
|
|
"""
|
|
|
|
|
|
if input_tokens is not None and (input_tokens > 0 or self.input_tokens == 0):
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.input_tokens = input_tokens
|
2025-12-16 00:02:49 +08:00
|
|
|
|
if output_tokens is not None and (output_tokens > 0 or self.output_tokens == 0):
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.output_tokens = output_tokens
|
2025-12-16 00:02:49 +08:00
|
|
|
|
if cached_tokens is not None and (cached_tokens > 0 or self.cached_tokens == 0):
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.cached_tokens = cached_tokens
|
2025-12-16 00:02:49 +08:00
|
|
|
|
if cache_creation_tokens is not None and (
|
|
|
|
|
|
cache_creation_tokens > 0 or self.cache_creation_tokens == 0
|
|
|
|
|
|
):
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.cache_creation_tokens = cache_creation_tokens
|
|
|
|
|
|
|
2026-01-10 18:43:53 +08:00
|
|
|
|
def mark_failed(
|
|
|
|
|
|
self,
|
|
|
|
|
|
status_code: int,
|
|
|
|
|
|
error_message: str,
|
2026-01-30 03:10:21 +08:00
|
|
|
|
upstream_response: str | None = None,
|
2026-01-10 18:43:53 +08:00
|
|
|
|
) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
标记请求失败
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
status_code: HTTP 状态码
|
|
|
|
|
|
error_message: 客户端友好的错误消息
|
|
|
|
|
|
upstream_response: 原始 Provider 响应(用于请求链路追踪)
|
|
|
|
|
|
"""
|
2025-12-12 15:42:45 +08:00
|
|
|
|
self.status_code = status_code
|
|
|
|
|
|
self.error_message = error_message
|
2026-01-10 18:43:53 +08:00
|
|
|
|
if upstream_response:
|
|
|
|
|
|
self.upstream_response = upstream_response
|
2025-12-12 15:42:45 +08:00
|
|
|
|
|
2025-12-16 02:39:03 +08:00
|
|
|
|
def record_first_byte_time(self, start_time: float) -> None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
记录首字时间 (TTFB - Time To First Byte)
|
|
|
|
|
|
|
|
|
|
|
|
应在第一次向客户端发送数据时调用。
|
|
|
|
|
|
如果已记录过,则不会覆盖(避免重试时重复记录)。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
start_time: 请求开始时间 (time.time())
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self.first_byte_time_ms is None:
|
|
|
|
|
|
self.first_byte_time_ms = int((time.time() - start_time) * 1000)
|
|
|
|
|
|
|
2026-02-07 02:59:02 +08:00
|
|
|
|
@property
|
|
|
|
|
|
def has_format_conversion(self) -> bool:
|
|
|
|
|
|
"""是否发生了真正的格式转换(client 和 provider 的 api_format 不同)
|
|
|
|
|
|
|
|
|
|
|
|
区别于 needs_conversion:后者包含 envelope rewrite(如 Antigravity v1internal),
|
|
|
|
|
|
不代表客户端与上游的数据格式真正不同。此属性用于 usage 展示层。
|
|
|
|
|
|
"""
|
|
|
|
|
|
return is_format_converted(self.provider_api_format, self.client_api_format)
|
|
|
|
|
|
|
2025-12-12 15:42:45 +08:00
|
|
|
|
def is_success(self) -> bool:
|
|
|
|
|
|
"""检查请求是否成功"""
|
|
|
|
|
|
return self.status_code < 400
|
|
|
|
|
|
|
2026-01-20 15:40:16 +08:00
|
|
|
|
def is_client_disconnected(self) -> bool:
|
|
|
|
|
|
"""检查是否因客户端断开连接而结束"""
|
|
|
|
|
|
return self.status_code == 499
|
|
|
|
|
|
|
2026-03-17 03:37:06 +08:00
|
|
|
|
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 而非 and:CancelledError 路径中 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()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-02-10 17:37:53 +08:00
|
|
|
|
def set_ttfb_ms(self, ms: int) -> None:
|
|
|
|
|
|
"""将首字节响应耗时(TTFB)注入到 proxy_info 中"""
|
|
|
|
|
|
if self.proxy_info is not None:
|
|
|
|
|
|
self.proxy_info["ttfb_ms"] = ms
|
|
|
|
|
|
|
2026-02-11 11:30:29 +08:00
|
|
|
|
def set_proxy_timing(self, headers: dict[str, str]) -> None:
|
|
|
|
|
|
"""从代理响应头中提取分阶段耗时信息(X-Proxy-Timing)"""
|
|
|
|
|
|
extract_proxy_timing(self.proxy_info, headers)
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def build_response_body(self, response_time_ms: int) -> dict[str, Any]:
|
2025-12-12 15:42:45 +08:00
|
|
|
|
"""
|
|
|
|
|
|
构建响应体元数据
|
|
|
|
|
|
|
|
|
|
|
|
用于记录到 Usage 表的 response_body 字段。
|
2026-02-21 01:56:28 +08:00
|
|
|
|
当有格式转换时,返回提供商原始 chunks;否则返回 parsed_chunks。
|
2025-12-12 15:42:45 +08:00
|
|
|
|
"""
|
2026-02-21 01:56:28 +08:00
|
|
|
|
chunks = self.provider_parsed_chunks if self.provider_parsed_chunks else self.parsed_chunks
|
|
|
|
|
|
return {
|
|
|
|
|
|
"chunks": chunks,
|
|
|
|
|
|
"metadata": {
|
|
|
|
|
|
"stream": True,
|
|
|
|
|
|
"total_chunks": len(chunks),
|
|
|
|
|
|
"data_count": self.data_count,
|
|
|
|
|
|
"has_completion": self.has_completion,
|
|
|
|
|
|
"response_time_ms": response_time_ms,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def build_client_response_body(self, response_time_ms: int) -> dict[str, Any] | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
构建客户端侧响应体元数据
|
|
|
|
|
|
|
|
|
|
|
|
仅当有格式转换时返回(parsed_chunks 是转换后的客户端格式);
|
|
|
|
|
|
无格式转换时返回 None(此时 parsed_chunks 已在 response_body 中)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not self.provider_parsed_chunks:
|
|
|
|
|
|
return None
|
2025-12-12 15:42:45 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"chunks": self.parsed_chunks,
|
|
|
|
|
|
"metadata": {
|
|
|
|
|
|
"stream": True,
|
|
|
|
|
|
"total_chunks": len(self.parsed_chunks),
|
|
|
|
|
|
"data_count": self.data_count,
|
|
|
|
|
|
"has_completion": self.has_completion,
|
|
|
|
|
|
"response_time_ms": response_time_ms,
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def get_log_summary(self, request_id: str, response_time_ms: int) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
获取日志摘要
|
|
|
|
|
|
|
|
|
|
|
|
用于请求完成/失败时的日志输出。
|
2025-12-16 02:39:03 +08:00
|
|
|
|
包含首字时间 (TTFB) 和总响应时间,分两行显示。
|
2025-12-12 15:42:45 +08:00
|
|
|
|
"""
|
2026-02-02 21:16:28 +08:00
|
|
|
|
if self.is_success():
|
|
|
|
|
|
status = "OK"
|
|
|
|
|
|
elif self.is_client_disconnected():
|
|
|
|
|
|
status = "CANCEL"
|
|
|
|
|
|
else:
|
|
|
|
|
|
status = "FAIL"
|
2025-12-16 02:39:03 +08:00
|
|
|
|
|
|
|
|
|
|
# 第一行:基本信息 + 首字时间
|
|
|
|
|
|
line1 = (
|
2026-02-01 17:28:00 +08:00
|
|
|
|
f"[{status}] {request_id[:8]} | {self.model} | " f"{self.provider_name or 'unknown'}"
|
2025-12-16 02:39:03 +08:00
|
|
|
|
)
|
|
|
|
|
|
if self.first_byte_time_ms is not None:
|
|
|
|
|
|
line1 += f" | TTFB: {self.first_byte_time_ms}ms"
|
|
|
|
|
|
|
|
|
|
|
|
# 第二行:总响应时间 + tokens
|
|
|
|
|
|
line2 = (
|
|
|
|
|
|
f" Total: {response_time_ms}ms | "
|
2025-12-12 15:42:45 +08:00
|
|
|
|
f"in:{self.input_tokens} out:{self.output_tokens}"
|
|
|
|
|
|
)
|
2025-12-16 02:39:03 +08:00
|
|
|
|
|
|
|
|
|
|
return f"{line1}\n{line2}"
|