docs: widen the body cap to 2048 and keep the tail
The 500-char head-only rule came from a single sample. k8s client-go caps the same thing at 2048; reprlib keeps head and tail because the text is meant to be read. Gateway error bodies are JSON whose code and request_id sit at the very end, so a head-only cut drops exactly what you need to chase the provider. Version pinned at 1.2.0, which forces the README install pin off ==1.1.*.
This commit is contained in:
@@ -90,11 +90,36 @@ message 形态:`"{源名} {标签}: {状态码} | {摘要}"`;**摘要为空时
|
||||
|
||||
### 3.4 常量取值
|
||||
|
||||
**机械规则(实现与测试逐字照此)**:`_ERROR_BODY_CAP = 500` 是**最终摘要的总长度上限**,含截断标记。折叠空白后长度 ≤ 500 则原样返回;超出则取前 **499** 字符并追加 `…`,总长恰为 500。
|
||||
**机械规则(实现与测试逐字照此)**:
|
||||
|
||||
> 这条必须写成算术而非叙述:`text[:500] + "…"`(501)与 `text[:499] + "…"`(500)都能被"截断至 cap 并补 `…`"这句话涵盖,而两者会让测试断言与遥测长度承诺对不上(Codex 审查 2026-08-16 提出)。
|
||||
```
|
||||
_ERROR_BODY_CAP = 2048 # 字符(非字节),含省略标记在内的最终总长上限
|
||||
_HEAD_CHARS = 1400
|
||||
_TAIL_CHARS = 600
|
||||
|
||||
取值理由:Issue 给出的真实样本约 160 字符;既有遥测文本截断常量是 200(`embedding.py:72`、`ocr.py:72`),对错误体偏紧(会切掉稍长的 `error.message`)。500 能容下绝大多数网关错误体,最坏情形(5xx 重试 3 次)每次调用向遥测多写约 1.5KB,`TEXT` 列可承受。
|
||||
折叠空白后 len ≤ 2048 → 原样返回
|
||||
否则 → s[:1400] + f"…(略 {len(s) - 2000} 字)…" + s[-600:]
|
||||
```
|
||||
|
||||
> 规则必须写成算术而非叙述:"截断至 cap 并补标记"能同时被读成总长 2048 与 2049,两者会让测试断言与遥测长度承诺对不上(Codex 审查 2026-08-16 提出)。
|
||||
|
||||
**头尾保留而非头部硬切**(2026-08-16 调研决策)。截断的对象是**结构化 JSON 错误体**,信息分布头重尾也重:人话(`message`)在前,机器可判的 `type` / `code` / `param` / `request_id` 在后。Issue 给出的真实样本即 `"code":"invalid_parameter_error"` 收尾——头部硬切正好切掉向网关方追查时唯一有用的那部分。省略标记记下**被省略的字符数**,读的人才知道自己丢了多少,不会误以为网关只说了这么多。
|
||||
|
||||
按**字符**而非字节切:多字节字符不会被切成半个(Sentry 曾为按字节切开 issue #1691),且 `error TEXT` 列无定长约束,无需字节口径。
|
||||
|
||||
### 3.4.1 取值依据:同场景开源实践
|
||||
|
||||
| 项目 | 场景 | 上限 | 保留策略 |
|
||||
|---|---|---|---|
|
||||
| **Kubernetes client-go** `rest/request.go` | **读 HTTP 错误体生成错误信息**(与本设计同构) | `maxUnstructuredResponseTextBytes = 2048` | 头部硬切 |
|
||||
| OpenAI Python SDK `_exceptions.py` | 异常对象持有 body | **不截断**(内存对象,不落库) | — |
|
||||
| Sentry Python `strip_string` | 事件写入前 trim | `max_value_length`,2.34.0 前默认 1024 | 头部 + `...`,另用 metadata 记原长 |
|
||||
| Elastic APM | 长字段 | keyword 1024 / long field 10000 | 截断带省略号 |
|
||||
| Python 标准库 `reprlib` | 给人读的长字符串 | `maxstring` | **头 + 尾,中间省略** |
|
||||
|
||||
**2048 对齐 k8s client-go**——它是唯一与本设计同场景(读 HTTP 错误体做诊断)的成熟先例。初稿的 500 仅以 issue 的单个样本(约 160 字符)为据,是拿一个样本定上限,已废弃。头部硬切在 k8s/Sentry 成立是因为它们截的是任意文本;本设计截的是结构化 JSON,故取 `reprlib` 的头尾策略。
|
||||
|
||||
遥测代价:纯 ASCII 约 2KB/条,纯中文最多约 6KB/条;5xx 重试 3 次即一次调用最多约 18KB。批处理场景(1050 次调用、5% 失败)约 300KB,`TEXT` 列可忽略。
|
||||
|
||||
message 与 `body_text` **共用同一变量**,不设两个长度:两份不同长度会让"遥测里看到的"与"下游 catch 到的"对不上,排查时反而多一层困惑。
|
||||
|
||||
@@ -107,6 +132,7 @@ message 与 `body_text` **共用同一变量**,不设两个长度:两份不同
|
||||
| `transports/openai_compat.py` | `_status_to_error` 表驱动重写;`_translate_429` 收 `ctx` |
|
||||
| `transports/monkey_ocr.py` | `_classify_status` 带摘要 |
|
||||
| `errors.py` docstring + `ARCHITECTURE.md` §6.2 | 中转拓扑下 400 的提醒(§5.2) |
|
||||
| `README.md:34` | 安装 pin `==1.1.*` → `>=1.2,<2`(§5.3,发布前置,漏改则下游拿不到本修复) |
|
||||
|
||||
三个调用点(`openai_compat.py:402` embed、`:417` stream、`:509` 非流式)**签名不变**,无需改动。
|
||||
|
||||
@@ -122,7 +148,7 @@ message 与 `body_text` **共用同一变量**,不设两个长度:两份不同
|
||||
|
||||
### 4.3 幂等与重复
|
||||
|
||||
纯函数,同输入同输出。`summarize_body` 对自身输出再调用一次是幂等的:输出总长恒 ≤ 500(§3.4)且不含需折叠的空白,第二次调用走"原样返回"分支,不会出现 `…` 被反复追加。
|
||||
纯函数,同输入同输出。`summarize_body` 对自身输出再调用一次是幂等的:输出总长恒为 `2000 + len(标记) ≤ 2048`(标记形如 `…(略 N 字)…`,8 + N 的位数,现实中远不足 48),且不含需折叠的空白,故第二次调用走"原样返回"分支,不会出现标记被反复嵌套。
|
||||
|
||||
### 4.4 持久化与原子性
|
||||
|
||||
@@ -151,7 +177,9 @@ Issue 报告了一个有说服力的观察:同字节 15 次重发全部成功、
|
||||
|
||||
`LLMResponse` 一族的"字段只增不删不改名"约束(ARCHITECTURE §5.1)同样适用于异常。本次是**纯新增关键字参数且带默认值**:既有构造点、既有 `except` 写法、既有 `str(exc)` 消费方全部不受影响。message 文本变化不构成破坏——现有测试对这些 message 无格式依赖(仅 `test_openai_compat.py:558` match 源名)。
|
||||
|
||||
版本建议 **1.2.0**(公共类型新增字段属 minor)。
|
||||
版本 **1.2.0**(公共类型新增字段属 minor;2026-08-16 人类定夺)。
|
||||
|
||||
**发布时必须同步改 README 的安装 pin**:`README.md:34` 现为 `"polygateway[redis,postgres,structured]==1.1.*"`,发 1.2.0 后照此命令安装的下游会**静默停在 1.1.2**——无报错、无警告,与 CLAUDE.md §4.4.1 点名的"极易漏改"完全同款(registry 长期停在 1.0.5 即此类事故)。本次改为 **`>=1.2,<2`**,把"每发一个 minor 就要通知三个下游改 pin"这一反复出现的麻烦一次性消除。此项列入实现计划的发布前置步骤,不是发布日的临时动作。
|
||||
|
||||
### 5.4 有意不夹带的两项(建议单开 issue)
|
||||
|
||||
@@ -188,8 +216,10 @@ Issue 的中转 400 场景确实指向这个方向,但当前只有一个使用
|
||||
|---|---|---|
|
||||
| 1 | **端到端遥测**:mock transport 返回 400 + 真实样本体 → 断言 recorder 收到的 `error` 含摘要 | G1 |
|
||||
| 2 | 参数化状态码(400 / 401 / 404 兜底 / 429 普通 / 429 `insufficient_quota` / 500)→ 断言 message 含摘要且 `exc.body_text` 非空,**分类与既有断言逐一不变** | G2, G4 |
|
||||
| 3 | 超长体 → `len(summary) == 500` 且以 `…` 结尾,前 499 字符与原文前 499 字符逐字相同;`body_text` 与 message 中的摘要逐字相同 | G3 |
|
||||
| 3b | 长度恰为 500 / 501 的体 → 前者原样不带 `…`,后者截为 500 带 `…`(边界) | §3.4 |
|
||||
| 3 | 超长体 → 前 1400 字符与原文头部逐字相同、**末 600 字符与原文尾部逐字相同**、中段为 `…(略 N 字)…` 且 N 等于实际省略数;`body_text` 与 message 中的摘要逐字相同 | G3 |
|
||||
| 3b | 长度恰为 2048 / 2049 的体 → 前者原样无标记,后者走头尾保留(边界) | §3.4 |
|
||||
| 3c | **尾部关键字段可见**:以 issue 的真实样本尾部 `"code":"invalid_parameter_error"}}` 构造超长体 → 断言该串出现在摘要中 | §3.4 头尾决策的验收 |
|
||||
| 3d | 摘要对自身幂等(再摘要一次不嵌套标记) | §4.3 |
|
||||
| 4 | 多行缩进 JSON → 折叠为单行 | G3 |
|
||||
| 5 | 空体 / 纯空白体 → 不拼悬空分隔符,`body_text == ""` | §3.3 |
|
||||
| 6 | 非 JSON 体、非 UTF-8 字节 → 不抛异常,分类不变 | §4.2 |
|
||||
@@ -199,8 +229,10 @@ Issue 的中转 400 场景确实指向这个方向,但当前只有一个使用
|
||||
|
||||
`tests/unit/test_errors.py:29` 现有的"四类构造形态"参数化用例需扩展 `body_text` 默认值断言(默认 `""`、可传入、`GatewayUnavailableError` 一族恒空)。
|
||||
|
||||
## 8. 待人类确认的点
|
||||
## 8. 人类定夺记录(2026-08-16)
|
||||
|
||||
1. `_ERROR_BODY_CAP = 500` 是否合适(§3.4)——它直接决定遥测表增量。
|
||||
2. 429 不设例外、一律拼摘要(§3.3)——高频限速场景下遥测 `error` 列会变长。
|
||||
3. 版本定 1.2.0(§5.3)。
|
||||
| 议题 | 定夺 |
|
||||
|---|---|
|
||||
| 摘要上限与保留策略 | 初稿 500 + 头部硬切被否:上限提至 **2048**(对齐 k8s client-go 同场景先例),策略改为**头 1400 + 尾 600 + 省略字数标记**——人类指出"有用的信息可能只在后半部分",经调研证实 JSON 错误体的 `code`/`request_id` 确实收尾(§3.4、§3.4.1) |
|
||||
| 429 是否设例外 | **不设**,一律拼摘要(§3.3) |
|
||||
| 版本 | **1.2.0**,并同步把 README pin 由 `==1.1.*` 改为 `>=1.2,<2`(§5.3) |
|
||||
|
||||
Reference in New Issue
Block a user