27 KiB
type, node_id, title, date
| type | node_id | title | date |
|---|---|---|---|
| plan | plan:2026-10-14-issue30-31-completion-verdict | 统一完成判定实现计划(#30/#31) | 2026-10-14 |
#30/#31 统一完成判定实现计划
目标: 按设计(下称"设计")实现统一完成判定,修复 #30/#31 并发布 1.3.10。
方案概述: 在 types.py 内核放一组纯判定函数(流完整性 → 结局分类 → 可缓存),transport 两条路径、CacheMW 读写共用;聚合器改为 null 优先 + 未知字段无损合并;LLMResponse/TransportResult 追加 finish_reason/stream_end,遥测加 stream_end 列。
技术: Python 3.12、httpx、pydantic、pytest(asyncio)、SQLite/PG 遥测、Redis 缓存。环境 conda run -n PolyGateway。
本计划不涉及参考实现迁移,保真校验不适用。
0. 执行纪律
- 分支
fix/issue30-31-completion-verdict。每个任务一次提交,经commitskill;pre-commit 钩子会跑全套件 + ruff + "改动触及函数复杂度 < C",故每个提交点必须全绿,改到的既有高复杂度函数(add_chunkF42、_complete_stream/_complete_onceC、_rehydrateC、OpenAIChatCompletion.__post_init__D)须在同一提交内拆到 B 及以下;钩子按 radon 全部块判定,类块(ChatCompletionAccumulatorC16、OpenAIChatCompletionD24)同样须降到 B 及以下。不改OpenAICompatTransport.complete()(C14),新增逻辑一律放在拆出的 helper 中。 - 每个任务的统一验证(与钩子一致):
conda run -n PolyGateway pytest tests/ -q(含 integration,本地有 Redis/PG 时真跑)+conda run -n PolyGateway ruff check src+conda run -n PolyGateway radon cc -n C <本任务改动文件>(输出必须为空,或仅含本任务未触及的存量块)。各任务列出的子集命令只作开发期快速回路。 - 测试结果门: 每个任务的新增/改写测试先在未改实现的代码上运行并记录失败输出(粘贴进提交说明正文的"Red:"一行,写明失败用例名与断言),再实现至通过。
- 后台长跑命令末尾禁止接管道(CLAUDE.md §3)。
1. 文件结构
| 文件 | 职责变化 |
|---|---|
src/polygateway/types.py |
新增 StreamEnd、FinishClass、_OutputSummary、_Verdict 与七个纯函数(§2 列出;另 _effective_finish);TransportResult/LLMResponse 末尾追加两字段;OpenAIChatCompletion 接受 tool_calls: None 并复用 _tool_snapshot_ok |
src/polygateway/__init__.py |
导出 StreamEnd;__version__ → 1.3.10(T6) |
src/polygateway/transports/_openai_completion.py |
聚合器: null 优先、未知字段合并、标签键、冲突路径、finish/usage 位置记录、reasoning 别名;parse_chat_completion 容忍 null |
src/polygateway/transports/openai_compat.py |
两条路径改走判定;有界告警节流;finish_reason/stream_end 填充;投影别名 |
src/polygateway/middleware/retry.py |
_build_response 搬运两字段 |
src/polygateway/middleware/cache.py |
写/读按 _is_cacheable;stream_end 校验;finish 回退 |
src/polygateway/ports.py |
record_llm_call 追加 stream_end |
src/polygateway/middleware/telemetry.py |
_AttemptUsage.stream_end、_record 搬运、两 emit 显式 None |
src/polygateway/telemetry/schema.py、sqlite.py、postgres.py |
新列 + 补列 |
| tests | 新增 tests/unit/test_completion_verdict.py;改写/扩充 test_openai_completion.py、test_openai_fidelity.py、test_openai_compat.py、test_cache.py、test_telemetry.py、test_types.py、test_ports.py、tests/integration/test_postgres_telemetry.py 及各 record_llm_call 替身 |
| 文档 | README、CHANGELOG、ARCHITECTURE §6.2/§7/§14、1.3.9 设计修订指针、schemas/llm-calls.md、migrations/ 三份、pyproject 版本 |
2. 跨任务接口(T1 定义,后续任务只消费)
StreamEnd = Literal["done", "eof_after_finish", "salvaged"]
FinishClass = Literal["natural", "truncated", "filtered", "upstream_error", "unknown", "absent"]
def _classify_finish(value: str | None) -> FinishClass: ...
def _tool_snapshot_ok(message: Mapping[str, Any]) -> bool: ...
def _stream_is_complete(
*, done: bool, finish_reason: str | None, usage_after_finish: bool, usage_measured: bool
) -> StreamEnd | None: ...
@dataclass(frozen=True)
class _OutputSummary:
any_output: bool # 设计 §4.3 "任何产物"
body_output: bool # 设计 §4.3 "正文产物"
tools_present: bool # 至少一个非 null 工具条目(仅保真有意义)
tools_ok: bool # tools_present 时 _tool_snapshot_ok 结果;无工具恒 True
@dataclass(frozen=True)
class _Verdict:
deliver: bool
error_kind: Literal["transient", "result_invalid"] | None # deliver 时为 None
code: str | None # 设计 §4.3 原因码
def _summarize_text(content: str, thinking: str) -> _OutputSummary: ...
def _summarize_message(message: Mapping[str, Any], projected_content: str) -> _OutputSummary: ...
def _judge_outcome(*, mode: ResponseMode, summary: _OutputSummary, finish: FinishClass) -> _Verdict: ...
def _is_cacheable(
*, mode: ResponseMode, content: str, finish_reason: str | None,
stream_end: str | None, completion: OpenAIChatCompletion | None,
) -> bool: ...
缓存读写共用(T1 定义,T4 消费),顶层优先、回退 completion.finish_reason,避免读写口径分叉:
def _effective_finish(finish_reason: Any, completion: OpenAIChatCompletion | None) -> str | None: ...
types.py 无 from __future__ import annotations: StreamEnd/FinishClass 必须定义在 TransportResult/LLMResponse 之前。
聚合器对外只读属性(T2 定义,T3 消费): finish_reason: str | None、finish_seen: bool、usage_after_finish: bool、conflicts: frozenset[str](归一路径)、raw_message() -> dict[str, Any](未校验的合并消息深拷贝,含按 index 排序组装的 tool_calls,与 finish() 同一组装函数,供 _tool_snapshot_ok 判定)、finish() -> OpenAIChatCompletion(保真快照,校验失败抛 Transient,与 1.3.9 同)。ChatDelta 追加字段 reasoning: str(别名片段,默认 "")。
3. 任务
T1 内核判定函数与公共字段
- 文件:
src/polygateway/types.py、src/polygateway/__init__.py;测试tests/unit/test_completion_verdict.py(新)、tests/unit/test_types.py、tests/unit/test_package.py(导出)。 - 行为:
_classify_finish按设计 §4.1 表,大小写敏感;None →absent。_tool_snapshot_ok:tool_calls缺省或 None → True;否则必须是 list,每个非 None 元素满足 1.3.9 规则(非空唯一 id、type == "function"、非空function.name、字符串arguments);list 中 None 元素跳过。OpenAIChatCompletion.__post_init__改为调用它(并拆分以降复杂度),tool_calls: None合法且原样保留键。_stream_is_complete:done→"done";否则finish_reason is not None and usage_after_finish and usage_measured→"eof_after_finish";否则 None。_summarize_text: any =content != "" or thinking != "";body =content.strip() != "";tools_present False、tools_ok True。_summarize_message: any = 递归遍历 message 中除role、tool_calls外所有值存在非空字符串,或 tools_present;tools_present =tool_calls为含非 None 元素的 list;tools_ok =_tool_snapshot_ok(message);body =projected_content.strip() != ""或 (tools_present 且 tools_ok)。_judge_outcome: 严格按设计 §4.3 九行首匹配;原因码字面量:upstream_finish_error、tool_truncated、tool_incomplete、length_without_content、filtered_without_content、empty_completion。行 2 仅mode == "openai"时生效。_is_cacheable: 设计 §6 四条,任一成立 False;stream_end不在StreamEnd域且非 None → False;openai 模式且completion is None→ False。TransportResult、LLMResponse末尾追加finish_reason: str | None = None、stream_end: StreamEnd | None = None(放在现有最后一个字段之后,不扰动位置构造);docstring 写清 None 语义与usage_source分工。- 包根导出
StreamEnd;_has_completion_output本任务保留不动(T3/T4 迁移后删除)。
- 测试(新文件):
_classify_finish全值表参数化;_stream_is_complete真值表(含 usage 早于 finish、usage 非 measured);_judge_outcome九行 × 两模式参数化(每行至少一例,含"正文非空+坏工具+stop → transient tool_incomplete");_summarize_message对 refusal-only、reasoning-only、whitespace content、未知嵌套字符串、tool_calls: [None];_is_cacheable四类否例 + 正例 + 非法 stream_end。test_types.py:OpenAIChatCompletion接受tool_calls=None、拒非 list、[None]合法;两新字段默认 None。 - 验证:
conda run -n PolyGateway pytest tests/unit/test_completion_verdict.py tests/unit/test_types.py tests/unit/test_package.py -q→ 全过;make lint无新增问题。 - 提交
feat: add kernel completion verdict functions and response fields
T2 聚合器: null 优先、未知字段合并、finish/usage 记录
- 文件:
src/polygateway/transports/_openai_completion.py;测试tests/unit/test_openai_completion.py。 - 保留
message、tools实例属性名与语义(test_openai_completion.py:70,93直接依赖)。 - 行为(设计 §5、§4.2):
- 每帧: 校验 chunk 对象;
usage非 null 时记录为last_usage,若finish_seen或同帧 choice 带非 null finish →usage_after_finish = True。 - choices: 保真维持单 choice/index 0 校验;文本模式取
index缺省或为 0 的第一个 choice,其余忽略。 - finish_reason: 保真走
_identity(非字符串/冲突 → Transient,同 1.3.9);文本模式首个字符串值生效,非字符串忽略。任一模式见到非 null 即finish_seen = True。 - null 五层(设计 §5.1): chunk 层
choices: null、choice 层delta: null视为缺省;例外: 顶层error键(含error: null)仍由_check_chat_error按 error 帧拒绝(不动,保test_error_before_done_is_failure)。 - delta 与工具条目、function 三层: 值为 None 的键 → 若目标无此键则
setdefault(key, None)(保真),不做类型校验;tool_callslist 中 None 元素跳过。 - 已知字段非 None 时逐字沿用 1.3.9 校验与合并(content/reasoning_content 拼接、role 必须 assistant、工具 id/type
_identity、name/arguments 拼接)。 - 未知字段(delta 层除 role/content/reasoning_content/tool_calls;工具层除 index/id/type/function;function 层除 name/arguments): 交给
_merge_unknown(target, key, value, path),规则见设计 §5.2;标签键集合{"index", "type", "id", "format"};冲突 → 从 target 删除该键、把归一路径(数组下标替换为[],如delta.reasoning_details[].format)加入conflicts与本尝试"封禁路径"集合,后续同路径值丢弃。_merge_unknown返回"是否并入非空字符串片段"。 reasoning: 在保真模式作为普通未知字符串合并进 message;两模式都把其片段放入ChatDelta.reasoning。- progress: 1.3.9 已知字段口径 或 任一
_merge_unknown返回 True(文本模式也调用合并函数计算活性,但合并结果不对外)。 - 新增只读属性见 §2;
finish()改为: 组装 message(含排序后工具)后构造OpenAIChatCompletion(校验失败仍抛 Transient)。 parse_chat_completion拆为两步:check_completion_body(body) -> tuple[dict, str | None](只校验 body 为对象、恰好一个 choice、index 缺省或为 0、message 为对象、保真下 finish 为字符串或 null,失败抛 Transient;返回原始 message 与 finish)与build_completion(message, body) -> OpenAIChatCompletion(构造快照,失败抛 Transient)。快照构造前剔除tool_calls中的 None 元素(与流式跳过一致,避免回传[null]被上游 400);tool_calls: null原样保留。_merge_unknown按值类型拆分私有函数(标签键、字符串、标量、对象、index 数组、其他数组),每个 ≤ B。- 拆分
add_chunk为_take_meta、_take_choice、_take_delta、_take_tools、_take_tool等私有方法,每个 ≤ B。
- 每帧: 校验 chunk 对象;
- 测试: 改写
test_invalid_delta_is_domain_error参数: 删除{"audio": {}}、{"index":0,"extra":1}与 null 已知字段参数(改为正例),保留 role 非 assistant、content 非字符串、tool index 非法等仍应拒绝的参数。新增:refusal:null+tool_calls:null帧成功且快照含refusal: None;只有reasoning的帧 progress=True 且ChatDelta.reasoning有值;二次构造的 OpenRouterreasoning_details分片(每帧重复format/id,text分片)合并后format不重复、text拼接完整;工具层嵌套对象extra_content={"google":{"thought_signature":"s"}}原样保留;数值冲突 → 键被删、conflicts含归一路径、调用成功;同路径冲突后再到达的值被丢弃;形态改变(字符串→对象、index 数组→普通数组)即冲突;布尔值冲突;其他数组追加;同帧多个字段/多个未知字符串只产生一次 progress;chunkchoices:null、choicedelta:null视缺省;role:null、arguments:null视缺省;全程只有role:null→finish()失败;非流式tool_calls:[None, 合法工具]快照只含合法工具;usage_after_finish四种顺序(finish 前 usage、finish 同帧、finish 后 usage-only 帧、无 usage);文本模式 index=1 的 choice 被忽略;重复 tool id 不拼接(既有用例保持)。 - 验证: §0 统一验证;快速回路
conda run -n PolyGateway pytest tests/unit/test_openai_completion.py tests/unit/test_openai_fidelity.py tests/unit/test_openai_compat.py -q。T2 期间 transport 仍调用parse_chat_completion的组合(保留为两步的薄包装),行为不变。 - 提交
fix: merge unknown and null stream fields losslessly in completion accumulator
T3 transport 两条路径接入判定
- 文件:
src/polygateway/transports/openai_compat.py;测试tests/unit/test_openai_fidelity.py、tests/unit/test_openai_compat.py。 - 行为:
_complete_stream: 流结束后先算usage_measured = _resolve_usage(sink.get("usage") or {})[2] == "measured",再stream_end = _stream_is_complete(done=..., finish_reason=acc.finish_reason, usage_after_finish=acc.usage_after_finish, usage_measured=...)。- None 且 openai → Transient
"{源} missing_done: …";None 且 text → 沿_check_done(零产物按_summarize_text的 any 判定 early_eof;retry抛;salvage→stream_end="salvaged")。 salvaged时 usage 沿_resolve_stream_usage(..., salvaged=True);done/eof_after_finish时 salvaged=False。
- None 且 openai → Transient
- 投影:
thinking= 非空 reasoning_content 片段拼接,否则ChatDelta.reasoning片段拼接;再走既有_finalize_text(think 标签)。 - 判定: openai →
summary = _summarize_message(acc.raw_message(), content);text →_summarize_text(content, thinking);verdict = _judge_outcome(mode, summary, _classify_finish(acc.finish_reason));非 deliver → 按error_kind抛TransientError或ResultInvalidError,message 固定格式f"{source.name} {code}: finish_reason={fr!r}, thinking_len={len(thinking)}",带source_name、operation="chat";ResultInvalidError显式raw_text=""(不回显推理内容)。deliver 且 openai →acc.finish()构造快照。 - 删除
_reject_empty_completion与 1.3.9 的"保真缺 DONE"分支;_check_done只保留文本截断语义。 _complete_once: 跳过第一步。保真: 先check_completion_body取原始 message 与 finish,用原始 message(剔除 None 工具后)算_summarize_message与_judge_outcome,仅 deliver 时调build_completion构造快照——否则坏工具 + length 会在构造时抛 Transient,到不了tool_truncated。文本: finish 取choices[0].get("finish_reason"),非字符串记 None;同一判定;stream_end=None;投影别名同上(message.get("reasoning_content") or message.get("reasoning"),仅取字符串)。- 两路径
TransportResult填finish_reason、stream_end。 - 告警节流: 实例属性
_warned_fields: set[tuple[str, str]],上限 1024,饱和时记一次"告警集合已饱和,后续同类告警省略";对acc.conflicts中每个路径、以及_classify_finish == "unknown"的 finish 值(截断 64 字符,文案写明"该 finish_reason 不缓存")各告警一次;日志只含源名与路径/值,不回显字段内容。 - 拆分
_complete_stream/_complete_once至 ≤ B(建议抽_consume_stream、_verdict_or_raise、_project_text)。
- 测试(假 SSE,复用文件内既有 helper):
- #31 验收 1-6(设计 §10),断言
stream_end、usage_source、attempts=1(经 GatewayClient 或 RetryMW 的既有测试设施)。 - usage 早于 finish + EOF → 失败;finish 后 usage
{}→ 失败;network_error+usage+EOF → Transient。 - 文本模式: stop+usage+EOF 无 DONE(
missing_done="retry")→ 成功eof_after_finish;无 finish + EOF → retry 抛、salvage 记salvaged。 - #30 验收: 保真 reasoning-only+length → 交付,
completion.message["content"]缺省或空、reasoning_content完整、finish_reason=="length";文本同输入 →ResultInvalidError且 message 含length_without_content;保真 content 空+length(无任何产物)→ 交付;文本同 → ResultInvalid;保真/文本 零增量+DONE(带 stop、不带 finish)→ Transient(test_stream_zero_content_with_done_is_transient保持不改);非流式 message 带tool_calls:null+refusal:null→ 成功;工具缺 name + length → ResultInvalidtool_truncated;content 非空 + 坏工具 + stop → Transient。 - 只有
reasoning字段的流不触发 TTFT 超时(用既有看门狗测试设施设置小 ttft)且文本thinking等于拼接值;同时有非空 reasoning_content 时以其为准。 - 冲突/未知 finish 告警各只一次(loguru sink:
logger.add(list.append, level="WARNING"),先例test_openai_compat.py:877;caplog 抓不到 loguru);饱和用例预先往_warned_fields灌 1024 条再触发一次,断言饱和告警一次。 - 结局表传输层覆盖: 设计 §4.3 九行 × 两模式,每行至少流式一例 + 非流式一例(行 2 只保真);非流式"工具缺 name + length → ResultInvalid
tool_truncated";文本 length 用例经 GatewayClient 断言 attempts=1 且熔断窗口失败计数为 0;#30 reasoning-only+length 经 GatewayClient 断言 attempts=1。 - 改写
test_openai_never_salvages: 断言"无 finish 的 EOF 在missing_done=salvage下仍失败",并新增"finish+usage+EOF 成功"作为对照。
- #31 验收 1-6(设计 §10),断言
- 验证:
conda run -n PolyGateway pytest tests/unit -q→ 全过。 - 提交
fix: judge stream completion by finish reason and trailing usage
T4 retry 搬运与缓存读写判据
- 文件:
src/polygateway/middleware/retry.py、src/polygateway/middleware/cache.py、src/polygateway/types.py(删除_has_completion_output);测试tests/unit/test_cache.py、tests/unit/test_retry.py、tests/integration/test_redis_cache.py。 - 行为:
_build_response搬运finish_reason、stream_end。- 写路径: 序列化前
_is_cacheable(mode=request.response_mode, content=response.content, finish_reason=response.finish_reason, stream_end=response.stream_end, completion=response.completion)为 False →logger.debug跳写。 - 读路径(
_rehydrate): 重建 completion 后、fields.update之前:stored_end = fields.get("stream_end")非 None 且不在域 → miss+warning;finish = fields.get("finish_reason"),非 None 且非 str → miss;为 None 且 completion 存在 → 回退completion.finish_reason;_is_cacheable(...)False → miss+warning;命中覆盖加stream_end=None、finish_reason=finish。拆分_rehydrate至 ≤ B。 - 删除
_has_completion_output及其 import(全仓 grep 为零)。
- 读写判定的 finish 统一取
_effective_finish(response.finish_reason, response.completion)(读路径对存储值同样调用)。 - 测试: 不可缓存六类(salvaged、reasoning-only length 保真、文本 length 带正文、unknown finish、保真 reasoning-only + stop(交付但无正文产物)、
content_filter带正文)均不写入(fake cache backend 断言 set 未调用)且第二次调用 attempts=1;手工注入上述条目 → 读视 miss;非法stream_end注入 → miss;1.3.9 旧文本条目(无新键)→ 命中;1.3.9 保真条目(无顶层 finish、completion.finish_reason="length")→ miss;命中返回stream_end is None、finish_reason为存储/回退值;retry 测试断言 LLMResponse 两字段来自 TransportResult。Redis integration 一例往返两新字段。 - 验证:
conda run -n PolyGateway pytest tests/unit -q→ 全过;conda run -n PolyGateway pytest tests/integration/test_redis_cache.py -q(需本地 Redis)→ 全过。 - 提交
fix: gate cache writes and reads on a shared cacheability verdict
T5 遥测 stream_end 列与 recorder 端口
- 文件:
src/polygateway/ports.py、src/polygateway/middleware/telemetry.py、src/polygateway/telemetry/schema.py、sqlite.py、postgres.py;测试tests/unit/test_telemetry.py、tests/unit/test_ports.py、tests/integration/test_postgres_telemetry.py,以及全仓def record_llm_call测试替身(grep -rn "def record_llm_call" tests)。 - 行为:
TelemetryRecorder.record_llm_call在stall_count之后追加 keyword-onlystream_end: str | None(无默认),docstring 注明 1.3.10。_AttemptUsage加stream_end: str | None = None,of()读response.stream_end;emit_attempt把它传给_record;_record加同名参数并传给 recorder;emit_cache_hit、emit_terminal_failure显式stream_end=None。schema.py: SQLite 与 PG CREATE 末尾stream_end TEXT;SQLITE_BACKFILL、_PG_BACKFILL_DECLS追加("stream_end", "TEXT");COLUMNS追加"stream_end"。sqlite/postgres 若有硬编码列数或参数位次随之更新(以COLUMNS为准)。
- 替身与计数(按审查实测清单): ① 固定字段 fixture
tests/unit/test_telemetry.py:142_record_minimal、tests/integration/test_postgres_telemetry.py:121_minimal_fields补stream_end: None(sqlite 按fields[col]取值,缺键 KeyError);② 硬编码计数 38→39 / 列清单追加:test_ports.py:325、test_telemetry.py:53(_EXPECTED_COLUMNS):271、:301-303、:779、:829,test_postgres_telemetry.py:33、:778、:1151、:1352(执行时以 grep 复核行号);③test_ports.py:112_DummyRecorder补参数,stream_end加入:332无默认值参数化列表;④**fields替身(14 处)无需改动。源码 docstring "38 字段"同步:ports.py:293、middleware/telemetry.py:373、sqlite.py:146、schema.py:8。形状闸middleware/telemetry.py:323_assert_recorder_shape从协议签名派生,不改闸本身。 - 测试: 成功流式行写
done/eof_after_finish;非流式、缓存命中、失败、取消、终态行为 NULL;旧库(无列)auto 档补列后写入;manual 档缺列 → 裁剪写入且告警点名stream_end;形状闸拒绝缺stream_end参数的 recorder;PG integration 同上(需本地 PG,沿既有 fixture)。所有替身补参数。 - 验证:
conda run -n PolyGateway pytest tests/unit -q→ 全过;conda run -n PolyGateway pytest tests/integration/test_postgres_telemetry.py -q→ 全过;conda run -n PolyGateway python -c "import inspect, polygateway.ports as p; print(len(inspect.signature(p.TelemetryRecorder.record_llm_call).parameters) - 1)"记录字段数供 T6 使用。 - 提交
feat: record stream end mode in telemetry
T6 文档、版本与另开 issue
- 文件:
README.md、CHANGELOG.md、research-wiki/ARCHITECTURE.md、research-wiki/designs/2026-10-01-issue29-openai-fidelity-design.md(顶部加"§4.1/§4.2/§5/§6 部分规则已由 2026-10-14 设计修订"指针)、research-wiki/schemas/llm-calls.md、research-wiki/migrations/*.md、pyproject.toml、src/polygateway/__init__.py。 - 内容:
- CHANGELOG
## 1.3.10(未发布): 修复段(#31 两模式、#30 两部分、非流式tool_calls:null、reasoning 别名、salvage 不再缓存);行为变更段(文本 length/过滤无正文 → ResultInvalid 不再经AllSourcesExhausted;未知 finish 不缓存;活性口径增补);破坏性段(record_llm_call新增必填stream_end);提醒(工具快照成立+length 时 arguments 可能半截;冲突删键以告警为准;旧缓存污染迁移 namespace/salt)。 - README: 能力表补完成判定/
stream_end;安装约束1.3.*核对;遥测字段数用 T5 实测值;活性口径一句。 - ARCHITECTURE: §6.2 空补全/missing_done 行、§7
usage_source与stream_end映射、§14 协议/失败/缓存行按设计改写。 schemas/llm-calls.md: 加stream_end列与取值表,修正陈旧计数。migrations/三份: 核对下游对ResultInvalidError的 catch 点,写一行结论(有/无受影响路径)。- 版本 1.3.10 两处一致。
docs-convention.md§2 清单逐项过: Gitea Wiki 站点下线,由 README/CHANGELOG/ARCHITECTURE/schemas 承接;.env.example无配置键变更(写明);research-wiki/ROADMAP.md状态同步。- 另开 Gitea issue(tea CLI): "文本模式 n>1 时多 choice 增量混流";"文本模式有产物无正文 + stop 的证据收集(决定是否改 ResultInvalid)"。
- CHANGELOG
- 验证:
grep -n "1.3.10" pyproject.toml src/polygateway/__init__.py;conda run -n PolyGateway pytest tests/unit/test_package.py -q→ 过。 - 提交
docs: document completion verdict and bump version to 1.3.10
T7 合并前验证、审查与发布
make lint、make test全绿(覆盖率不降);tests/integration中 Redis/PG 相关全绿。- 派全新上下文 verifier subagent(
verification-before-completion),逐条对照设计 §4-§7、§10 与本计划验收,输出证据表。 requesting-code-review整分支审查;按receiving-code-review核验并修复。- slow 子集(按 diff 交集): 动了公开入口与 transport →
pytest -m slow tests/e2e/test_smoke_gateway.py tests/e2e/test_openai_tools_live.py;不跑test_thinking_live.py(未动 thinking 能力表)。另做一次 magpie/glm 真实探针: 记录是否出现eof_after_finish,作为 #31 外部证据。 - 按 CLAUDE.md §4.4.1 九步发布 1.3.10(CHANGELOG 定版日期、合并 main
--no-ff、main 上重跑、tag、构建、上传、下载验证、Release + 包关联 + 页面核对)。 - 在 #30/#31 评论验收证据与版本,关闭。
4. 设计覆盖自查
| 设计条目 | 任务 |
|---|---|
§3 公共字段、StreamEnd 导出、tool_calls:null |
T1 |
| §3 recorder 端口、列 | T5 |
| §4.1 纯函数与 finish 分类 | T1 |
| §4.2 流完整性、usage 位置与 measured | T2(记录)+T3(判定) |
| §4.3 结局表、原因码、错误格式 | T1(判定)+T3(翻译) |
| §4.4 reasoning 别名 | T2+T3 |
| §5.1-5.3 null、合并、标签键、活性 | T2 |
| §5.4 告警节流 | T3 |
| §6 缓存读写 | T4 |
| §7 遥测落点 | T5 |
| §8 旧行为审计中的保留项(零增量+DONE、error 帧、多 choice) | T3 回归测试 |
| §10 文档、另开 issue、发布 | T6+T7 |
5. 审查修订记录
| 轮次 | 意见 | 处置 |
|---|---|---|
| P1 | B1 非流式先构造快照导致 tool_truncated 不可达 |
采纳: T2 拆 check_completion_body/build_completion,T3 先判定后构造 |
| P1 | S1 T5 替身/计数清单 | 采纳: T5 逐项列出 |
| P1 | S2 类块复杂度、S3 验证与钩子一致 | 采纳: §0 |
| P1 | S4 保留内部属性、S5 五层 null、S6 缓存漏两类、S7 结局表与熔断断言、S8 合并规则测试 | 采纳: T2/T3/T4 |
| P1 | 建议 1-8 | 全部采纳;建议 4 选"剔除 None 工具元素" |