• v1.2.0 17dcff41c3

    iomgaa released this 2026-08-17 11:36:32 +08:00 | 148 commits to main since this release

    网关拒绝一次调用时,它说的话不再丢失(issue #10)。下游一轮 1050 张医学影像的批处理里,1 张在读表格这一步收到 400、被判确定性失败而放弃;事后想知道"这张图到底哪里不合规",无从查起——响应体在 transport 翻译层之后就不存在于进程任何位置了。

    根因是三条留存通道同时为空: _status_to_error 手上握着 body_text 却只用于 429 的类型细分,该模块没有任何 logger 调用,异常类也没有承载响应体的字段。而库的逐次遥测写的是 str(exc),即 message——所以只给异常加字段并不能让它进遥测表,必须两者都做。

    新增

    • 四分类错误新增 body_text 字段(加在 PolyGatewayError 基类): 非 2xx 响应体的摘要。与 ResultInvalidError.raw_text 分工明确——前者是"对方拒绝的理由"(非 2xx),后者是"2xx 但内容不可解析时的模型输出"。scope 级错误(GatewayUnavailableError 一族)恒为空串: 它们没有单一响应体可言。
    • 同一份摘要同时进入异常 message,故 SQLite/Postgres 遥测的 error 列里直接可查,下游不必为此单独埋点。

    行为变更

    • 非 2xx 的 message 末尾追加 | {响应体摘要},覆盖两个 transport 的全部分支: chat 的 400 / 401·403 / 4xx 兜底 / 5xx / 429 两支(含 insufficient_quota),以及 OCR 的全部分支。issue 只报告了 chat 的 400,但 401 会 force_open 整个源、OCR 侧 message 原本只有一个状态码,是同一个缺陷的其余分支。
    • 摘要口径: 先折叠空白(错误体常是缩进 JSON,原样拼进 message 会把一行日志炸成多行),再限长 2048 字符(对齐 Kubernetes client-go 同场景的 maxUnstructuredResponseTextBytes)。超长时保留头 1400 + 尾 600并记下省略字数——JSON 错误体的 code / request_id 收在尾部,头部硬切正好会切掉向网关方追查时唯一有用的那部分。
    • 遥测 error 列因此变长: 纯 ASCII 约 2KB/条,最坏(5xx 重试 3 次)一次调用约 6KB。

    不变

    • 状态码 → 错误分类的映射逐条未动(ARCHITECTURE §6.2 表),retry_after_s 解析、429 免重试预算、insufficient_quota 细分全部保持——429 的类型判定仍解析未截断的原文,若改用摘要,超长 body 的配额耗尽会退化成普通限速、该源不再 force_open
    • 异常类型树、str(exc) 之外的字段、遥测 22 字段与列序、DDL 全部未变。错误面零变更,下游 except 写法不受影响。
    • 400 仍按确定性失败处理(不重试不换源)。但请注意: 经第三方中转部署时,中转自身抖动也会回 400,从状态码上与"你的输入有问题"分不开(下游实测: 同一份字节 sha256 一致、重发 15 次全部成功,失败那次 prompt_tokens=0 且耗时远低于任何成功调用)。库不改默认语义——直连供应商时重试只会白烧配额——但 body_text 现在给了下游自行区分的判据。

    升级提示

    README 的安装 pin 由 ==1.1.* 改为 >=1.2,<2仍按 ==1.1.* 安装的下游会静默停在 1.1.2,拿不到本次修复且没有任何报错,请同步改自己的依赖约束。

    • 打包元数据补齐: readme[project.urls]。1.1.2 及之前的包在 registry 页面上没有任何说明正文(缺 readme 时 twine 只警告不阻塞),也没有仓库链接。代码零变更,自本版生效。
    Downloads