2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
响应解析器工厂
|
|
|
|
|
|
|
|
|
|
|
|
直接根据格式 ID 创建对应的 ResponseParser 实现,
|
|
|
|
|
|
不再经过 Protocol 抽象层。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
2026-01-16 11:01:05 +08:00
|
|
|
|
import re
|
2026-01-30 03:10:21 +08:00
|
|
|
|
from typing import Any
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
from src.api.handlers.base.response_parser import (
|
|
|
|
|
|
ParsedChunk,
|
|
|
|
|
|
ParsedResponse,
|
|
|
|
|
|
ResponseParser,
|
|
|
|
|
|
StreamStats,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-01-21 18:07:10 +08:00
|
|
|
|
# is_cli_format 权威定义在 core 层
|
|
|
|
|
|
from src.core.api_format import is_cli_format
|
2026-03-17 02:34:32 +08:00
|
|
|
|
from src.core.usage_tokens import extract_cache_creation_tokens, extract_cache_read_tokens
|
2026-01-21 18:07:10 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def _check_nested_error(response: dict[str, Any]) -> tuple[bool, dict[str, Any] | None]:
|
2025-12-11 11:33:07 +08:00
|
|
|
|
"""
|
|
|
|
|
|
检查响应中是否存在嵌套错误(某些代理服务返回 HTTP 200 但在响应体中包含错误)
|
|
|
|
|
|
|
|
|
|
|
|
检测格式:
|
|
|
|
|
|
1. 顶层 error: {"error": {...}}
|
|
|
|
|
|
2. 顶层 type=error: {"type": "error", ...}
|
|
|
|
|
|
3. chunks 内嵌套 error: {"chunks": [{"error": {...}}]}
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
response: 响应字典
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
(is_error, error_dict): 是否为错误,以及提取的错误信息
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 顶层 error
|
|
|
|
|
|
if "error" in response:
|
|
|
|
|
|
error = response["error"]
|
|
|
|
|
|
if isinstance(error, dict):
|
|
|
|
|
|
return True, error
|
|
|
|
|
|
return True, {"message": str(error)}
|
|
|
|
|
|
|
|
|
|
|
|
# 顶层 type=error
|
|
|
|
|
|
if response.get("type") == "error":
|
|
|
|
|
|
return True, response
|
|
|
|
|
|
|
|
|
|
|
|
# chunks 内嵌套 error (某些代理返回这种格式)
|
|
|
|
|
|
chunks = response.get("chunks", [])
|
|
|
|
|
|
if chunks and isinstance(chunks, list):
|
|
|
|
|
|
for chunk in chunks:
|
|
|
|
|
|
if isinstance(chunk, dict):
|
|
|
|
|
|
if "error" in chunk:
|
|
|
|
|
|
error = chunk["error"]
|
|
|
|
|
|
if isinstance(error, dict):
|
|
|
|
|
|
return True, error
|
|
|
|
|
|
return True, {"message": str(error)}
|
|
|
|
|
|
if chunk.get("type") == "error":
|
|
|
|
|
|
return True, chunk
|
|
|
|
|
|
|
|
|
|
|
|
return False, None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def _extract_embedded_status_code(error_info: dict[str, Any] | None) -> int | None:
|
2026-01-16 11:01:05 +08:00
|
|
|
|
"""
|
|
|
|
|
|
从错误信息中提取嵌套的状态码
|
|
|
|
|
|
|
|
|
|
|
|
支持多种格式:
|
|
|
|
|
|
1. 直接的 code 字段: {"code": 400}
|
|
|
|
|
|
2. status 字段: {"status": 400}
|
|
|
|
|
|
3. 从 message 中正则提取: "Request failed with status code 400"
|
|
|
|
|
|
4. 从 type 字段映射: "invalid_request_error" -> 400
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
error_info: 错误信息字典
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
提取的状态码,如果无法提取则返回 None
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not error_info:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 1. 直接的 code 字段(Gemini 等)
|
|
|
|
|
|
code = error_info.get("code")
|
|
|
|
|
|
if isinstance(code, int) and 100 <= code < 600:
|
|
|
|
|
|
return code
|
|
|
|
|
|
if isinstance(code, str) and code.isdigit():
|
|
|
|
|
|
code_int = int(code)
|
|
|
|
|
|
if 100 <= code_int < 600:
|
|
|
|
|
|
return code_int
|
|
|
|
|
|
|
|
|
|
|
|
# 2. status 字段
|
|
|
|
|
|
status = error_info.get("status")
|
|
|
|
|
|
if isinstance(status, int) and 100 <= status < 600:
|
|
|
|
|
|
return status
|
|
|
|
|
|
if isinstance(status, str) and status.isdigit():
|
|
|
|
|
|
status_int = int(status)
|
|
|
|
|
|
if 100 <= status_int < 600:
|
|
|
|
|
|
return status_int
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 从 message 中正则提取 (例如 "Request failed with status code 400")
|
|
|
|
|
|
message = error_info.get("message", "")
|
|
|
|
|
|
if message:
|
|
|
|
|
|
# 匹配 "status code XXX" 或 "status XXX" 或 "HTTP XXX"
|
|
|
|
|
|
match = re.search(r"(?:status\s*(?:code\s*)?|HTTP\s*)(\d{3})", message, re.IGNORECASE)
|
|
|
|
|
|
if match:
|
|
|
|
|
|
code_int = int(match.group(1))
|
|
|
|
|
|
if 100 <= code_int < 600:
|
|
|
|
|
|
return code_int
|
|
|
|
|
|
|
|
|
|
|
|
# 4. 从 type 字段映射常见的错误类型
|
|
|
|
|
|
error_type = error_info.get("type", "")
|
|
|
|
|
|
type_to_status = {
|
|
|
|
|
|
"invalid_request_error": 400,
|
|
|
|
|
|
"authentication_error": 401,
|
|
|
|
|
|
"permission_error": 403,
|
|
|
|
|
|
"not_found_error": 404,
|
|
|
|
|
|
"rate_limit_error": 429,
|
|
|
|
|
|
"overloaded_error": 503,
|
|
|
|
|
|
"api_error": 500,
|
|
|
|
|
|
"internal_error": 500,
|
|
|
|
|
|
}
|
|
|
|
|
|
if error_type and error_type.lower() in type_to_status:
|
|
|
|
|
|
return type_to_status[error_type.lower()]
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
class OpenAIResponseParser(ResponseParser):
|
|
|
|
|
|
"""OpenAI 格式响应解析器"""
|
|
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
API_FORMAT = "openai:chat"
|
|
|
|
|
|
|
2025-12-15 14:30:53 +08:00
|
|
|
|
def __init__(self) -> None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.api.handlers.openai.stream_parser import OpenAIStreamParser
|
|
|
|
|
|
|
|
|
|
|
|
self._parser = OpenAIStreamParser()
|
2026-02-19 21:26:18 +08:00
|
|
|
|
self.name = self.API_FORMAT
|
|
|
|
|
|
self.api_format = self.API_FORMAT
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not line or not line.strip():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
if line.startswith("data: "):
|
|
|
|
|
|
data_str = line[6:]
|
|
|
|
|
|
else:
|
|
|
|
|
|
data_str = line
|
|
|
|
|
|
|
|
|
|
|
|
parsed = self._parser.parse_line(data_str)
|
|
|
|
|
|
if parsed is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
chunk = ParsedChunk(
|
|
|
|
|
|
raw_line=line,
|
|
|
|
|
|
event_type=None,
|
|
|
|
|
|
data=parsed,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取文本增量
|
|
|
|
|
|
text_delta = self._parser.extract_text_delta(parsed)
|
|
|
|
|
|
if text_delta:
|
|
|
|
|
|
chunk.text_delta = text_delta
|
|
|
|
|
|
stats.collected_text += text_delta
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否结束
|
|
|
|
|
|
if self._parser.is_done_chunk(parsed):
|
|
|
|
|
|
chunk.is_done = True
|
|
|
|
|
|
stats.has_completion = True
|
|
|
|
|
|
|
2026-01-05 12:50:05 +08:00
|
|
|
|
# 提取 usage 信息(某些 OpenAI 兼容 API 如豆包会在最后一个 chunk 中发送 usage)
|
|
|
|
|
|
# 这个 chunk 通常 choices 为空数组,但包含完整的 usage 信息
|
2026-01-27 02:17:18 +08:00
|
|
|
|
# 使用取最大值策略确保正确统计
|
2026-01-05 12:50:05 +08:00
|
|
|
|
usage = parsed.get("usage")
|
|
|
|
|
|
if usage and isinstance(usage, dict):
|
|
|
|
|
|
chunk.input_tokens = usage.get("prompt_tokens", 0)
|
|
|
|
|
|
chunk.output_tokens = usage.get("completion_tokens", 0)
|
|
|
|
|
|
|
2026-01-27 02:17:18 +08:00
|
|
|
|
# 取最大值更新 stats
|
|
|
|
|
|
if chunk.input_tokens > stats.input_tokens:
|
|
|
|
|
|
stats.input_tokens = chunk.input_tokens
|
|
|
|
|
|
if chunk.output_tokens > stats.output_tokens:
|
|
|
|
|
|
stats.output_tokens = chunk.output_tokens
|
2026-01-05 12:50:05 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
stats.chunk_count += 1
|
|
|
|
|
|
stats.data_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
return chunk
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result = ParsedResponse(
|
|
|
|
|
|
raw_response=response,
|
|
|
|
|
|
status_code=status_code,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取文本内容
|
|
|
|
|
|
choices = response.get("choices", [])
|
|
|
|
|
|
if choices:
|
|
|
|
|
|
message = choices[0].get("message", {})
|
|
|
|
|
|
content = message.get("content")
|
|
|
|
|
|
if content:
|
|
|
|
|
|
result.text_content = content
|
|
|
|
|
|
|
|
|
|
|
|
result.response_id = response.get("id")
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 usage
|
2026-01-29 09:44:12 +08:00
|
|
|
|
usage = response.get("usage") or {}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result.input_tokens = usage.get("prompt_tokens", 0)
|
|
|
|
|
|
result.output_tokens = usage.get("completion_tokens", 0)
|
2026-03-17 02:34:32 +08:00
|
|
|
|
result.cache_read_tokens = extract_cache_read_tokens(usage)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2025-12-11 11:33:07 +08:00
|
|
|
|
# 检查错误(支持嵌套错误格式)
|
|
|
|
|
|
is_error, error_info = _check_nested_error(response)
|
|
|
|
|
|
if is_error and error_info:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result.is_error = True
|
2025-12-11 11:33:07 +08:00
|
|
|
|
result.error_type = error_info.get("type")
|
|
|
|
|
|
result.error_message = error_info.get("message")
|
2026-01-16 11:01:05 +08:00
|
|
|
|
result.embedded_status_code = _extract_embedded_status_code(error_info)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
|
2026-01-29 09:44:12 +08:00
|
|
|
|
usage = response.get("usage") or {}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"input_tokens": usage.get("prompt_tokens", 0),
|
|
|
|
|
|
"output_tokens": usage.get("completion_tokens", 0),
|
|
|
|
|
|
"cache_creation_tokens": 0,
|
2026-03-17 02:34:32 +08:00
|
|
|
|
"cache_read_tokens": extract_cache_read_tokens(usage),
|
2025-12-10 20:52:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def extract_text_content(self, response: dict[str, Any]) -> str:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
choices = response.get("choices", [])
|
|
|
|
|
|
if choices:
|
|
|
|
|
|
message = choices[0].get("message", {})
|
|
|
|
|
|
content = message.get("content")
|
2025-12-15 14:30:53 +08:00
|
|
|
|
if isinstance(content, str):
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return content
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def is_error_response(self, response: dict[str, Any]) -> bool:
|
2025-12-11 11:33:07 +08:00
|
|
|
|
is_error, _ = _check_nested_error(response)
|
|
|
|
|
|
return is_error
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OpenAICliResponseParser(OpenAIResponseParser):
|
2026-02-07 12:24:42 +08:00
|
|
|
|
"""OpenAI CLI / Responses API 格式响应解析器
|
|
|
|
|
|
|
|
|
|
|
|
OpenAI Responses API 与 Chat Completions API 的关键差异:
|
|
|
|
|
|
- Usage 字段: input_tokens/output_tokens(而非 prompt_tokens/completion_tokens)
|
|
|
|
|
|
- 响应结构: output[].content[].text(而非 choices[].message.content)
|
|
|
|
|
|
- 流式事件: response.completed 事件中 usage 嵌套在 response 对象内
|
|
|
|
|
|
"""
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
API_FORMAT = "openai:cli"
|
|
|
|
|
|
|
2025-12-15 14:30:53 +08:00
|
|
|
|
def __init__(self) -> None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
super().__init__()
|
2026-02-19 21:26:18 +08:00
|
|
|
|
self.name = self.API_FORMAT
|
|
|
|
|
|
self.api_format = self.API_FORMAT
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-07 12:24:42 +08:00
|
|
|
|
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
|
|
|
|
|
|
result = ParsedResponse(
|
|
|
|
|
|
raw_response=response,
|
|
|
|
|
|
status_code=status_code,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Responses API: 文本在 output[].content[].text 中
|
|
|
|
|
|
result.text_content = self._extract_responses_api_text(response)
|
|
|
|
|
|
result.response_id = response.get("id")
|
|
|
|
|
|
|
|
|
|
|
|
# Responses API usage: input_tokens / output_tokens
|
|
|
|
|
|
usage = self._extract_responses_api_usage(response)
|
|
|
|
|
|
result.input_tokens = usage.get("input_tokens", 0)
|
|
|
|
|
|
result.output_tokens = usage.get("output_tokens", 0)
|
|
|
|
|
|
result.cache_creation_tokens = usage.get("cache_creation_tokens", 0)
|
|
|
|
|
|
result.cache_read_tokens = usage.get("cache_read_tokens", 0)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查错误(支持嵌套错误格式)
|
|
|
|
|
|
is_error, error_info = _check_nested_error(response)
|
|
|
|
|
|
if is_error and error_info:
|
|
|
|
|
|
result.is_error = True
|
|
|
|
|
|
result.error_type = error_info.get("type")
|
|
|
|
|
|
result.error_message = error_info.get("message")
|
|
|
|
|
|
result.embedded_status_code = _extract_embedded_status_code(error_info)
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
|
|
|
|
|
|
usage = self._extract_responses_api_usage(response)
|
|
|
|
|
|
return usage
|
|
|
|
|
|
|
|
|
|
|
|
def extract_text_content(self, response: dict[str, Any]) -> str:
|
|
|
|
|
|
return self._extract_responses_api_text(response)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _extract_responses_api_usage(response: dict[str, Any]) -> dict[str, int]:
|
|
|
|
|
|
"""从 Responses API 响应或流式事件中提取 usage
|
|
|
|
|
|
|
|
|
|
|
|
支持多种结构:
|
|
|
|
|
|
1. 顶层 usage(非流式响应 / 部分转换后的响应)
|
|
|
|
|
|
2. response.usage(流式 response.completed 事件)
|
|
|
|
|
|
3. 兼容 Chat Completions 字段名(prompt_tokens/completion_tokens)
|
|
|
|
|
|
"""
|
|
|
|
|
|
usage: dict[str, Any] = {}
|
|
|
|
|
|
|
|
|
|
|
|
# 优先从顶层 usage 提取
|
|
|
|
|
|
top_usage = response.get("usage")
|
|
|
|
|
|
if isinstance(top_usage, dict):
|
|
|
|
|
|
usage = top_usage
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 流式事件: response.completed 中 usage 嵌套在 response 对象内
|
|
|
|
|
|
resp_obj = response.get("response")
|
|
|
|
|
|
if isinstance(resp_obj, dict):
|
|
|
|
|
|
nested_usage = resp_obj.get("usage")
|
|
|
|
|
|
if isinstance(nested_usage, dict):
|
|
|
|
|
|
usage = nested_usage
|
|
|
|
|
|
|
|
|
|
|
|
if not usage:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"input_tokens": 0,
|
|
|
|
|
|
"output_tokens": 0,
|
|
|
|
|
|
"cache_creation_tokens": 0,
|
|
|
|
|
|
"cache_read_tokens": 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# Responses API 使用 input_tokens/output_tokens
|
|
|
|
|
|
# 兼容 Chat Completions 的 prompt_tokens/completion_tokens(以防转换后的响应)
|
|
|
|
|
|
input_tokens = usage.get("input_tokens") or usage.get("prompt_tokens") or 0
|
|
|
|
|
|
output_tokens = usage.get("output_tokens") or usage.get("completion_tokens") or 0
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"input_tokens": int(input_tokens),
|
|
|
|
|
|
"output_tokens": int(output_tokens),
|
2026-03-17 02:34:32 +08:00
|
|
|
|
"cache_creation_tokens": extract_cache_creation_tokens(usage),
|
|
|
|
|
|
"cache_read_tokens": extract_cache_read_tokens(usage),
|
2026-02-07 12:24:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _extract_responses_api_text(response: dict[str, Any]) -> str:
|
|
|
|
|
|
"""从 Responses API 响应中提取文本内容
|
|
|
|
|
|
|
|
|
|
|
|
支持结构: output[].content[].text 或 output[].text
|
|
|
|
|
|
"""
|
|
|
|
|
|
text_parts: list[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
output = response.get("output")
|
|
|
|
|
|
if isinstance(output, list):
|
|
|
|
|
|
for item in output:
|
|
|
|
|
|
if not isinstance(item, dict):
|
|
|
|
|
|
continue
|
|
|
|
|
|
# message 类型: output[].content[].text
|
|
|
|
|
|
if item.get("type") == "message":
|
|
|
|
|
|
content = item.get("content")
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
for part in content:
|
|
|
|
|
|
if isinstance(part, dict):
|
|
|
|
|
|
ptype = str(part.get("type") or "")
|
|
|
|
|
|
if ptype in ("output_text", "text") and isinstance(
|
|
|
|
|
|
part.get("text"), str
|
|
|
|
|
|
):
|
|
|
|
|
|
text_parts.append(part["text"])
|
|
|
|
|
|
# 直接文本类型: output[].text
|
|
|
|
|
|
elif item.get("type") in ("output_text", "text") and isinstance(
|
|
|
|
|
|
item.get("text"), str
|
|
|
|
|
|
):
|
|
|
|
|
|
text_parts.append(item["text"])
|
|
|
|
|
|
|
|
|
|
|
|
# 兼容: 部分实现可能直接给 output_text
|
|
|
|
|
|
if not text_parts and isinstance(response.get("output_text"), str):
|
|
|
|
|
|
text_parts.append(response["output_text"])
|
|
|
|
|
|
|
|
|
|
|
|
# 兼容: 如果是 Chat Completions 格式(可能来自转换后的响应),回退到 choices 结构
|
|
|
|
|
|
if not text_parts:
|
|
|
|
|
|
choices = response.get("choices", [])
|
|
|
|
|
|
if isinstance(choices, list) and choices:
|
|
|
|
|
|
message = choices[0].get("message", {}) if isinstance(choices[0], dict) else {}
|
|
|
|
|
|
content = message.get("content") if isinstance(message, dict) else None
|
|
|
|
|
|
if isinstance(content, str):
|
|
|
|
|
|
text_parts.append(content)
|
|
|
|
|
|
|
|
|
|
|
|
return "".join(text_parts)
|
|
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
class ClaudeResponseParser(ResponseParser):
|
|
|
|
|
|
"""Claude 格式响应解析器"""
|
|
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
API_FORMAT = "claude:chat"
|
|
|
|
|
|
|
2025-12-15 14:30:53 +08:00
|
|
|
|
def __init__(self) -> None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.api.handlers.claude.stream_parser import ClaudeStreamParser
|
|
|
|
|
|
|
|
|
|
|
|
self._parser = ClaudeStreamParser()
|
2026-02-19 21:26:18 +08:00
|
|
|
|
self.name = self.API_FORMAT
|
|
|
|
|
|
self.api_format = self.API_FORMAT
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
if not line or not line.strip():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
if line.startswith("data: "):
|
|
|
|
|
|
data_str = line[6:]
|
|
|
|
|
|
else:
|
|
|
|
|
|
data_str = line
|
|
|
|
|
|
|
|
|
|
|
|
parsed = self._parser.parse_line(data_str)
|
|
|
|
|
|
if parsed is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
chunk = ParsedChunk(
|
|
|
|
|
|
raw_line=line,
|
|
|
|
|
|
event_type=self._parser.get_event_type(parsed),
|
|
|
|
|
|
data=parsed,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取文本增量
|
|
|
|
|
|
text_delta = self._parser.extract_text_delta(parsed)
|
|
|
|
|
|
if text_delta:
|
|
|
|
|
|
chunk.text_delta = text_delta
|
|
|
|
|
|
stats.collected_text += text_delta
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否结束
|
|
|
|
|
|
if self._parser.is_done_event(parsed):
|
|
|
|
|
|
chunk.is_done = True
|
|
|
|
|
|
stats.has_completion = True
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 usage
|
2026-01-27 02:17:18 +08:00
|
|
|
|
# Claude 流式响应的 usage 可能在首个 chunk(message_start)或最后一个 chunk(message_delta)中
|
|
|
|
|
|
# 首个 chunk 通常包含 input_tokens,最后一个 chunk 包含 output_tokens
|
|
|
|
|
|
# 使用取最大值策略确保正确统计
|
2025-12-10 20:52:44 +08:00
|
|
|
|
usage = self._parser.extract_usage(parsed)
|
|
|
|
|
|
if usage:
|
|
|
|
|
|
chunk.input_tokens = usage.get("input_tokens", 0)
|
|
|
|
|
|
chunk.output_tokens = usage.get("output_tokens", 0)
|
|
|
|
|
|
chunk.cache_creation_tokens = usage.get("cache_creation_tokens", 0)
|
|
|
|
|
|
chunk.cache_read_tokens = usage.get("cache_read_tokens", 0)
|
|
|
|
|
|
|
2026-01-27 02:17:18 +08:00
|
|
|
|
# 取最大值更新 stats
|
|
|
|
|
|
if chunk.input_tokens > stats.input_tokens:
|
|
|
|
|
|
stats.input_tokens = chunk.input_tokens
|
|
|
|
|
|
if chunk.output_tokens > stats.output_tokens:
|
|
|
|
|
|
stats.output_tokens = chunk.output_tokens
|
|
|
|
|
|
if chunk.cache_creation_tokens > stats.cache_creation_tokens:
|
|
|
|
|
|
stats.cache_creation_tokens = chunk.cache_creation_tokens
|
|
|
|
|
|
if chunk.cache_read_tokens > stats.cache_read_tokens:
|
|
|
|
|
|
stats.cache_read_tokens = chunk.cache_read_tokens
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查错误
|
|
|
|
|
|
if self._parser.is_error_event(parsed):
|
|
|
|
|
|
chunk.is_error = True
|
|
|
|
|
|
error = parsed.get("error", {})
|
|
|
|
|
|
if isinstance(error, dict):
|
|
|
|
|
|
chunk.error_message = error.get("message", str(error))
|
|
|
|
|
|
else:
|
|
|
|
|
|
chunk.error_message = str(error)
|
|
|
|
|
|
|
|
|
|
|
|
stats.chunk_count += 1
|
|
|
|
|
|
stats.data_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
return chunk
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result = ParsedResponse(
|
|
|
|
|
|
raw_response=response,
|
|
|
|
|
|
status_code=status_code,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取文本内容
|
|
|
|
|
|
content = response.get("content", [])
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
text_parts = []
|
|
|
|
|
|
for block in content:
|
|
|
|
|
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
|
|
|
|
text_parts.append(block.get("text", ""))
|
|
|
|
|
|
result.text_content = "".join(text_parts)
|
|
|
|
|
|
|
|
|
|
|
|
result.response_id = response.get("id")
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 usage
|
2026-01-29 09:44:12 +08:00
|
|
|
|
usage = response.get("usage") or {}
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result.input_tokens = usage.get("input_tokens", 0)
|
|
|
|
|
|
result.output_tokens = usage.get("output_tokens", 0)
|
2025-12-16 00:02:49 +08:00
|
|
|
|
result.cache_creation_tokens = extract_cache_creation_tokens(usage)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result.cache_read_tokens = usage.get("cache_read_input_tokens", 0)
|
|
|
|
|
|
|
2025-12-11 11:33:07 +08:00
|
|
|
|
# 检查错误(支持嵌套错误格式)
|
|
|
|
|
|
is_error, error_info = _check_nested_error(response)
|
|
|
|
|
|
if is_error and error_info:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result.is_error = True
|
2025-12-11 11:33:07 +08:00
|
|
|
|
result.error_type = error_info.get("type")
|
|
|
|
|
|
result.error_message = error_info.get("message")
|
2026-01-16 11:01:05 +08:00
|
|
|
|
result.embedded_status_code = _extract_embedded_status_code(error_info)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
|
2025-12-16 00:02:49 +08:00
|
|
|
|
# 对于 message_start 事件,usage 在 message.usage 路径下
|
|
|
|
|
|
# 对于其他响应,usage 在顶层
|
2026-01-29 09:44:12 +08:00
|
|
|
|
usage = response.get("usage") or {}
|
2025-12-16 00:02:49 +08:00
|
|
|
|
if not usage and "message" in response:
|
2026-01-29 09:44:12 +08:00
|
|
|
|
usage = (response.get("message") or {}).get("usage") or {}
|
2025-12-16 00:02:49 +08:00
|
|
|
|
|
2025-12-10 20:52:44 +08:00
|
|
|
|
return {
|
|
|
|
|
|
"input_tokens": usage.get("input_tokens", 0),
|
|
|
|
|
|
"output_tokens": usage.get("output_tokens", 0),
|
2025-12-16 00:02:49 +08:00
|
|
|
|
"cache_creation_tokens": extract_cache_creation_tokens(usage),
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"cache_read_tokens": usage.get("cache_read_input_tokens", 0),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def extract_text_content(self, response: dict[str, Any]) -> str:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
content = response.get("content", [])
|
|
|
|
|
|
if isinstance(content, list):
|
|
|
|
|
|
text_parts = []
|
|
|
|
|
|
for block in content:
|
|
|
|
|
|
if isinstance(block, dict) and block.get("type") == "text":
|
|
|
|
|
|
text_parts.append(block.get("text", ""))
|
|
|
|
|
|
return "".join(text_parts)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def is_error_response(self, response: dict[str, Any]) -> bool:
|
2025-12-11 11:33:07 +08:00
|
|
|
|
is_error, _ = _check_nested_error(response)
|
|
|
|
|
|
return is_error
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class GeminiResponseParser(ResponseParser):
|
|
|
|
|
|
"""Gemini 格式响应解析器"""
|
|
|
|
|
|
|
2026-02-19 21:26:18 +08:00
|
|
|
|
API_FORMAT = "gemini:chat"
|
|
|
|
|
|
|
2025-12-15 14:30:53 +08:00
|
|
|
|
def __init__(self) -> None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
from src.api.handlers.gemini.stream_parser import GeminiStreamParser
|
|
|
|
|
|
|
|
|
|
|
|
self._parser = GeminiStreamParser()
|
2026-02-19 21:26:18 +08:00
|
|
|
|
self.name = self.API_FORMAT
|
|
|
|
|
|
self.api_format = self.API_FORMAT
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def parse_sse_line(self, line: str, stats: StreamStats) -> ParsedChunk | None:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
解析 Gemini SSE 行
|
|
|
|
|
|
|
|
|
|
|
|
Gemini 的流式响应使用 SSE 格式 (data: {...})
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not line or not line.strip():
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# Gemini SSE 格式: data: {...}
|
|
|
|
|
|
if line.startswith("data: "):
|
|
|
|
|
|
data_str = line[6:]
|
|
|
|
|
|
else:
|
|
|
|
|
|
data_str = line
|
|
|
|
|
|
|
|
|
|
|
|
parsed = self._parser.parse_line(data_str)
|
|
|
|
|
|
if parsed is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
chunk = ParsedChunk(
|
|
|
|
|
|
raw_line=line,
|
|
|
|
|
|
event_type="content",
|
|
|
|
|
|
data=parsed,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取文本增量
|
|
|
|
|
|
text_delta = self._parser.extract_text_delta(parsed)
|
|
|
|
|
|
if text_delta:
|
|
|
|
|
|
chunk.text_delta = text_delta
|
|
|
|
|
|
stats.collected_text += text_delta
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否结束
|
|
|
|
|
|
if self._parser.is_done_event(parsed):
|
|
|
|
|
|
chunk.is_done = True
|
|
|
|
|
|
stats.has_completion = True
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 usage
|
2026-01-27 02:17:18 +08:00
|
|
|
|
# Gemini 流式响应的 usage 可能出现在多个 chunk 中
|
|
|
|
|
|
# 使用取最大值策略确保正确统计
|
2025-12-10 20:52:44 +08:00
|
|
|
|
usage = self._parser.extract_usage(parsed)
|
|
|
|
|
|
if usage:
|
|
|
|
|
|
chunk.input_tokens = usage.get("input_tokens", 0)
|
|
|
|
|
|
chunk.output_tokens = usage.get("output_tokens", 0)
|
|
|
|
|
|
chunk.cache_read_tokens = usage.get("cached_tokens", 0)
|
|
|
|
|
|
|
2026-01-27 02:17:18 +08:00
|
|
|
|
# 取最大值更新 stats
|
|
|
|
|
|
if chunk.input_tokens > stats.input_tokens:
|
|
|
|
|
|
stats.input_tokens = chunk.input_tokens
|
|
|
|
|
|
if chunk.output_tokens > stats.output_tokens:
|
|
|
|
|
|
stats.output_tokens = chunk.output_tokens
|
|
|
|
|
|
if chunk.cache_read_tokens > stats.cache_read_tokens:
|
|
|
|
|
|
stats.cache_read_tokens = chunk.cache_read_tokens
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
# 检查错误
|
|
|
|
|
|
if self._parser.is_error_event(parsed):
|
|
|
|
|
|
chunk.is_error = True
|
|
|
|
|
|
error = parsed.get("error", {})
|
|
|
|
|
|
if isinstance(error, dict):
|
|
|
|
|
|
chunk.error_message = error.get("message", str(error))
|
|
|
|
|
|
else:
|
|
|
|
|
|
chunk.error_message = str(error)
|
|
|
|
|
|
|
|
|
|
|
|
stats.chunk_count += 1
|
|
|
|
|
|
stats.data_count += 1
|
|
|
|
|
|
|
|
|
|
|
|
return chunk
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def parse_response(self, response: dict[str, Any], status_code: int) -> ParsedResponse:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
result = ParsedResponse(
|
|
|
|
|
|
raw_response=response,
|
|
|
|
|
|
status_code=status_code,
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 提取文本内容
|
|
|
|
|
|
candidates = response.get("candidates", [])
|
|
|
|
|
|
if candidates:
|
|
|
|
|
|
content = candidates[0].get("content", {})
|
|
|
|
|
|
parts = content.get("parts", [])
|
|
|
|
|
|
text_parts = []
|
|
|
|
|
|
for part in parts:
|
|
|
|
|
|
if "text" in part:
|
|
|
|
|
|
text_parts.append(part["text"])
|
|
|
|
|
|
result.text_content = "".join(text_parts)
|
|
|
|
|
|
|
|
|
|
|
|
result.response_id = response.get("modelVersion")
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 usage(调用 GeminiStreamParser.extract_usage 作为单一实现源)
|
|
|
|
|
|
usage = self._parser.extract_usage(response)
|
|
|
|
|
|
if usage:
|
|
|
|
|
|
result.input_tokens = usage.get("input_tokens", 0)
|
|
|
|
|
|
result.output_tokens = usage.get("output_tokens", 0)
|
|
|
|
|
|
result.cache_read_tokens = usage.get("cached_tokens", 0)
|
|
|
|
|
|
|
|
|
|
|
|
# 检查错误(使用增强的错误检测)
|
|
|
|
|
|
error_info = self._parser.extract_error_info(response)
|
|
|
|
|
|
if error_info:
|
|
|
|
|
|
result.is_error = True
|
|
|
|
|
|
result.error_type = error_info.get("status")
|
|
|
|
|
|
result.error_message = error_info.get("message")
|
2026-01-16 11:01:05 +08:00
|
|
|
|
result.embedded_status_code = _extract_embedded_status_code(error_info)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def extract_usage_from_response(self, response: dict[str, Any]) -> dict[str, int]:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
从 Gemini 响应中提取 token 使用量
|
|
|
|
|
|
|
|
|
|
|
|
调用 GeminiStreamParser.extract_usage 作为单一实现源
|
|
|
|
|
|
"""
|
|
|
|
|
|
usage = self._parser.extract_usage(response)
|
|
|
|
|
|
if not usage:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"input_tokens": 0,
|
|
|
|
|
|
"output_tokens": 0,
|
|
|
|
|
|
"cache_creation_tokens": 0,
|
|
|
|
|
|
"cache_read_tokens": 0,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"input_tokens": usage.get("input_tokens", 0),
|
|
|
|
|
|
"output_tokens": usage.get("output_tokens", 0),
|
|
|
|
|
|
"cache_creation_tokens": 0,
|
|
|
|
|
|
"cache_read_tokens": usage.get("cached_tokens", 0),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def extract_text_content(self, response: dict[str, Any]) -> str:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
candidates = response.get("candidates", [])
|
|
|
|
|
|
if candidates:
|
|
|
|
|
|
content = candidates[0].get("content", {})
|
|
|
|
|
|
parts = content.get("parts", [])
|
|
|
|
|
|
text_parts = []
|
|
|
|
|
|
for part in parts:
|
|
|
|
|
|
if "text" in part:
|
|
|
|
|
|
text_parts.append(part["text"])
|
|
|
|
|
|
return "".join(text_parts)
|
|
|
|
|
|
return ""
|
|
|
|
|
|
|
2026-01-30 03:10:21 +08:00
|
|
|
|
def is_error_response(self, response: dict[str, Any]) -> bool:
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
判断响应是否为错误响应
|
|
|
|
|
|
|
|
|
|
|
|
使用增强的错误检测逻辑,支持嵌套在 chunks 中的错误
|
|
|
|
|
|
"""
|
2025-12-15 14:30:53 +08:00
|
|
|
|
return bool(self._parser.is_error_event(response))
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-16 11:00:48 +08:00
|
|
|
|
# 注册解析器到 core 层注册表(供 services 层通过 format_id 获取)
|
|
|
|
|
|
from src.core.stream_types import get_parser_for_format, register_parser
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-02-16 11:00:48 +08:00
|
|
|
|
def register_default_parsers() -> None:
|
2026-02-19 21:26:18 +08:00
|
|
|
|
"""自动发现所有 ResponseParser 子类并注册
|
|
|
|
|
|
|
|
|
|
|
|
通过 __subclasses__() 递归收集所有 ResponseParser 子类,
|
|
|
|
|
|
使用类级别 API_FORMAT 属性获取格式 ID,无需实例化。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
def _collect_subclasses(base: type) -> list[type]:
|
|
|
|
|
|
subs = base.__subclasses__()
|
|
|
|
|
|
return subs + [s for c in subs for s in _collect_subclasses(c)]
|
|
|
|
|
|
|
|
|
|
|
|
for cls in _collect_subclasses(ResponseParser):
|
|
|
|
|
|
api_format = getattr(cls, "API_FORMAT", None)
|
|
|
|
|
|
if api_format:
|
|
|
|
|
|
register_parser(api_format, cls)
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
2026-02-01 17:28:00 +08:00
|
|
|
|
|
2026-02-16 11:00:48 +08:00
|
|
|
|
# 模块加载时自动注册(保证 import parsers 即可用,测试也不需要手动初始化)
|
|
|
|
|
|
# main.py lifespan 中的显式调用是冗余但无害的安全保障(dict 覆盖幂等)
|
|
|
|
|
|
register_default_parsers()
|
2025-12-10 20:52:44 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
__all__ = [
|
|
|
|
|
|
"OpenAIResponseParser",
|
|
|
|
|
|
"OpenAICliResponseParser",
|
|
|
|
|
|
"ClaudeResponseParser",
|
|
|
|
|
|
"GeminiResponseParser",
|
2026-02-16 11:00:48 +08:00
|
|
|
|
"register_default_parsers",
|
2025-12-10 20:52:44 +08:00
|
|
|
|
"get_parser_for_format",
|
|
|
|
|
|
"is_cli_format",
|
|
|
|
|
|
]
|