feat: add provider compatibility error detection for intelligent failover

- Introduce ProviderCompatibilityException for unsupported parameter/feature errors
- Add COMPATIBILITY_ERROR_PATTERNS to detect provider-specific limitations
- Implement _is_compatibility_error() method in ErrorClassifier
- Prioritize compatibility error checking before client error validation
- Remove 'max_tokens' from CLIENT_ERROR_PATTERNS as it can indicate compatibility issues
- Enable automatic failover when provider doesn't support requested features
- Improve error classification accuracy with pattern matching for common compatibility issues
This commit is contained in:
fawney19
2025-12-19 13:28:26 +08:00
parent c69a0a8506
commit 3bbc1c6b66
2 changed files with 76 additions and 1 deletions

View File

@@ -442,6 +442,36 @@ class EmbeddedErrorException(ProviderException):
self.error_status = error_status self.error_status = error_status
class ProviderCompatibilityException(ProviderException):
"""Provider 兼容性错误异常 - 应该触发故障转移
用于处理因 Provider 不支持某些参数或功能导致的错误。
这类错误不是用户请求本身的问题,换一个 Provider 可能就能成功,应该触发故障转移。
常见场景:
- Unsupported parameter不支持的参数
- Unsupported model不支持的模型
- Unsupported feature不支持的功能
"""
def __init__(
self,
message: str,
provider_name: Optional[str] = None,
status_code: int = 400,
upstream_error: Optional[str] = None,
request_metadata: Optional[Any] = None,
):
self.upstream_error = upstream_error
super().__init__(
message=message,
provider_name=provider_name,
request_metadata=request_metadata,
)
# 覆盖状态码为 400保持与上游一致
self.status_code = status_code
class UpstreamClientException(ProxyException): class UpstreamClientException(ProxyException):
"""上游返回的客户端错误异常 - HTTP 4xx 错误,不应该重试 """上游返回的客户端错误异常 - HTTP 4xx 错误,不应该重试

View File

@@ -15,6 +15,7 @@ from src.core.enums import APIFormat
from src.core.exceptions import ( from src.core.exceptions import (
ConcurrencyLimitError, ConcurrencyLimitError,
ProviderAuthException, ProviderAuthException,
ProviderCompatibilityException,
ProviderException, ProviderException,
ProviderNotAvailableException, ProviderNotAvailableException,
ProviderRateLimitException, ProviderRateLimitException,
@@ -81,7 +82,9 @@ class ErrorClassifier:
"context_length_exceeded", # 上下文长度超限 "context_length_exceeded", # 上下文长度超限
"content_length_limit", # 请求内容长度超限 (Claude API) "content_length_limit", # 请求内容长度超限 (Claude API)
"content_length_exceeds", # 内容长度超限变体 (AWS CodeWhisperer) "content_length_exceeds", # 内容长度超限变体 (AWS CodeWhisperer)
"max_tokens", # token 数超限 # 注意:移除了 "max_tokens",因为 max_tokens 相关错误可能是 Provider 兼容性问题
# 如 "Unsupported parameter: 'max_tokens' is not supported with this model"
# 这类错误应由 COMPATIBILITY_ERROR_PATTERNS 处理
"invalid_prompt", # 无效的提示词 "invalid_prompt", # 无效的提示词
"content too long", # 内容过长 "content too long", # 内容过长
"input is too long", # 输入过长 (AWS) "input is too long", # 输入过长 (AWS)
@@ -136,6 +139,19 @@ class ErrorClassifier:
"CONTENT_POLICY_VIOLATION", "CONTENT_POLICY_VIOLATION",
) )
# Provider 兼容性错误模式 - 这类错误应该触发故障转移
# 因为换一个 Provider 可能就能成功
COMPATIBILITY_ERROR_PATTERNS: Tuple[str, ...] = (
"unsupported parameter", # 不支持的参数
"unsupported model", # 不支持的模型
"unsupported feature", # 不支持的功能
"not supported with this model", # 此模型不支持
"model does not support", # 模型不支持
"parameter is not supported", # 参数不支持
"feature is not supported", # 功能不支持
"not available for this model", # 此模型不可用
)
def _parse_error_response(self, error_text: Optional[str]) -> Dict[str, Any]: def _parse_error_response(self, error_text: Optional[str]) -> Dict[str, Any]:
""" """
解析错误响应为结构化数据 解析错误响应为结构化数据
@@ -261,6 +277,25 @@ class ErrorClassifier:
search_text = f"{parsed['message']} {parsed['raw']}".lower() search_text = f"{parsed['message']} {parsed['raw']}".lower()
return any(pattern.lower() in search_text for pattern in self.CLIENT_ERROR_PATTERNS) return any(pattern.lower() in search_text for pattern in self.CLIENT_ERROR_PATTERNS)
def _is_compatibility_error(self, error_text: Optional[str]) -> bool:
"""
检测错误响应是否为 Provider 兼容性错误(应触发故障转移)
这类错误是因为 Provider 不支持某些参数或功能导致的,
换一个 Provider 可能就能成功。
Args:
error_text: 错误响应文本
Returns:
是否为兼容性错误
"""
if not error_text:
return False
search_text = error_text.lower()
return any(pattern.lower() in search_text for pattern in self.COMPATIBILITY_ERROR_PATTERNS)
def _extract_error_message(self, error_text: Optional[str]) -> Optional[str]: def _extract_error_message(self, error_text: Optional[str]) -> Optional[str]:
""" """
从错误响应中提取错误消息 从错误响应中提取错误消息
@@ -425,6 +460,16 @@ class ErrorClassifier:
), ),
) )
# 400 错误:先检查是否为 Provider 兼容性错误(应触发故障转移)
if status == 400 and self._is_compatibility_error(error_response_text):
logger.info(f"检测到 Provider 兼容性错误,将触发故障转移: {extracted_message}")
return ProviderCompatibilityException(
message=extracted_message or "Provider 不支持此请求",
provider_name=provider_name,
status_code=400,
upstream_error=error_response_text,
)
# 400 错误:检查是否为客户端请求错误(不应重试) # 400 错误:检查是否为客户端请求错误(不应重试)
if status == 400 and self._is_client_error(error_response_text): if status == 400 and self._is_client_error(error_response_text):
logger.info(f"检测到客户端请求错误,不进行重试: {extracted_message}") logger.info(f"检测到客户端请求错误,不进行重试: {extracted_message}")