# ADR-0018: 引擎能力完整暴露 -- 三档模型知识 (静态目录 / 现场发现 / 实测能力) + capability-probe 抽包 + 来源戳数据流 (settings UI 可视化) - Status: **Accepted (PM 2026-06-06 拍 "引擎能力不体现到前端就不完整, 平台必须完整暴露引擎能力, 不是只露物流够用的那部分" + ultracode 授权连续开工)** -- 本文把引擎已有的模型能力知识 (静态规格表 + 现场模型发现 + capability-probe 实测) 完整接到 settings UI. - 关联: ADR-0017 (运行时引擎配置, 本 ADR 在其 provider 实例 + ConfigSnapshot 之上叠 capability 读面) · ADR-0007 (capability tracking intake discipline, probe 的来源戳与其 documented/probed 纪律同源) · ADR-0008 (quote-probe 协议, ModelInfo 注册模式) · ADR-0015 (前端底座, settings.tsx 承载页) · ADR-0006 (fail-loud, 不可达实例 probe/discover 显式报错不静默). - 不在本 ADR: 深度 probe 的异步 job 系统 (见 §3 + §7, 当前无 async 基建, cheap-tier 同步先行) · 把 per-instance 发现结果回灌引擎 ModelRegistry (现仅 UI 读面) · 多租户 capability 隔离 (沿用 ADR-0017 单租户现实). --- ## 1. 背景 / Context ### 1.1 问题: 引擎手握三层模型知识, 前端只露了第一层的一行标签 引擎本身就有完整的模型能力知识, 但 GUI 只暴露了一小角: 1. **静态规格目录** (engine 内置): 每个 provider 包自带一张 `[]flyto.ModelInfo` 表 (`anthropic.anthropicModels` / `deepseek.deepseekModels` / `minimax.minimaxModels` 等), 含 context window / max output / 每百万 token 进出单价 / vision / thinking / caching. 引擎 `engine.New` 时 `autoRegisterProviderModels` 把它们吸进 `config.ModelRegistry`. ADR-0017 的 `GET /config/providers` 已把这张表喂给 settings, 但 UI 只在路由下拉里挤了个 `131k ctx · vision` 的小标签 -- pricing / max output / caching 全没露. 2. **现场模型发现** (live discovery): 自托管端点引擎能实时拉"这台在跑哪些模型" -- `ollama.Models()` 打 `/api/tags`, `lmstudio.Models()` 打 `/v1/models` (实现都在 `core/internal/wire/openai.go` 的 `OpenAICompatClient.Fetch*Models`). 但 **fmlx (飞驼 Mac 推理) 是经 `openai.New` 构造的, 而 `openai.Models()` 返回写死的 GPT 静态表, 从不打网络** -- 所以 fmlx 实例配了 URL 也显零模型 (ADR-0017 §218 记为 "自托管无静态目录 -> 空 + needs_url"). 3. **capability-probe 实测** (empirical probe): `core/cmd/capability-probe` 是一个**独立命令行工具**, 向 provider/model 发最小探测请求, **实测** streaming / thinking / tool_use / structured_output / caching / schema_ref / max_tools, 并给每条能力打**来源戳** (`Source`: probed / documented / manual / untested / untestable, ADR-0007 同源纪律), 输出 `ModelCapabilities` 矩阵. 但它锁在 `package main` 里, server 调不动. ### 1.2 为什么现在: 引擎是产品, 平台必须完整体现它 PM 原话: "引擎能力不体现到前端就不完整", "我们不是为了飞驼 (单一 vertical) 才做的, 飞驼平台要完整体现引擎能力". 这是产品定位决策 -- Flyto 卖的是**领域无关的 Agent 引擎** (CLAUDE.md 原则 9, memory `project_strategy`), 平台是引擎能力的窗口, 不能只露物流够用的子集. "加个自托管端点, 界面当场探出它能干什么"是真销售门票 (memory `feedback_ui_is_core_moat`: UI 视觉冲击 = 护城河; "看上去比实际强一档"产品哲学). ### 1.3 诚实重述: 大半是"把已有能力接出来", net-new 只有抽包 + 持久化 + 两个端点 逆向看清楚避免 scope 失控: - **数据模型不新造**: 静态 spec 复用 `flyto.ModelInfo` + ADR-0017 的 `ModelSpec` 投影; 实测矩阵复用 capability-probe 已有的 `Source` / `Capability` / `ModelCapabilities` (JSON tag 逐字保留, 见 §2.2). - **发现逻辑不新写**: `/v1/models` 的 GET + parse 在 `OpenAICompatClient.FetchOpenAIModels` 里已实现且被 lmstudio 验证过; fmlx 只差一个开关 (§2.3). - **真正 net-new**: (a) 把 capability-probe 从 cmd 抽成 `core/pkg/capability` 可复用包; (b) per-instance 发现/实测结果的持久化 (两张表); (c) 两个端点 (discover / probe) + 前端两档卡片. --- ## 2. 决策 / Decision ### 2.0 confirmed (设计前提, 不 relitigate) 1. **三档分层, 按 key 维度区分**: 静态目录 keyed by provider TYPE (一类一目录, 无实例); 现场发现 + 实测 keyed by INSTANCE NAME (打某实例的 URL / 对某实例+模型实测). 前端据此分两段, 不强行塞进同一张卡 (TYPE 卡无实例身份). 2. **capability-probe 抽包落 core, 不落 platform**: probe 逻辑是引擎能力 (core 关注点), 抽成 `core/pkg/capability`; platform 经端点调用它. (不在 platform 重写一份 -- 会 drift.) 3. **来源戳是第一性公民**: 每条能力都带 `Source`. UI 每个格子都诚实标"这事实从哪来" (实测 / 文档 / 人工 / 未测 / 无法测). 这是整个特性的诚实兑现点, 也是 ADR-0007 纪律的产品化. 4. **probe 永远手动触发, 绝不自动**: probe 花真 token + 真时间 + 可能连不上. 任何启动路径 / 保存路径 / rebuild 路径都不调 probe; 只有 UI 点按钮才打. ### 2.1 第一档已落 (静态目录可视化) settings.tsx 的 `CapabilityPanel` + `TypeCapabilityCard` 把 `GET /config/providers` 的每个 TYPE 渲成完整规格表 (模型 / 上下文 / 最大输出 / 进+出单价 / 看图·思考·缓存), 直读引擎非硬编码. 自托管类型 (needs_url, models 空) 渲占位, 给后两档留位. (已实现 + 部署验真.) ### 2.2 第二档: 现场发现 (live discovery) - `core/pkg/providers/openai` 加 `Config.LiveDiscovery bool` (默认 false = 现状静态, 真 OpenAI 零回归). 为 true 时 `Models()` 改打 `{BaseURL}/v1/models` (复用既有 `OpenAICompatClient.FetchOpenAIModels`, lmstudio 已验证). - fmlx factory (`cmd/common/main.go`) 传 `openai.Config{..., LiveDiscovery: true}` -> fmlx 实例的 `provider.Models()` 现在实时拉 MLX 真目录. - 端点 `POST /config/instances/{name}/discover`: 取 `snapshot.ProviderFor(name)` 的活 provider, 调 `Models(ctx)` (15s 硬超时, 镜像 buildProviderCaps 的 bounded-ctx 纪律), 投影成 `[]ModelSpec`, 持久化 (source=discover), 返回. 不可达 -> 显式 error (ADR-0006 fail-loud). - 持久化: 新表 `engine_discovered_models` (tenant_id, instance_name, models JSONB, discovered_at, PK (tenant, instance), FK -> engine_provider_instances ON DELETE CASCADE). ### 2.3 第三档: 实测能力 (capability probe) - **抽包**: `core/cmd/capability-probe/main.go` (2255 行) 的纯逻辑搬进 `core/pkg/capability/` (types.go / documented.go / probe.go). 公共入口 `ProbeModel(ctx, model, ProbeOpts) (*ModelCapabilities, error)`. `ProbeOpts` 携带 probe 需要的四个 provider 句柄 (base / thinking / cachingClient *transport.Client / cachingProvider) + providerKind + per-probe skip 开关 + `MaxProbeTools` (替换写死的 128). cmd 退成薄壳 (保留 flag / loadEnvFile / 实例构造 / 持久化缓存 / printMatrix), 对每个 target 改调 `capability.ProbeModel`. JSON tag + streaming/ToolUse/Thinking 门控顺序逐字保留, 零行为变更. - **同步 + cheap-tier 默认 + 硬超时** (核心取舍, 见 §3): 端点默认只跑**便宜探针** (streaming / thinking / tool_use / structured_output / schema_ref / schema_features, 各 ~秒级小 token; schema_features 是 4 个 ToolUse-gated 子探针, 仍属秒级), 经 skip 开关**跳过四个贵探针** (caching 阶梯 ~14400 token / max_output 128k 预算 / tool_count 二分 ~9 次调用 / reasoning_passback 2 往返). cheap-tier + documented 表 + `Models()` 规格合并 -> 完整源戳矩阵, ~30-90s, 同步可扛. 贵探针在矩阵里显**未测** (源戳诚实). 深度实测 (跑贵探针) = 异步 follow-up. - 端点 `POST /config/instances/{name}/probe` body `{model}`: 取 `snapshot.ProviderFor(name)`, 用同一 provider 派生 thinking/caching 句柄, 调 `capability.ProbeModel` (skip 四贵), 持久化 (keyed by model), 返回 `ProbedModelCapabilities`. 90s 硬超时 (从 r.Context() 派生, client 断开即取消). probe 失败 = 成功的 probe 结果 (HTTP 200 + ok 语义), 不是 HTTP error. - provider 构造: Manager 加公共 accessor `ProviderFactory() ProviderFactory` (probe 未保存实例时临时构造, 与 rebuild 同一构造路径, 消除 supportedTypes/factory 双份 drift). 已保存实例直接走 `snapshot.ProviderFor(name)`. - 持久化: 新表 `engine_probed_capabilities` (tenant_id, instance_name, model_id, capabilities JSONB, probed_at, PK (tenant, instance, model), FK -> engine_provider_instances ON DELETE CASCADE). ### 2.4 store / manager / 前端缝 (镜像 ADR-0017) - store: 新方法落**三处** (Store interface + PostgresStore ON CONFLICT upsert + InMemoryStore RWMutex map; 漏 inmemory 会静默断本地无 postgres 栈). specs/capabilities 存 `json.RawMessage` 解耦, store 不 import server (避环). - manager: `DiscoverInstanceModels` / `ProbeInstanceModel` / `GetInstanceCapabilities`. 关键: 这些**只记展示数据, 绝不调 rebuildLocked** (不重建 provider, 不重解密 key). - 前端: api.ts 加 `CapabilitySource` / `Capability` / `ProbedModelCapabilities` / `DiscoverResponse` / `ProbeResult` / `InstanceCapabilities` 类型 + discover/probe/get 方法. settings.tsx `CapabilityPanel` 加 Section B (per-instance 卡), 新 `InstanceCapabilityCard` (现场发现按钮 + per-model 实测按钮) + `ProbedMatrix` + `SourceBadge` (5 个 Source 映射到既有 fc-tag 修饰符). 复用既有 .fc- 组件词汇, 零新 CSS. --- ## 3. 替代方案 / Alternatives 1. **probe 走异步 job 系统 (status enum + poll/SSE)** -- 否决 (v1). 理由: dispatch 的 SSE/job 端点全是 stub (`server_p2_stubs.go` 单合成事件即关), **无任何真 async job 基建可镜像**; 为一个 cheap-tier ~分钟内的同步调用造 job 子系统 = 过度工程. 不可达 fmlx 的 hang 用硬超时 + ctx 取消解, 不靠 async. **深度 probe (贵探针, 真多分钟) 确实需要 async** -> 显式登记为 follow-up, cheap-tier 同步先兑现可见价值. ProbeResult 设成 discriminated union (done / running / error), 后端将来转 async 不破前端契约. 2. **现场发现写独立 `DiscoverModels` 包 / 独立 HTTP 调用** -- 否决. `/v1/models` 的 GET+parse 在 `OpenAICompatClient.FetchOpenAIModels` 已实现且被 lmstudio 验证; fmlx 已走 `openai.New`, 一个 `LiveDiscovery` 开关零新 wiring 即通. 独立包要么复制 wire body 要么落 internal (外部不可导). extend-openai 足迹最小. 3. **复用 lmstudio provider 跑 fmlx** -- 否决. lmstudio 写死 `Provider="lmstudio"` 且语义是桌面 GUI 后端; 指它打服务端 MLX 是命名/语义 smell. fmlx 概念上就是 openai 兼容服务端点 = openai provider 已表达的东西. 4. **probe 逻辑在 platform 重写一份** -- 否决. probe 是引擎能力, 重写会与 cmd 版 drift (ADR-0007 的 documented 表 / 门控顺序 / 阶梯逻辑都得双维护). 抽成 core/pkg/capability 单一真相. 5. **真 OpenAI 也 LiveDiscovery** -- 否决. 真 api.openai.com `/v1/models` 返 embedding/audio/tts 一堆无 pricing/context 的垃圾 (`openai/provider.go` LEGACY 注释已述). 故 `LiveDiscovery` 默认 false, 仅 fmlx/自托管 opt-in. 6. **probe/discover 一张表 source+model_id 合并 PK** -- 否决. discover 是 per-instance ([]ModelSpec), probe 是 per-model (一个 ModelCapabilities), 两种 shape 塞一个 JSONB 列丑. 两张表各持一种 shape 更清晰. --- ## 4. 影响 / Consequences ### 正面 - 引擎三层模型知识全部可视化, 平台真正"完整体现引擎能力" (PM 核心诉求). - fmlx 自托管实例从"显零模型"变成"现场拉真目录"; 任意实例+模型可一键实测, 出带源戳的能力矩阵. - capability-probe 从孤立 CLI 变成引擎可复用能力 (`core/pkg/capability`), 未来 registry / pricing / 引擎自身校验都能复用. - probe 经真 factory 构造 -> 消除 supportedTypes 与 factory switch 的双份 drift 风险. ### 负面 / 成本 - core 多一个 import `internal/transport` 的 pkg (capability), 仅 core 内可用 (probe 的 cachingClient 走 transport 直路); 真外部 SDK 复用需另经 ModelProvider 接口重表达 (out of scope). - 实测矩阵的四个贵能力 (caching/max_output/tool_count/reasoning) v1 显"未测" -- 诚实但不完整, 深度 probe 待 async follow-up. - per-instance 发现/实测结果 live 之外多两张表 + 落库读写; DeleteInstance 经 FK CASCADE 连带清. - fmlx `/v1/models` 若某 oMLX build 未实现 -> 现场发现 false-negative (live 返空); 不自动降级到 Stream (避免隐式花 token). --- ## 5. 验证 / Validation - `go test -race ./...` 在 core (capability 包 + openai LiveDiscovery) 与 platform/common (store/manager/endpoints) 全绿; capability 包搬来的 14 个纯 helper 测试随迁. - cmd/capability-probe 抽包后 `go build` + 至少一个 target 跑通 (行为不变回归). - labtest 端到端: (a) 第一档静态规格面板可见 (已验); (b) 对一个可达云实例 (deepseek/anthropic) discover 返目录; (c) 对一个云实例+模型 probe 返带源戳矩阵 (实测 streaming/tool_use 等为 probed, vision/pdf 等为 documented, 四贵为 untested); (d) 对不可达 fmlx-m5max discover/probe 返清晰 error 不 hang (硬超时验证). - 真验证靠对照真值非 LLM 自评 (memory `feedback_verify_against_truth`): probe 结果须对照该 provider 已知事实抽查 (如 deepseek 无 vision -> 矩阵 vision 应为 documented=false). ## 6. 触发重新评估的条件 / Trigger conditions - 深度 probe (贵探针) 需求落地 -> 触发 async job 子系统设计 (届时复用 ProbeResult 的 running 分支). - per-instance 发现结果需回灌引擎 ModelRegistry (让路由/budget 用真发现的 spec) -> 触发 discover -> registry 注册链 (ADR-0008 C2 registerQuoteDispatchModels 模式). - 多租户 row-level 隔离落地 -> capability 表的 tenant_id 从前瞻列变强制. - 外部 SDK 要复用 capability 包 -> cachingClient 的 transport 直路需经 ModelProvider 接口重表达. ## 7. 工程量 / Engineering footprint - core: 新包 `pkg/capability` (types.go/documented.go/probe.go + test, 抽自 cmd 2255 行) · `openai.Config.LiveDiscovery` + `Models()` 分支 · cmd/capability-probe 退薄壳. - platform/common: 2 DDL 表 (pool.go) · store 2 方法 ×3 实现 (interface/postgres/inmemory) · manager 3 方法 + `ProviderFactory()` accessor · server_config.go discover/probe/get handler + wire 类型 · server.go 3 路由. - frontend: api.ts 6 类型 + 3 方法 · settings.tsx Section B + InstanceCapabilityCard + ProbedMatrix + SourceBadge + helpers. - follow-up (登记): 深度 probe async · discover 回灌 ModelRegistry · 未保存实例 test-before-save probe (POST /config/probe). ## 8. 修订记录 / Revision history - 2026-06-06 v1 (Accepted): 三档模型知识完整暴露. PM ultracode 授权连续开工. 5-agent 并行勘测定死全部插入点后落本 ADR. 第一档 (静态规格面板) 先行实现 + 部署验真; 二三档 (现场发现 + 实测能力) 本 ADR 定架构, 同 commit 实现.