feat: let every gateway error carry what the gateway said

Issue #10 Task 1: a rejected call's reason had nowhere to live. The
field goes on the base class because these errors all come from one HTTP
response - which class it is and what the peer said are orthogonal.
This commit is contained in:
2026-08-16 06:01:38 -04:00
parent 1489aab95d
commit e302247022
2 changed files with 70 additions and 5 deletions
+32 -2
View File
@@ -35,7 +35,26 @@ SOURCE_REASONS = frozenset(
class PolyGatewayError(Exception):
"""库内一切领域错误的基类,携带来源上下文便于遥测与日志定位。"""
"""库内一切领域错误的基类,携带来源上下文便于遥测与日志定位。
`body_text` 是**非 2xx 响应体的摘要**——网关拒绝这次调用时说的话(issue #10)。
它与 `ResultInvalidError.raw_text` 是两回事,严禁混用:
============== ==================================================
``body_text`` **非 2xx** 的 HTTP 错误响应体: 对方**拒绝**的理由
``raw_text`` **2xx** 但内容不可解析时的模型输出原文
============== ==================================================
加在基类而非某个子类,是因为这些错误全部由同一个 HTTP 响应翻译而来——
"对方说了什么""它属于哪一类"正交。scope 级错误(`GatewayUnavailableError`
一族)继承到的恒空值不是噪音,而是"没有单一响应体可言"的如实表达。
**内容已由 transport 层截断**(`transports/_http_errors.summarize_body`),
且可能包含网关对请求的回显——库不做脱敏: 它不知道下游哪些字段敏感,
猜测式脱敏只会同时丢掉诊断价值与安全性。
本字段是**旁路数据**,不参与任何治理判定(重试/换源/熔断计数/限流结算)。
"""
def __init__(
self,
@@ -44,11 +63,13 @@ class PolyGatewayError(Exception):
source_name: str | None = None,
status_code: int | None = None,
operation: str | None = None,
body_text: str = "",
) -> None:
super().__init__(message)
self.source_name = source_name
self.status_code = status_code
self.operation = operation
self.body_text = body_text
class TransientError(PolyGatewayError):
@@ -64,7 +85,16 @@ class SourceDeadError(PolyGatewayError):
class RequestRejectedError(PolyGatewayError):
"""请求被拒(400/坏输入): 不重试不换源,直接上抛。"""
"""请求被拒(400/坏输入): 不重试不换源,直接上抛。
**经中转部署时请注意**(issue #10 下游实测): 第三方 API 中转服务自身抖动
时也会回 400,从状态码上与供应商说"你的输入有问题"无法区分。下游曾观测到
同一份字节(sha256 一致)重发 15 次全部成功,且失败那次 `prompt_tokens=0`、
耗时远低于任何成功调用——请求在推理开始前就被挡了。本库仍按确定性失败处理
(对直连供应商而言重试只会白烧配额),批处理场景的下游宜自备兜底分类;
`body_text` 即为此提供判据: 中转抖动的响应体与供应商的 `invalid_request_error`
形态不同。
"""
class ResultInvalidError(PolyGatewayError):