dsh-model-proxy:按模型名走出站代理,代理不可用时直连回退

前言

在 DeepSeek Harness(DSH)里,一个网关往往挂多种模型,背后可能是不同上游。同一 host、同一 API key、同一请求形态下,有的模型在本地区正常,有的会因区域限制、限速或只能经公司代理访问而失败——这与「隔壁模型」无关。

进程级环境变量代理按 host 路由:HTTP_PROXY 无法区分同一 host 上的不同模型。整网关走代理会把本来正常的模型一起拖进代理;完全不设代理则受限模型一直失败。下面介绍社区插件 dsh-model-proxy,它从每次请求体里读出 model 字段,只把你指定的模型名发往代理,其余保持原有出站路径。

这是什么

dsh-model-proxy(GitHub:windwhiterain/dsh-model-proxy,维护者 windwhiterain)是 DSH 的 bundle 插件,分类为模型推理。一句话:在 Harness 内做按模型名的出站代理路由,并在代理 TCP 不可达时可选地回退到插件加载前的直连路径。

插件以当前 dsh 进程权限运行;安装前应自行查看仓库源码与 MIT 许可证。社区目录页见 SkillHub 插件页,与 DeepSeek / 幻方无官方从属关系。

路由方式

DSH、pi-ai 与各类 provider SDK 通过 globalThis.fetch 发请求,最终落到 undici 的全局 dispatcher 槽位(Symbol.for('undici.globalDispatcher.1'))。本插件占用该槽位,按顺序决策:

  1. Origin:目标 host 不在 origins 内,或请求无 body,则保持插件加载前的路由。
  2. 代理可达性:在开启 fallbackToDirect 且代理 host:port TCP 拒绝连接时,候选请求仍走上述「先前路由」,避免代理停掉后拖垮全进程调用。
  3. 模型名:读取 body 头部 JSON 的 "model" 字段;若匹配 models 中的模式,则经 proxy 出站。

为解析模型名,插件只缓冲 body 的前若干 chunk,并在解析前 yield 已读 chunk,不改变 chunk 边界、背压与流式响应行为。配置里的 direct 指进程在装插件前已有的路由;若启动环境未设代理,则未匹配请求与未装插件时行为一致。

核心配置项

bundle 自带的 cordis.patch.yml 会插入 id: model-proxy 行;也可在 <DSH_HOME>/profiles/<profile>/cordis.patch.yml 热重载编辑,或通过 settings 层覆盖同 id。无效配置会导致激活失败,而不是静默走错路由。

- id: model-proxy
  config:
    enabled: true
    proxy: http://127.0.0.1:10793
    origins:
      - opencode.ai
    models:
      - grok-*
      - gpt-*-luna
    fallbackToDirect: true
    probeTimeoutMs: 300
    probeCacheMs: 2000
    peekBytes: 16384
字段 含义
enabled 为 false 时不安装拦截,所有请求保持原路由
proxy 匹配模型使用的代理端点,支持 http: / https: 及可选凭据
origins 仅对这些 host 上的请求做模型匹配
models 模型名模式,* 匹配任意字符段
fallbackToDirect 代理不可连时,匹配请求改走先前路由
probeTimeoutMs 可达性探测 TCP 连接超时
probeCacheMs 探测结果缓存时长,突发请求只探一次
peekBytes 为找 model 字段最多窥视的 body 字节数

安装与启用

在目标 profile 的 package.json(路径:<DSH_HOME>/profiles/<profile>/package.json)注册依赖与 bundle:

{
  "dependencies": {
    "dsh-model-proxy": "github:windwhiterain/dsh-model-proxy"
  },
  "dsh": {
    "profile": {
      "bundles": ["@deepseek-ai/dsh-base", "dsh-model-proxy"]
    }
  }
}

本地开发可用 link:C:/path/to/dsh-model-proxy 等形式。随后安装该 profile 并重启 dsh 一次;bundle 的 cordis.patch.yml 会写入默认 model-proxy 配置。

不要同时为 dsh 进程设置 HTTP_PROXY / HTTPS_PROXY。启动环境中的代理会让 dsh 安装进程级 dispatcher,本插件会把该路径当作「非代理模型」的保留路由,与「仅部分模型走代理」的目标相反。启动环境保持直连,由本插件单独处理需要代理的流量。

典型用法

同一网关内,部分模型需翻墙或区域代理

README 示例:OpenCode Go 在同一网关提供 grok-* 与 gpt-*-luna,在受限地区上游返回 unsupported_country_region_territory,而 GLM、Kimi、DeepSeek 等同 key 同 region 正常。用上文 YAML 只把两类受限模型经支持地区的代理发出,其余直连;关闭代理后,在 fallbackToDirect: true 下直连模型不受影响。

直接调该网关时,请求仍需带 x-opencode-session 头(否则 400 MissingSessionID),与是否启用本插件无关。

单一 provider 走公司代理

- id: model-proxy
  config:
    proxy: http://proxy.corp.example:3128
    origins: [api.example.com]
    models: ['*']

整段网关经不同出口,进程其他 host 不变

与上例相同,将 models 设为 ['*'];路由仍按请求决策,之后在别处新增的 host 不会被这一条连带代理。

诊断与自测

决策会写入宿主 logger,例如:

dsh-model-proxy: models grok-*, gpt-*-luna on opencode.ai via http://127.0.0.1:10793, direct while it is down
dsh-model-proxy: route=proxy via=http://127.0.0.1:10793 reason=model:grok-4.7
dsh-model-proxy: route=direct via=direct reason=model-other:glm-5.3-flash
dsh-model-proxy: route=direct via=direct reason=proxy-down
dsh-model-proxy: route=direct via=direct reason=origin

卸载时会恢复被替换的 dispatcher,便于后续插件占用同一槽位。

仓库提供 probe 脚本(需配置可读凭据):

node probe/smoke.mjs              # 代理可达
node probe/smoke.mjs --proxy-down # 回退路径

适用场景与注意

适合谁:在同一 DSH 网关混用多模型、且只有部分模型需要独立出站代理的智能体或网关维护者;需要代理宕机时其余模型仍可用的部署。

进程内生效:路由逻辑只在 dsh 进程内生效;shell 工具拉起的 curl、git 等仍遵循启动环境,本插件刻意不修改后者。

模型名来源:仅从 body 读取 model;仅写在 header、或 model 出现在 peekBytes 之后的请求不会匹配,保持直连。

代理探测范围:探测回答「端口是否在监听」,不验证上游是否可用;代理能连但上游失败时,该请求仍会失败。

单条配置一个 proxy:不同模型族要走不同代理,需要多条 model-proxy 行(README 说明当前实现对此有约束,截断处未展开完整表述)。

依赖版本:插件将 undici 固定为 ~8.10.0;8.11.0 在 Node 24 下接管全局 dispatcher 时可能破坏 content-encoding 解码。与 @deepseek-ai/dsh-http-proxy 的 undici 约束同类,锁文件升级需谨慎并重跑 probe 断言。

兼容性:依赖 Node 自带 fetch 所暴露的全局 dispatcher 槽位;除「带 fetch 的 Node」外 README 未假定更细版本。

结尾

dsh-model-proxy 解决的是「同一 API host、不同模型不同可达性」下的出站问题:用模型名模式选择代理,可选在代理不可连时回退,并避免整进程 HTTP_PROXY 的一刀切副作用。目录与源码:

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

Xiaoye