| |
| """ |
| API 请求处理中的错误处理辅助函数。 |
| """ |
| import httpx |
| import json |
| import logging |
| from typing import Optional, Dict, Any, Tuple |
|
|
| |
| from app.core.keys.manager import APIKeyManager |
|
|
| |
| logger = logging.getLogger("my_logger") |
|
|
| |
|
|
| def _format_api_error(http_err: httpx.HTTPStatusError) -> Tuple[str, int]: |
| """ |
| (内部辅助函数) 根据 httpx.HTTPStatusError 格式化 API 错误消息和状态码。 |
| 尝试从响应体中解析更具体的错误信息。 |
| |
| Args: |
| http_err (httpx.HTTPStatusError): httpx 抛出的 HTTP 状态错误异常。 |
| |
| Returns: |
| Tuple[str, int]: 包含错误消息字符串和 HTTP 状态码的元组。 |
| """ |
| status_code = http_err.response.status_code |
| error_message = f"API 请求失败,状态码: {status_code}。" |
|
|
| try: |
| |
| error_detail = http_err.response.json() |
| |
| if isinstance(error_detail, dict) and "error" in error_detail and "message" in error_detail["error"]: |
| error_message = f"API 错误 (状态码 {status_code}): {error_detail['error']['message']}" |
| else: |
| |
| error_message = f"API 请求失败,状态码: {status_code}。响应体: {http_err.response.text[:200]}" |
| except json.JSONDecodeError: |
| |
| error_message = f"API 请求失败,状态码: {status_code}。响应体: {http_err.response.text[:200]}" |
| except Exception as e: |
| |
| logger.error(f"解析 API 错误响应体时发生意外错误: {e}", exc_info=True) |
| error_message = f"API 请求失败,状态码: {status_code},且无法解析错误详情。" |
|
|
| return error_message, status_code |
|
|
| def _handle_429_daily_quota(http_error: httpx.HTTPStatusError, api_key: Optional[str], key_manager: APIKeyManager) -> bool: |
| """ |
| (内部辅助函数) 处理 HTTP 429 错误,专门检查是否为每日配额耗尽。 |
| 如果是每日配额耗尽,则调用 Key 管理器标记该 Key。 |
| |
| Args: |
| http_error (httpx.HTTPStatusError): 429 错误异常对象。 |
| api_key (Optional[str]): 当前使用的 API Key。 |
| key_manager (APIKeyManager): Key 管理器实例。 |
| |
| Returns: |
| bool: 如果确定是每日配额耗尽错误,返回 True;否则返回 False。 |
| """ |
| if not api_key: |
| return False |
|
|
| try: |
| |
| error_detail = http_error.response.json() |
| is_daily_quota_error = False |
| |
| if error_detail and "error" in error_detail and "details" in error_detail["error"]: |
| for detail in error_detail["error"]["details"]: |
| |
| if detail.get("@type") == "type.googleapis.com/google.rpc.QuotaFailure": |
| |
| quota_id = detail.get("quotaId", "") |
| if "PerDay" in quota_id: |
| is_daily_quota_error = True |
| break |
|
|
| if is_daily_quota_error: |
| |
| key_manager.mark_key_daily_exhausted(api_key) |
| logger.warning(f"Key {api_key[:8]}... 因每日配额耗尽被标记为当天不可用。") |
| return True |
|
|
| except json.JSONDecodeError: |
| logger.error(f"无法解析 429 错误的 JSON 响应体 (Key: {api_key[:8]}...)") |
| except Exception as parse_e: |
| logger.error(f"解析 429 错误详情时发生意外异常 (Key: {api_key[:8]}...): {parse_e}") |
|
|
| return False |
|
|
| async def _handle_http_error_in_attempt( |
| http_err: httpx.HTTPStatusError, |
| current_api_key: Optional[str], |
| key_manager: APIKeyManager, |
| is_stream: bool, |
| request_id: Optional[str] = None |
| ) -> Tuple[Dict[str, Any], bool]: |
| """ |
| (内部辅助函数) 处理在单次 API 调用尝试 (`_attempt_api_call`) 中发生的 `httpx.HTTPStatusError`。 |
| 根据 HTTP 状态码判断错误类型,格式化错误信息,并决定是否需要重试(通常意味着尝试其他 Key)。 |
| 对于特定错误(如 429 每日配额、400 Key 无效、401/403),会调用 Key 管理器标记 Key 状态。 |
| |
| Args: |
| http_err (httpx.HTTPStatusError): 捕获到的 HTTP 状态错误异常。 |
| current_api_key (Optional[str]): 当前尝试使用的 API Key。 |
| key_manager (APIKeyManager): Key 管理器实例。 |
| is_stream (bool): 当前请求是否为流式请求。 |
| request_id (Optional[str]): 当前请求的 ID。 |
| |
| Returns: |
| Tuple[Dict[str, Any], bool]: |
| - 第一个元素 (Dict): 包含格式化错误信息的字典 ('message', 'type', 'code')。 |
| - 第二个元素 (bool): 指示是否需要重试 (True 表示需要,False 表示不需要)。 |
| """ |
| |
| error_message, status_code = _format_api_error(http_err) |
| |
| logger.error(f"API HTTP 错误 ({'流式' if is_stream else '非流式'}, Key: {current_api_key[:8] if current_api_key else 'N/A'}, Request: {request_id}): {status_code} - {error_message}", exc_info=False) |
|
|
| |
| error_info: Dict[str, Any] = { |
| "message": error_message, |
| "type": "api_error", |
| "code": status_code |
| } |
| needs_retry = False |
|
|
| |
| if status_code in [500, 503]: |
| error_info["type"] = "server_error" if status_code == 500 else "service_unavailable_error" |
| needs_retry = True |
| logger.warning(f"HTTP 状态码 {status_code} 表示服务器临时错误,标记需要重试。") |
| |
| if current_api_key: |
| key_manager.mark_key_temporarily_unavailable(current_api_key, duration_seconds=60, issue_type=f"HTTP {status_code}") |
|
|
| elif status_code == 429: |
| error_info["type"] = "rate_limit_error" |
| |
| if _handle_429_daily_quota(http_err, current_api_key, key_manager): |
| needs_retry = True |
| error_info["message"] = f"Key {current_api_key[:8] if current_api_key else 'N/A'} 每日配额耗尽,尝试其他 Key。" |
| else: |
| |
| |
| needs_retry = False |
| logger.warning(f"HTTP 状态码 429 (非每日配额) 表示当前 Key 速率限制,标记无需重试。") |
| |
| if current_api_key: |
| key_manager.mark_key_temporarily_unavailable(current_api_key, duration_seconds=60, issue_type="Rate Limit (429)") |
|
|
| elif status_code in [401, 403]: |
| error_info["type"] = "authentication_error" if status_code == 401 else "permission_error" |
| needs_retry = False |
| logger.warning(f"HTTP 状态码 {status_code} 表示认证/权限问题,标记无需重试。") |
| |
| if current_api_key: |
| key_manager.mark_key_temporarily_unavailable(current_api_key, duration_seconds=300, issue_type=f"Auth/Permission ({status_code})") |
|
|
| elif status_code == 400: |
| error_info["type"] = "invalid_request_error" |
| needs_retry = False |
| |
| if "API key not valid" in error_message: |
| logger.warning(f"HTTP 状态码 400 (明确 Key 无效) 表示 Key 无效,标记无需重试。") |
| |
| if current_api_key: |
| key_manager.mark_key_temporarily_unavailable(current_api_key, duration_seconds=300, issue_type="Invalid API Key (400)") |
| else: |
| logger.warning(f"HTTP 状态码 400 (非 Key 无效) 表示请求体问题,标记无需重试。") |
|
|
| else: |
| needs_retry = False |
| logger.warning(f"HTTP 状态码 {status_code} 表示其他错误,标记无需重试。") |
|
|
| return error_info, needs_retry |
|
|
| async def _handle_api_call_exception( |
| exc: Exception, |
| current_api_key: Optional[str], |
| key_manager: APIKeyManager, |
| is_stream: bool, |
| request_id: Optional[str] = None |
| ) -> Tuple[Dict[str, Any], bool]: |
| """ |
| (内部辅助函数) 统一处理在 `_attempt_api_call` 中捕获到的各类异常。 |
| 根据异常类型调用不同的处理逻辑,格式化错误信息,并决定是否需要重试。 |
| |
| Args: |
| exc (Exception): 捕获到的异常对象。 |
| current_api_key (Optional[str]): 当前尝试使用的 API Key。 |
| key_manager (APIKeyManager): Key 管理器实例。 |
| is_stream (bool): 当前请求是否为流式请求。 |
| request_id (Optional[str]): 当前请求的 ID。 |
| |
| Returns: |
| Tuple[Dict[str, Any], bool]: |
| - 第一个元素 (Dict): 包含格式化错误信息的字典 ('message', 'type', 'code')。 |
| - 第二个元素 (bool): 指示是否需要重试 (True 表示需要,False 表示不需要)。 |
| """ |
| needs_retry = False |
| error_info: Dict[str, Any] = { |
| "message": f"API 调用中发生意外异常: {exc}", |
| "type": "internal_error", |
| "code": 500 |
| } |
|
|
| if isinstance(exc, httpx.HTTPStatusError): |
| |
| error_info, needs_retry = await _handle_http_error_in_attempt( |
| exc, current_api_key, key_manager, is_stream, request_id |
| ) |
| elif isinstance(exc, httpx.TimeoutException): |
| error_message = f"请求超时 (Key: {current_api_key[:8] if current_api_key else 'N/A'}, Request: {request_id}): {exc}" |
| logger.error(error_message) |
| error_info["message"] = "请求 Gemini API 超时,请稍后重试。" |
| error_info["type"] = "timeout_error" |
| error_info["code"] = 504 |
| needs_retry = True |
| |
| if current_api_key: |
| key_manager.mark_key_temporarily_unavailable(current_api_key, duration_seconds=60, issue_type="Timeout") |
| elif isinstance(exc, httpx.RequestError): |
| error_message = f"网络连接错误 (Key: {current_api_key[:8] if current_api_key else 'N/A'}, Request: {request_id}): {exc}" |
| logger.error(error_message) |
| error_info["message"] = "连接 Gemini API 时发生网络错误。" |
| error_info["type"] = "connection_error" |
| error_info["code"] = 503 |
| needs_retry = True |
| |
| if current_api_key: |
| key_manager.mark_key_temporarily_unavailable(current_api_key, duration_seconds=60, issue_type="Connection Error") |
| else: |
| error_message = f"API 调用中发生未知内部错误 (Key: {current_api_key[:8] if current_api_key else 'N/A'}, Request: {request_id}): {exc}" |
| logger.error(error_message, exc_info=True) |
| error_info["message"] = "处理请求时发生意外的内部错误。" |
| error_info["type"] = "internal_error" |
| error_info["code"] = 500 |
| needs_retry = False |
|
|
| return error_info, needs_retry |
|
|
| |
| |
|
|