
DeepSeek Harness Models 页面声明 Provider从浏览器一步接入 OpenAI 兼容网关的架构实现【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness导读在 DeepSeek Harness 中接入一个 OpenAI 兼容网关、自托管模型服务或比内置目录更新的模型过去意味着打开$DSH_HOME/settings.yaml手写 provider profile——不熟悉 profile 结构就无法完成而模型上下文窗口过时也只能靠升级 pi-ai 包来解决。本文基于项目已实施的架构笔记.agents/notes/implemented/architecture/2026-08-04-declaring-a-provider-from-the-models-page.md拆解从 Models 页面声明 Provider这一功能的两大组件——共享模型列表编辑器ModelListEditor与独立的CustomProviderCard创建卡片并落到packages/client/ui-settings-models、packages/llm/llm-pi-ai的源码与测试说明端点探测、schema 驱动的协议选择、凭据分离与 Provider ID 不可变等设计决策。读完你将掌握该功能的完整使用路径、底层调用链与设计取舍。背景能力早已存在只是界面没有暴露在本次改动之前pi-ai 路由已经被设计为声明式 provider而非目录查找dsh-llm-pi-ai的 catalog.ts 将内置目录与 profile 自身条目合并resolveProfiles不再用getBuiltinProviders()校验路由键且宿主层获得了对草稿端点进行 interrogate探测的能力见 2026-08-03-pi-ai-declared-provider-catalog.md 与 2026-08-04-draft-provider-endpoint-interrogation.md。但这两层都没有触达不编辑 YAML 的人Models 页面仍然只给每个 provider 一个 API key 输入框和一个带 base URL 的折叠区。于是添加一个网关 打开$DSH_HOME/settings.yaml且必须懂 profile 形状修正一个过时的上下文窗口 同样要改 YAML能力存在界面却不暴露。这篇笔记指出缺失的两件事形状并不相同场景本质需要的界面编辑既有路由的模型卡片上的一个字段已存在的卡片行内编辑列表声明一条新路由一次创建route id 尚未确定独立的创建卡片因为 route id 在创建时才被选定在选定之前settings 地址根本不存在——这正是创建流程必须独立成卡的原因。核心设计一ModelListEditor——两条流程共享的模型列表编辑器ModelListEditor源码同时服务于编辑既有路由与创建新路由两条流程。它编辑一个 provider profile 的models数组一行一个模型字段为id模型 id、name显示名、contextWindow上下文窗口、maxTokens输出上限与项目语义一致空列表的含义是提供该路由的内置目录——行只能被有意添加绝不会被偷偷写回目录清空一个可选字段 从 profile 中删除该字段而不是存一个会被 schema 拒绝的空值非正整数的容量值根本不会落盘。容量字段的 K/M 编辑词汇容量以文本编辑背后是共享的formatCapacity/parseCapacity工具与 DeepSeek 目录编辑器共用同一套 K/M 词汇保证两个界面读写一致。展开行chevron会显示两个容量输入其占位符来自适配器自己的路由级兜底值contextWindow显示256K、maxTokens显示32Kllm-pi-ai的defaultContextWindow/defaultMaxTokens的人类可读拼写。注意占位符只是提示而非镜像页面按 1000 计 K所以输入256K存的是 256000而留空则保留适配器的 262144。探测Fetch问的是表单此刻显示的内容ModelListEditor拥有 fetch 动作ProbeTarget定义了探测目标export interface ProbeTarget { settingsNs: string // 回答问题的适配器所属 settings 命名空间 provider?: string // 被编辑的路由适配器已描述它时可从自身注册表直接回答 baseURL?: string // 表单当前显示的端点 api?: string // 表单选择的 wire 协议 apiKey?: string // 已输入但尚未存储的 key }关键设计fetch 询问的是表单此刻显示的内容——一个被编辑但未保存的 base URL、一个已输入但未落库的 key。这让添加 provider成为一趟流程而不是先保存再返回。探测请求通过api.llm.discoverModels(settingsNs, request)发出对应llm-pi-ai的 discovery.ts回复打开一个候选选择器picker而非直接写入已配置的候选默认不勾选所以采纳选择永远不会覆盖用户修正过的容量候选采纳时adopt()只带上端点披露的字段id/name/contextWindow/maxTokens按 id 合并——用户已调好的行胜出无法被探测的 provider 是绕行而非死路适配器自己的错误消息显示在仍然可手工编辑的行旁边。底层探测实现llm-pi-ai 侧discovery.ts 中可读列表的协议只有openai-completions与openai-responses——它们共享 OpenAIGET /models形状 Bearer 认证。Azure 虽属 OpenAI 血统但被排除需要api-key头与api-version查询参数Codex 走 OAuth其余协议回答DISCOVERY_UNSUPPORTED界面回落到手工输入而非把猜错的响应形状报告成空 providerbase URL 按前缀处理而非 URL 解析目标listingUrl()做${baseURL.replace(/\/$/, )}/models部署路径如https://gateway.example/openai/v1保留其分段4 MB 响应上限作用于实际读取的字节数先检查声明的content-length礼貌但不可信再在流式累积中强制封顶与dsh-web-fetch处理调用方提供 URL 的两阶段形状一致已配置路由的凭据读取只在必要分支发生request.provider有内置目录时直接由注册表回答、零网络调用草稿携带的apiKey优先正在测试的那个 key否则才读存储凭据无 key 的探测保持未认证用于依赖 provider 自身环境发现的场景。核心设计二CustomProviderCard——声明 pi-ai 不内置的 providerCustomProviderCard源码声明 pi-ai 未随包提供的路由。它必须是独立卡片因为route id 在这里被选定一次settings.mutate把整个 profile 写到providers.routekey 通过credentials.set单独传送引用名沿用既有 provider 的ROUTE_API_KEY派生规则deriveKeyRef大写 非字母数字转__API_KEY后缀。三个不能默认的门槛字段手工声明的路由无法默认三件事endpoint、protocol、至少一个模型。它们成为创建按钮的 gate——失败时用户还看着该字段错误就能指名道姓。表单校验包括ROUTE_PATTERN /^[a-z][a-z0-9]*(?:-[a-z0-9])*$/——route id 既是 settings 键又是凭据名的词干而凭据引用是 POSIX shell 标识符不能以数字开头否则会在凭据缝合处抛出一段用户无从处理的裸正则错误路由不能与已声明路由重名taken检查 revision乐观并发控制卡片打开后其他标签页声明的路由会得到settings-conflict拒绝而非静默覆盖整个 profile模型行复用与编辑器卡片相同的逐行校验器validateDeepSeekModels坏行按位置指名key 空白的语义在此是该路由可能通过 provider 自身环境发现或 OAuth 认证对应keyBlankNew文案profile 只在确实要存 key 时才记录apiKeyEnv引用。写路径先 profile 后凭据createOnce()的写序是有意的settings.mutate写入 profile含apiKeyEnv引用仅当本卡要存 key置committed true此后 profile 字段锁定credentials.set写入 key。profile 落盘后若 key 写入失败重试路径直接回到凭据写入——不会因 revision 已被本次写入取代而再次得到settings-conflict。整卡同时持有busy与committed两个状态保证并发与重试的正确性。协议选择来自命名空间自己的 schema协议候选来自llm-pi-ai命名空间自身的 schema通过页面已获取的 settings descriptor 读取providers.*.api是适配器supportedProtocols()的 union见 config.ts 的api: z.union(supportedProtocols())与 provider.ts。protocolChoices()store.ts在 schema 的 union 节点上读出字符串列表没有新的 wire 字段客户端也没有硬编码常量页面提供的选项与适配器接受的选项同源不可能漂移协议顺序即 provider 表顺序、稳定不变第一个即默认值——openai-completions网关最常说的协议排在首位。声明路由declared route的额外字段目录报告为已声明declared的路由编辑器还会到达它为自己命名的两个字段display name 与协议。二者渲染在端点旁边的折叠区里协议同样来自 schema 读取清空 display name 取消设置回落值读取组合层composition layer——cordis.yml可能为目录未内置的路由钉住名字只有什么也没钉住时才回落为 route id协议没有可清除的回落值目录路由catalog route两者都没有名字默认取自目录条目且其每个模型各自携带协议路由级协议只会覆盖它们全部。因为一次 apply 可以重命名路由保存通知以刷新后目录报告的名字为准而非卡片打开时捕获的目标名。核心设计三Provider ID 为什么不可编辑CustomProviderCard上唯一固定不变的字段是Provider ID这不是因为缺少控件它是providers.route字典键——改它是一次移动而非编辑而编辑器正是通过settingsPath寻址的改名会让这个路径失效它被本命名空间之外引用agent-default-model存有provider字符串每个会话日志的request/header都记录着一个——重命名会让页面看不见的引用者悄悄失去意义它是派生凭据引用的词干页面只能写 key、永远读不回 key所以它无法把OLD_API_KEY移成NEW_API_KEY——重命名要么孤立已存 key要么让 profile 指向旧名下的引用。因此声明新路由 删除旧路由是诚实的替代方案而页面已经同时提供了这两半removeProviderProfile先删凭据再删 profile两步均可安全重试。备选方案与拒绝理由决策记录笔记的 Alternatives considered 记录了六个被拒方案理解它们能更好把握最终形态备选方案被拒原因用ProviderEditor加字段声明编辑器按settingsPath寻址被命名的路由还没有路径逐键重算路径会重挂卡片丢失草稿为协议列表加 wire 字段settings schema 已跨 wire 且已含 union第二份拷贝可能与第一份不一致允许编辑 Provider ID、页面执行移动凭据无法随行页面只持脱敏 descriptor从不持有值其他命名空间与会话日志中的引用没有改名路径所有 pi-ai 路由都提供协议 inherit 选项无消费者需求误选会悄悄重指整条路由的全部模型settings.yaml仍可表达有意的重指针对已存 profile 而非实时表单探测最需要探测的恰恰是什么都还没存的流程端点被编辑的表单会悄悄探测旧端点采纳的候选直接写入列表更少点击但 fetch 会覆盖用户修正过的容量且只披露 id 的列表会用无替换真实数字影响与代价收益网关、自托管服务器、比安装目录更新的模型无需离开浏览器即可配置端点自身能提供时供应模型 id用户从候选中选择而非手工抄写文档页面新增两个组件和一个共享列表编辑器编辑器卡片的 pi-ai 折叠区从两个字段扩展为一个列表声明路由还多了名字与协议。代价原文如实记录只有 pi-ai 路由可手工声明llm-pi-ai是唯一 profile 描述整个 provider的命名空间llm-deepseek路由仍然是组合层事实探测只覆盖 OpenAI 兼容端点说其他协议的网关会报告无法被询问模型需要手工输入页面在 fetch 期间于组件状态中持有一枚 key与credentials.set已有的暴露面相同且不超出卡片生命周期配置生效仍以settings.yaml为唯一事实源模型列表的新程度等于最近一次编辑。测试如何验证这套设计组件级provider-form.client.spec.tsxprovider-form.client.spec.tsx 通过脚本化的 wire 面驱动渲染页面覆盖行的添加、编辑、删除清空可选字段会离开 profile、非整数容量永不进入 profile探测携带编辑后的端点、未保存的 key 与 profile 的协议picker 的默认选择、切换、取消以及采纳保留已调优行空结果、被拒、传输拒绝三条路径创建写入一个 profile 加一条凭据创建按钮的每个 gate只读姿态。此外还有若干针对性断言protocolChoices对声明 union 的 schema与不声明 union 的 schema分别覆盖样式 gate 读取包自身源码任何select若不带.selectInput而只带.input即失败否则保留的 OS 箭头会贴死在select.input施加的 240px 上限内编辑器字段清单按路由种类断言——目录路由止步于 key 与端点声明路由还带协议协议编辑以单个api路径 op 行进、重命名以单个displayNameop 行进、清空名字是 unset 而非存储适配器拒绝的空串、声明 profile 未命名协议时不选第一个选项而是什么都不选。端到端models-settings.e2e.tsapps/web/tests/models-settings.e2e.ts 通过真实 wire 重新打开声明路由捕获卡片并断言选择的协议与新名字都到达settings.yaml行在重命名后重新注册。该 e2e 是零模型调用场景——配置是纯 settings/credentials/llm 域流量无 fixture且适配器注册表为空时任何越界流都会大声失败。测试选用minimax-cn作为被测 provider避免开发者真实的ANTHROPIC/OPENAI环境变量遮蔽派生引用。如何上手验证启动 Web 应用参见 docs/development.md 与 docs/user/guide 的运行说明打开 Models 设置页点击自定义添加custom add进入CustomProviderCard填写 Provider ID小写字母开头、-连接的合法路由键如acme-gateway、display name、base URL选择协议默认openai-completions输入 API key可留空以走 provider 原生认证点击Fetch models让端点自报模型 id在 picker 中勾选要采纳的候选或直接手工添加模型行并填写容量点击Create——profile 落入providers.routekey 落入派生引用ROUTE_API_KEY回到编辑器卡片可继续修正 display name 与协议保存通知会按刷新后的目录报告命名路由。配置的最终落点是$DSH_HOME/settings.yaml声明路由必须写明api、baseURL与非空models列表这正是设计上不能默认的三件事的持久化形态。总结从 Models 页面声明 Provider把一条原本只有 YAML 专家能走的路压缩进了浏览器的两张卡片里ModelListEditor以空列表即内置目录的语义安全地编辑模型数组并对表单当前状态发起端点探测、以候选 picker 形式交还用户CustomProviderCard以create 即独立卡片的姿态一次写入 profile 与凭据用三个必填门槛替代加载期校验让失败直接点名字段。协议的选项与适配器接受的协议同源于同一份 schema杜绝漂移Provider ID 因牵涉 settings 路径、外部引用与派生凭据而刻意不可变。结合provider-form.client.spec.tsx的组件级覆盖与models-settings.e2e.ts的端到端验证这一设计在可配置性、一致性与安全性之间给出了可复盘的取舍样本。【免费下载链接】deepseek-harnessDeepSeek Harness: Everything is a Plugin.项目地址: https://gitcode.com/gh_mirrors/de/deepseek-harness创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考