diff --git a/research-wiki/designs/2026-09-09-134-thinking-contracts-design.md b/research-wiki/designs/2026-09-09-134-thinking-contracts-design.md new file mode 100644 index 0000000..871b999 --- /dev/null +++ b/research-wiki/designs/2026-09-09-134-thinking-contracts-design.md @@ -0,0 +1,325 @@ +# 1.3.4:推理意图的可满足性与测试证据契约 + +> 日期:2026-09-09。状态:**已通过独立审查并获人类正式批准;进入实施计划阶段,编码须先完成计划审查**。 +> 用户已批准合并处理 #25/#26/#21,及 D1(未知 AUTO 尽力+告警)、D2(受管推理与 raw 冲突拒绝)、D3(下游显式迁移 namespace/salt)。下文细则供正式设计审查,不重新悬置已决方向。 +> 本轮只修订本文件;未改生产代码/测试、未提交、未启动子代理、未执行真实付费调用。既有审查历史与剩余证据门见 §11。 + +## 1. 目标、非目标与事实源 + +本批修复共同问题:库声明的推理意图、实际发送参数和验证证据应一致;无法确认的事实不得变成成功声明。 + +| 范围 | 交付 | 明确不做 | +| --- | --- | --- | +| #21 | 已登记 AUTO 成员检查;移除默认 provider 偷选 medium;冲突守卫;能力证据审计与迁移说明 | 新推理 DSL、通用 overlay 系统、预算型推理、模型名猜测、无证据批量加 AUTO | +| #25 | 测试侧窄归因函数、逐轮留证、未覆盖汇总;纯本地断言离线化 | 改四分类、整类异常 skip、重跑到绿、扩大生产遥测、自动豁免发布覆盖 | +| #26 | 真实 client→治理→emitter→recorder 守护无推理路径,附变异证据 | 新端口、替换 reasoning_applies、遥测 schema 改造 | +| 后续批次 | #19/#23 归 1.3.5,#22 归 1.3.6,#24 独立设计 | 调用上下文 API、deadline、429 算法、对冲或取消结算重构 | + +规范依据:CLAUDE.md、brainstorming/structured-logging skill、ARCHITECTURE D11、§4.5/5.1/6/7.5/7.8、docs-convention。 +旧设计:`2026-09-04-reasoning-effort-design.md` §3–6/8/12;旧实验:`findings/2026-08-02-thinking-switch-and-reasoning-tokens.md`、`2026-08-25-thinking-observability-regression.md`。 +Issue 原文:`/tmp/polygateway-issue-triage/open-issues.json`。旧文档的“26 模型”“仅 reasoning_tokens 可靠”“下游尚未迁移”不是本轮验证结论。 + +## 2. 当前代码审计 + +| 证据 | 事实与根因 | +| --- | --- | +| `thinking.py::_settle_tier` | `effort is AUTO or effort in supported_efforts` 无条件放行 AUTO;形态与模型能力不能共同约束它 | +| `providers.py::ThinkingWire` | 空 on_base 当前文案混同协议形态与“任意模型默认推理”;需分开 | +| `DEFAULT_PROFILES[minimax]` | on_base 当前为 reasoning_effort=medium,M3 不是仍必然不推理;问题是 applied_effort=auto 却发 medium | +| `openai_compat.py::_build_payload` | resolution 后依次浅层 update extra_body/overlay;raw 可改写或新增推理控制,成功档位与最终字节失配 | +| `client.py::_guard_thinking`/`middleware/cache.py` | 工厂仅解析源级意图;缓存命中早于 transport,请求级、全量注入及同版本策略变化都可能回放旧语义 | +| `middleware/telemetry.py` | 成功尝试、失败尝试、缓存命中、终态失败的档位来源不同,不能统称“成功记实际档” | +| `embedding.py::_emit`/`ocr.py::_emit` | 当前正确传 False;只测 emitter 不覆盖调用点,成功响应默认 None 还会掩盖变异 | +| `test_thinking_live.py`/`test_embed_probe.py` | 前者按异常类别 skip、用正文子串识别 model_not_found;后者捕全部 PolyGatewayError 当“不支持”,都可能遮蔽库回归 | + +本轮核对 `origin/main..HEAD` 为既有 `758a127`/`6a09054`,保留历史提交、不重写;其外因判据按 §6 窄修正。 +当前 `reference/` 只有 cherry-studio、litellm、new-api、vercel-ai-sdk;GovDoc-SaaS、Video-Tree-TRM5、CHSAnalyzer 工作区均缺失,**未核验三项目现行配置**。迁移示例不是下游迁移完成证据。 + +## 3. 备选方案与已批方向 + +| 方案 | 收益 | 代价/结论 | +| --- | --- | --- | +| A:supported_efforts 包含 AUTO 的语义能力 | 不新增模型字段,统一可满足性判据 | 部分旧 True 配置报错,须审计清单并迁移;**用户已选 A** | +| B:新增模型默认推理三态 | 默认开启与未知可分别表达 | 多维护一套事实,易与能力清单漂移;不选 | +| C:provider/模型把 AUTO 映射固定档 | 保持旧配置字节 | 库代选付费档位,模型事实塞进 provider;用户不选 | + +| 决策 | 已批准 | 不采纳的备选及理由 | +| --- | --- | --- | +| D1 | 未登记 AUTO 保留尽力注入+warning,不保证开启;已登记一律成员检查 | 不改成未知即拒绝;也不把未知当能力已验证 | +| D2 | 有受管意图时拒绝 raw 推理冲突;无受管意图保留 raw | 不保留双来源互相覆盖,不从最终 payload 反向猜档位 | +| D3 | 下游显式换 namespace/salt 隔离语义变化 | 不加自动 revision/包版本,不让所有 chat 擅自冷启动,不把能力解析搬进缓存 | + +Issue #25 选“离线硬契约+有限可执行归因+显式未覆盖”;#26 选真实客户端与轻量 recorder 为日常测试、临时 SQLite 为持久化锚点,不要求远程 PG。 + +## 4. 推理语义与最终请求体契约 + +### 4.1 词汇与决策矩阵 + +`supported_efforts` 表示模型在**当前登记的 OpenAI 兼容 wire** 下、有依据可执行的语义集合,不是上游字符串枚举的逐字镜像。 +`AUTO` = 要求开启、不指定强度;不是 Python None(不表态),不是库任选一档,也不是上游必须接收 `auto` 字面值。 +`on_base=None` 是开启形态未知;`on_base={}` 是该协议不加开启字节,**能否据此满足已登记模型 AUTO 另查清单**。非空开关也不豁免成员检查。 +请求档>源级 reasoning_effort>enable_thinking 糖(True→AUTO、False→NONE);None 不覆盖源级表态,源上两字段矛盾的既有构造校验保留。 + +| 生效输入/条件 | 结果 | +| --- | --- | +| effort=None | 不注入,applied_effort=None;raw 仍按旧优先级发送,不推断其档位 | +| 所需方向形态未知 | ThinkingUnsupportedError,指路注册 profile;不因模型未登记而假造 wire | +| 已登记 AUTO 在清单 | 仅发 on_base,不附强度,applied_effort=AUTO | +| 已登记 AUTO 不在清单 | 明确拒绝并列可用档;fallback=nearest 同样拒绝,不能建议 nearest 修复 | +| 已登记显式强度不支持+nearest | 保留最近开启档、等距弱侧;只有纯开关候选时可强度→已登记 AUTO,不能 AUTO→强度 | +| NONE 不支持/没有 off 形态 | 保留专用不可关闭错误;替代配置由用户选择,不自动执行 | +| 未登记 AUTO/强度/NONE | 按已知 wire 尽力注入+warning;wire 表达不了仍拒绝,不编造支持清单,不保证开启/关闭/强度有效 | + +未知 AUTO 的空 on_base 可能发送零推理字节,返回 AUTO 仅表示尽力编码的选择,**不构成能力验证 PASS**。既有 warning 需明确能力未知、可能不生效,保留实例节流。 +NONE 方向仍按 `_wire_unknown_for` 既有分工:off、on_base 皆 None 才是整体未知;off None 而 on_base 已知是缺关闭形态。 + +### 4.2 MiniMax 与能力证据 + +移除默认 minimax.on_base 的 medium,改为空映射;M3 不加 AUTO。显式 medium 仅作为恢复旧字节的迁移示例,不是库推荐的最优成本档。 +按本轮源码清点,默认表 24 个型号、10 个含 AUTO;live 候选 26 型号不等于能力表 26 条。**本候选不授权新增任何 AUTO 条目**;追加须另附逐型号证据并复审,不因未知、不可达或同厂近代型号有能力而补登。 + +| 模型组 | 已有证据与本批处置 | +| --- | --- | +| MiniMax-M3 | 2026-08-25 裸 HTTP 无参数不推理、medium 有推理;2026-09-05 evidence 同向。不加 AUTO,显式档保留 | +| MiniMax-M2.5/M2.7 | 已有 AUTO;历史默认/mandatory 旁证及 T10 开启观测。T10 未留最终 wire,移除 medium 后需补空 on_base 实测,不冒充已复验 | +| qwen3.7-plus/max、qwen3.6-plus、qwen3.5-flash、qwen-plus-latest、glm-4.6v | 保留已有 AUTO 与逐型号证据,不推及其他型号 | +| glm-5/5.1 | 保留既有文档推定 AUTO;T10 回报 glm-5.3,身份不足,不算本型号实测 | +| deepseek-v4-pro/flash/flash-vision-exp、glm-5.2/5.3/5.3-flash、kimi-k3/kimi-for-coding | 现无 AUTO;显式档成立不等于 AUTO wire 成立,本批仍拒绝其 AUTO | +| gpt-5.5 | 现无 AUTO;历史报告 `tier_probe_20260905_184307.md:97` 无参数基线 5/5 rt=18、身份一致,是候选线索,但最终 payload/raw 覆盖未留证,不直接追加 | +| gpt-5.4、claude-opus-5/sonnet-5、gemini-3.1-pro | 现无 AUTO;限额/上游错误导致未覆盖,不能以“默认 medium”或同代替代证据 | +| claude-haiku-5、gemini-3-flash | 未登记 live 候选,按 D1 尽力+告警;不自动登记 | + +历史报告仅局部抽查;本轮未全表复验、未新跑付费实验。默认 wire 改变的 M2 两型是发布前显式缺测项,不能靠缓存回放旧 medium 的成功结果过门。 + +### 4.3 D2 冲突规则(窄边界,不做通用 overlay) + +“受管意图”指按 §4.1 得到的 effort 非 None,**包括 NONE、AUTO、糖及未知模型**。单凭 raw 不构成受管意图。 +当前浅层次序保持为 `基础 payload → resolution.payload → source.extra_body → request.overlay`;禁止改成深合并。守卫只校验所有权,不重写/删除 raw,不反推档位。 + +| 检查对象 | 精确规则 | +| --- | --- | +| 已知 raw 推理控制 | 顶层 `reasoning_effort`、`enable_thinking`、`thinking`、`thinking_budget`、`reasoning`、`thinkingConfig` 为控制根;`output_config` 为对象且含 `effort` 也算控制。这是显式有限词表,不按任意键的子串/模型名猜测 | +| 当前 profile 的控制根 | 加入 on_base、off 的全部顶层键及非 None 的 effort_key;即使本次为 AUTO 且 on_base 为空,也保护 effort_key/off 根。保护范围取开关两向并集,不只取本次实际注入键 | +| 同值与遮蔽 | 有受管意图时,extra_body 或 overlay **任一层**出现控制根即拒绝,值相同也拒绝;被后一层遮蔽也不豁免,避免双来源随配置变化重新失配 | +| 嵌套/浅覆盖 | `thinking={}`、`thinking={"budget_tokens":100}` 也拒绝:替换整个根会删除受管 type。当前 wire 持有某根时,raw 仅改其看似无关子键仍拒绝。根未被 wire 持有时,`output_config={"format":"json"}` 不因兄弟键 effort 被保护而误拒 | +| 新增而非覆盖 | AUTO 的空片段遇 raw reasoning_effort=high 仍拒绝;qwen 受管开关遇 raw reasoning_effort/thinking_budget 也拒绝,不能只检查字典交集 | +| 无受管意图 | 即请求、源级档与糖都不表态,保留 extra_body→overlay 原有浅覆盖(含 raw 推理),applied_effort=None;原 model/messages/stream/stream_options 禁写规则照旧 | + +自定义 wire 不新增路径 DSL:effort_key 仍是一个**顶层字面键**,不把点号解释成嵌套路径;on_base/off 可含嵌套对象,raw 守卫保护其整个顶层根。 +自定义 on_base 不得包含自己的 effort_key,也不得借标准 reasoning_effort 偷带档位;无论其值是 medium、auto 或 None 都拒绝为配置错误,不能默默删键。标准嵌套 `output_config.effort` 同属禁带强度的已知路径;不解析任意私有嵌套方言。需要强度请走显式档,不能将其固化在开启片段。 +其余自定义不透明方言的语义真实性由注册者提供证据;本批保证声明键不被 raw 冲掉,**不宣称可以识别所有未声明的私有别名/预算语义**。扩展别名应登记 wire 后受控,不新增通用参数解释器。 + +### 4.4 守卫时机、错误与保证范围 + +| 入口 | 时机/职责 | +| --- | --- | +| 工厂 `_guard_thinking` | 已有 profile/capability/source 材料齐全;装配期(网络及准入前)校验 wire、源级可满足性与源 extra_body 冲突,抛 ThinkingUnsupportedError(配置 ValueError)。工厂拒绝的源不能靠未来请求覆盖“救活” | +| `chat` 前置参数校验 | 保留 validate_request_overlay;请求显式档+调用 overlay 的已知控制词表冲突可在进入洋葱/准入前报配置 ValueError。不新增全源能力预解析,不声称这里可见自定义 transport 的注册表 | +| 默认 transport `_build_payload` | 以本次选中源、实际 profile、请求覆盖后的意图,对两层 raw 再做完整守卫;全量注入与自定义注册表同样覆盖。ThinkingUnsupportedError 翻译 RequestRejectedError,HTTP 发送前拒绝,无重试/换源/故障熔断计数 | +| 自定义 Transport | 不通过默认 transport 的调用仍由端口实现方履约;本批不添加 preflight 端口,不反射读取私有注册表,不承诺能验证任意注入实现 | + +transport 守卫**可能已经经过选源、限流预扣与准入**,拒绝后的结算沿既有 finally 路径;“零 HTTP”不等于“零准入操作”。不移动洋葱层次以制造所有请求均前置拒绝的过宽保证。 +真实成功尝试将同一 resolution.applied_effort 传给响应与 emitter,不重算;AUTO 是未指定强度的选择,不是服务端内部强度。服务端是否接受/执行推理由 ThinkingObservation 回答,失败与缓存行另见 §8。 +缓存命中不经过上述 transport 检查,所有“缓存不得绕过新拒绝”验收都以 §5 完成迁移为前置。 + +## 5. D3 缓存迁移与下游配置 + +### 5.1 显式迁移边界 + +缓存身份继续沿 ARCH §7.5;**不加入自动 revision、包版本、fallback、能力表或 wire 版本字段**,不删除旧键、修改共享 Redis 或重写历史遥测。 +语义变更包括本次 AUTO 成员检查/raw 冲突规则、MiniMax wire 变化,以及同版本下 fallback=nearest→error、能力增删/映射变化、自定义 profile wire 变化;这些都由下游显式换 namespace/salt 隔离。现有源指纹包含部分配置不代表包含全部语义。 + +| 操作 | 下游必须做/边界 | +| --- | --- | +| 迁移前 | 盘点工厂与全量注入、scope/租户、源级糖/档/raw、请求级覆盖、fallback、自定义 wire,以及实际 per-call namespace/salt 覆盖 | +| 切换 | 为受影响调用集合选择从未承载旧语义的 namespace 或 salt;在首次新语义读写前部署到该集合全部调用者。租户前缀与原 epoch salt 保留后再追加人工迁移标记,不共享租户身份 | +| 多源 | 一个缓存 scope 内只要有源受影响,必须隔离该共享身份的调用集合;要求更细范围由下游拆独立 scope/namespace,本批不替下游重分组 | +| 并行与回滚 | 新旧客户端不得共享迁移后身份;回滚到旧 namespace 会重见旧语义,不能说旧键已被清理。未来策略变更须再次显式迁移,不能复用一次标记包办所有变化 | +| 未完成迁移 | 旧缓存或同版本 nearest 写入可被 error 客户端命中,库不会自动验证其新可满足性。文档警示是操作前置,不是新增的自动安全机制 | + +**不要求所有 chat 冷启动**;未受影响调用可保留身份。若受影响与不受影响调用原本共享一套身份,下游须明确选择整体换标记的成本或先拆分,库不代选。 + +### 5.2 旧→新配置与验证矩阵 + +下表是**配置迁移示例及拟新增离线节点**,不是已执行测试。显式档均为当前清单成员示例,不代表所有渠道已实测、不作成本代选。所有受影响且启用缓存的行还须执行 §5.1。 + +| ID/旧配置 | 受影响模型 | 新行为(源级工厂/请求级默认 transport) | 用户明确选择的新配置 | 离线验证锚点(拟) | +| --- | --- | --- | --- | --- | +| M1 `ENABLE_THINKING=true` 或 `REASONING_EFFORT=auto` | MiniMax-M3 | 装配 ThinkingUnsupportedError/请求 RequestRejected,零 HTTP | 删除糖,`REASONING_EFFORT=medium` 可恢复旧 medium 字节;也可选择表内其他档 | `test_thinking.py`:M3 AUTO 拒绝/medium payload | +| M2 同上 | deepseek-v4-pro/flash/flash-vision-exp、glm-5.2 | 同上;开关 on_base 非空也拒绝 | 删除糖,显式 `REASONING_EFFORT=high` 或经用户选择 max | `test_thinking.py`:非空 wire AUTO 成员约束 | +| M3 同上 | glm-5.3/5.3-flash、kimi-k3/kimi-for-coding | 同上,nearest 不能解 AUTO | 删除糖,显式 `REASONING_EFFORT=low`(也可选 high/max) | `test_thinking.py`:AUTO+nearest 拒绝 | +| M4 同上 | gpt-5.4/5.5、claude-opus-5/sonnet-5、gemini-3.1-pro | 同上;不可达不补 AUTO | 删除糖,用户选表内 `REASONING_EFFORT=medium`;这不是新增 live 证明 | `test_thinking.py`:空 wire 非成员拒绝 | +| M5 同上 | MiniMax-M2.5/M2.7 | 仍 AUTO,on_base 从 medium 改空,真实语义待补测 | 保留 True/AUTO 并迁移缓存;需要旧 raw 字节者须完全退出受管意图,不可谎称该模型支持 medium | `test_thinking.py`:空 wire AUTO 字节;live 单列缺测 | +| M6 同上 | qwen 五型、glm-5/5.1/4.6v | AUTO 仍是成员,原开关形态保留 | 保留配置;glm-5/5.1 仍身份未覆盖 | `test_thinking.py`:已登记 AUTO 放行 | +| M7 同上 | 未登记模型(含两个 live 候选) | 已知形态尽力+warning,不保证开启 | 可保留 AUTO;要求确定保证者先取得能力证据再登记,不自动加表 | `test_thinking.py`:未知空/非空 wire 警告 | +| M8 源 HIGH+`EXTRA_BODY={"reasoning_effort":"high"}`;或请求 AUTO+raw HIGH | 所有受管模型,含未知 | 同值也拒绝;工厂或前置/transport 对应守卫报错 | 保留受管档并删除两层 raw 控制键;或清空源糖/档、请求不表态,仅 raw | transport 单测:同值、嵌套、被遮蔽、raw-only | +| M9 请求 medium,源 `EFFORT_FALLBACK=nearest` 改 error | 如 glm-5.3(映射 low→拒绝) | 源未表态时两工厂均可装配;旧身份可命中,新隔离身份在 transport 拒绝 | 改 error 的同时显式迁移 namespace/salt | cache 单测:nearest 写入→error 读,新身份必须 miss | + +配置实例(仅列需替换项,其他已校验的源配置保留): + +| 场景 | 旧 | 新 | +| --- | --- | --- | +| M3 源级 | `LLM__MINIMAX__1__ENABLE_THINKING=true` | 删除该键;`LLM__MINIMAX__1__REASONING_EFFORT=medium` | +| 请求级 AUTO | 源不表态;`chat(..., reasoning_effort="auto")` | 源仍不表态;用户选择 `chat(..., reasoning_effort="medium", cache_salt="epoch-7:thinking-134-a")`(M3 示例) | +| 工厂缓存 | `PGW_CACHE_NAMESPACE=lab:tenant-a` | `PGW_CACHE_NAMESPACE=lab:tenant-a:thinking-134-a`(人工标记,不是新增配置键) | +| per-call 租户覆盖 | `cache_namespace="tenant-a", cache_salt="epoch-7"` | `cache_namespace="tenant-a:thinking-134-a", cache_salt="epoch-7"`;只改工厂默认值对此路径无效 | +| 全量注入 | 构造参数 `cache_namespace="lab:tenant-a"` | 改成上述新 namespace;既有 cache/ttl 参数照常注入 | +| 自定义 wire 偷带强度 | `on_base={"reasoning_effort":"high"}` | `on_base={}`、保留 effort_key;用户显式选 HIGH(须模型支持),并迁移缓存 | + +三项目迁移验收须由各自负责人提供脱敏的实际配置/装配与调用位置,映射 M1–M9、提交所选替代与缓存身份切换证据。**当前三项目均为未核验**;Protocol 合成兼容测试通过也不能代替现行配置迁移验收。 + +### 5.3 旧行为处置 + +| 旧行为 | 处置 | +| --- | --- | +| True→AUTO、请求>源>糖、None/NONE、未知尽力+warning | 保留;未知仍受 wire 可表达性约束 | +| 已登记 AUTO 无条件放行、MiniMax 偷带 medium、受管与 raw 双来源 | 替换,明确放弃这些隐式兼容;迁移见上 | +| 原 raw-only、普通采样优先级、显式强度 nearest | 保留,不做通用 overlay 重构 | +| 缓存旧键/历史遥测/响应字段/端口签名/成本口径 | 保留数据及签名,语义变化靠显式缓存身份迁移,历史不伪造新观测 | +| 任务恢复/断点续跑 | 不适用,无任务状态;并行版本与持久化纪律见 §5.1 | + +## 6. #25:可执行的测试侧归因与证据 + +### 6.1 输入来自哪里 + +仅在测试侧增加一个纯分类函数及窄取证 fixture,复用 Markdown 报告;不新增生产事件、字段、端口或通用诊断框架。**不能从压平的最终异常字符串重建缺失尝试**。 + +| 输入 | 可实现来源与限制 | +| --- | --- | +| 预期请求 | 测试矩阵显式提供目标 model、POST 端点路径、stream、允许的源 origin、预期推理/结构化片段与提示词摘要;不能调用待测 `_build_payload` 生成“预期” | +| 实际请求与 HTTP 响应 | 利用既有 `OpenAICompatTransport(client_factory=...)` 注入带 httpx request/response hooks 的真实 AsyncClient;request hook 检查 method、规范 URL、JSON model/stream/控制片段,Authorization 与该源凭据仅在内存精确比较,输出布尔值 | +| HTTP 错误体 | response hook 保留本次响应引用;该次 complete 结束后读取**已缓冲** content(最多接受 64 KiB 完整内容作为分类输入)。未缓冲/超限/解析失败均标证据不足;不在 hook 预读成功 SSE,不另发请求,不以摘要假装完整 JSON | +| attempt 关联 | 测试专用窄 Transport 委托器原样转发 complete/embed 参数与异常,将入参 call_id(attempt UUID)放入任务局部 ContextVar 供 hooks 使用;finally 复位。只改测试装配,实际 payload/解析仍由真实 transport 执行 | +| 逻辑调用与错误 | 每轮 chat 显式传 session_id=运行 ID、parent_call_id=本轮 UUID,并由测试在 chat 外围将同一二元组绑定任务局部上下文、finally 复位;委托器从该上下文建立二元组→attempt UUID 关联,保存原始异常类/cause 与 HTTP 记录。归因用逐次记录,不靠最后一个错误推断所有前序 | + +带取证的 live 用例走既有全量注入路径,工厂与默认 client_factory 的装配/鉴权构造另有离线回归,不能用测试工厂替换后宣称原工厂已验证。需验证工厂本身的 live 用例若没有该证据通道,失败就保持 FAIL,不补生产接口凑证据。 +请求 hook 校验不通过须记证据并使测试 FAIL;不得改写请求后再称原请求正确。凭据不写哈希、不落盘;URL 去 userinfo/query,只存安全 origin 标识与路径。 + +### 6.2 精确分类:默认 FAIL + +分类输出为 FAIL 或外部未覆盖;外部未覆盖在 pytest 中可呈 SKIP,但报告与覆盖汇总必须记“未覆盖”。成功响应的行为断言仍单独执行,不经此分类器放宽。 + +| 情形 | 精确规则 | +| --- | --- | +| 环境前置缺失 | 测试矩阵列出的必需凭据/可选外部 Protocol 包未提供,网络前记未覆盖;键存在但配置格式错误、值校验失败是 FAIL | +| 404 model_not_found | 仅当本轮恰有一次实际 HTTP 尝试、无其他错误,请求检查全通过,响应为 404,完整 JSON 对象的 `error` 是对象且 `error.type == "model_not_found"`,默认 transport 对外为 RequestRejectedError 且 status 一致,才记“端点回报该请求型号不可用,未覆盖”。JSON 重复键也拒绝作为证据 | +| 普通 404/伪机器字段 | 只在 message 出现子串、字段类型错误、截断体、未缓冲体、端点/model/鉴权不符、响应与 attempt 无法配对,一律 FAIL | +| 429/5xx/401/403、网络错误 | **本批不建立自动外因豁免**:当前资料未给可核验的网关机器码白名单及独立归因来源,全部 FAIL 并保存实收证据;不能仅凭 HTTP 状态、Transient/SourceDead 类或“请求离线测过”跳过 | +| no_sources/stalled/retry_exhausted/AllSourcesExhausted | FAIL;本批不从生产遥测补失踪逐次原因,不将“曾见过一条 429”推断为整个终态均外因 | +| SSE/JSON 解析、空补全、ValueError、一般 RequestRejected/ResultInvalid、断言失败 | FAIL;请求正确不证明解析器或治理正确 | +| CancelledError | 原样穿透,finally 清理,不变成 SKIP | +| 成功但模型身份缺失/不符 | 实发 model 检查不通过是 FAIL;最终 model_reported 缺失/不符且没有独立原始响应身份取证时,保持 FAIL 并标记“身份来源无法区分”,不能推断上游没报。只有独立原始响应证据证明上游身份缺失/不在显式别名集合,才可记能力未覆盖;原始身份正确而解析/搬运丢失或改错必须 FAIL。本批不新增成功 SSE 捕获器,无该证据通道时按 FAIL 处理;别名不按前缀猜测 | + +没有证据通道时宁可 FAIL,不用抽象的“已证明外因”做逃生条件。运维人工确认可附外部证据供发布负责人决定豁免,**不自动把 FAIL 改 PASS 或扩大分类白名单**;未来扩大自动归因须独立给真实样本及反例契约。 +不追加裸 HTTP 对照诊断、自动重跑或悄悄缩小超时/stall;新增调用须先有人类模型/轮次/并发预算。 + +### 6.3 覆盖判据与报告 + +**先声明测试命题,再解释观测**:下述关闭成功判据只适用于“支持 NONE 的型号应成功关闭”,不是要求所有型号均可关闭。 + +| 测试命题 | 观测与结论 | +| --- | --- | +| 已声明可关闭,验证受支持 NONE | 任一 OBSERVED 证伪关闭保证,FAIL;完整必需轮次且请求/身份合格、每轮 ABSENT 才可关闭覆盖 PASS;混入 UNKNOWN 记未覆盖 | +| 已声明不可关闭,T10 绕过库能力守卫验证上游 | 合格 OBSERVED 是本轮仍推理的证据,可支持该测试条件下的不可关闭声明,不是关闭成功,也不得仅因 OBSERVED 而 FAIL;必须按预先固定命题及轮次集合比较观测与声明,UNKNOWN 不补足证据,不据有限探测声称证明所有上游参数均无法关闭 | +| 有意请求不支持档位,验证预期拒绝 | 独立于外因分类器,直接断言预先声明的拒绝类型/状态与机器字段。符合预期的 400/RequestRejected 是负向契约通过,不是外因 SKIP;非预期错误仍 FAIL。上游探测照过请求资格、独立身份可得性与逐轮证据要求 | + +增加离线反例:可关闭声明+OBSERVED 必须 FAIL;不可关闭声明+合格 OBSERVED 不得仅因出现推理而 FAIL;原始 JSON/SSE 含正确 model 但公共响应丢失/改错,必须 FAIL 而非 SKIP。全 UNKNOWN 绝不能算关闭覆盖通过。 +本候选不采用 completion_tokens 长短作为通用关闭证明;旧 prompt_tokens 差异只保留为指定历史样本的 wire/观测回归锚点,不使 UNKNOWN 升格关闭能力 PASS。没有经单独审定的独立关闭证据就记未覆盖,运行时 UNKNOWN 不告警的既有语义不变。 +开启测试保留显式轮数和多数 OBSERVED 规则(`observed_count > planned_rounds / 2`),但必须先保证全部计划轮次完成且请求/身份合格;失败/缺轮不得从分母删除。未知模型单次成功不自动变成能力登记。 + +逐轮先保存结果再判断后续处置;第 2 轮失败不能丢第 1 轮。报告字段包含矩阵 ID、provider、请求/回报模型、stream、请求档/响应 applied_effort、轮次、parent/session、attempt UUID、请求校验结果、异常类/status/安全摘要、已完成轮数及覆盖状态。 +完整错误体仅内存分类,落盘只存机器字段与沿用 2048 字符上限的脱敏摘要;清除已知凭据及私有提示词回显,无法安全保留则摘要省略并标明。不得写 Authorization、完整 .env 或私有提示词。 +每轮用唯一运行目录与轮次文件安全写入,汇总不能覆盖前轮失败;报告写失败使测试 FAIL,不允许无证据 skip。取消 finally 关闭自建资源,不引入无界网络等待。 + +| 既有测试接缝 | 窄修正 | +| --- | --- | +| L9 `_MYSTERY_PROFILE` | 保留显式全 None profile,将“未知形态报错”纳入离线断言;默认 openai 已非未知 | +| L8 `can_disable=False` | 装配拒绝只记本地契约通过,不写“与实测一致”;L8/T10 的 UNKNOWN 按上述覆盖门修正 | +| `_rounds_or_skip`、默认基线、T10 | 统一精确分类与逐轮留证;部分档未覆盖不能汇总为模型全覆盖 | +| compat Protocol/平铺键 | 完整合成 env/注入组件离线测装配;外部 Protocol 缺包只标该兼容项未覆盖,不声称读过缺失项目 | +| embedding 探测 | 去掉捕全错误为“不支持”,使用同一窄判据及 finally 关闭;不扩展端点能力 | + +发布门分开统计离线契约与预先声明的 live 单元。必需单元 SKIP/UNKNOWN/缺行/未完成不能凭 pytest exit 0 放行,须重测或人类明示豁免;不自动缩小覆盖集合。 + +## 7. #26:真实调用链与变异证据 + +复用 `test_embedding.py`、`test_ocr_client.py` 的真实 client、脚本 transport、recorder 工装;仅替换外部传输,不 mock emitter,不手工 emit_attempt 伪造 False。 + +| 验证 | 必须断言 | +| --- | --- | +| embed、recognize_text、parse_layout | 源分别填 True 糖/显式 HIGH;一次 Transient 后成功,每路径确有两条尝试行(一失败一成功),reasoning_effort 全 None | +| 拒绝与耗尽 | 至少一条已发生的失败尝试落库且档位 NULL,主异常仍上抛;不凭本 issue 新增不存在的终态行 | +| chat 阳性 | 已登记 AUTO 源的 True 糖失败行 auto;显式档失败行请求意图;nearest 成功行映射档;emitter 恒 NULL 必须被抓住 | +| SQLite 锚点 | 每个无推理入口至少一组真实 client+临时 SQLite,断言总行数、失败行数和 NULL 数,不用 all([]),不接共享 llm_calls | +| wire 边界 | MockTransport/录制响应验证 embedding、OCR 不发推理参数;layout POST+ZIP GET 属同一次治理尝试,不误算两条遥测 | +| 并发 | 共享 recorder 并发 chat/无推理调用,**按测试指定的 (session_id, parent_call_id) 分组逻辑调用**;组内 call_id 是不同 attempt UUID,集合无交集、档位不串,不把 call_id 当所有重试共用的 ID | + +隔离副本/worktree 逐一变异并还原:embedding False→True;OCR False→True(两个入口分别红);chat True→False;移除 emitter applies 短路。记录节点、目标语义失败断言、退出码,原实现及还原后通过。 +Issue #26 当前实现正确,先红来自上述变异,不为 TDD 改坏主工作区;仅因无关签名异常变红不算杀死目标变异。注入资源由测试自己关闭。 + +## 8. 非功能与四种遥测行口径 + +继续通过 `TelemetryEmitter` 唯一出口与现有 `llm_calls.reasoning_effort`;**不新增生产遥测字段、表、事件或旁路日志流水**。 + +| 行类型 | reasoning_effort 来源 | 分析限制 | +| --- | --- | --- | +| 真实成功尝试 | response.applied_effort(nearest 后);无推理路径由 applies=False 短路为 NULL | 不重算;未知 AUTO 仅尽力编码,不证明上游能力;raw-only 为 NULL | +| 失败尝试 | effective_effort(请求>源>糖);无推理路径 NULL | 是请求意图,不是已发出的/映射后的档,也可能零 HTTP | +| 缓存命中 | **本次请求级** reasoning_effort | 不取历史 response.applied_effort,不推源级档;thinking_observation 回放历史,不是本次实测 | +| scope 终态失败 | **本次请求级** reasoning_effort | 可能未选源;不推源级档或实际档,不凭本批补充逐次根因 | + +实际档分析仅使用真实成功尝试(排除 cache_hit、失败/终态行),还须保留未知/raw-only 的语义限制。#26 的 NULL 与 chat 阳性不能误套到缓存/终态来源上。 + +| 维度 | 约束 | +| --- | --- | +| 并发/幂等 | 能力表不可变、注册返回新表;解析局部结果,不用全局最后档;同声明同意图同结果;报告按运行/逻辑调用/attempt 隔离 | +| 取消 | 同步守卫不捕 BaseException;CancelledError 穿透,既有 in-flight finally 释放;不重构真实调用/遥测取消时序 | +| 降级 | 缓存/遥测故障 warning 降级,限流/熔断后端不可用仍报错;配置守卫与运行期四分类见 §4.4 | +| 持久化/原子性 | 无 schema/DDL;SQLite 临时文件,旧缓存不改写,报告安全写且失败显式失败;无任务恢复子系统 | +| 告警/评估 | 未登记与对账 warning 保持实例节流;错误含 model、请求档、支持集合与可执行配置例,不泄露凭据;离线矩阵和变异须全过,live 覆盖基线待实际运行 | + +## 9. 实施接缝与文档同步 + +| 接缝 | 预期改动 | +| --- | --- | +| thinking/providers | AUTO 成员约束、nearest 边界、文案、空 wire 语义、D2 窄纯校验;已有能力证据保留出处,不无证据追加 | +| client/默认 transport | 工厂及请求前置可执行的守卫、最终双 raw 守卫与错误翻译;不改端口、不移动层序、不引入全源请求准入解析 | +| cache | 本批不加语义 revision 或自动校验;只交付显式迁移回归与边界说明 | +| 单元/轻集成/e2e | 真实调用链+SQLite、分类输入与反例、逐轮完整性、隔离变异;M3 True 改为“本地拒绝”和“medium 真实开启”两个命题 | +| 文档 | 实施时同步 ARCH D11/§5.1/7.5/7.8、README、CHANGELOG、.env.example、源码 docstring;旧设计标注被替代段,不追改历史实验事实 | + +Wiki 已下线,按 docs-convention 落到上述文件,不虚报 wiki 多页;schema/端口未变不 bump 字段数。本次仅写本设计,其他同步留正式批准后的计划。 + +## 10. 验收矩阵与审批门 + +下列均为**待实施验收**,不是本轮通过记录。凡缓存不得绕过/救活新拒绝的断言,统一以**已完成 §5.1 显式迁移前置**为条件。 + +| 层 | 必需验收 | +| --- | --- | +| 解析离线 | None/NONE/AUTO/强度、已登记含/不含 AUTO、未知空/非空/未知 wire、nearest 两方向、默认与自定义注册表;AUTO 拒绝不提示 nearest | +| raw 冲突 | 源/请求双入口,同值、被遮蔽、嵌套根替换、新增控制键、自定义 effort_key/off 根、非法 on_base;无受管意图保留 raw,普通采样不误拒 | +| 守卫时机 | 工厂失败零准入/零 HTTP;前置可判请求冲突零准入;默认 transport 拒绝允许既有准入但零 HTTP、正确结算、无重试换源;全量注入同测 | +| 缓存迁移 | 旧客户端写旧身份→新客户端使用全新身份 miss 并执行新拒绝;nearest 写→error 读、能力表/wire 变化均换身份;工厂默认、per-call 覆盖、全量注入、多源集合、并行旧新客户端覆盖 | +| 缓存已知边界 | 另测未迁移的共享身份可能命中并绕过 transport;将其明确记录为操作风险,不把迁移前未拒绝伪装为迁移后安全已验证;无强制全部 chat 冷启动 | +| 归因/防假绿 | 404 精确 type/仅 message/截断/重复键;400/429/503/SSE/no_sources 默认 FAIL;故意改错 model、Authorization、端点、SSE 解析必须红;第二轮失败保留第一轮证据;取消穿透 | +| 遥测 | 四种行来源逐项断言;三个无推理入口真实调用链/SQLite NULL 与 chat 阳性;按 parent/session 归组且 attempt UUID 唯一;四类变异被目标断言杀死 | +| 真实核心 | M3 AUTO/True 拒绝零网络、M3 medium 流/非流、M2.5/M2.7 空 wire AUTO、qwen AUTO;缺观测保留未覆盖,不靠旧缓存 | +| 真实扩展 | 逐型号/模式列出默认基线、开启/关闭;身份缺失、UNKNOWN、不可达不算关闭 PASS,不用同系替代;新增 AUTO 如另获批准,逐型单列最终 wire/身份/信号 | +| 迁移/全局门 | 三项目实际配置另取证;conda 静态检查、日常套件、独立 verifier;发布按 CLAUDE 显式跑 slow 与下游视角包验证,不能以本设计代替运行结果 | + +付费 live 必须先通过离线构造契约,再经人类批准模型/轮次/并发预算;生产超时不为赶结果压小。本文件不授权任何新真实付费调用。 + +## 11. 审查历史、修订对应与当前状态 + +前稿记录 Codex 4 项 Important;本轮按下表修订,**修订不等于独立复审通过**。上一轮 Claude 输出不对应本仓库,整份无效且不作为任何技术结论/通过证据引用。 + +| Codex 项目 | 本轮修订与复审锚点 | +| --- | --- | +| 缓存迁移边界不足 | §5.1/5.2 M9、§10:D3 显式迁移,不加 revision;同版本 fallback/能力/wire 变化、全量注入和 per-call 覆盖均纳入;未迁移仍可能绕过 | +| 遥测“成功=实际档”过宽 | §8 四种行逐项来源,真实成功尝试才读 applied_effort;缓存与终态仍读请求级,不扩大生产遥测 | +| UNKNOWN 被计关闭覆盖 PASS | §6.3/§10:UNKNOWN 不证实关闭,未覆盖不占 PASS;长度差不作通用关闭证明,运行时 UNKNOWN 语义不变 | +| 缺下游迁移矩阵 | §5.2 M1–M9 与具体键/调用实例;受影响型号、错误时机、人工替代、离线锚点齐全;三项目现行配置明确未核验 | + +已决 D1–D3 的旧“未决推荐”段已移除。2026-09-09 第二轮 Codex 审查对修订版无 Critical/Important/Minor;native reviewer 另发现两个测试归因 Important:公共响应身份不足以归因上游,以及 NONE 关闭成功与不可关闭负向研究混同。父会话已在 §6.2/6.3 作最小修订(证据不足 FAIL、不新增成功 SSE 捕获器、按测试命题判定),native reviewer 定向复审通过(run `2f92c90a-298d-459d-8fcb-087dea475770`),无新增 Critical/Important/Minor。 +剩余证据门为 M2 空 wire 实测、其他 live 缺测及三项目实际迁移,不重新悬置已决 D1–D3。2026-09-09 用户已明确选择“批准并继续”,正式批准本文件;独立审查及 native reviewer 定向复审均已通过。 +**当前结论:设计已批准,进入实施计划;计划独立审查通过后直接执行。** 设计批准不等于变异、真实矩阵或下游迁移已通过;尚未取得的证据仍按 §10 门控。 diff --git a/research-wiki/plans/2026-09-09-134-thinking-contracts.md b/research-wiki/plans/2026-09-09-134-thinking-contracts.md new file mode 100644 index 0000000..772842f --- /dev/null +++ b/research-wiki/plans/2026-09-09-134-thinking-contracts.md @@ -0,0 +1,370 @@ +# 1.3.4 推理契约与测试证据实施计划 + +> 日期:2026-09-09。状态:**自审及 Codex 独立计划审查通过(复审 run ea38c3a7-12ef-4bf0-bb04-257ce37eb96f),进入执行;尚未编码/执行验收**。 +> 设计:`research-wiki/designs/2026-09-09-134-thinking-contracts-design.md`,用户已正式批准。 +> 目标:解决 #21 的受管推理语义漏洞、#25 的测试归因漏洞、#26 的客户端遥测守卫缺口,不扩展生产端口或遥测 schema。 +> 方案:在既有推理决策层添加窄校验并接入工厂/默认 transport;测试侧独立保留请求与响应证据,按明确命题判定覆盖。缓存仍由下游显式迁移,生产治理循环不重写。 +> 技术:Python 3.12+、asyncio、httpx hooks/MockTransport、frozen dataclass、pytest、临时 SQLite、ruff、import-linter;不新增依赖。 + +本计划不涉及参考实现迁移,保真校验不适用;不得变更 Redis Lua、429/stall、取消结算、结构化重试或 #19/#23/#24 的生产机制。 + +## 1. 基线、授权与执行纪律 + +| 项目 | 固定边界 | +| --- | --- | +| 分支/历史 | `feature/1.3.4-thinking-contracts`;保留已有 `758a127`/`6a09054`,不重写 main 历史;开始时记录实际 HEAD 与 origin/main | +| D1 | 已登记 AUTO 必须为清单成员;未知 AUTO 保留尽力+warning,空 wire 可能不发送推理字节,不保证开启 | +| D2 | 受管意图非 None 时,两层 raw 推理控制同值/被遮蔽也拒绝;raw-only 保留,不反向推断实际档 | +| D3 | 不添加 fallback 指纹、语义 revision、能力表版本或新缓存前置解析;显式更换 namespace/salt 是操作前置,未迁移可能回放旧语义 | +| 实施权限 | 一工作区仅一 writer;父会话负责前台委派与审核。用户已授权门通过后自行合并 main、测试并发布 1.3.X,无需逐步请示;1.4、新公共决策、验证豁免须停下确认 | +| 证据与秘密 | 不打印 `.env`、token、Authorization、私有提示词;不提交 `.pi/`、运行报告或 reference;命令输出只记录安全路径、状态、退出码 | + +T0 开始调用 `writing-plans`;T1–T7 行为测试执行 `test-driven-development` 并阅读其 testing-anti-patterns;T1/T5 落日志前执行 `structured-logging`。每次提交执行 `commit` skill(英文祈使标题、无 AI 签名、显式路径暂存),T8 前执行 `requesting-code-review`/`verification-before-completion`,收到意见执行 `receiving-code-review`;异常先用 `systematic-debugging` 定根因。 + +用户自主发布授权涵盖既有发布清单的真实 slow 套件,沿既有配置/轮次/并发执行,不重复索取这一授权。超出既有测试矩阵的新研究实验先提交型号、轮次、并发和费用预算;禁止借研究名义追加裸 HTTP 对照。设计要求的新增覆盖先用现有矩阵表达,无法表达且增加调用量时升级该预算决策。 + +## 2. 文件职责与不变接缝 + +| 创建/修改 | 精确路径 | 职责 | +| --- | --- | --- | +| 修改 | `src/polygateway/thinking.py` | AUTO 成员检查、nearest 边界、wire 与 raw 冲突纯校验、错误及未知告警文案 | +| 修改 | `src/polygateway/providers.py` | MiniMax on_base 改空;修正空 wire docstring,不在声明层引入决策依赖 | +| 修改 | `src/polygateway/client.py` | `_guard_thinking` 校验源 raw;`chat` 请求显式档+已知 raw 冲突前置校验;指纹不改 | +| 修改 | `src/polygateway/transports/openai_compat.py` | `_build_payload` 完整守卫,两层浅覆盖次序不改,沿 complete 的异常翻译 | +| 修改 | `tests/unit/test_thinking.py`、`tests/unit/test_providers.py` | 纯解析、声明、已知/未知/自定义 wire、告警与边界 | +| 修改 | `tests/unit/test_client.py`、`tests/unit/test_config.py`、`tests/unit/test_openai_compat.py`、`tests/unit/test_retry.py` | 工厂、请求前置、真实 transport、无 HTTP 拒绝与治理收尾;离线兼容 | +| 修改 | `tests/unit/test_cache.py` | 显式迁移和未迁移风险回归;保留旧键黄金值 | +| 修改 | `tests/unit/test_embedding.py`、`tests/unit/test_ocr_client.py`、`tests/unit/test_telemetry.py`、`tests/unit/test_monkey_ocr.py` | 三入口 NULL、阳性、SQLite 与 wire;不改变生产 emitter/client 循环 | +| 新建 | `tests/live_evidence.py` | 测试专用 frozen 证据、有限归因、身份与覆盖判据、逐轮安全报告;无环境自读取 | +| 新建 | `tests/e2e/conftest.py` | 测试侧 hooks、任务局部关联、薄 transport 委托与配置装配;不复制生产 payload/重试算法 | +| 新建 | `tests/unit/test_live_evidence.py` | 分类、hooks/委托器和报告离线反例;导入新 conftest 中无副作用定义,不导入读 .env 的 live 模块 | +| 修改 | `tests/e2e/test_smoke_gateway.py`、`tests/e2e/test_compat_projects.py`、`tests/e2e/test_embed_probe.py`、`tests/e2e/test_thinking_live.py` | 迁入窄证据通道、逐轮完整性与命题分流,保留必须真实执行的行为断言 | +| 修改 | `README.md`、`CHANGELOG.md`、`.env.example`、`research-wiki/ARCHITECTURE.md`、`research-wiki/designs/2026-09-04-reasoning-effort-design.md` | 用户可达迁移说明、架构同步、旧设计被替代指针;不追改历史实验事实 | +| 修改/登记 | `research-wiki/schemas/llm-calls.md`、`research-wiki/metrics/call-telemetry-coverage.md`、`research-wiki/graph/edges.json`、`research-wiki/index.md`、`research-wiki/log.md` | 复用既有实体,登记本计划与四种遥测口径;只接受工具对相关实体的必要索引更新 | +| 新建(验收时) | `research-wiki/findings/2026-09-09-134-thinking-contracts-validation.md` | 红绿、变异、失败与豁免索引,≤300 行;原始输出留 `tests/outputs/134/` | +| 修改(发布时) | `pyproject.toml`、`src/polygateway/__init__.py` | 两处版本一致到 1.3.4,不改变依赖或导出面 | + +生产不修改 `ports.py`、`types.py`、`errors.py`、cache/telemetry 实现及 embedding/OCR 循环;若实际实现需要突破该清单,先说明设计要求与最小原因,由父会话核定,不顺手改动。 + +## 3. 跨任务接口(内部实现约定,不新增公共导出) + +### 3.1 推理守卫 + +新函数置于 `thinking.py`,其余模块显式 import;保持决策方向 `client/transport → thinking → providers/types`。`Mapping`、`Any`、`Effort`、`ThinkingWire` 均为既有类型。函数体由 T2 实现,以下固定消费者签名: + +```python +def validate_thinking_wire(wire: ThinkingWire, *, model: str) -> None: + """拒绝 on_base 偷带已知强度,抛 ThinkingUnsupportedError。""" +def validate_thinking_raw( + raw: Mapping[str, Any], *, effort: Effort | None, + wire: ThinkingWire | None, origin: str, +) -> None: + """effort 表态时拒绝 raw 控制;wire=None 只检查标准词表。""" +``` + +签名中的 wire 必填但可 None,None 是 chat 前置看不到实际 profile 的事实,不是容错默认;origin 只取固定位置名/源名,不含 raw 值。两函数无 I/O,不改变输入,抛现有 `ThinkingUnsupportedError`(ValueError 子类),不新建错误类。`validate_thinking_wire` 在 `resolve_thinking` 的 None 早退之前验证声明结构;不会要求无意图时 wire 必须已知,只拒绝结构上偷带强度。 + +标准 raw 根:`reasoning_effort`、`enable_thinking`、`thinking`、`thinking_budget`、`reasoning`、`thinkingConfig`;`output_config` 为 Mapping 且有 `effort` 时冲突。wire 可见时并入 on_base/off 全部顶层根与 effort_key,点号只是字面键。 + +wire 校验只拒绝 on_base 中自己的非 None effort_key、标准 reasoning_effort、标准嵌套 output_config.effort(含值为 auto/None);不解析任意私有方言。未知/非当前方向的形态检查继续由现有 `_wire_unknown_for` 负责,不改变 off-only 可用性。 + +### 3.2 测试证据与归因 + +`tests/live_evidence.py` 不 import e2e conftest、不读环境、不发网络。上下文中的密钥只做内存比较,不进下列值类型。一个 attempt 可以记录零/一/多 HTTP 事件;不能用 len(attempts) 冒充 HTTP 数。 + +```python +@dataclass(frozen=True) +class HttpEvidence: + call_id: str + request_checks: tuple[tuple[str, bool], ...] + status_code: int + error_body: bytes | None + raw_identity: tuple[bool, str | None] +``` + +`raw_identity` 是 T5 生产、T6 消费的**成功非流式原始身份快照**:(False, None) 表示未取证,(True, None) 表示已独立解析完整 JSON 对象且 model 缺失/为 null,(True, str) 表示原始字符串。由 response hook 保存的响应引用在该次 complete 结束后读取已缓冲 content,独立 JSON 解码(拒绝重复键)取得;不得从 TransportResult/LLMResponse.model_reported 回填。未缓冲、无可配对响应、非成功非流式、JSON 非对象/非法或 model 非字符串非 null 均不生成肯定证据,并保存原因供 FAIL;不把解析异常解释成身份缺失。成功 SSE 始终 (False, None),不新增捕获器、不预读流。 + +`request_checks` 必须完整包含 method/origin/path/model/stream(embedding 为 input_shape)/authorization/control/messages_digest,缺项不算全过;error_body 只允许 ≤65536 字节已缓冲完整错误体,超限或未缓冲为 None,并在安全报告写证据不足,不把截断文本拿来解析。记录只存在测试内存,不能直接 asdict 后落盘。 + +```python +@dataclass(frozen=True) +class AttemptEvidence: + call_id: str + http: tuple[HttpEvidence, ...] + error: Exception | None +``` + +```python +@dataclass(frozen=True) +class LiveVerdict: + status: Literal["PASS", "FAIL", "UNCOVERED"] + reason: str +``` + +消费者固定为 T5→T6,纯函数与报告出口如下;参数所用 Path、Mapping、Sequence、ThinkingObservation、Effort 为标准库/现有领域类型: + +```python +def classify_live_failure( + error: Exception, attempts: Sequence[AttemptEvidence], +) -> LiveVerdict: + """异常分类仅 FAIL/UNCOVERED;取消不交给此函数。""" +def write_live_round( + output_dir: Path, *, run_id: str, matrix_id: str, round_index: int, + safe_fields: Mapping[str, Any], +) -> Path: + """只接受已脱敏报告字段,唯一文件写失败必须冒泡。""" +``` + +身份与命题判定继续放 `tests/live_evidence.py`,避免 e2e 中四份条件分支: + +```python +def assess_model_identity( + *, requested: str, aliases: frozenset[str], reported: str | None, + raw_identity: tuple[bool, str | None], request_valid: bool, +) -> LiveVerdict: + """raw_identity[0] 表示有独立原始身份取证;无证据不得归因上游。""" +def assess_thinking_coverage( + observations: Sequence[ThinkingObservation], *, planned_rounds: int, + proposition: Literal["enabled", "disabled", "cannot_disable"], +) -> LiveVerdict: + """输入须先过请求/身份资格;缺轮与 UNKNOWN 不补足证明。""" +``` + +拒绝能力测试不送入以上观测函数,按预声明异常类型/状态/机器字段独立断言。`cannot_disable` 保留 T10“实际未关闭与声明比较”的反证形态:完整合格轮次有 OBSERVED 可支持本条件下不可关闭;全 ABSENT 证伪声明;无 OBSERVED 但有 UNKNOWN 只能未覆盖。不得扩大成“证明所有私有上游参数都无法关闭”。 + +### 3.3 测试侧取证装配 + +新 `tests/e2e/conftest.py` 内 `ObservedTransport` 包裹**同一个**真实 `OpenAICompatTransport`,complete/embed 签名逐字保持 `ports.py`,参数原样传递。每次调用将 call_id 绑定实例持有的 ContextVar,finally reset;异常原样上抛,CancelledError 不转普通错误。不得在委托器做治理重试或 payload 修正。 + +```python +class LiveCapture: + def __init__(self, *, expectations: Mapping[str, Mapping[str, Any]]) -> None: + """按源名持有测试矩阵显式预期,不含凭据或真实响应。""" + def round_context(self, *, session_id: str, parent_call_id: str) -> AbstractContextManager[None]: + """外围绑定逻辑轮次,finally 复位,嵌套任务不串线。""" + def client_factory(self, source: SourceConfig) -> httpx.AsyncClient: + """按实际源构造带 hooks 客户端,沿生产 timeout/trust_env。""" + def attempts(self, *, session_id: str, parent_call_id: str) -> tuple[AttemptEvidence, ...]: + """返回本轮快照,含零 HTTP 的尝试,不从最终异常猜前序。""" + def raw_identity(self, *, session_id: str, parent_call_id: str, call_id: str) -> tuple[bool, str | None]: + """精确读取本逻辑轮次和成功 attempt 唯一 HTTP 事件的原始身份快照。""" +``` + +`LiveCapture.expectations` 由调用者按源名提供,内层必需键为 model、origin、path、stream(embedding 用 input_shape)、control、messages_digest;缺键直接测试配置错误,不自动从待测 payload 补齐。结构化预期片段放 control 的显式预期对象,输出路径由 write_live_round 单独接收;并发异构轮次使用各自 capture 实例,不共享可变“当前预期”。预期不调用生产 `_build_payload` 生成。实际请求 hooks 逐项比较,凭据不进 repr/序列化;可保留失败事实后由轮次出口 FAIL,不能改写实发请求“修正”它。 + +取证 live 使用 `GatewayClient(...)` 全量注入。conftest 可复用 `client.py` 现有 `_build_limiter`/`_build_breaker`/`_build_selector`/`_build_structured` 等装配函数,但不新增生产注入口、不复制它们实现;自己创建的组件用 ExitStack/显式 finally 关闭,注入 GatewayClient 不会代关。工厂行为单独离线验证。未获得取证通道的工厂 live,异常一律保存后 FAIL。 + +T5 按 `(session_id, parent_call_id) → AttemptEvidence.call_id → HttpEvidence` 保存快照;T6 使用最终 `LLMResponse.call_id` 调用 `capture.raw_identity(...)`,将返回值交给 `assess_model_identity(raw_identity=...)`,不可取本轮最后一条响应猜关联。查找必须精确匹配本轮且只有一条成功 HTTP 事件;重复/跨轮 call_id、多个候选响应是取证契约错误,报告后 FAIL,不降成外因未覆盖。未发 HTTP 或没有独立身份快照时返回 (False, None)。 +成功非流式原始 model 通过上述快照接口交付;成功 SSE 不加捕获器、不预读流。原始身份无从确认而公共结果缺失/不符时 FAIL;原始正确而结果丢失/改错也 FAIL。只要公共身份合格且请求合格,正常能力测试不要求新增成功 SSE 的原始副本。 + +## 4. 任务与提交点 + +### T0:基线、计划审查与文档回滚点 + +- [ ] 修改设计批准状态,新增本计划;父会话自审后前台 Codex 独立审,具体问题修正后方可执行 T1。计划无需再走人类门,不能把“已生成”当“已审”。 +- [ ] 记录 `git status --short --branch`、`git log --oneline origin/main..HEAD`、实际 HEAD;确认源代码零差异,保存未跟踪文件清单,禁止暂存 `.pi/`。 +- [ ] 执行基线:`make check`;`conda run -n PolyGateway pytest tests/unit/test_thinking.py tests/unit/test_providers.py tests/unit/test_client.py tests/unit/test_config.py tests/unit/test_openai_compat.py tests/unit/test_cache.py tests/unit/test_embedding.py tests/unit/test_ocr_client.py tests/unit/test_telemetry.py -q`。记录实际失败,不能先改期待绕过;本步骤预期现有非 slow 测试通过。 +- [ ] 以 `research-wiki` 工具登记 design/plan 节点及 implements 边,现有同路径文档不可被 add_entity 模板覆盖;先读工具已有文件处理行为,再登记、重建索引、检查生成 diff。只在本计划 writer 移交后由父会话执行这些额外文件写入。 +- [ ] 调用 commit skill,提交点 `docs: record approved thinking contracts and implementation plan`,形成生产修改前回滚点。 + +### T1:AUTO 成员语义与默认 MiniMax wire + +**文件**:`thinking.py`、`providers.py`;`tests/unit/test_thinking.py`、`tests/unit/test_providers.py`,路径均按 §2。 + +1. 先添加/替换 `test_auto_never_trips_phase5`,以已登记不含 AUTO 的空/非空 on_base 为反例;调用 resolve_thinking 的 error/nearest 都明确拒绝且不提示 nearest。先跑新测试,旧实现因未拒绝而红;再修改 `_settle_tier`、`_tier_unsupported`,避免 AUTO 进入 `EFFORT_ORDER.index`。 +2. 保留强度→纯开关 AUTO、等距弱侧、NONE 不自动映射、None 不表态、未知空/非空 wire 尽力警告。删除 default MiniMax 的 medium 并修正注释;先测试 M3 AUTO 拒绝、M3 medium payload、M2.5/M2.7 AUTO 空 payload 与 applied=AUTO。默认能力表成员不增删。 +3. 用 loguru sink 检查未知告警确实说明不保证生效,default transport 实例节流不改;不引入新日志通道,不输出 raw 密钥。按 structured-logging 明确这是既有 warning 与既有列的修正。 + +**验证**:`conda run -n PolyGateway pytest tests/unit/test_thinking.py tests/unit/test_providers.py -q`;新拒绝和 wire 回归先红后绿,其余保留行为通过。若旧下游形态测试依赖 M3 True,需要在 T3 明确改为已批准迁移样本,不能暗改能力表让它绿。 + +- [ ] 提交点:`fix: enforce registered auto reasoning capabilities`。 + +### T2:纯 wire/raw 所有权校验 + +**文件**:`src/polygateway/thinking.py`、`tests/unit/test_thinking.py`。实现 §3.1 两个签名;`resolve_thinking` 集中校验 wire,providers 不反向 import thinking。 + +| 红绿组 | 最小反例/保留不变量 | +| --- | --- | +| 标准控制 | 六个顶层根+output_config.effort;AUTO 空片段仍拒绝 raw high;NONE/糖/未知同测 | +| 双来源 | 同值仍拒绝;分别传源与请求 raw 验证,被后层遮蔽也拒绝;不修改两个 Mapping | +| 自定义 | on_base/off 根并集、effort_key 自定义字面键、点号不解释路径;on_base 偷带自己的键或标准强度值(含 None)拒绝 | +| 嵌套 | 替换 thinking 整个根即拒绝;profile 不拥有 output_config 时仅 format 可过、effort 不可过;拥有根时 format 也不可覆写 | +| 不误伤 | effort=None 时 raw 原样允许;temperature/seed/response_format 不属词表,合法普通采样保持;off-only 形态及当前方向未知语义保持 | + +新校验首次未实现导致的 import 错误不算行为红;可先在隔离基线把同输入经现有 payload 路径表现记录为“覆盖成功但本应拒绝”,或待 T3 在旧实现回放其失败断言,补齐语义红证据。纯函数自身还需逐例断言异常及未修改输入。 + +**验证**:`conda run -n PolyGateway pytest tests/unit/test_thinking.py -q`,每类目标反例有有效红绿,保留行为绿。 + +- [ ] 提交点:`fix: validate ownership of managed reasoning parameters`。 + +### T3:接入工厂、请求入口和默认 transport + +**文件**:`src/polygateway/client.py`、`src/polygateway/transports/openai_compat.py`;`tests/unit/test_client.py`、`test_config.py`、`test_openai_compat.py`、`test_retry.py`。 + +先在旧路径跑源 HIGH+相同 raw HIGH、本次 AUTO+raw HIGH 的行为反例,确认旧实现实际发出 raw 参数而未拒绝。再接入两守卫:工厂先求 effective_effort 并校验 source.extra_body;chat coerce 后调用 wire=None 的已知词表检查;transport 对当前 profile 和 effective_effort 分别检查 extra_body/overlay,再保持原浅 update 顺序。 + +| 接缝 | 验收 | +| --- | --- | +| 工厂 | SourceConfig 仍能表达 raw-only;from_env/from_settings 对受管源拒绝发生在 limiter/HTTP client 创建之前;记录构建计数零,不以网络偶然没发代替 | +| 请求前置 | 显式请求档与 overlay 已知键冲突,ValueError 且 handler/准入未触发;源级意图或自定义根留 transport 再查 | +| 全量注入 | 真实 OpenAICompatTransport 翻译为 RequestRejectedError;MockTransport 记录零 HTTP,RetryMW 不换源不重试、limiter inflight=0、已有探针收尾路径正常 | +| 参数保真 | raw-only 允许,applied=None;普通采样源<请求<结构化 overlay 的现状保留;显式档/nearest 成功 payload、TransportResult/LLMResponse applied 与真实成功遥测相符 | +| 多源/并发 | 每次以选中源 profile 校验,不因另一个源清单不同提前判整个池死;共享 client 无“最后档”串线;错误不包含 raw 值 | + +工厂将来被请求覆盖不能救活一个已拒绝源,这是已批行为。不要为全量注入自定义 transport 添加 preflight 端口。已有 fixture 需要调整时,只将不再合法的受管+raw 双来源改为显式单来源,新增拒绝反例保留迁移证明。 + +**新增生产默认 HTTP factory 离线守卫**:现有 `tests/unit/test_openai_compat.py` 没有 auth/timeout/trust_env 构造断言,不能写作“保留”。新增 `TestDefaultClientFactory`,直接调用生产 `_default_client_factory(source)` 返回真实 AsyncClient,不使用 T5 的测试 factory,也不 mock 整个 AsyncClient。用两组不同假 api_key、非默认 timeout_s、trust_env=True/False 参数化;不发送网络,finally aclose。节点为 `test_authorization_uses_source_api_key`(检查 client.headers 及 build_request 生成的 Authorization)、`test_timeout_uses_source_timeout_for_all_phases`(connect/read/write/pool 全部等于输入 timeout_s)、`test_trust_env_uses_source_setting`(检查 client.trust_env)。在隔离副本逐个删除 Authorization 传入、遗漏 timeout 参数、遗漏 trust_env 参数/写死 True,指定节点必须因值不符红,再恢复通过;当前实现本来正确,以这些语义变异作为红证据,不改生产 factory 凑红。 +独立命令:`conda run -n PolyGateway pytest tests/unit/test_openai_compat.py::TestDefaultClientFactory -q`,原始实现绿、每个遗漏变异被对应断言杀死、恢复绿;这套测试与 T5 hooks 校验分别验证生产装配和测试取证两条路径。 + +**验证**:`conda run -n PolyGateway pytest tests/unit/test_client.py tests/unit/test_config.py tests/unit/test_openai_compat.py tests/unit/test_retry.py -q`,加 T1/T2 的测试一起跑;工厂/请求/全量注入拒绝均有旧实现红、新实现绿。 + +- [ ] 提交点:`fix: reject conflicting raw reasoning overrides before sending`。 + +### T4:显式缓存迁移和四种遥测口径回归 + +**文件**:`tests/unit/test_cache.py`、`tests/unit/test_client.py`、`tests/unit/test_telemetry.py`;不改生产 cache、指纹或 emitter。 + +使用真实 InMemoryCache、GatewayClient、默认 transport+MockTransport 构造两个客户端。旧语义 payload 可按 1.3.3 真实序列化形态预写(历史数据夹具,不需要在当前生产放回漏洞);同版本 nearest→error 则运行真实客户端写入。 + +| 场景 | 断言 | +| --- | --- | +| 旧 AUTO/raw 记录 | 旧身份可回放是已知风险;换全新 namespace 或 salt 后 miss,实际进入新拒绝,异常不缓存 | +| nearest→error | 源不表态、请求 medium、模型 glm-5.3;nearest 写入后 error 同身份可命中;error 换身份后零 HTTP 拒绝,不添加 fallback 指纹 | +| 能力表变化 | 新增 `TestExplicitCacheMigration::test_capability_change_requires_explicit_identity`:相同源配置、请求 AUTO 和 wire,两客户端注入同一测试模型的不同能力表(旧含 AUTO+HIGH,新仅 HIGH),源级不表态以允许装配。旧客户端真实写入后新客户端同身份回放且无新 HTTP;换全新 namespace 或 salt(参数化)后 miss,进入新能力表并 RequestRejected、无新 HTTP、不写失败值。只用局部测试能力表,不修改 DEFAULT | +| 自定义 wire 变化 | 新增 `TestExplicitCacheMigration::test_custom_wire_change_requires_explicit_identity`:相同源/模型/能力表及请求 HIGH,分别注入同名自定义 profile 的旧/新 effort_key(例如 depth_a/depth_b),on_base 均为空。旧客户端写缓存,新客户端同身份回放旧值且无新 HTTP;新 namespace 或 salt 后 miss,MockTransport 必须收到 depth_b=high 且无 depth_a,返回可区分的新结果,旧身份仍能回放旧值。profile 仅局部注入,不改源配置让现有指纹意外变化 | +| 入口与范围 | 工厂默认 namespace、per-call 覆盖默认、构造全量注入、共享多源 scope、两个租户原前缀保留;只改默认无法覆盖 per-call,需专门反例 | +| 并行/回滚 | 旧新身份可并行且不覆盖对方;回到旧身份确实重见旧值;未受影响调用 key 黄金值逐字不变 | +| 四行口径 | 真实成功=applied、失败尝试=effective 意图、cache_hit/scope 终态=本次请求级;缓存不读取历史 applied 作本次遥测档 | + +该任务多数是已有正确行为的守卫,不人为改生产获得红:隔离变异遗漏 namespace/salt、将 cache_hit 遥测改读历史 applied,要求相应行为断言红,恢复后绿。T3 新拒绝路径旧实现红绿可复用,但不能只报它替代迁移维度证据。 + +**验证**:`conda run -n PolyGateway pytest tests/unit/test_cache.py tests/unit/test_client.py tests/unit/test_telemetry.py -q`。两项新增能力/wire 迁移节点均置于 `tests/unit/test_cache.py::TestExplicitCacheMigration`,单跑 `conda run -n PolyGateway pytest tests/unit/test_cache.py::TestExplicitCacheMigration -q`;分别在隔离副本去掉其 namespace/salt 隔离输入,必须因没有新拒绝/新 wire 而红,恢复后绿,不能只以 nearest→error 的测试代替这两类。两客户端的源指纹必须断言相等,生产指纹算法一字不改。 + +- [ ] 提交点:`test: pin explicit cache migration and reasoning row semantics`。 + +### T5:有限测试归因和独立取证 + +**文件**:新增 `tests/live_evidence.py`、`tests/e2e/conftest.py`、`tests/unit/test_live_evidence.py`。按 §3.2/3.3 实现;不读 .env 的模块可被日常单测安全 import。执行 structured-logging:记录内容按设计 §6,不另建库表。 + +1. 纯分类默认 FAIL。仅一条完整 HTTP 错误、请求检查齐全、无别的 attempt 异常、404、完整无重复键 JSON 的 error.type 精确匹配、外抛 RequestRejectedError 且 status 一致,才 UNCOVERED;多次尝试/多 HTTP、不同 call_id、空检查元组、重复 JSON 键均 FAIL。 +2. hooks 不预读成功 SSE,不将 summary 当 JSON;response 引用等该次 transport 完成后检查 content 是否已缓冲。64 KiB 上限、0 字节、非对象 error、重复键、坏编码各有反例。薄委托器 finally 恢复上下文,零 HTTP 尝试也保存。 +3. 身份函数区分 raw 取证缺失和原始响应明确缺 model;增加 `test_raw_identity_snapshot_reaches_round_consumer`,用真实默认 transport+MockTransport 非流式响应依次覆盖正确 model、缺失/null,以及 JSON 非对象/非法/重复键,断言 §3.3 accessor 的来源和区别;故意让公共响应丢 model 时原始快照仍保留正确串并判 FAIL。并发两逻辑轮次+一次重试验证按 session/parent/成功 call_id 精确选择,不回放前次失败的身份;成功 SSE 快照未取证且公共身份异常时必须 FAIL。覆盖函数按 enabled/disabled/cannot_disable 命题判断,UNKNOWN 不假绿;预期 400 负向契约单独测。 +4. 用 MockTransport 驱动 request/response hooks:改错 model、Authorization、端点、SSE/JSON 解析→FAIL;成功 SSE 不被提前消费;原始 model 正确但公共字段错误→FAIL。交错并发及取消证明 context reset、凭据不泄露、资源释放;薄委托器不额外调用一次 HTTP。 +5. 安全报告每轮独立文件,采用 run UUID+轮次与矩阵安全标识;只接受白名单 safe_fields,拒绝原始异常/HttpEvidence 对象直接序列化。第二轮失败仍可读第一轮;写入失败是 FAIL;最终汇总统计 PASS/FAIL/UNCOVERED 和缺轮,不能只数 pytest 退出码。 + +对旧策略红证据:用合成记录隔离执行现有“整类 skip/正文子串/UNKNOWN 安静”判据,目标测试要求 FAIL/UNCOVERED,确认语义不符;恢复新纯函数后通过。新文件缺失造成 import error 不计红。 + +**验证**:`conda run -n PolyGateway pytest tests/unit/test_live_evidence.py -q`。安全测试使用假的唯一 sentinel 凭据/私有提示词,逐文件检查不出现 sentinel,不能拿真实密钥做输出搜索。 + +- [ ] 提交点:`test: distinguish unsupported live coverage from library failures`。 + +### T6:迁移四个 live 文件并离线化装配断言 + +**文件**:四个 `tests/e2e/test_*.py` 路径见 §2,`tests/e2e/conftest.py`、`tests/unit/test_live_evidence.py`、`tests/unit/test_client.py`、`tests/unit/test_config.py`。 + +| 原接缝 | 改动与离线验收 | +| --- | --- | +| smoke/compat chat | 每轮 session_id+parent_call_id,委托原参数,先报告再 skip/raise;仍验证流/非流、结构化 JSON/模型。断言异常也必须留报告,不只包 await 的异常 | +| compat 平铺键 | 移到 test_config.py 的完整合成 env,删除逐源 TIMEOUT_S 才能验证 LLM_TIMEOUT 回落;不靠真实配置“恰好已有覆盖”过测。无 HTTP、无可达 Redis/PG,明确其仅是本库兼容契约 | +| compat Protocol | 既有真实外部 Protocol 缺包时记录未覆盖;合成 runtime Protocol 和本库调用签名在 test_client.py 无 slow 执行,不能宣称缺失仓库原测试通过 | +| embedding probe | 走同一薄 embed 委托和窄分类,所有路径 finally 关闭;model_not_found 不写成“网关不支持 embeddings”;timeout/trust_env 取已校验源配置,不用 30s 硬编码压紧生产预算 | +| L1–L9 | M3 True 拒绝单独离线/本地断言,真实开启用显式 medium,保留未登记/未知 wire 场景;L8 装配拒绝只计本地契约,不算 live 能力 | +| T10 | 预声明 NONE 可关闭/不可关闭/档位预期拒绝;每轮结束即留证。去掉“仅保留可用轮降低分母”、completion 长短提升 UNKNOWN 的成功逻辑;模型部分档未覆盖不可汇总全 PASS | + +保持原 `_MODEL_PROVIDER`/显式别名表,不新增 AUTO 成员。`_run_rounds`、`_probe_effort` 返回路径不许遗漏失败轮;默认基线也走同一证据出口。T10 临时能力表仅为探测绕过清单,不写回 DEFAULT;其控制字段预期由矩阵声明,不能调用被测 resolver 产生预期。 + +既有 `_tier_settings` 将 stall 强制压到 60s,迁移时去掉该临时缩小值,沿已校验生产配置;不得因持续 429 慢而修改 #22 算法。保留既有轮次/并发设置;先收集矩阵和预计调用数,额外研究不自动展开。M2.5/M2.7 AUTO 复用 T10 现有登记档,M3 medium 流/非流复用对应原开启用例,不以新增多轮研究暗增预算。 + +测试工厂替代仅改变取证装配,不能靠调用生产私有 `_client_factory` 的同一实现来证明鉴权构造正确;生产默认 factory 的头/timeout/trust_env 离线守卫由 T3 **新增** `TestDefaultClientFactory`,T6 验证时一并运行,不宣称旧源码已有覆盖。各能力轮次用 §3.3 的 `raw_identity(session_id=..., parent_call_id=..., call_id=resp.call_id)` 给身份函数提供独立快照,缺失来源不得猜测。live 无取证通道的失败按 FAIL,不为凑分类额外开生产接口。 + +**验证**:`conda run -n PolyGateway pytest tests/unit/test_live_evidence.py tests/unit/test_client.py tests/unit/test_config.py tests/unit/test_openai_compat.py::TestDefaultClientFactory -q`;`conda run -n PolyGateway pytest tests/e2e/ -m slow --collect-only -q`(只采集,不视作真实通过)。在离线注入旧整类 skip、丢第一轮、UNKNOWN→PASS、identity 丢失→skip 变异,分别红;新实现恢复绿。 + +- [ ] 提交点:`test: apply evidence-based live checks without hiding regressions`。 + +### T7:无推理路径真链路与四类变异 + +**文件**:`tests/unit/test_embedding.py`、`tests/unit/test_ocr_client.py`、`tests/unit/test_telemetry.py`、`tests/unit/test_monkey_ocr.py`;生产不改。 + +复用 `_embed_client`/`_client`/脚本 transport/内存 recorder,参数化 embed、recognize_text、parse_layout 与源 True/HIGH,两次尝试(Transient→成功)必须恰有 2 行、一错一成、所有 reasoning_effort None。再覆盖 RequestRejected 一行和耗尽非零失败行;不要求新增不存在的逻辑终态。SQL 锚点用真实 `SQLiteRecorder(tmp_path / "reasonless.sqlite", auto_migrate=True)`,三入口分别走 client→emitter→SQLite,查询总数/失败数/NULL 数,finally 同步 close 注入 recorder。 + +chat 阳性走真实 RetryMW+emitter:True 糖失败 auto、显式请求失败保留意图、nearest 成功为实际映射档。四行遥测继续沿 T4 口径。共享 recorder 并发用测试 session/parent 配对,attempt call_id 唯一且集合不相交。embedding 默认 transport 和 MonkeyOCR text/layout 真实 MockTransport 回包验证 wire 无推理键,不仅断言 emitter 的 False 实参。 + +| 隔离变异 | 必须被哪些断言杀死 | +| --- | --- | +| `embedding.py::_emit` False→True | 误配 True/HIGH 的失败尝试 NULL 断言 | +| `ocr.py::_emit` False→True | text 和 layout 各一个独立节点均因错误行非 NULL 红 | +| `middleware/retry.py::_emit` True→False | chat 阳性实际档/请求档断言,不是签名 TypeError | +| `middleware/telemetry.py::_attempt_effort` 去掉 applies 短路 | 无推理真实失败行断言;确认不是全空数据或假 recorder | + +工作方法:以 T7 当前提交建仓库外临时副本(仅 src/tests/必要工程文件,不复制 `.env`/reference/.pi),用 `PYTHONPATH=<副本>/src` 和副本 cwd 执行 conda pytest;先检查 `polygateway.__file__` 指向副本。逐个变异、跑指定节点记录 exit 1 和目标断言、恢复文件校验散列,再跑 exit 0。绝不在主工作区改 False 假装先红。 + +**验证**:`conda run -n PolyGateway pytest tests/unit/test_embedding.py tests/unit/test_ocr_client.py tests/unit/test_telemetry.py tests/unit/test_monkey_ocr.py tests/unit/test_retry.py -q`,再执行上表隔离变异;原实现绿、四类有效红、还原绿。 + +- [ ] 提交点:`test: guard reasoning-free telemetry through real client paths`。 + +### T8:文档、日志登记与独立验证 + +**文件**:§2 列出的用户文档/架构/旧设计/schema/metric/知识索引,以及验收 finding;不新增运行时字段。 + +同步设计 M1–M9 到 README 可执行迁移节与 `.env.example` 注释,保留型号证据来源;CHANGELOG 未发布段点名 AUTO 新拒绝、未知尽力、raw 同值拒绝、显式缓存身份迁移、UNKNOWN/SKIP 限制。schema 既有 reasoning_effort “实际发出”总括修成四种行来源,不修改 DDL;metric 复用既有 call-telemetry-coverage,记录三个无推理入口错误行 NULL/chat 阳性为 100% 契约,真实覆盖基线留待首次实际运行,不能填伪百分比。 + +由父会话前台派全新 verifier:只给批准设计、计划、分支 diff、验证命令,不给实现自评。至少覆盖正确性/回归和测试归因/范围两个角度;Critical/Important 清零。审查先核对实际路径与仓库语言,不接受不存在文件的结果。补丁回到单 writer,重跑受影响红绿及静态门。 + +| 检查 | 命令/证据要求 | +| --- | --- | +| 静态与边界 | `make check`;`git diff --check`;`conda run -n PolyGateway python -m compileall -q src/polygateway tests/live_evidence.py tests/e2e/conftest.py` | +| 日常全量 | `make test`,保存真实退出码/coverage ≥80%,不能只运行改动文件;连接依赖 skip 单列 | +| LSP | 若会话已有 LSP diagnostics 工具,对四个生产变更文件和新增测试支持文件取诊断;本轮检查 conda 内 pyright/basedpyright 均未安装且工程无其配置,不安装新依赖或虚报 LSP 通过。可用时命令 `conda run -n PolyGateway pyright src/polygateway/thinking.py src/polygateway/providers.py src/polygateway/client.py src/polygateway/transports/openai_compat.py tests/live_evidence.py tests/e2e/conftest.py`,不可用明确记未执行,ruff/import-linter/compileall 是实际既有静态门,不冒称等价 LSP | +| 真实采集清单 | `conda run -n PolyGateway pytest tests/ -m slow --collect-only -q`,先列必需节点、型号/模式/轮次/并发/所用配置身份(不含秘密) | +| 真实执行 | `conda run --no-capture-output -n PolyGateway pytest tests/ -m slow -ra`;保持生产超时,检查每项报告而非仅 exit 0 | +| 反回归 | 四类 #26 变异+T4 缓存迁移+T5/T6 假绿反例全部有独立失败断言与还原通过,finding 引用原始报告路径 | + +长跑用 tmux,`PYTHONUNBUFFERED=1`,命令 stdout/stderr 重定向到 `tests/outputs/134/`,原命令后立刻独立保存 `$?`;不得接 `tail` 管道改写退出码。父会话等待准确 tmux 完成信号/PID,不能 pgrep 完整命令自匹配。不把初次失败覆盖成重跑后的单一绿日志。 + +- [ ] 提交点:`docs: document reasoning ownership and explicit cache migration`;必要修复各自按 commit skill 提交,不把 verifier 自动反馈当授权扩范围。 + +### T9:发布准备、合并后复验与 1.3.4 发布 + +仅在 T8 无未处理阻塞后执行。用户已授权所有本节动作,无需为 merge/push/上传再请示;发现 1.3.4 已存在不可覆盖,停下协调版本,不能私自跳到 1.4。 + +| 顺序 | 精确动作与完成证据 | +| --- | --- | +| 文档先行 | 更新 README 安装约束与能力说明;CHANGELOG 定版为 1.3.4(实际日期),pyproject 和包 `__version__` 同步;本计划复选框只能按已得证据勾选 | +| 发版提交 | 执行 commit skill,标题 `chore: prepare release 1.3.4`,先核对测试报告和 staged 无秘密;运行 `conda run -n PolyGateway pytest tests/unit/test_package.py -q`,不改变公共字段计数 | +| 合并 | `git fetch origin`,确认远端未出现未审变更;`git switch main`,`git merge --no-ff feature/1.3.4-thinking-contracts`。保留已有两个本地提交,禁止 reset/force push | +| 合并后门 | main 上重新 `make lint`、`make test`、`conda run --no-capture-output -n PolyGateway pytest tests/ -m slow -ra`;若 lint --fix 改代码,重新审 diff、提交并重跑,不把脏代码与 tag 分离 | +| 推送与 tag | `git push origin main`;`git tag -a v1.3.4 -m "Release 1.3.4"`;`git push origin v1.3.4`,核对远端 tag 指向最终已验证提交 | +| 构建 | 核实 cwd 后按 CLAUDE 清除旧 dist 产物;`conda run -n PolyGateway python -m build`;`conda run -n PolyGateway python -m twine check dist/*`;缺构建工具先报告环境缺项,不更改核心依赖 | +| 上传 | 从既有 tea 配置安全取 token,仅放 TWINE_PASSWORD 环境变量;`conda run -n PolyGateway python -m twine upload --repository-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi dist/*`,TWINE_USERNAME 沿已有账号;不在 argv/日志输出 token,不把命令成功当最终发布完成 | +| 下载检查 | `conda run -n PolyGateway pip download --no-deps --index-url https://gitea.iomgaa.online/api/packages/iomgaa/pypi/simple/ polygateway==1.3.4 -d <临时目录>`;解包核对新守卫与 MiniMax wire、版本、README 元数据;从仓库外使用该环境 Python 将 wheel 安装到独立 target 并验证 import 来源及拒绝行为 | +| 外部可见 | 建 Gitea v1.3.4 Release(正文来自定版 CHANGELOG);调用 `POST /api/v1/packages/iomgaa/pypi/polygateway/-/link/PolyGateway`;查看 Release/registry 包页面正文、仓库链接、下载产物,逐项记录 URL 与实际结果 | + +测试不在 wheel 内,下载后以无网络小调用核对已安装 `resolve_thinking` 和冲突守卫;不要从工作树 src import 后宣称发布包通过。只在外部结果确认后评论/关闭 #21/#25/#26,正文引用各自验证与迁移边界,不能称所有渠道故障已自动识别。若上传成功但页面/下载校验失败,记录部分发布状态,不重发同版本不同字节。 + +- [ ] 提交/发布点:main 的发布提交与 `v1.3.4` 对齐;Release 与 registry 外部验证全部成立。 + +## 5. 阻塞矩阵:哪些可以执行,哪些不能冒充通过 + +| 缺口 | 本库可完成 | 不可自行宣称/处置 | +| --- | --- | --- | +| 下游工作区缺失 | 本库完整合成 env、runtime Protocol、M1–M9 与显式缓存迁移回归 | GovDoc/CHS 实际配置未取证、Video-Tree 已退出迁移但历史兼容面仍可测;三者均不能虚构实测。提前向父会话登记缺口,发布前须拿到相关负责人脱敏配置与验证证据,或人类明确豁免缺失项;不阻止独立离线实现继续 | +| M2 空 wire AUTO | 默认 wire 单测、实际既有 T10 型号档位复验 | 真实缺身份/无信号/不可达不能当已验证;需有效重测或人类具名豁免,不补回 medium 或无证据改表 | +| M3 非流式 UNKNOWN | 可验证 payload、响应形态、UNKNOWN 不假绿 | 不把长度差当开启/关闭证明;必需能力单元无法满足时保留未覆盖并走人类决策,不新增临时“通过”阈值 | +| 429/5xx/网络失败 | 完整证据保存,库回归用离线契约定位 | 本批归因默认 FAIL 是设计批准范围,不能为了 #25 关闭率改宽 skip;外部证据由人类决定发布豁免 | +| 研究新增预算 | 原有 slow 套件按已有授权跑,已有配置保持 | 额外模型/轮次/对照实验需预算批准;不得将批准设计偷换成无限研究调用授权 | +| 设计外漏洞 | 独立记录实际文件与反例,父会话核定是否阻塞 | 不顺手实施 #19/#22/#23/#24、新 schema 或新 deadline;无强制单源分支 | + +## 6. 自审与验收映射 + +| 设计节/需求 | 任务 | +| --- | --- | +| §4.1/4.2 AUTO 与 MiniMax、未知尽力 | T1,T3 双入口,T6/T8 真实证据 | +| §4.3/4.4 raw 同值/嵌套/自定义/时机 | T2、T3;无公共端口新增、无深合并 | +| §5 D3 与 M1–M9 | T4、T8;未迁移风险显式保留,绝不补指纹 | +| §6 归因/身份/UNKNOWN/负向命题 | T5、T6;完整请求证据、默认 FAIL、逐轮持久化 | +| §7 无推理路径与变异 | T7;三入口真 client+SQLite,四类隔离变异 | +| §8 四种行口径与日志 | T1/T4/T7/T8;既有 schema/emitter,不新增数据面 | +| §9/10 文档、下游与发布门 | T8/T9及阻塞矩阵;外部结果与测试缺口不冒充通过 | + +自审已核对:生产守卫所有消费者在 §3 定义;新增测试文件有确定路径;conftest 当前不存在故明确新建;默认工厂不支持 transport 注入故使用已批准全量注入而非偷扩 API;缓存不改指纹;无从公共 model_reported 倒推上游身份;所有命令均在 conda 环境;未执行的测试不写为已通过。 +计划审查由父会话组织,完成后直接实施,不新增人类计划审批门。执行中本文件任务勾选与 finding 保持实际状态一致;本次计划编写未运行 pytest、变异或真实模型调用。