fix: 增强嵌入式错误的状态码提取与处理

- ParsedResponse 新增 embedded_status_code 字段存储嵌套状态码
- 新增 _extract_embedded_status_code 函数支持从多种格式提取状态码
- 非流式响应增加嵌套错误检测逻辑
- 统一使用 embedded_status_code 替代从 error_type 解析数字的方式
- fallback_orchestrator 使用嵌入式状态码进行错误记录
This commit is contained in:
fawney19
2026-01-16 11:01:05 +08:00
parent adc82532eb
commit e6a31a61db
7 changed files with 102 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ Chat Handler Base - Chat API 格式的通用基类
""" """
import asyncio import asyncio
import json
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Any, AsyncGenerator, Callable, Dict, Optional from typing import Any, AsyncGenerator, Callable, Dict, Optional
@@ -803,6 +804,26 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
upstream_status=resp.status_code, upstream_status=resp.status_code,
upstream_response=raw_content, upstream_response=raw_content,
) )
# 检查响应体中的嵌套错误HTTP 200 但响应体包含错误)
if isinstance(response_json, dict):
parser = get_parser_for_format(api_format)
if parser.is_error_response(response_json):
parsed = parser.parse_response(response_json, 200)
logger.warning(
f" [{self.request_id}] 非流式检测到嵌套错误: "
f"Provider={provider.name}, "
f"error_type={parsed.error_type}, "
f"embedded_status={parsed.embedded_status_code}, "
f"message={parsed.error_message}"
)
raise EmbeddedErrorException(
provider_name=str(provider.name),
error_code=parsed.embedded_status_code,
error_message=parsed.error_message,
error_status=parsed.error_type,
)
return response_json if isinstance(response_json, dict) else {} return response_json if isinstance(response_json, dict) else {}
try: try:

View File

@@ -5,6 +5,7 @@
不再经过 Protocol 抽象层。 不再经过 Protocol 抽象层。
""" """
import re
from typing import Any, Dict, Optional, Tuple, Type from typing import Any, Dict, Optional, Tuple, Type
from src.api.handlers.base.response_parser import ( from src.api.handlers.base.response_parser import (
@@ -58,6 +59,71 @@ def _check_nested_error(response: Dict[str, Any]) -> Tuple[bool, Optional[Dict[s
return False, None return False, None
def _extract_embedded_status_code(error_info: Optional[Dict[str, Any]]) -> Optional[int]:
"""
从错误信息中提取嵌套的状态码
支持多种格式:
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
class OpenAIResponseParser(ResponseParser): class OpenAIResponseParser(ResponseParser):
"""OpenAI 格式响应解析器""" """OpenAI 格式响应解析器"""
@@ -141,6 +207,7 @@ class OpenAIResponseParser(ResponseParser):
result.is_error = True result.is_error = True
result.error_type = error_info.get("type") result.error_type = error_info.get("type")
result.error_message = error_info.get("message") result.error_message = error_info.get("message")
result.embedded_status_code = _extract_embedded_status_code(error_info)
return result return result
@@ -273,6 +340,7 @@ class ClaudeResponseParser(ResponseParser):
result.is_error = True result.is_error = True
result.error_type = error_info.get("type") result.error_type = error_info.get("type")
result.error_message = error_info.get("message") result.error_message = error_info.get("message")
result.embedded_status_code = _extract_embedded_status_code(error_info)
return result return result
@@ -417,6 +485,7 @@ class GeminiResponseParser(ResponseParser):
result.is_error = True result.is_error = True
result.error_type = error_info.get("status") result.error_type = error_info.get("status")
result.error_message = error_info.get("message") result.error_message = error_info.get("message")
result.embedded_status_code = _extract_embedded_status_code(error_info)
return result return result

View File

@@ -87,6 +87,8 @@ class ParsedResponse:
is_error: bool = False is_error: bool = False
error_type: Optional[str] = None error_type: Optional[str] = None
error_message: Optional[str] = None error_message: Optional[str] = None
# 从响应体解析出的嵌套状态码(当 HTTP 200 但响应体含错误时使用)
embedded_status_code: Optional[int] = None
class ResponseParser(ABC): class ResponseParser(ABC):

View File

@@ -286,15 +286,12 @@ class StreamProcessor:
f" [{self.request_id}] 检测到嵌套错误: " f" [{self.request_id}] 检测到嵌套错误: "
f"Provider={provider.name}, " f"Provider={provider.name}, "
f"error_type={parsed.error_type}, " f"error_type={parsed.error_type}, "
f"embedded_status={parsed.embedded_status_code}, "
f"message={parsed.error_message}" f"message={parsed.error_message}"
) )
raise EmbeddedErrorException( raise EmbeddedErrorException(
provider_name=str(provider.name), provider_name=str(provider.name),
error_code=( error_code=parsed.embedded_status_code,
int(parsed.error_type)
if parsed.error_type and parsed.error_type.isdigit()
else None
),
error_message=parsed.error_message, error_message=parsed.error_message,
error_status=parsed.error_type, error_status=parsed.error_type,
) )

View File

@@ -186,15 +186,12 @@ def check_prefetched_response_error(
f" [{request_id}] 检测到 JSON 错误响应: " f" [{request_id}] 检测到 JSON 错误响应: "
f"Provider={provider_name}, " f"Provider={provider_name}, "
f"error_type={parsed.error_type}, " f"error_type={parsed.error_type}, "
f"embedded_status={parsed.embedded_status_code}, "
f"message={parsed.error_message}" f"message={parsed.error_message}"
) )
raise EmbeddedErrorException( raise EmbeddedErrorException(
provider_name=provider_name, provider_name=provider_name,
error_code=( error_code=parsed.embedded_status_code,
int(parsed.error_type)
if parsed.error_type and parsed.error_type.isdigit()
else None
),
error_message=parsed.error_message, error_message=parsed.error_message,
error_status=parsed.error_type, error_status=parsed.error_type,
) )

View File

@@ -388,7 +388,7 @@ class AuthService:
if key_record.is_locked: if key_record.is_locked:
logger.warning("API认证失败 - 密钥已被管理员锁定") logger.warning("API认证失败 - 密钥已被管理员锁定")
raise ForbiddenException("API密钥已被管理员锁定,请联系管理员") raise ForbiddenException("该密钥已被管理员锁定,请联系管理员")
# 检查过期时间 # 检查过期时间
if key_record.expires_at: if key_record.expires_at:

View File

@@ -358,6 +358,8 @@ class FallbackOrchestrator:
# 需要检查错误消息是否为客户端错误(如 prompt is too long这类错误不应重试 # 需要检查错误消息是否为客户端错误(如 prompt is too long这类错误不应重试
if isinstance(cause, EmbeddedErrorException): if isinstance(cause, EmbeddedErrorException):
error_message = cause.error_message or "" error_message = cause.error_message or ""
# 使用嵌入式状态码(如果有),否则默认 200
embedded_status = cause.error_code or 200
if self._error_classifier.is_client_error(error_message): if self._error_classifier.is_client_error(error_message):
logger.warning( logger.warning(
f" [{request_id}] 嵌入式客户端错误,停止重试: {error_message[:200]}" f" [{request_id}] 嵌入式客户端错误,停止重试: {error_message[:200]}"
@@ -366,7 +368,7 @@ class FallbackOrchestrator:
client_error = UpstreamClientException( client_error = UpstreamClientException(
message=error_message or "请求无效", message=error_message or "请求无效",
provider_name=str(provider.name), provider_name=str(provider.name),
status_code=200, # 嵌入式错误的 HTTP 状态码通常是 200 status_code=embedded_status,
upstream_error=error_message, upstream_error=error_message,
) )
RequestCandidateService.mark_candidate_failed( RequestCandidateService.mark_candidate_failed(
@@ -374,7 +376,7 @@ class FallbackOrchestrator:
candidate_id=candidate_record_id, candidate_id=candidate_record_id,
error_type="UpstreamClientException", error_type="UpstreamClientException",
error_message=error_message, error_message=error_message,
status_code=200, status_code=embedded_status,
latency_ms=elapsed_ms, latency_ms=elapsed_ms,
concurrent_requests=captured_key_concurrent, concurrent_requests=captured_key_concurrent,
) )
@@ -396,7 +398,7 @@ class FallbackOrchestrator:
candidate_id=candidate_record_id, candidate_id=candidate_record_id,
error_type="EmbeddedErrorException", error_type="EmbeddedErrorException",
error_message=error_message, error_message=error_message,
status_code=200, status_code=embedded_status,
latency_ms=elapsed_ms, latency_ms=elapsed_ms,
concurrent_requests=captured_key_concurrent, concurrent_requests=captured_key_concurrent,
) )