# ADR-0007: Capability tracking 接入纪律 — wire 层 capability-aware 替代兜底假设 - **Status**: Accepted (PM 拍板 2026-05-01, "能力补") - **Date**: 2026-05-01 - **Deciders**: PM (产品经理) + Flyto Agent core team - **Related code**: `core/pkg/flyto/provider.go`, `core/pkg/providers/{anthropic,openai,minimax,gemini,openrouter}/provider.go`, `core/internal/wire/openai.go`, `core/internal/wire/tools.go` - **Related TODO**: r22-r25 实证序列 wire 协议 gap; ADR-0006 follow-up - **Related ADRs**: ADR-0005 引擎中性化精神连续 (engine 不替消费方做产品决策); ADR-0006 fail-loud cause 链 (typed error 路径) 互补 - **Commit chain**: C1 (`09b6bc1` Bug W tool_call_id dedup hotfix, 切出 ADR-0007 范围) + C2 (`dac31d6` ModelInfo 加 3 字段) + C3 (`f77b2c3` 4 direct provider modelInfo 静态填值) + C4 (`17f7ba4` wire 层 capability-aware passback + ValidateToolNames pre-flight) + C5 (`16423ac` OpenRouter live metadata 消费扩展) + C6 (本 commit doc + TODO + CHANGELOG + CLAUDE.md) --- ## 1. 背景 / Context ### 1.1 r22-r25 序列暴露的"修一层揭露下一层"问题 物流报价表抽取业务命门 (memory `project_billcost_quote_extraction.md`) 跑 OpenRouter + deepseek/deepseek-v4-flash: | Round | 跑挂的 wire 协议 gap | 当时修复路径 | |---|---|---| | r22 | tool 名 `billcost.reflect` 含 dot 违反 OpenAI regex `^[a-zA-Z0-9_-]+$` | ADR-0006 业务 fix tool 改名 `billcost_reflect` | | r24 | DeepSeek-R1 thinking 模式必须 prior assistant turn `reasoning_content` passback | ADR-0006 fail-loud 路径暴露真因, 但实际修需 wire 层 capability-aware | | r25 | DeepSeek 严协议 reject 单 message 内 `tool_call_id` 重复 | ADR-0007 C1 wire 层 transport-level dedup (跟 capability 无关, 是 engine/wire 实现 bug) | | (待实证) | OpenAI o1/o3 协议要求 / Gemini thinking inline / etc. | 等真实证 | PM 反思: 这不是 5 个独立 bug, 是同一个底层问题 — **引擎对每个 (provider × model) 的能力没真测过, 全走兜底假设**. 修一个 bug 跑一次 r 是用业务阻塞换试错成本, 累积无穷. ### 1.2 调研: 业界 capability tracking 模式 3-agent 并行 review reconcile 调研得出: | 框架 | 模式 | 维度 | 兜底策略 | |---|---|---|---| | Anthropic Python SDK | string + server-validates | n/a | 服务端 4xx | | OpenAI Python SDK | 同上 | n/a | 服务端 4xx | | LiteLLM | PR-maintained model_prices_and_context_window.json | 30+ | 未知 model 走 default 兜底 | | LangChain | community pkg PR + integration test suite | 透传 | 各 provider 类决定 | | Vercel AI SDK | 透传 string + 隐式能力 | 0 显式 capability | community provider 自决 | | Aider | 走 LiteLLM model db 启动期校验 | 30+ (LiteLLM) | warning + fall through | **核心 split**: - **SDK 都走 "string + server-validates"** — 接 unknown model 不卡, 让 server 4xx 自己说话 - **聚合层 (LiteLLM/Aider) 走 "PR-maintained model db + 30+ 维度 capability flag"** — 维护静态表 Flyto 当前架构跟 LiteLLM 同档但**维度数远薄 (~7 vs ~30) + 无 capability-aware wire 行为驱动**, ADR-0007 是把 Flyto 拉到 LiteLLM 同档的扩展. ### 1.3 OpenRouter live API 是 capability data 金矿 `/api/v1/models` 已返高价值 capability 元数据 (调研 agent 揭示): - `supported_parameters[]` 完整 15 项 (`tools / tool_choice / response_format / seed / frequency_penalty / presence_penalty / stop / logit_bias / logprobs / top_logprobs / min_p / repetition_penalty / top_k / max_tokens / temperature / top_p`) - `architecture.input_modalities` (text/image/audio/video/file) - `top_provider.max_completion_tokens` (实际后端上界 vs root 聚合 fallback) - `pricing.input_cache_read` (caching 自动判) - `default_parameters` (推荐 Temp/TopP) - `tokenizer / knowledge_cutoff / expiration_date` Flyto 此前**几乎全丢弃** (仅消费 ContextLength + Pricing 2 字段). C5 解这条. --- ## 2. 决策 / Decision ### 2.1 ModelInfo 加 3 capability 字段 (C2) `pkg/flyto/provider.go ModelInfo` 加: | 字段 | 取值 | 作用 | |---|---|---| | `ToolNameRegex string` | 空=未知; `^[a-zA-Z0-9_-]+$` (OpenAI 兼容); `^[a-zA-Z0-9_-]{1,64}$` (Anthropic) | wire 层 pre-flight 校验 tool 名, 违规返 ErrModelToolUnsupported 早期拒绝 | | `ReasoningPassbackMode string` (enum) | `""=未知` / `"none"=server 管 state` / `"string"=DeepSeek-R1 协议` / `"details_array"=部分 OpenRouter` (TBD) | wire 层 mode==string 时 inject reasoning_content 字段, 其他 mode 跳过 | | `ProviderKind string` (enum) | `""=未知视作 aggregator 保守` / `"direct"=固定 (provider × model) 集 strict 适用` / `"aggregator"=动态路由 strict 不可行` | 与 ADR-0007 § 2.2 bifurcation 配套 | 零回归: 静态表不显式填写时全 `""` 零值, 现有 caller 完全不变. ### 2.2 Bifurcate direct vs aggregator (§ 红线) **direct provider** (anthropic / openai / minimax / gemini): 固定 (provider × model) 集. C3 给 4 provider 的 modelInfo 列表静态填 ToolNameRegex / ReasoningPassbackMode / ProviderKind 默认值. **aggregator** (openrouter / lmstudio / ollama): 动态路由 300+ model, **物理不可能 strict refuse**. 业界共识 (LiteLLM/Aider/Vercel AI SDK/LangChain 全 passthrough). Flyto 走 wire 层 capability 推断 (C5 OpenRouter live metadata 消费扩展) + provider 4xx 自然冒泡 (ADR-0006 typed error 路径兜底). ### 2.3 § 3 红线: capability 信号驱动 wire 行为, 不驱动 auto-fallback ADR-0007 capability code 仅供 wire 层**信号化**决定 inject / validate / 不做; 引擎层**禁用** capability code 驱动 auto-fallback 行为 (例如 `ErrModelToolUnsupported` 静默关 tools 重试 / `ReasoningPassbackMode==string` 自动改 model). 与 ADR-0005 引擎中性化精神连续 (engine 不替消费方做产品决策), 与 ADR-0006 § 3 红线 (typed error code 仅供分类不驱动 fallback) 性质相同. ### 2.4 Schema 不试图穷举所有 capability (rule of two) ADR-0006 已立 typed error + provider 4xx 自然冒泡路径. ADR-0007 只 strict 关键 wire-critical 维度 (3 条已 r22-r25 实证: ToolNameRegex / ReasoningPassbackMode / ProviderKind), 其他维度 (parallel_tool_calls / strict_json / vision PDF batch / etc.) 走 ADR-0006 fail-loud 等第二个 driver 触发抽接口. ### 2.5 ModelRegistry 由消费者注册 (engine 不预设供应商) ADR-0005 精神: engine 不预设 anthropic/openai/etc. CLAUDE.md "引擎不预设任何供应商的模型信息--各 provider 通过 RegisterModels(registry) 注册自己的模型". C3 4 provider 静态填值是给消费者注册时的 capability 数据基础, 注册仍是消费者责任. 当前 quote-engine-probe / cmd/common 等 caller 未 RegisterModels, ADR-0007 字段全走零回归路径 (req.Capabilities=nil → wire 跳过 capability-aware 行为). --- ## 3. 替代方案 / Alternatives ### 3.1 方案 A 完整版 (registry strict + 全维度 + CI gate) 否决理由: - OpenRouter 物理不可 strict (300+ 动态路由) - rule of two 不满足全 strict (deepseek-v4-flash + 物流业务 = 1 真消费者 1 业务) - 5x 工程量; PM 隐性约束 (v0.5 周期物流锁 minimax) 期间 model 探索就被挡 ### 3.2 方案 C 流程化 + 边界 strict + 不扩 schema 否决理由: - 不治本 r24/r25 — wire 层 thinking passback 不修, deepseek-v4-flash 仍跑不通 - 流程纪律靠 PR review 人盯, 工程师跳过 checklist 概率高 - 把 r22-r25 真因甩给 ADR-0006 事后捕获, 违反 PM 反思 "fail-loud 不到位 + 业务 bug 是两件事" 精神 ### 3.3 方案 B' (本 ADR 实施) = 方案 B (3 字段 schema 扩 + opt-in flag 无 strict) + 调研 reconcile insights: - Bug W 切出 ADR-0007 范围 (engine/wire 实现 bug 不是 capability 维度) → C1 单独 commit - OpenRouter live metadata 消费扩展 (调研揭示 live API 已返高价值字段) → C5 加入 ADR-0007 范围 - bifurcate direct vs aggregator (§ 2.2) - § 物流业务 v0.5 周期锁 minimax sub agent 隐性约束写明白 工程量: ~400 LOC + 18 测试 + 6 commit + 1 ADR. 已落地. ### 3.4 方案 D 把 ErrorCode 移到 engine 删 flyto sibling 否决理由 (与 ADR-0006 § 3.4 相同): wire 层 import engine 引入循环依赖. 当前 sibling 实现是历史成本; 跟 ADR-0006 一样保留 sibling, errors.As 链 try 两 type 是架构 patch 不是新设计. 真正合并是另一个 ADR (TD-11 v1.0 评估). --- ## 4. 影响 / Consequences ### 4.1 得益 1. **r22 类 tool name 协议错**: ToolNameRegex pre-flight reject 早期 fail-loud (OpenRouter 路径默认填 `^[a-zA-Z0-9_-]+$` 自动生效) 2. **r24 类 reasoning passback 错**: ReasoningPassbackMode="string" 时 wire 层 inject reasoning_content (DeepSeek-R1 类协议) 3. **OpenRouter capability 自动推断**: live metadata `architecture.input_modalities / top_provider.max_completion_tokens / supported_parameters / pricing.input_cache_read` 自动映射, 不必手填 4. **零回归**: 现有 caller (req.Capabilities=nil 路径) 完全不变. 新 capability 行为是 opt-in 5. **业界对位**: 跟 LiteLLM 静态表 + LangChain integration test suite 模式相同 6. **ADR-0005/0006 兼容**: capability 信号驱动 wire 行为 ≠ auto-fallback. 与中性化精神连续 ### 4.2 不便 / 代价 1. **历史 sibling 重复加深**: ADR-0006 已加 sibling EngineError, 本 ADR 又给 ModelInfo 加 sibling 字段需要 wire 层 mirror. TD-11 v1.0 合并 2. **ModelRegistry 由消费者注册**: 当前 caller 未注册的 capability 数据不生效. 消费者接 RegisterModels 才是真正生效点 (TD-24) 3. **capability-probe 实测扩 3 项未做**: 静态表填值是 ADR-0007 当前路径, probe 实测留 TD-20 4. **OpenRouter ReasoningPassbackMode 默认空**: 各底层模型协议差异大需逐个实测填 (TD-22) ### 4.3 与 ADR-0006 互补 (不替代) ADR-0006 (typed error fail-loud cause 链) 是事后归类 — 让 provider 4xx 暴露真因. ADR-0007 是事前预防 — 让 wire 层不再发出会 4xx 的请求. 两者叠加: 已知 capability 字段非空时 wire pre-flight reject (ADR-0007); 未知字段或新协议错时 provider 4xx 自然冒泡 (ADR-0006). 不互斥. --- ## 5. 验证 / Validation ### 5.1 单元测试 (-race 全绿) | 测试 | 验证内容 | 文件 | |---|---|---| | `TestFlytoMessagesToOpenAI_ReasoningPassback_StringMode` | mode="string" inject reasoning_content (DeepSeek-R1 协议) | `wire/openai_test.go` | | `TestFlytoMessagesToOpenAI_ReasoningPassback_NoneMode` | mode=""/"none"/"details_array" 都跳过 inject 零回归 | 同上 | | `TestFlytoMessagesToOpenAI_ToolCallIDDedupe` | Bug W 单 message 内同 tool_use_id 去重 | 同上 | | `TestFlytoMessagesToOpenAI_DistinctToolCallIDs` | 不同 ID 全保留不误伤并行调用 | 同上 | | `TestFlytoMessagesToOpenAI_EmptyToolUseID` | 空 ID 不 collision 误吞 | 同上 | | `TestValidateToolNames_*` × 5 | regex 空跳过 / OpenAI regex reject dot / 合法名通过 / bad regex silent skip / 空名跳过 | `wire/tools_test.go` | | `TestFetchOpenRouterModels_LiveMetadataExpanded` | r24 实证 deepseek-v4-flash + claude-sonnet-4.6 fixture, 6 字段映射 | `wire/openai_test.go` | ### 5.2 实证待跑 r26 (改 `billcost_reflect` 名 + Bug W dedup + Bug V capability-aware passback) 用 deepseek-v4-flash 实际跑: - 预期: tool name pre-flight 通过 (regex 命中) - 预期: thinking 模式 reasoning_content 自动 inject (需消费者注册 ModelInfo with ReasoningPassbackMode="string" — quote-engine-probe 当前未注册, 跑挂时 ADR-0006 fail-loud 暴露真因即视为正常) - 预期: 单 message 内 tool_call_id 重复时 wire dedup, 不 reject 4xx 物流业务命门维持 r21 minimax sub 0 violations 收敛 ✅, ADR-0007 期间 v0.5 周期锁定 minimax sub agent 不切换其他 model 探索. --- ## 6. 触发重新评估的条件 / Trigger conditions ADR-0007 在以下情况之一时重新评估或扩充: 1. **第二个 ReasoningPassbackMode 消费者驱动** → e.g. qwen3-thinking / Llama-4-reasoning 等出现 string/details_array 模式; rule of two 满足后抽 wire 层 reasoning passback 通用接口 2. **第三次 capability 缺失导致业务事故** → 评估升级到方案 A (strict mode + ErrModelCapabilitiesMissing typed code) 3. **OpenRouter `supported_parameters` 完整 15 项消费驱动** → 消费方需要 seed/frequency_penalty/etc. capability 时扩 ModelInfo 字段 4. **input_modalities 完整集消费驱动** → SupportsAudio / SupportsPDF / SupportsVideo 等 ModelInfo 字段扩展 (TD-23) 5. **direct provider 出现 capability tracking 必要的协议错** → 例如 anthropic 加新 thinking 协议变形需 ReasoningPassbackMode 改值 6. **engine 内出现 capability code 驱动 auto behavior 的合理需求** → 重新评估 § 2.3 红线 (强证据 + 立 ADR 修订) --- ## 7. Tracked debt ADR-0007 当前路径外, 留 follow-up TD: - **TD-19**: engine/wire 重复 emit tool_use block 真因调研 (Bug W 临时 dedup 是 transport-level 防御, root cause 在 retry 路径 / 模型 final response / SSE 流式重组 by Index 一处) - ~~**TD-20**: capability-probe 扩 3 项实测~~ → **drained v1.1** (probeToolNameRegex + probeReasoningPassback + ProviderKind 静态标全部实施 + 实证, 见 § 8 v1.1) - **TD-21**: wire reasoning_details 数组 mode 实装 (当前仅 string mode 生效, details_array TBD 等首遇) - **TD-22**: OpenRouter per-model ReasoningPassbackMode 实测填 → **partial v1.1** (probe 已跑出 8 model 实测数据落 capabilities.json, 真消费者用 RegisterModels 接通留 follow-up) - **TD-23**: input_modalities 全 modality 字段扩展 (SupportsAudio / SupportsPDF / SupportsVideo) - **TD-24**: 消费者 (cmd/common / quote-engine-probe / etc.) 加 RegisterModels 接通 ModelRegistry 让静态填的 capability 数据真生效 - **TD-25**: lmstudio / ollama 等其他 aggregator capability 推断 (live API 形态各异) - **TD-26** (v1.1): anthropic provider 静态 ToolNameRegex `^[a-zA-Z0-9_-]{1,64}$` 实际太严. OpenRouter 经 Anthropic / Azure 路由 claude 4.5/4.6 实测服务端字面回 `^[a-zA-Z0-9_-]{1,128}$` — 调研 anthropic 官方真实上限 + 调整静态值 - **TD-27** (v1.1): OpenRouter→Azure 路径新发现 — claude 4.6 系列经 OpenRouter 路由到 Azure 不是 Anthropic 直接, capability 跟 Anthropic 直连可能不一致, 监控 + 对照实证 - **TD-28** (v1.1): anthropic / minimax provider 也跑 TD-20 prober (本次未跑节省配额, openrouter 已有 minimax/m2.7 经 OpenRouter 数据可参照) --- ## 8. 修订记录 / Revision history - **v1.0 (2026-05-01)**: 初版. PM 拍板 "能力补" 方向, 走方案 B' (B + reconcile insights). 6 commit (C1 Bug W dedup hotfix + C2 ModelInfo schema + C3 4 provider 静态填值 + C4 wire 接通 + C5 OpenRouter live metadata + C6 doc). - **v1.1 (2026-05-01 后续)**: DeepSeek 官方接入 + TD-20 实证落地 + Mix engine 物流胜利. 5 commit (C1 wire/openai.go DeepSeek prompt_cache_hit_tokens 顶级字段 fallback + C2 deepseek provider 子包 第 5 个 direct provider + C3 capability-probe 接入 deepseek + C4 TD-20 三 prober 实测 + C5 quote-engine-probe 接入 deepseek). PM 反向论证否决 "openai provider + BaseURL 借壳" 工程捷径, 拍板 deepseek 走独立子包 (与 § 2.1 direct provider 列表一致). - **§ 2.2 bifurcate direct vs aggregator 实证矩阵**: 同一 model id `deepseek-v4-flash`, 经官方直连 (regex=strict 服务端字面 `^[a-zA-Z0-9_-]+$` + passback=string 字面 "must be passed back") vs 经 OpenRouter (regex=permissive + passback=none, 后端路由不一致). 决策得到强力实证支持. - **新发现 TD-27**: OpenRouter 把 claude 4.6 系列路由到 **Azure** (不是 Anthropic 直接), 服务端字面回 `^[a-zA-Z0-9_-]{1,128}$` — anthropic provider 静态填的 `^[a-zA-Z0-9_-]{1,64}$` 实际更严 (TD-26 调研真实上限). - **§ 7 TD 状态调整**: TD-13 + TD-20 drained / TD-22 partial / 新 TD-26 + TD-27 + TD-28 (anthropic/minimax 也跑 TD-20 prober). - **物流 r 系列实证胜利**: r22-r25 deepseek-v4-flash via OpenRouter 全失败 → r26+ deepseek-v4-flash 官方直连 round 2 收敛 (7m37s, ADR-0005 + ADR-0006 + ADR-0007 累积修复全栈胜利).