From 3bbf3073dfe747ce14e07b38dfe42cde3a980feb Mon Sep 17 00:00:00 2001 From: fawney19 Date: Sun, 4 Jan 2026 23:50:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=89=80=E6=9C=89=20Provider=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E6=97=B6=E9=80=8F=E4=BC=A0=E4=B8=8A=E6=B8=B8?= =?UTF-8?q?=E9=94=99=E8=AF=AF=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FallbackOrchestrator 在所有候选组合失败后保留最后的错误信息 - 从 httpx.HTTPStatusError 提取上游状态码和响应内容 - ProviderNotAvailableException 携带上游错误信息 - ErrorResponse 在返回错误时透传上游状态码和响应 --- src/core/exceptions.py | 14 +++++++--- .../orchestration/fallback_orchestrator.py | 27 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/core/exceptions.py b/src/core/exceptions.py index 6f6eeb5..120c672 100644 --- a/src/core/exceptions.py +++ b/src/core/exceptions.py @@ -547,11 +547,19 @@ class ErrorResponse: - 所有错误都记录到日志,通过错误 ID 关联 """ if isinstance(e, ProxyException): + details = e.details.copy() if e.details else {} + status_code = e.status_code + message = e.message + # 如果是 ProviderNotAvailableException 且有上游错误,直接透传上游信息 + if isinstance(e, ProviderNotAvailableException) and e.upstream_response: + if e.upstream_status: + status_code = e.upstream_status + message = e.upstream_response return ErrorResponse.create( error_type=e.error_type, - message=e.message, - status_code=e.status_code, - details=e.details, + message=message, + status_code=status_code, + details=details if details else None, ) elif isinstance(e, HTTPException): return ErrorResponse.create( diff --git a/src/services/orchestration/fallback_orchestrator.py b/src/services/orchestration/fallback_orchestrator.py index 00ce509..cb13f4d 100644 --- a/src/services/orchestration/fallback_orchestrator.py +++ b/src/services/orchestration/fallback_orchestrator.py @@ -543,7 +543,9 @@ class FallbackOrchestrator: raise last_error # 所有组合都已尝试完毕,全部失败 - self._raise_all_failed_exception(request_id, max_attempts, last_candidate, model_name, api_format_enum) + self._raise_all_failed_exception( + request_id, max_attempts, last_candidate, model_name, api_format_enum, last_error + ) async def _try_candidate_with_retries( self, @@ -565,6 +567,7 @@ class FallbackOrchestrator: provider = candidate.provider endpoint = candidate.endpoint max_retries_for_candidate = int(endpoint.max_retries) if candidate.is_cached else 1 + last_error: Optional[Exception] = None for retry_index in range(max_retries_for_candidate): attempt_counter += 1 @@ -599,6 +602,7 @@ class FallbackOrchestrator: return {"success": True, "response": response} except ExecutionError as exec_err: + last_error = exec_err.cause action = await self._handle_candidate_error( exec_err=exec_err, candidate=candidate, @@ -630,6 +634,7 @@ class FallbackOrchestrator: "success": False, "attempt_counter": attempt_counter, "max_attempts": max_attempts, + "error": last_error, } def _attach_metadata_to_error( @@ -678,6 +683,7 @@ class FallbackOrchestrator: last_candidate: Optional[ProviderCandidate], model_name: str, api_format_enum: APIFormat, + last_error: Optional[Exception] = None, ) -> NoReturn: """所有组合都失败时抛出异常""" logger.error(f" [{request_id}] 所有 {max_attempts} 个组合均失败") @@ -693,9 +699,28 @@ class FallbackOrchestrator: "api_format": api_format_enum.value, } + # 提取上游错误响应 + upstream_status: Optional[int] = None + upstream_response: Optional[str] = None + if last_error: + # 从 httpx.HTTPStatusError 提取 + if isinstance(last_error, httpx.HTTPStatusError): + upstream_status = last_error.response.status_code + try: + upstream_response = last_error.response.text + except Exception: + pass + # 从异常属性提取 + elif hasattr(last_error, "upstream_status"): + upstream_status = getattr(last_error, "upstream_status", None) + if hasattr(last_error, "upstream_response"): + upstream_response = getattr(last_error, "upstream_response", None) + raise ProviderNotAvailableException( f"所有Provider均不可用,已尝试{max_attempts}个组合", request_metadata=request_metadata, + upstream_status=upstream_status, + upstream_response=upstream_response, ) async def execute_with_fallback(