news 2026/9/19 20:22:44

DeepSeek Harness Models 页面声明 Provider:从浏览器一步接入 OpenAI 兼容网关的架构实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DeepSeek Harness Models 页面声明 Provider:从浏览器一步接入 OpenAI 兼容网关的架构实现

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-modelspackages/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显示256KmaxTokens显示32Kllm-pi-aidefaultContextWindow/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-completionsopenai-responses——它们共享 OpenAIGET /models形状 + Bearer 认证。Azure 虽属 OpenAI 血统但被排除(需要api-key头与api-version查询参数),Codex 走 OAuth,其余协议回答DISCOVERY_UNSUPPORTED,界面回落到手工输入而非把猜错的响应形状报告成"空 provider";
  • base 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 不内置的 provider

CustomProviderCard(源码)声明 pi-ai 未随包提供的路由。它必须是独立卡片,因为route id 在这里被选定

  • 一次settings.mutate把整个 profile 写到providers.<route>
  • key 通过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()的写序是有意的:

  1. settings.mutate写入 profile(含apiKeyEnv引用,仅当本卡要存 key);
  2. committed = true,此后 profile 字段锁定;
  3. credentials.set写入 key。

profile 落盘后若 key 写入失败,重试路径直接回到凭据写入——不会因 revision 已被本次写入取代而再次得到settings-conflict。整卡同时持有busycommitted两个状态,保证并发与重试的正确性。

协议选择来自命名空间自己的 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,这不是因为缺少控件:

  1. 它是providers.<route>字典键——改它是一次"移动"而非"编辑",而编辑器正是通过settingsPath寻址的,改名会让这个路径失效;
  2. 它被本命名空间之外引用agent-default-model存有provider字符串,每个会话日志的request/header都记录着一个——重命名会让页面看不见的引用者悄悄失去意义;
  3. 它是派生凭据引用的词干:页面只能写 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.tsx

provider-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.ts

apps/web/tests/models-settings.e2e.ts 通过真实 wire 重新打开声明路由,捕获卡片,并断言:

  • 选择的协议与新名字都到达settings.yaml
  • 行在重命名后重新注册

该 e2e 是"零模型调用"场景——配置是纯 settings/credentials/llm 域流量,无 fixture,且适配器注册表为空时任何越界流都会大声失败。测试选用minimax-cn作为被测 provider,避免开发者真实的ANTHROPIC/OPENAI环境变量遮蔽派生引用。

如何上手验证

  1. 启动 Web 应用(参见 docs/development.md 与 docs/user/guide 的运行说明);
  2. 打开 Models 设置页,点击自定义添加(custom add)进入CustomProviderCard
  3. 填写 Provider ID(小写字母开头、-连接的合法路由键,如acme-gateway)、display name、base URL,选择协议(默认openai-completions),输入 API key(可留空以走 provider 原生认证);
  4. 点击Fetch models让端点自报模型 id,在 picker 中勾选要采纳的候选,或直接手工添加模型行并填写容量;
  5. 点击Create——profile 落入providers.<route>,key 落入派生引用<ROUTE>_API_KEY
  6. 回到编辑器卡片,可继续修正 display name 与协议;保存通知会按刷新后的目录报告命名路由。

配置的最终落点是$DSH_HOME/settings.yaml:声明路由必须写明apibaseURL与非空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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/19 20:21:44

73 份 DESIGN.md 设计系统:让 AI 代理还原品牌界面的完整指南

73 份 DESIGN.md 设计系统&#xff1a;让 AI 代理还原品牌界面的完整指南 【免费下载链接】awesome-design-md A collection of DESIGN.md files analysis by popular brand design systems. Drop one into your project and let coding agents generate a matching UI. 项目…

作者头像 李华
网站建设 2026/9/19 20:20:43

B/S架构宾馆系统测试实战:分层策略与状态机验证

简介&#xff1a;本资源是一份面向软件工程专业学生与测试初学者的宾馆管理系统软件测试实践报告&#xff0c;聚焦互联网环境下酒店管理类应用的质量保障全流程。报告完整覆盖测试计划制定、单元/集成/系统三阶段测试设计、JMeter与Selenium等工具实操、需求可追溯性分析及实验…

作者头像 李华
网站建设 2026/9/19 20:20:12

Unity实时口型同步插件 AudioToFace:音素分类驱动虚拟角色开口说话

做数字人和虚拟主播的&#xff0c;应该都体会过这种痛苦&#xff1a;模型捏得再好看&#xff0c;一开口说话&#xff0c;嘴型和音频对不上&#xff0c;整个角色就像在念经&#xff0c;瞬间掉价。口型同步这件事&#xff0c;看着只是一个小环节&#xff0c;实际做起来坑特别多。…

作者头像 李华
网站建设 2026/9/19 20:17:02

Vue3源码中的位运算:如何用二进制构建高效虚拟DOM

读 Vue3 源码读到一半&#xff0c;很多人会被一个“老古董”知识点勾住&#xff1a;位运算。Vue 3 的模板编译、运行时 diff、响应式副作用管理&#xff0c;四处都藏着二进制的影子。比起用字符串、数组、布尔字段去表达状态&#xff0c;Vue3 更习惯用几个数字把状态压在一个整…

作者头像 李华