abcdf6930e
- fidelity cacheability also requires a non-empty projected body, so think-tag-only answers are no longer cached - a no-argument tool sent with arguments null only is a deterministic dialect: ResultInvalid tool_arguments_null instead of retried - text mode treats choice index null as 0 and no longer lets False pose as 0 - null-only tool_calls elements no longer produce an empty tool_calls list; non-stream fidelity checks role before the verdict; warning paths are length-bounded - docs: ARCHITECTURE 7.8 field counts, README cache/watchdog wording, CHANGELOG notes, review record in the design, hook note in the plan Red: 8 new tests failed before this change (e.g. test_openai_cacheable_requires_projected_body, test_null_arguments_tool_is_result_invalid_stream, test_text_mode_choice_index_null_is_zero[None-True]).
252 lines
21 KiB
Markdown
252 lines
21 KiB
Markdown
---
|
|
type: design
|
|
node_id: design:2026-10-14-issue30-31-completion-verdict-design
|
|
title: 统一完成判定设计(#30/#31)
|
|
date: 2026-10-14
|
|
---
|
|
|
|
# #30/#31 统一完成判定设计
|
|
|
|
> 状态: 方向已获人类批准(2026-10-14,四项取舍与五个拍板点按推荐);人类授权"Claude 与独立 subagent 确认无误后直接执行",不再走人类设计审阅。两轮独立审查已闭合(R1 3 阻塞 + 9 应修;R2 通过,2 应修已并入),见 §11。
|
|
> 基线: `22609c9`(1.3.9);目标版本 **1.3.10**;分支 `fix/issue30-31-completion-verdict`。
|
|
> 需求: [#30](https://gitea.iomgaa.online/iomgaa/PolyGateway/issues/30)、[#31](https://gitea.iomgaa.online/iomgaa/PolyGateway/issues/31)。
|
|
> 修订对象: [1.3.9 保真设计](2026-10-01-issue29-openai-fidelity-design.md) §4.1/§4.2/§5/§6、ARCHITECTURE §6.2/§7/§14。
|
|
|
|
## 1. 问题与根因
|
|
|
|
两条 issue 是**同一根因**: 库判定"生成结束了、结果可交付"时,依赖库自己的推断(`[DONE]` 存在、正文非空、字段在白名单内、快照字段类型),而不是上游的权威声明(`finish_reason`)与传输完整性(干净 EOF + 结尾 usage)。推断失败一律落 `TransientError` → 重试/换源/计熔断,把确定性结局放大成 `max_attempts` 次付费重抽并推高健康源的熔断计数。
|
|
|
|
| 表现 | 位置(1.3.9) | 后果 |
|
|
| --- | --- | --- |
|
|
| #31 缺 `[DONE]` 但 finish+usage 完整 | `openai_compat.py:668-671`(保真无条件拒);`_check_done:714`(文本 `missing_done=retry` 默认同样拒) | magpie/glm 两模式均重试到耗尽;实测 1216s 挂起、dissect3 38 次终败 |
|
|
| #30-1 reasoning-only + `length` | `types.py:478-490`、`_reject_empty_completion` | 每次写满 max_tokens 后丢弃重抽 |
|
|
| #30-2 未知键 / `tool_calls:null` | `_openai_completion.py:97-98,112-114,139-140,146-147`;**非流式也中**: `OpenAIChatCompletion` 拒 `tool_calls: None`(`types.py:451-453`,已实测) | 渠道一换 100% 失败,且以"瞬时"形态出现 |
|
|
| 隐患: 只有 `reasoning` 字段的流 | 活性只认 content/reasoning_content | 两模式被看门狗当无进展 → TTFT 超时 |
|
|
| 隐患: 文本 salvage 的截断结果被缓存 | `cache.py:197-204` 写路径无判定 | 半截结果 TTL 内反复回放 |
|
|
|
|
补充事实: GLM 的 finish_reason 含 `network_error`/`sensitive`;2026-07-20 空补全裁决针对的是 stop/无 finish 的"零增量"抖动(`findings/2026-07-21-m25-acceptance.md:48`);vLLM 等实现每帧带累计 usage,finish 之前的 usage 不证明走完。
|
|
|
|
## 2. 方案比较
|
|
|
|
| 方案 | 内容 | 结论 |
|
|
| --- | --- | --- |
|
|
| A 局部放行 | 按 issue 字面在三处各加例外 | 否决: 流式/非流式/缓存判据继续分叉;文本 #31 残留;`network_error` 被当成功 |
|
|
| B 统一完成判定(选定) | 内核纯函数分两步(流完整性 → 结局分类),流式/非流式/缓存读写四处共用;"可交付"与"可缓存"拆两判据;未知字段无损合并 | 一次消除根因;代价是公共字段追加与遥测端口加参 |
|
|
| C 透明透传 | 保真模式不做结局判定,原样交回 | 否决: 零增量抖动回归(违背 2026-07-20 裁决),截断流也被交出 |
|
|
|
|
## 3. 公共契约变更(只增不改)
|
|
|
|
| 位置 | 变更 |
|
|
| --- | --- |
|
|
| `types.py` | 新增 `StreamEnd = Literal["done", "eof_after_finish", "salvaged"]`,包根导出 |
|
|
| `TransportResult` / `LLMResponse` 末尾 | 追加 `finish_reason: str \| None = None`、`stream_end: StreamEnd \| None = None` |
|
|
| `OpenAIChatCompletion` 校验 | `tool_calls: null` 合法(视同无工具,原样保留键) |
|
|
| `TelemetryRecorder.record_llm_call` | 追加**无默认值** keyword-only `stream_end: str \| None`(**破坏性**: 自建 recorder 装配期被形状闸拒绝,CHANGELOG 单列) |
|
|
| `llm_calls` 表 | 末尾追加可空列 `stream_end TEXT`(落点见 §7) |
|
|
| `Transport.complete` 签名 | **不变** |
|
|
|
|
`finish_reason` 取值: 流式取 choice 0(保真已强制单 choice;文本取 `index` 缺省或为 0 的 choice)首个非 null 值;非流式取 `choices[0].finish_reason`。保真模式非字符串维持 1.3.9 拒绝(Transient);文本模式非字符串记 None。`finish_reason` 不进遥测列(未获批)。
|
|
|
|
| 场景 | `stream_end` |
|
|
| --- | --- |
|
|
| 流式收到 `[DONE]` | `"done"` |
|
|
| 干净 EOF + finish + 结尾 measured usage | `"eof_after_finish"` |
|
|
| 文本 `missing_done=salvage` 打捞 | `"salvaged"` |
|
|
| 非流式 / 缓存命中 / 失败行 | `None`(与 `ttft_ms`/`stall_ms` 同"无从观测"口径) |
|
|
|
|
与 `usage_source` 分工: 后者答"账单可信吗",前者答"内容完整吗"。`eof_after_finish ⇒ measured`(由 §4.2 条件保证);`salvaged ⇒ estimated|unavailable`;`estimated ⇒ salvaged`,逆命题不成立。
|
|
|
|
## 4. 统一完成判定
|
|
|
|
### 4.1 位置与形态
|
|
|
|
全部放 `types.py` 内部纯函数(下划线前缀,不导出): CacheMW 要复用,import-linter 禁止 middleware 依赖 transports,而内核层 `ports : types : errors : streaming` 允许。函数只返回判定值,**不抛领域错误**(错误需 `source_name` 等上下文,由调用点翻译),不捕获任何异常,取消天然穿透。
|
|
|
|
| 函数 | 职责 |
|
|
| --- | --- |
|
|
| `_tool_snapshot_ok(message)` | 工具快照是否成立(id/type/function.name/arguments 规则 + 唯一 id);`OpenAIChatCompletion.__post_init__` 复用 |
|
|
| `_classify_finish(value)` | 映射到 finish 类别(下表) |
|
|
| `_stream_is_complete(...)` | 第一步,返回 `StreamEnd \| None`(None = 截断) |
|
|
| `_judge_outcome(...)` | 第二步,返回 `deliver` 或 `(reject_transient \| reject_result, 原因码)` |
|
|
| `_is_cacheable(...)` | §6 判据 |
|
|
|
|
| finish 类别 | 值(大小写敏感) |
|
|
| --- | --- |
|
|
| natural | `stop`、`tool_calls`、`function_call` |
|
|
| truncated | `length` |
|
|
| filtered | `content_filter`、`sensitive` |
|
|
| upstream_error | `network_error`、`error` |
|
|
| unknown | 其余非空字符串: 按 natural 交付、**不可缓存**、按源+值告警一次(文案写明"该值不缓存",便于下游定位命中率下降);`end_turn`/`stop_sequence` 等无证据不预先归类 |
|
|
| absent | None |
|
|
|
|
### 4.2 第一步: 流完整性(仅流式)
|
|
|
|
| 条件 | 结果 |
|
|
| --- | --- |
|
|
| 读取异常 / 活性超时 / error 帧 / 畸形帧 | 照旧 Transient(不动) |
|
|
| 收到 `[DONE]` | `done` |
|
|
| 干净 EOF、无 DONE、finish 已见、finish 帧**同帧或之后**收到的 usage 被 `_resolve_usage` 判为 measured | `eof_after_finish`,usage 记 measured(不降级) |
|
|
| 干净 EOF、其余情形 | 截断: openai → Transient `missing_done`(仍不打捞);text → 原 `_check_done` 语义(零产物 early_eof、`retry` 抛、`salvage` 记 `salvaged`) |
|
|
|
|
聚合器每尝试记录 `finish_seen` 与 `usage_after_finish`: 收到非 null usage 时,若 `finish_seen` 已真或同帧带 finish,则置后者为真;是否 measured 以**最后一个**非 null usage(与计费所用 `sink["usage"]` 同一对象)在流结束时判定;usage-only 帧(`choices: []`)在 finish 之后到达是常规形态,正确置位。状态随重试/对冲每尝试新建。
|
|
|
|
诚实边界: 中转站在上游出错后主动正常结束响应,与真完整在传输层不可区分;防线是"finish 必须存在、不属 upstream_error、且其后有 measured usage",文档如实写明。
|
|
|
|
### 4.3 第二步: 结局分类
|
|
|
|
产物定义——**保真模式**: 任何产物 = 消息中任一非空字符串片段(content、reasoning_content、reasoning、refusal、合并保留的未知字符串,空白也算)或至少一个非 null 工具条目;**文本模式**: 任何产物 = 投影 content 或 thinking(含 §4.4 别名)非空串(含空白),未知字段与工具增量只计活性不计产物,`_check_done` 零产物判据同口径。两模式**正文产物** = 投影 content strip 后非空,或(仅保真)工具快照成立且非空。
|
|
|
|
表格**自上而下取首个匹配行**:
|
|
|
|
| # | 条件 | openai 模式 | text 模式 |
|
|
| --- | --- | --- | --- |
|
|
| 1 | finish 为 upstream_error | Transient `upstream_finish_error` | 同左 |
|
|
| 2 | (仅保真)存在工具条目但 `_tool_snapshot_ok` 不成立 | truncated → ResultInvalid `tool_truncated`;其余 → Transient `tool_incomplete` | 不适用 |
|
|
| 3 | 无任何产物 + truncated/filtered | 交付(#30;隐藏推理模型写满预算的形态) | ResultInvalid `length_without_content` / `filtered_without_content` |
|
|
| 4 | 无任何产物 + natural/unknown/absent | Transient `empty_completion`(保 2026-07-20 裁决) | 同左 |
|
|
| 5 | 有正文产物 | 交付 | 交付 |
|
|
| 6 | 有产物无正文 + truncated | 交付(#30) | ResultInvalid `length_without_content` |
|
|
| 7 | 有产物无正文 + filtered | 交付 | ResultInvalid `filtered_without_content` |
|
|
| 8 | 有产物无正文 + natural/unknown | 交付 | Transient `empty_completion`(待证据;消息带 finish 与 thinking 长度) |
|
|
| 9 | 有产物无正文 + absent(流式仅 DONE 收尾,或非流式 body finish 为 null) | Transient `empty_completion` | 同左 |
|
|
|
|
ResultInvalid 理由: `retry.py:409-414` 对其"不重试、不换源、熔断记成功不计窗口";length/过滤是预算或内容策略的确定性结局,不是源坏了。对冲下原路 ResultInvalid 等对冲路裁定,两败原样上抛(`retry.py:548-560`);StructuredMW 只捕 parse 抛出的 ResultInvalid,不会误重问(`structured.py:79`)。已知代价: 该分支 TPM 全额退款、用量不落成本(`actual=0`),与 Transient 丢成本同级,登记为已知边界。文本 length 改抛 ResultInvalid 后不再经 `AllSourcesExhausted` 包装,§10 要求核对下游 catch 点。
|
|
|
|
原因码以固定格式 `"{源} {code}: …"` 写进异常 message,从而进遥测 `error` 列(LIKE 可检索);不新增列、不改 `_failure_reason`(Transient 一族熔断原因记 `network_error` 属已知口径)。
|
|
|
|
保真模式行 2 之外的工具快照成立 + `length`: 交付,arguments 可能是半截 JSON——docstring 与 CHANGELOG 提醒调用方先查 `finish_reason` 再执行工具。非流式跳过第一步,直接以 body 的 finish_reason 走第二步。
|
|
|
|
### 4.4 文本模式 reasoning 别名
|
|
|
|
投影 `thinking`: 本次尝试出现过**非空** `reasoning_content` 则只用它,否则用 `reasoning`。保真 `completion.message` 保留上游原键不改名。两模式活性、产物摘要、`thinking_observation` 均基于该投影。
|
|
|
|
## 5. 字段合并与活性
|
|
|
|
### 5.1 null 与已知字段
|
|
|
|
null 规则**优先于**已知字段类型校验: chunk、choice、delta、工具条目、function 五层任意键值为 null 视为缺省,数组中的 null 元素(如 `tool_calls: [null]`)跳过;键从未出现时保留 `key: null`(含 `tool_calls`、`refusal`、`role`、`function.arguments`)。保真成功仍须最终 role=assistant(全程只有 `role: null` 即缺 role,照旧失败)。其余已知字段(content/reasoning_content/role/tool_calls 及工具 id/type/function.name/arguments)非 null 时语义逐字沿用 1.3.9。
|
|
|
|
### 5.2 未知字段(delta、`tool_calls[]` 条目、`function` 三层统一)
|
|
|
|
| 值 | 规则 |
|
|
| --- | --- |
|
|
| 标签键 `index`/`type`/`id`/`format`(任意未知层级) | 同值幂等,不拼接;不同值 = 冲突 |
|
|
| 其他字符串 | 按到达顺序拼接(OpenAI SDK 流式累加惯例) |
|
|
| 数字/布尔 | 同值幂等;不同值 = 冲突(不学 SDK 相加) |
|
|
| 对象 | 递归套用本表 |
|
|
| 数组且元素均为带整数 `index` 的对象 | 按 index 合并(`reasoning_details` 一类);合并策略由该键首次出现形态决定,之后形态改变 = 冲突 |
|
|
| 其他数组 | 追加 |
|
|
| 冲突 | 删除该键路径,本尝试内不再接收;按 (源, 归一路径) 告警一次 |
|
|
|
|
冲突删键是有损的: docstring 与 CHANGELOG 写明"可能缺字段,以告警为准"。上游方言确定,重拉同样结果,不视为缓存毒化。
|
|
|
|
### 5.3 活性
|
|
|
|
已知字段活性逐字沿用 1.3.9(非空 content/reasoning_content、工具 name/arguments 非空增量、首次建立工具身份)。未知字段仅当本帧在非标签键上并入非空字符串片段(含追加型数组中的字符串元素)时算进展(`reasoning` 由此计入);同帧只记一次。残余风险如实登记: 上游每帧重复下发的非标签字符串(如 `status`)会被拼接并持续刷新看门狗。文本模式同口径。README 中"增量 chunk = content 或 reasoning_content"的 stall/TTFT 口径表述同步更新为本节口径。
|
|
|
|
### 5.4 告警节流
|
|
|
|
节流集合放 transport 实例(与 `_warned_mismatches` 分立,纯 asyncio 中立),键为 (源, 归一路径) 与 (源, 未知 finish 值):路径中数组下标归一为 `[]`,finish 值截断至 64 字符;集合上限 1024,饱和后记一次"告警已饱和"不再增长。日志只写源名与路径,不回显值(可能含工具参数)。聚合器以只读集合交出冲突路径,transport 负责告警。
|
|
|
|
## 6. 可交付 vs 可缓存
|
|
|
|
可缓存 ⊂ 可交付。两模式同一判据 `_is_cacheable`,读写共用:
|
|
|
|
| 条件(任一成立即不可缓存) | 理由 |
|
|
| --- | --- |
|
|
| `stream_end == "salvaged"` | 截断结果,修现存毒化 |
|
|
| 无正文产物 | 不把空回答固化进 TTL |
|
|
| finish 类别为 truncated/filtered/unknown | 截断有随机性,同 key 换时可成功;未知值不可证明是自然结束 |
|
|
| openai 模式 completion 原文 content 为空且无工具(沿 1.3.9) | 不信任旧投影 |
|
|
|
|
| 路径 | 规则 |
|
|
| --- | --- |
|
|
| 写 | 不可缓存 → 跳过,debug 日志 |
|
|
| 读 | `_is_cacheable` 在命中覆盖(`cache.py:224` `fields.update`)**之前**、对存储原值判定;不可缓存 → miss + warning |
|
|
| 读: 非法 `stream_end` | 不在 Literal 域内 → miss + warning(关乎内容完整性,故与 `_coerce_observation` 的宽容方向相反) |
|
|
| 读: 顶层无 finish_reason | 回退 `completion.finish_reason`(1.3.9 保真 length 条目因此可识别);文本旧条目两字段缺省 → None → 仍可命中 |
|
|
| 命中返回 | `stream_end=None`;`finish_reason` 取判定所用值(顶层缺省时为回退值),非字符串视 miss |
|
|
| 序列化 | `_RESPONSE_FIELDS` 由 `dataclasses.fields` 自动派生,无需改动 |
|
|
|
|
旧 salvage/length 文本污染无法辨认: CHANGELOG 继续要求受影响集合迁移 namespace/salt。
|
|
|
|
## 7. 遥测落点
|
|
|
|
| 位置 | 变更 |
|
|
| --- | --- |
|
|
| `middleware/telemetry.py` `_AttemptUsage.of(response)` | 读 `response.stream_end`(本次 LLMResponse 有字段,不像 1.3.8 走 `emit_attempt` kw,避免约 50 处调用点) |
|
|
| `_record` | 新增参数,单一 helper 下沉 recorder |
|
|
| `emit_cache_hit` / `emit_terminal_failure` / 失败与取消 attempt 行 | 显式 None |
|
|
| `telemetry/schema.py` | SQLite/PG 两份 CREATE 末尾、`SQLITE_BACKFILL`、`_PG_BACKFILL_DECLS`(列序对齐)、`COLUMNS`;`telemetry_schema_sql()` 随之输出 |
|
|
| sqlite/postgres recorder | 按 `COLUMNS` 写入;auto 档幂等补列、manual 档裁剪写入并点名缺列(沿 1.3.8) |
|
|
| tests | 约 20 处测试替身 `record_llm_call` 补参数 |
|
|
| 文档 | README "必录字段数"按 `inspect.signature` 实测更新;`schemas/llm-calls.md` 加列并修正陈旧计数 |
|
|
|
|
## 8. 旧行为审计
|
|
|
|
| 既有行为 | 处置 |
|
|
| --- | --- |
|
|
| 保真缺 DONE 一律失败 | **替换**: §4.2 判完整时成功;其余仍失败、仍不打捞 |
|
|
| 文本 `missing_done=retry` 遇缺 DONE 即抛 | **替换**: 同上;`missing_done` 只管真截断 |
|
|
| 文本 salvage + estimated | 保留,另标 `salvaged`,不再写缓存 |
|
|
| 零增量 + DONE → Transient | **保留**(`test_stream_zero_content_with_done_is_transient` 红线回归) |
|
|
| 保真 reasoning-only/空产物失败 | **替换**: §4.3 行 3/6/7/8 |
|
|
| 未知 delta/工具/function 字段拒绝;null 已知字段拒绝 | **替换**: §5 |
|
|
| 非流式 `tool_calls: null` 拒绝 | **替换**: 合法 |
|
|
| 多 choice/index≠0 拒绝、最终 role 必须 assistant、error 帧立即失败 | 保留 |
|
|
| 工具快照强校验 | 保留;仅 length 截断时改归 ResultInvalid |
|
|
| finish_reason 冲突拒绝(Transient) | 保留(登记: 确定性方言,将来可改 ResultInvalid) |
|
|
| 已知字段活性口径 | 保留;未知字段按 §5.3 增补 |
|
|
| 缓存写路径无判定 | **替换**: 按 `_is_cacheable` |
|
|
| 文本 n>1 混流 | 不在范围,另开 issue |
|
|
|
|
## 9. 非功能维度
|
|
|
|
| 维度 | 回答 |
|
|
| --- | --- |
|
|
| 并发与取消 | 新状态在每尝试聚合器/sink 内;判定为纯函数,不新增 try/except,`CancelledError` 原路穿透;finally 结算与 permit 释放不变 |
|
|
| 降级方向 | 缓存跳写/视 miss 只 debug/warning;遥测新列缺失按 manual 档裁剪 + 告警,不冒泡;限流/熔断不涉及 |
|
|
| 幂等与重复 | 减少重复付费重抽;标签键与身份字段幂等;告警节流有界 |
|
|
| 持久化与原子性 | 仅新增一可空列,补列沿既有探测后 ALTER 幂等流程;缓存仍成功后单次原子 set |
|
|
| 错误分类 | 仅落 Transient 与 ResultInvalid 两类,不新增类;原因码见 §4.3 |
|
|
| 可观测 | `stream_end` 字段 + 遥测列;冲突路径与未知 finish 有界告警;遵 structured-logging 更新 `schemas/llm-calls.md` |
|
|
|
|
## 10. 测试、文档与交付
|
|
|
|
全部用假 SSE / MockTransport 复现,每条先红后绿;真实中转冒烟标 slow 按发布清单 diff 交集跑。
|
|
|
|
| 组 | 用例 |
|
|
| --- | --- |
|
|
| #31 验收 | 1 stop+usage+EOF 成功、`eof_after_finish`、measured、attempts=1;2 tool_calls 同上工具完整;3 无 finish 即关 → 失败;4 finish 无 usage → 失败;5 中途 `RemoteProtocolError` → 失败;6 带 DONE → `done` 不变 |
|
|
| #31 补充 | usage 早于 finish(累计 usage)+EOF → 不完整;finish 后 usage 为 `{}` → 不完整;`network_error`+usage+EOF → Transient;文本模式 1/3/4(`retry`、`salvage` 两档) |
|
|
| #30 验收 | reasoning-only+length → 交付、content 空、思考完整、attempts=1;流式 `refusal:null`+`tool_calls:null` 成功;非流式 message 带 `tool_calls:null`+`refusal:null` 成功 |
|
|
| 结局表 | §4.3 每行至少一例,两模式、流式与非流式;文本 length → ResultInvalid 且熔断未计失败;零增量+DONE(带 stop/不带 finish)仍 Transient;content 非空 + 坏工具 + stop → Transient |
|
|
| 合并/活性 | 只有 `reasoning` 刷新活性不超时;二次构造 OpenRouter `reasoning_details` 往返(`format`/`id` 不拼接);工具层嵌套对象原样保留可回传;数值冲突删键、告警一次、调用成功;节流饱和;`role:null`/`arguments:null` 视缺省 |
|
|
| 缓存 | 四类不可缓存均不写、注入后读视 miss;非法 `stream_end` 视 miss;1.3.9 旧文本条目仍命中;1.3.9 保真 length 条目经回退识别为不可缓存;命中 `stream_end=None` |
|
|
| 遥测 | sqlite/pg(integration)新列写入、旧库补列、manual 档裁剪;形状闸拒绝缺参 recorder |
|
|
| 改写 | `test_openai_never_salvages`;`test_invalid_delta_is_domain_error` 中 `{"audio": {}}`、`{"index":0,"extra":1}`、null 已知字段参数;提交说明列出 |
|
|
|
|
同步: 源码 docstring、README(能力表、`1.3.*` 安装约束核对、活性口径、字段数)、CHANGELOG 1.3.10(行为变更段、破坏性段: recorder 端口、namespace/salt 提醒、半截工具参数提醒、冲突删键提醒)、ARCHITECTURE §6.2/§7/§14、1.3.9 设计加修订指针、`schemas/llm-calls.md`、`research-wiki/migrations/` 三份核对下游对 ResultInvalid 的 catch 点、`docs-convention.md` §2 清单。另开 issue: 文本 n>1 混流;文本"有产物无正文 + stop"证据收集。发布按 CLAUDE.md §4.4.1 九步,#30/#31 在 registry 可安装且验收证据齐备后关闭。
|
|
|
|
证据: `retry.py:404-450,548-560`;`cache.py:197-236`;`structured.py:79`;[openai-python `_streaming.py`](https://github.com/openai/openai-python/blob/main/src/openai/_streaming.py) 与 `lib/streaming/_deltas.py`;[vLLM reasoning outputs](https://docs.vllm.ai/en/latest/features/reasoning_outputs/);GLM finish 枚举来自第三方镜像,实现前以真实中转探针再核;Gemini `extra_content` 路径未核实原文,测试按通用嵌套对象构造。
|
|
|
|
## 11. 审查修订记录
|
|
|
|
| 轮次 | 意见 | 处置 |
|
|
| --- | --- | --- |
|
|
| R1 | B1 `tool_calls:null` 被快照拒(非流式现存) | 采纳: §3/§5.1/§10 |
|
|
| R1 | B2 结局表行重叠 | 采纳: 首匹配行序 + `_tool_snapshot_ok` 前置 |
|
|
| R1 | B3 文本"任何产物"不可计算 | 采纳: §4.3 分模式定义 |
|
|
| R1 | S1 eof_after_finish 但 usage 非 measured | 采纳: 条件加 measured |
|
|
| R1 | S2/S3 活性冲突与标签拼接 | 采纳: §5.2/§5.3 |
|
|
| R1 | S4 告警集合无界 | 采纳: §5.4 |
|
|
| R1 | S5 无产物 + length 与 #30 原文冲突 | 采纳(人类授权代决): 保真交付、文本 ResultInvalid、stop/absent 仍 Transient |
|
|
| R1 | S6-S9 缓存顺序、遥测落点、原因码、null 优先 | 采纳: §6/§7/§4.3/§5.1 |
|
|
| R1 | 建议 1-7 | 采纳 1/2/3/5/6/7;建议 4 不标不可缓存(上游确定性,非毒化) |
|
|
| R2 | 结论通过;N1 measured 以最后 usage 判定、N2 非空 reasoning_content | 采纳: §4.2/§4.4 |
|
|
| R2 | 建议 1-4: 命中回退值、未知 finish 告警文案、数组字符串活性与残余风险、null 五层与数组 null 元素 | 全部采纳: §6/§4.1/§5.3/§5.1 |
|
|
| 代码审查 | I1 保真可缓存须同时要求投影正文非空(think 标签剥净后为空不缓存) | 采纳: `_is_cacheable` |
|
|
| 代码审查 | I2 无参函数 `arguments` 全程 null → 快照不成立被判 Transient 重试 | 人类授权代决: 归 ResultInvalid `tool_arguments_null`(确定性方言);不改写交付值(改写破坏保真,重试救不回) |
|
|
| 代码审查 | I3 文本 `index:null` 整帧丢弃、`index:False` 冒充 0 | 采纳: null 视 0,bool 拒 |
|
|
| 代码审查 | Minor: `[null]` 产出空 `tool_calls`、非流式缺 role 先于判定、告警路径长度有界、README 表述 | 采纳;index 数组缺 index 整键删除登记于 CHANGELOG 提醒 |
|