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 json
from abc import ABC, abstractmethod
from typing import Any, AsyncGenerator, Callable, Dict, Optional
@@ -803,6 +804,26 @@ class ChatHandlerBase(BaseMessageHandler, ABC):
upstream_status=resp.status_code,
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 {}
try:

View File

@@ -5,6 +5,7 @@
不再经过 Protocol 抽象层。
"""
import re
from typing import Any, Dict, Optional, Tuple, Type
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
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):
"""OpenAI 格式响应解析器"""
@@ -141,6 +207,7 @@ class OpenAIResponseParser(ResponseParser):
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
@@ -273,6 +340,7 @@ class ClaudeResponseParser(ResponseParser):
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
@@ -417,6 +485,7 @@ class GeminiResponseParser(ResponseParser):
result.is_error = True
result.error_type = error_info.get("status")
result.error_message = error_info.get("message")
result.embedded_status_code = _extract_embedded_status_code(error_info)
return result

View File

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

View File

@@ -286,15 +286,12 @@ class StreamProcessor:
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=(
int(parsed.error_type)
if parsed.error_type and parsed.error_type.isdigit()
else None
),
error_code=parsed.embedded_status_code,
error_message=parsed.error_message,
error_status=parsed.error_type,
)

View File

@@ -186,15 +186,12 @@ def check_prefetched_response_error(
f" [{request_id}] 检测到 JSON 错误响应: "
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=provider_name,
error_code=(
int(parsed.error_type)
if parsed.error_type and parsed.error_type.isdigit()
else None
),
error_code=parsed.embedded_status_code,
error_message=parsed.error_message,
error_status=parsed.error_type,
)

View File

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

View File

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