为 DeepSeek Harness 提供免费额度多服务商路由。
30.2/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/25 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/lion231226/dsh-freellmapi.git状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:待复验
是什么
生态应用
桌面端 / Web 外壳,不以 dsh plugin add 安装
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 11 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件
查看扫描证据(共 3 处,展示前 3 条)
- 读取本地文件lib/index.js:623
const bytes = await readFile(hostPath) - 写入本地文件lib/routes.js:211
try { mkdirSync(PERSIST_DIR, { recursive: true }) } catch { } - 写入本地文件lib/routes.js:212
try { writer = createWriteStream(file, { flags: 'a', encoding: 'utf8' }) } catch { return }
扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery@deepseek-ai/cordis给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
DeepSeek Harness 的 FreeLLMAPI(自动)服务商路由:选自动模型,本地 FreeLLMAPI 网关挑最合适的免费模型,自动故障转移,再以原生 DSH 分块流式返回。
README
由 DeepSeek 最新模型翻译生成dsh-freellmapi
为 DeepSeek Harness 提供免费额度多服务商路由。
十八个服务商条目藏在一条 DSH 服务商路由后面——其中五个默认启用,这五个全都带免费额度——外加按请求运行的复杂度分类器、能力与速度排序,以及自动 429/5xx 故障转移。
install the plugin → start DSH → pick provider "freellmapi" + model "auto" → it answers
不用起外部进程。不用跑网关。第一次拿回答不需要 API key——内置池里有免密钥服务商,维护者用真实、免凭据的请求验证过。
非官方插件——请先读
这是社区项目。它不隶属于它对接的任何上游服务商,也未获其认可、赞助或支持,与 DeepSeek 同样无关。它不是 DSH 官方插件。
服务商名称与「DeepSeek Harness」/ @deepseek-ai/ 归各自所有者。
以 MIT License 授权。你有责任遵守各服务商的服务条款,也要对发往免费额度的内容负责——对方可能记录并用于训练。
展开完整文档(README 剩余 1 节)
dsh-freellmapi
为 DeepSeek Harness 提供免费额度多服务商路由。
十八个服务商条目藏在一条 DSH 服务商路由后面——其中五个默认启用,这五个全都带免费额度——外加按请求运行的复杂度分类器、能力与速度排序,以及自动 429/5xx 故障转移。
install the plugin → start DSH → pick provider "freellmapi" + model "auto" → it answers
不用起外部进程。不用跑网关。第一次拿回答不需要 API key——内置池里有免密钥服务商,维护者用真实、免凭据的请求验证过。
非官方插件——请先读
这是社区项目。它不隶属于它对接的任何上游服务商,也未获其认可、赞助或支持,与 DeepSeek 同样无关。它不是 DSH 官方插件。
服务商名称与「DeepSeek Harness」/ @deepseek-ai/ 归各自所有者。
以 MIT License 授权。你有责任遵守各服务商的服务条款,也要对发往免费额度的内容负责——对方可能记录并用于训练。
目录
- 快速开始
- 你能得到什么
- 零配置:完全不用 key 也能跑
- 添加 key
- 怎么读模型列表
- 固定模型是严格的
- 它怎么挑模型
- 长时间运行的智能体会话:选 auto 之前先读这段
- 故障转移与健康状态
- 两种模式:direct 与 gateway
- 配置
- 怎么看它做了什么
- 只有一个 key?没有 key?
- 架构
- 故障排查
- 限制与未经验证的说法
- 兼容性
- 开发
- 上游与致谢
- 许可证
快速开始
1. 安装——打开 DSH 网页 GUI,进入侧边栏的 Plugins 管理器,安装 bundle,粘贴下面任一 spec:
github:lion231226/dsh-freellmapi # from GitHub (no npm publish needed)
github:lion231226/dsh-freellmapi#v0.7.18 # pinned to a tag
dsh-freellmapi # from npm, once published
命令行等价写法与手动安装路径见 安装细节。
2. (可选)粘贴几个 key——见 添加 key。跳过这步也是受支持的配置:两个免密钥服务商照样能回答。
3. 选模型——在 DSH 模型选择器里选服务商 FreeLLMAPI (Auto),模型选 auto。
4. 看它跑起来 —— 打开 logRouting(默认开启)后,每个请求都会打一行日志:
dsh-freellmapi: tier=complex -> openrouter/nvidia/nemotron-3-ultra-550b-a55b:free (capability=5, speed=3, 2 candidates tried)
配置到这里就完了。下面全是细节。
安装细节
| 方式 | 怎么做 |
| 插件管理器(推荐) | 侧边栏 → Plugins → 安装 → 粘贴 github:lion231226/dsh-freellmapi(发布后也可用 npm 包名),确认,DSH 要求重启就重启。管理器接受 registry 名称、绝对路径、git 地址和 tarball URL;只要某个 spec 的 manifest 没声明 dsh.bundle,管理器就拒绝安装 —— 本包声明了。 |
| dsh plugin 命令行 | dsh plugin --profile desktop add github:lion231226/dsh-freellmapi —— 这些子命令来自 DSH 自己的运行时错误文本;⚠️ 这里没有做端到端验证,请对照你用的 DSH 版本确认。 |
| 手动放置 | 把包复制到 $DSH_HOME/profiles/<profile>/node_modules/dsh-freellmapi/,在配置档的 package.json 里把它同时加进 dependencies 和 dsh.profile.bundles,然后运行 dsh plugin --profile <profile> install 并重启。只有列进 bundles,DSH 才会加载它。 |
本包自带 cordis.patch.yml(- insert: [{ id: freellmapi, name: 'dsh-freellmapi' }]),所以安装正确的 bundle 不需要手写补丁行。千万别在配置档自己的 cordis.patch.yml 里再加一行相同 id 的条目:同一个 id 出现两次,bundle 组合会中止,报 duplicate loader entry id "freellmapi"。
你能得到什么
| 自带路由 | 路由智能归插件自己管。direct 模式(默认)不需要网关、不需要守护进程、不需要 Docker —— 只要 DSH 本身。 |
| 内置 17 个服务商 / 46 个模型 | Groq、Cerebras、OpenRouter、Mistral、Z.ai(智谱)、火山引擎方舟、LongCat、LLM7、Kilo、NVIDIA NIM 和 OVH AI Endpoints,再加上 0.7.0 新增的六个 —— ModelScope(魔搭)、讯飞星火、硅基流动、SambaNova、HuggingFace Router 和 Routeway —— 每个模型都带能力、速度、上下文窗口、视觉和工具调用信息。十七个里只有五个开箱即用。 |
| 按请求做复杂度路由 | 每个请求都会打分,分成 simple / medium / complex;档位决定排序时看能力还是看速度。一句「列一下这里的文件」和一项用 12 个工具的重构任务,不会发给同一个模型。 |
| 自动故障转移 | 候选模型被限流(429)、出错(5xx)、超时或 model_unavailable,就先搁置,换排名下一个 —— 只在第一个 chunk 发出之前,所以一个回答绝不会由两个模型拼接而成。 |
| 健康跟踪与冷却 | 第一次可重试的失败就会让候选进入冷却(2 分钟),之后每连续失败一次就升一档:2 分钟 → 10 分钟 → 1 小时 → 24 小时。服务商返回的 Retry-After 始终优先;凭据被拒(401/403)会让该服务商冷却 24 小时。评分惩罚按 60 秒半衰期衰减,所以单次 429 不会永远压着它。 |
| 零配置启动 | Kilo 和 LLM7 不需要任何凭据就能应答;全新安装在你配置任何东西之前,候选池就已经非空。 |
| 诚实的诊断信息 | 每个请求一行日志,写明档位、胜出者和尝试了多少个候选;每次失败都是带稳定错误码的带内 finish 块,绝不抛异常。 |
| 不做静默替换 | 选定具体模型 id 就是严格锁定:如果它没有凭据、不存在或正在冷却,你会收到明确的错误和操作指引,而不是别人的答案。模型列表只显示真正能服务的模型,另外每个未配置密钥的服务商各有一条 setup:<provider> 指引。 |
| gateway 模式原样保留 | v0.1.0 的行为——转发到外部 freellmapi 网关——仍然在 mode: gateway 后面,未作改动,留给想要网关自带 bandit 评分、缓存和 fusion 的人。 |
零配置:完全不用密钥也能跑什么
候选池把三个服务商标为免密钥(完全不发 Authorization 头)。维护者在 2026-10-13 用真实请求验证过(_lead/live-probe-2026-10-13.md),发送时不带任何凭据:
| 服务商 | 模型 | 结果 |
| kilo (https://api.kilo.ai/api/gateway/v1) | nvidia/nemotron-3-super-120b-a12b:free | HTTP 200,真实补全 |
| kilo | nvidia/nemotron-3-ultra-550b-a55b:free | 免费路由,550B 级别 |
| ovh (https://oai.endpoints.kepler.ai.cloud.ovh.net/v1) | gpt-oss-120b | HTTP 200 |
| llm7 (https://api.llm7.io/v1) | DeepSeek-V4-Flash-0731 | HTTP 200(匿名) |
| llm7 | codestral-latest | HTTP 200(匿名) |
kilo 和 llm7 默认启用,所以「装完就能用」的承诺由这两个扛。ovh 也免密钥,但默认禁用(每分钟 2 个请求——太紧,不适合放进默认候选集);想要就显式启用。
依赖免密钥候选池之前,有三件事你得知道:
- 免密钥不等于无限量。 Kilo 文档写的是每 IP 约 200 请求/小时,LLM7 约 100/小时,OVH 是 2 RPM。本文档里的配额数字来自上游注册表基线(2026-04),可能已经过时;插件是照着这些数字做反应,而不是信任它们——遇到 429 就让该候选冷却,然后继续下一个。
- 免费流量可能被记录并用于训练。 Kilo 在自己的服务商注册表里写明了这一点(免费提示词和补全会被记录用于训练)。如果你不能接受,就禁用它:
providers:
kilo:
yaml
enabled: false
关掉 kilo 后,零配置仍能靠 llm7 单独跑通。
- 免费模型目录会漂移。 2026-10-13 那次探测中,llm7 对 gpt-oss-20b 和 GLM-4.6V-Flash 返回了 400 model_unavailable —— 前一天还能用的同一类 id。路由器把 model_unavailable 当作单个模型的
状况:它搁置这个 (provider, model) 组合,改试下一个候选,而不是让你的请求失败。看到这类报错属于
正常运转,不是 bug。
添加密钥
密钥是提升质量最有效的一招:默认候选池是 groq、cerebras、openrouter、kilo、llm7,加上前三个就能用上又强又快的模型。
要设置密钥,导出 apiKeyEnv 指定的环境变量 —— 或者通过 DSH 的凭据服务保存,解析时最先查的就是它。
解析顺序为:凭据服务 → config.providers[id].apiKey → apiKeyEnv 指定的环境变量 → 约定名 <PROVIDER_ID>_API_KEY。空值视为「未配置」。
在 GUI 里改哪里。 Settings → Models 把本插件的服务商路由(FreeLLMAPI)列成一张普通卡片,密钥行就渲染在这张卡片里 —— 每个已启用但没有可用凭据的上游各一行,每行带一个链接指向它的密钥页、一个将用来保存密钥的环境变量名、一个密码框和 Save。保存会写入 DSH 的凭据服务,读取时不走缓存,所以密钥在下一个请求就生效,无需重启。
关于宿主自带的编辑器,有两点说明,免得把它的行为当成 bug:
- 卡片旁的编辑按钮打开的是 DSH 的服务商编辑器,它只能为自己的两个服务商家族(llm-deepseek、 llm-pi-ai)渲染字段;遇到其他命名空间,它会显示「other fields live in cordis.patch.yml」并禁用 Save。这是宿主当前的限制,上面的密钥行就是应对办法。
- 根级 apiKeyEnv(FREELLMAPI_API_KEY)未设置时,卡片会显示一个缺少凭据的小圆点。直连模式不需要这个密钥 —— 圆点指的是可选的网关凭据,可以忽略。
完整字段集(包括按上游划分的 providers 映射和路由开关)在插件自己的设置页:Settings → Plugins → dsh-freellmapi,由 schema 生成;也可以在 cordis.patch.yml 里编辑。
卡片为候选池里默认启用的服务商(groq、cerebras、openrouter、llm7、kilo)提供密钥输入框。
浏览器里的表格在构建时按这些默认值生成,所以你自己通过 providers.<id>.enabled 启用的服务商不会多出一行密钥 —— 这种情况请设置它的 apiKeyEnv,直接保存凭据。0.7.0 新增的六个服务商都适用这一点,它们故意默认关闭:启用其中一个不会让密钥行出现在卡片(这是 0.6.1 更新日志里记录的已知缺口,不是 bug)。
哪些服务商真的能连通——以及怎么问
免费额度访问对网络很敏感,所以这一节讲的是怎么测,而不是让你信什么。
这里原先有一张表,声称 groq 和 cerebras 被地区封锁,「加个 key 也没用」。那张表是错的,错的原因值得记下来:探测时没发 Authorization 头。2026-10-15 实测:
| 服务商 | 不带凭证 | 语法合法但伪造的 key |
| groq | HTTP 403 | HTTP 401 → 能连通,只是要真 key |
| cerebras | HTTP 403 | HTTP 401 → 能连通 |
| ovh(免 key) | HTTP 200 | HTTP 403 → 给免 key 的服务商发 key 反而会坏* |
所以 403 从来不能自己说明问题。 它按你发了什么,可能是三种完全不同的意思:匿名请求被拒、凭证被拒、地区被拒。只报其中一个、不报另一个,就是那张错表写出来的原因。
按服务商类型正确探测:
- 免 key 的服务商(llm7、kilo、ovh)——不要发 Authorization 头;200 就是健康。
- 需要 key 的服务商——发一个语法合法但伪造的 Authorization: Bearer … 头; 401 是健康(端点存在,只是礼貌地拒绝了),200 也行。
- 403 当作有歧义处理:得换另一种方法再探一次,它才有意义。
- 404/400/422 通常说明路径不一样,不是主机挂了。
请求体也是方法的一部分。 拿一个不存在的 model id 去探聊天端点,会得出相反的结论:modelscope 回 400 Invalid model id,sambanova 回 404 model_not_found,而这两者都没法跟「这个服务商的聊天不需要凭证」区分开——请求根本没走到凭证检查那一步。用目录里的真实 id 重放同一个请求,两家都回了 401。同样的 URL、同样的头、不同的请求体,结论相反。
gate 把头的拆分自动化了:node tools/gate.mjs --online → G7 用适合每个服务商的方法去探池子里所有服务商,并在每个结果旁边打印所用的方法,这样结论可以审计,而不是只能信。G7 只问 GET {baseURL}/models,所以它的绿灯要读成「主机有应答」,不是「你加上 key 之后这个服务商就会给你出补全」。
G10 回答 G7 回答不了的问题。 主机有应答,说明不了我们发布的那些具体 model id 还在不在服务——这个缺口让同一个缺陷上线了三次:四个 OpenRouter id 和两个 Groq id 已被服务商下线,每次都是人发现的,不是 gate 发现的。G10 把每个声明的 id 跟目录实际列出的内容比对。对默认启用的服务商,死 id 会阻断;对默认禁用的,报为潜在问题,一旦启用该服务商就立刻阻断。目录需要凭据的探测记为 not checked —— 401 不能证明某个 id 缺失。
按这种方式在 2026-10-15 测得:
- 0.7 之前的 12 个池服务商全部有响应(G7,2026-10-15)—— 包括旧表里标为 blocked 的那两个。
- 0.7.0 新增的六个,分别用匿名请求和伪造请求头探测:modelscope、xfyun、 siliconflow、sambanova 和 huggingface 均可访问。routeway 的匿名 chat 路径是唯一一个开放的 AMBIGUOUS:不带凭据时,它的 POST /chat/completions 会拿到 Cloudflare 拦截页(403,HTML,"Attention Required");而同一个请求带上伪造的 Authorization 头就能到达源站,拿到纯 JSON 的 401 Invalid API key。单次响应无法区分匿名被拒 和 地区被拒;用真实 key,或者换第二个出口 IP,才能定论。本 README 不依赖该路径是否开放。
每次探测走哪条出口,也是结果的一部分。 探测主机运行分流模式 VPN:发往中国大陆目的地的流量走直连,其余全部走代理,代理出口在美国洛杉矶。洛杉矶出口是海外路径的位置 —— 它不是主机的位置,本 README 也不作此声称。 所以上面的结果按路径分开,并且把分法写出来,而不是留给读者推断:
- 直连(中国大陆境内): modelscope、xfyun 和 siliconflow —— 0.7.0 新增里三个中国大陆主机。每个都走直连路径应答了探测(xfyun 和 siliconflow 对伪造凭据探测返回 401; modelscope 也访问到了,见下面的文档站说明)。
- 走代理(海外出口,美国洛杉矶): 另外三个新增服务商,以及全部 12 个 0.7 之前的服务商。
routeway 的 Cloudflare 行为是在这条路径上测的,所以它的 403 仍然无法与地区拒绝区分开 —— 见上面的 AMBIGUOUS 说明。
- 应用层结论(401、RFC 6750 凭据质询、源站错误 JSON)完全不依赖出口;"从你的网络可达" 则依赖出口,所以按服务商记录路径。
工具的限制不是站点的属性。 本节早先的草稿从一次失败的 web_fetch 推断 —— 抓取某中国厂商文档站时报 "hostname resolves to a non-public IP address" —— 得出 "中国厂商和文档站从这台机器不可达。"
这个推断是错的。 这条消息来自本项目自己的 DNS 防护:分流 VPN 用伪 IP 应答 DNS,地址取自保留的基准测试网段 198.18.0.0/15,而 web_fetch 拒绝抓取任何解析到该网段的主机名。
它在同一个会话里用同一句话拒绝了 www.xfyun.cn 和 github.com,这恰好说明它是防护而不是测量。改用普通 HTTP 客户端走直连路径重新测量(2026-10-15):
| 主机 | 直连路径的结果 |
| www.xfyun.cn(厂商文档页) | HTTP 200,128 382 B,618 ms,Server: Tengine |
| modelscope.cn | HTTP 200,4 639 B,290 ms |
| spark-api-open.xf-yun.com(POST /v1/chat/completions) | HTTP 401,{"message":"HMAC signature cannot be verified: apikey not found"} |
| siliconflow 的 api.siliconflow.com(GET /v1/models) | HTTP 401,{"code":30014,…"message":"Token is invalid."} |
所以中文文档站点从这台机器一直都能访问,只是 web_fetch 读不到。任何关于「某国站点无法访问」的结论,都需要可达性测量(用 HTTP 客户端直接和主机通信),绝不能拿抓取工具报错当依据:后者反映的是工具的策略,前者反映的才是网络。
如果某个服务商确实从你的网络访问不到,关掉它就是全部改动,位置在 $DSH_HOME/profiles/<profile>/cordis.patch.yml(或插件自己的 providers 字段): yaml
- id: freellmapi
config:
providers:
someprovider: { enabled: false }
关键申请页(前七个已在 2026-10-14 验证可达): groq <https://console.groq.com/keys>、cerebras <https://cloud.cerebras.ai/>、 openrouter <https://openrouter.ai/keys>、mistral <https://console.mistral.ai/api-keys>、 zhipu <https://open.bigmodel.cn/usercenter/apikeys>、volcengine <https://console.volcengine.com/ark>、 longcat <https://longcat.chat/platform/api_keys>。
下面 0.7.0 新增的六个服务商,其申请页取自池子自己的 keyURL 值,没有独立验证——只有 modelscope 的带上游引用。(早先一版草稿用「中文厂商/文档站点从这台机器解析到非公网地址」来解释这个缺口。该说法撤回:那是 web_fetch 的 DNS 防护造成的假象,厂商文档页实际上就是走直连路径抓到的——见上面的可达性表。申请页本身仍未验证,错的只是先前给出的理由。)
| 服务商 | apiKeyEnv | 申请页 | 免费额度(上游基线) | 默认 | 备注 |
| groq | GROQ_API_KEY | <https://console.groq.com/keys> | 30 RPM / 1000 RPD / ~8K TPM / 200K TPD | ✅ 开启 | 速度快;openai/gpt-oss-120b、qwen/qwen3.8-27b、openai/gpt-oss-20b(_id 于 2026-10-15 重新测量:有两个旧 id 已被上游下线_) |
| cerebras | CEREBRAS_API_KEY | <https://cloud.cerebras.ai/> | 30 RPM / 无 RPD / 60K TPM / 1M TPD | ✅ 开启 | 有两个模型能力评 5 且速度评 5;上游提到还有第二版更紧的配额(5 RPM / 30K TPM) |
| openrouter | OPENROUTER_API_KEY | <https://openrouter.ai/keys> | 20 RPM / 200 RPD | ✅ 开启 | complex 档的主要供应方(nvidia/nemotron-3-ultra-550b-a55b:free、nemotron-3-super-120b-a12b:free、cohere/north-mini-code:free) |
| llm7 | LLM7_API_KEY | <https://token.llm7.io/> | 约 100 请求/小时 | ✅ 开启 | 无 key 也能用;有 key 能提高上限。key 页面是转述的,没有独立核实 |
| kilo | KILO_API_KEY | — | 每 IP 约 200 请求/小时 | ✅ 开启 | 无 key 也能用。 免费流量会被记录用于训练 |
| mistral | MISTRAL_API_KEY | <https://console.mistral.ai/api-keys> | 500K TPM | ⬜ 关闭 | mistral-large-latest、magistral-medium-latest、codestral-latest |
| zhipu | ZHIPU_API_KEY | — | 约 1M TPD | ⬜ 关闭 · ⚠️ 推测 | glm-4.7-flash、glm-4.6v-flash。baseURL 是整个池子里风险最高的一行:国内站和海外站不共用 key,选错主机名必然 401。只有确定自己的 key 属于哪个站,才开启它。 |
| volcengine | ARK_API_KEY | — | 约 2M TPD | ⬜ 关闭 · ⚠️ 推测 | 国内服务商,需要实名认证;模型 id 是推断的,没有核实 |
| longcat | LONGCAT_API_KEY | — | — | ⬜ 关闭 · ⚠️ 推测 | 不要求实名;模型 id 是推断的,没有核实 |
| nvidia | NVIDIA_API_KEY | — | 40 RPM | ⬜ 关闭 | 能力上限高(qwen/qwen3-coder-480b-a35b-instruct),但试用条款写着「仅限评估」,首个 token 可能要等几分钟,另外还有 限制 里提到的已知缺口 |
| ovh | OVH_AI_ENDPOINTS_KEY | — | 2 RPM | ⬜ 关闭 | 无 key 也能用,但只有 2 RPM |
| modelscope | MODELSCOPE_API_KEY | <https://modelscope.cn/my/myaccesstoken> | 上游写的是 2 000 请求/天,随后又标注该数字已成历史(计费改为魔粒) | ⬜ 关闭 | 阿里 ModelScope(魔搭)。token 必须先绑定到阿里云国内站账号并完成中国实名认证,才能跑流量 —— 未绑定的 token 每次调用都返回 401 please bind your alibaba cloud account before use。它的 GET /models 对任何凭证都返回 200,所以没法用它校验 key;改用 1 个 token 的对话来校验 |
| xfyun | XFYUN_API_KEY | <https://console.xfyun.cn/> | 没有公布 token/QPS 上限 —— 上游直接设为 null,不去猜 | ⬜ 关闭 · ⚠️ 推测 | 讯飞星火。需要中国实名认证。 鉴权方式是把控制台的 APIPassword 当作 bearer token。它的模型 id lite 现在有厂商文档背书,不再是占位符:官方 HTTP API 页面(spark-api-open.xf-yun.com/v1,2026-10-15 经直连路径抓取)把 4.0Ultra / generalv3.5 / max-32k / generalv3 / pro-128k / lite 列为可接受的 model 值,其版本表把 Lite 描述为轻量级、支持免费使用。这里仍然没有用有效凭证实际跑过;confidence 保持 speculative |
| siliconflow | SILICONFLOW_API_KEY | <https://cloud.siliconflow.com/account/ak> | 没有可循环使用的免费对话额度 —— .com 端点只提供一次性 $1 试用额度 | ⬜ 关闭 · ⚠️ 推测 | SiliconFlow(硅基流动)。需要完成中国实名认证。 上游保留该平台,是为了它的免费生成式媒体模型(图像/TTS),而本插件的 LLM 接缝无法消费这些模型;它曾列出的对话模型是付费的($0.06/M),上游已删除该行 |
| sambanova | SAMBANOVA_API_KEY | <https://cloud.sambanova.ai/apis> | 没有 —— 免费额度已永久下线。 即使密钥有效,对话也会返回 402 payment method required | ⬜ 关闭 · ⚠️ 推测 | 仅为存档而列出,不是免费额度。 上游正是因为这个原因在 V23 中删掉了它。它的 baseURL 不是从上游转录的(上游零匹配),而是取自厂商文档;它的 /models 仍返回 200 —— 上游自己把这种情况记为计费闸门的误报 |
| huggingface | HUGGINGFACE_API_KEY | <https://huggingface.co/settings/tokens> | 可循环的 $0.10/月 router 额度,无需绑卡(上游:70B 模型上约 1–3M token/月) | ⬜ 关闭 | Inference Providers router。那里的「免费」是账户额度,不是目录标记:其目录的 296 条服务商记录中,0 条报告 is_free: true。同一个 id 在不同后端上 contextWindow 不同 |
| routeway | ROUTEWAY_API_KEY | <https://routeway.ai/> | 文档说 20 RPM / 200 RPD;上游实测看到更严格的 5 RPM —— 上游自相矛盾,所以所有限制都记为 null | ⬜ 关闭 · ⚠️ 推测 | 带 :free 路由的聚合器。它的匿名对话路径是一个开放的 AMBIGUOUS(见上文);解锁它靠的不是伪装 User-Agent —— 而是 Authorization 头 |
把这六行读作「已收录」,而不是「免费且可用」。 六者全部以禁用状态发布(enabled: false),每一个都需要你自己提供密钥,而且没有一个属于「装好就能用」的默认集合 ——那个集合是 groq、cerebras、openrouter、llm7、kilo,在 0.7.0 中未变。
免费额度这一列的实际含义:
- huggingface 和 routeway 是仅有的两个有可复用免费对话额度证据的条目,而且两者都来自上游自己的说法(可循环的 $0.10/月 router 额度;:free 路由费用为 $0)。
两者都未在此处用真实密钥验证过。
- sambanova 已无免费额度。 它留在池子里是为了存档,不是让你免费使用。
- siliconflow 的免费那一半,正是本插件用不了的那一半 —— 免费模型是生成式媒体,对话模型是付费的。modelscope 和 xfyun 未公布限制,所以它们的免费额度没有凭证就无法验证;不要把这两个条目上的 enabled: true 读成免费容量的承诺。
- xfyun 和 siliconflow 需要完成中国实名认证,密钥才能承载流量,而且 modelscope 还需要绑定阿里云中国站。这对「零配置」的说法是一道实打实的门槛,也正因如此,这些条目只做标注,不默认启用。
- ⚠️ speculative 标记的是池中 confidence 为 speculative 的条目:转录是尽力而为,可能出错。xfyun 的模型 id 已不再是示例——它现在有厂商文档(见上方对应行);对它来说,仍未验证的是:从未有有效凭据用过它。启用这类条目,就意味着你接受这一点。
- 这六个条目只要保持禁用,设置卡片里的关键行就不会增加。 这是预期行为:浏览器表格在构建时只根据 enabled === true 的条目生成(见 Adding keys 和 changelog 中 0.6.1 的说明)。启用某个条目后,自己设置它的 apiKeyEnv,或者在插件自己的设置页里粘贴密钥。
apiKeyEnv 是本插件自己的约定,不是上游的事实。 freellmapi 没有这个概念——它的密钥放在仪表盘后面的加密存储里。上面每个名字都是默认值,你可以按服务商逐个覆盖,所以猜错了也无妨: yaml providers: groq: apiKeyEnv: MY_GROQ_KEY # any name you like mistral: enabled: true acme-gateway: # a provider that is not in the built-in pool at all baseURL: https://llm.example.com/v1 apiKeyEnv: ACME_KEY keyless: false models:
- id: acme-large
name: Acme Large
capability: 5
speed: 3
contextWindow: 131072
maxTokens: 16384
toolCalling: true
如何看懂模型列表
选择器按以下顺序由三组内容构成:
1. 虚拟路由 —— auto、auto:simple、auto:medium、auto:complex。这些始终存在。
2. 所有服务商确实能提供服务的模型 —— 免密钥服务商,加上凭据已解析成功的服务商。
3. 每个已启用但没有可用凭据的服务商,各有一条 setup:<provider> 引导条目。 它会替换该服务商的模型行,因为那些行一旦被选中,只会以 MISSING_CREDENTIAL 告终。
禁用的服务商(enabled: false,默认十七个里有十二个属于此类)什么都不贡献,所以它的模型永远不会出现。
setup: 条目不是模型。选中它不会发出任何请求——它只是一块路标,名字里带着确切的环境变量名和免费密钥的获取地址: text setup:groq Groq ▸ needs GROQ_API_KEY (free key: console.groq.com/keys) setup:cerebras Cerebras ▸ needs CEREBRAS_API_KEY (free key: cloud.cerebras.ai) setup:openrouter OpenRouter ▸ needs OPENROUTER_API_KEY (free key: openrouter.ai/keys)
| 你配置了什么 | 选择器显示什么 |
| Nothing(零凭据) | 13 项:先是 4 条虚拟路由,然后是两家免密钥服务商的 6 个模型(llm7/codestral-latest、llm7/DeepSeek-V4-Flash-0731、四个 kilo/…:free),最后是 3 条引导项(setup:groq、setup:cerebras、setup:openrouter)。没有一项是无法应答的。 |
| GROQ_API_KEY 已设置 | 15 项:结构相同,但 groq 的三个真实模型出现了(按池自身的顺序,因此它们排在具体条目最前),setup:groq 消失。setup:cerebras 和 setup:openrouter 保留。 |
每次调用 listModels() 都会重建列表,所以设置密钥后你只需重新打开选择器——真实模型出现,该服务商的引导项消失。无需重启,无需改配置。
The reason for the fold: in the zero-credential install of v0.2.0 the picker advertised 21 entries, of
which only 6 could answer — and the unreachable Groq/Cerebras rows happened to sit at the very top. That is a picker that looks broken. Working models are now sorted ahead of the guidance, always.
Pinned models are strict
auto and auto:simple|medium|complex ask the plugin to choose. Any other model id — the composite provider/model from the picker, or a bare id such as qwen/qwen3.8-27b — is a pin: it asks for that one model, and the plugin will never answer with a different one.
That strictness is the point. Up to v0.2.0 a pinned model could be silently substituted: with no key
configured, picking Llama 3.3 70B · Groq was answered by a different provider's model and reported as a successful turn, and a completely invented id (totally-made-up-model-xyz) was answered too. Nothing in the transcript said so. Since v0.3.0 a pin that cannot be served fails loudly, before any provider is contacted:
| Situation | Failure code | What the message tells you |
| The provider serving that model has no credential | MISSING_CREDENTIAL | Which exact environment variable is missing, where to get a free key, that you can paste it in the DSH settings page instead, and that auto works right now |
| No provider in the pool serves that id | UNKNOWN_MODEL | The three closest model ids in the pool, whether the provider prefix you used is merely disabled, and a suggestion to use auto |
| It exists, but every provider serving it is gated out (cooldown, vision, tools, context window) | MODEL_UNAVAILABLE | The per-candidate exclusion reasons, and a suggestion to use auto or retry after the cooldown |
All three end the stream as an in-band finish error chunk with zero upstream requests — that last part is what the tests assert, because "reports an error after quietly asking somebody else" would be the same bug wearing a hat.
auto and the tier overrides are unaffected. They deliberately select freely inside the pool and fail
over between candidates; that is what they are for. If you want the plugin to decide, use auto.
How it picks a model
Every request is scored by a pure, deterministic classifier (lib/complexity.js): no clock, no
randomness, no network, no mutation. The same conversation always routes the same way, and every signal
that produced the verdict is reported.
The three tiers
| Tier | Score | What it means for ranking |
| simple | < 0.34 | Capability counts 0.25, speed 0.75 — take the fastest model that can do the job |
| medium | 0.34 – 0.67 | 0.50 / 0.50 — balanced |
| complex | ≥ 0.67 | Capability 0.80, speed 0.20 — take the strongest model, latency be damned |
The signals
Seven signals, each normalised to [0, 1] and multiplied by its own weight. They are combined with a
noisy-OR, so several independently demanding facets compound instead of averaging away:
text
score = 1 − Π (1 − wᵢ · sᵢ) − simplePenalty
| Signal | Weight | Saturates at | What it reads |
| tokens | 0.46 | 2 500 estimated tokens | Input size. CJK counts ≈ 1 token/char, Latin ≈ 1 token/4 chars |
| tools | 0.52 | 8 tool schemas | Every tool the model must follow instructions for |
| keywords | 0.60 | 2 distinct word families | refactor, architect, prove, debug, 重构, 架构, 证明, 为什么, … |
| reasoning | 0.50 | — | Math notation (0.25), comparison words (0.25), "step by step" (0.5) |
| code | 0.32 | 2 blocks / 120 lines | Fenced code blocks (40 %) and their line count (60 %) |
| depth | 0.20 | 20 messages / 6 turns | Conversation length and assistant+tool turns |
| images | 0.06 | 2 images | Presence of an image — plus a hard floor at medium, because only a vision-capable model can serve it |
Simple-task words (translate, summarize, list, 翻译, 总结, 列出, …) subtract a 0.22 penalty — the one place a signal can lower the score.
Worked examples (measured, not invented)
Run against this implementation:
| Request | Tier | Score | Why |
| 列出这个目录下的文件 | simple | 0.000 | 10 tokens, one simple-task keyword that subtracts everything |
| 解释这段代码 + a 200-line fenced block | medium | 0.349 | 659 estimated tokens and 200 code lines — just over the medium boundary |
| 重构这个模块并给出架构权衡与失败模式分析 + 12 tool schemas | complex | 0.822 | 12 tools, 4 complexity keywords, 1 comparison marker — and only ~27 tokens, which a weighted mean would have averaged away |
And what the tier actually changes, with the default pool and every key configured:
| Rank | simple | complex |
| 1–5 | cerebras llama-4-maverick-17b (c5/s5), cerebras qwen-3-coder-480b (c5/s5), cerebras gpt-oss-120b (c4/s5) | same two c5/s5 models, then kilo nemotron-3-super-120b (c5/s3) |
| 6–7 | cerebras llama3.1-8b (c2/s5), groq openai/gpt-oss-20b (c2/s5) | openrouter nvidia/nemotron-3-super-120b-a12b:free (c5/s3) |
| 16–17 | openrouter cohere/north-mini-code:free (c3/s4) | cerebras llama3.1-8b (c2/s5), groq openai/gpt-oss-20b (c2/s5) |
(The two Cerebras models are rated capability 5 and speed 5, so they legitimately top every tier. The
tier decides the order below them, and that is where a small fast model jumps from 17th to 6th.)
When it gets it wrong, and how to override it
This is a heuristic, not semantic understanding. It counts tokens, fences, tool schemas and words. It
can be wrong in both directions, and here are real cases from this implementation:
| Request | Tier | Why it is wrong |
| 证明这个定理 ("prove this theorem") | simple (0.304) | One complexity word family saturates only halfway, and the request is six tokens long — a genuine reasoning task lands in the fast lane |
| 3 750 tokens of keyword-free prose | medium (0.463) | Tokens alone cap out at 0.46, below the complex boundary — a long, demanding analysis never reaches complex on size alone |
| 翻译这段话 + 12 tools | medium (0.413) | The tool signal pushes it up while the simple-task penalty pulls it down |
So when you know better than the classifier, say so — the model id is the override:
| Model | Effect |
| auto | Classify, then route (the default) |
| auto:simple | Force the fast lane |
| auto:medium | Force the balanced lane |
| auto:complex | Force the strongest lane |
Any other id — llm7/codestral-latest, or a bare qwen/qwen3.8-27b — is a strict pin rather than a lane: see Pinned models are strict.
The classifier still runs for the forced tiers — the score and the reasons are recorded, and the log line
says (forced by the selected model id) — but the tier no longer decides.
Tuning, if you want to bend the thresholds rather than pin a lane (advanced; edit in the profile patch):
yaml
routing:
thresholds:
medium: 0.25 # default 0.34 — more requests become "medium" complex: 0.55 # default 0.67 — more requests become "complex" weights: tokens: 0.60 # standalone maximum contribution, not a share of a total tools: 0.40
Long-running agent sessions: read this before you pick auto
auto is built for one request at a time. A long agent session is a different shape of problem, and the
difference is not obvious from the model picker — so it is stated here rather than left to be discovered.
What auto declares. DSH decides when to compress a session from the context capacity the adapter
declares for the model it routed to. auto is one model name standing for a whole pool, so it declares
min(pool, config.contextWindow) — never wider than the smallest window the pool will actually serve, and
never below 131,072 (below roughly that figure DSH's own threshold collapses and it would compact on almost
every step). Models smaller than the declaration are excluded from auto; they remain available by pinning
them.
Where that still bites. Compression replaces the oldest span of a session with a summary, and the summary
call itself carries the history being compressed. If that call lands on a model that cannot hold it, the
compaction cannot succeed at any retry — the session never gets smaller. Two mechanisms make that reachable in a long session:
- the summary is written by whichever model the router picks for that call; and
- the pool's windows differ by more than an order of magnitude.
What to do. For a long agent task, prefer a model you have pinned explicitly, or a provider with a large
single window, over auto. Pinning is strict by design: it never substitutes another model behind your back,
which is exactly the property you want when a task has been running for an hour.
What the plugin does about it now. A summarisation call (purpose: 'compaction') is never pinned, is
subject to a capacity floor of its own (routing.compactionMinContextWindow, default 262,144), and fails
loudly with the floor named when nothing in the pool can hold the history — so the failure is visible instead of an unbounded retry loop. The routing log records every exclusion with its reason.
Honest status. This reduces the failure; it does not make a free pool equivalent to one wide-window model.
The measurements behind these numbers are in _lead/research/routing-redesign.md (workspace notes, not
shipped).
Thinking effort (reasoningEffort) has no effect on auto
auto stands for a whole pool, and that pool contains models which cannot do reasoning at all. DSH validates a
requested effort against the set a model declares, and there is no honest set to declare for a synthetic
route: any set wide enough to be useful would be silently ignored whenever the router happened to pick a model
that does not honour it — the silent-degradation shape this plugin exists to avoid.
So reasoning_effort is withheld by default and the withholding is logged (see forwardReasoningEffort).
A profile-level reasoningEffort such as max therefore has no effect on auto routes. If you need
thinking effort, pin a specific model and set forwardReasoningEffort: true: the adapter then declares that
model's measured vocabulary, so DSH checks your effort against what the model actually accepts instead of
forwarding a value upstream to be rejected. Unmeasured models still declare nothing, and an unsupported effort
on a pin is refused by DSH with UNSUPPORTED_REASONING_EFFORT rather than sent.
The vocabulary is per model — measured 2026-10-15, Groq accepts low / medium / high and rejects anything else with an explicit allowlist, while qwen/qwen3.8-27b also accepts none and both gpt-oss models do not. That is why the effort options you see differ depending on which model you pin.
Failover and health
The one hard rule: failover may only happen before a single chunk has reached DSH. Once any chunk has
been yielded — even a bare block-start — a failure ends the stream with an error instead of switching provider, because splicing two models' output into one assistant message is worse than an honest failure. The rule is enforced twice (in dispatchOnce and again in the failover loop), and tested against a socket that dies mid-stream plus a healthy standby that must receive zero requests.
What gets retried:
| Condition | Failover? | Why |
| 429, 408, 409, 410, 422, 5xx | ✅ | The provider's problem; the next one may well answer |
| 404 | ✅ | Usually means "this provider does not serve that id" — and it parks the pair, like model_unavailable below |
| 400 model_unavailable | ✅ | The deliberate exception to "400 is a bad request": aggregators rotate their catalogs, so this is a per-(provider, model) state → park it exactly like a 404 model_not_found, then try the next candidate |
| Other 400s | ❌ | A malformed request stays malformed everywhere |
| 401 / 403 | ❌ | A rejected credential belongs to that provider; the provider is parked for 24 h |
Health and cooldowns:
- The first retryable failure already parks the candidate, for 2 minutes — no "three strikes" grace period, because a free-tier 429 means "back off now", and paying the same wall twice only adds latency. Each further consecutive failure escalates one step: 2 m → 10 m → 1 h → 24 h. A Retry-After header overrides the ladder, because the provider is the only party that knows when its budget resets.
- Failures also lower a candidate's score, so a recently failed route ranks below healthy peers:
1 / (1 + 0.15 · effectiveFailures) (floor 0.4). effectiveFailures decays with a 60-second half-life, so the penalty cannot starve a candidate forever: by the time the 2-minute cooldown has expired, a single failure weighs only 0.25, and one success clears the counter outright.
- Latency participates only when it is real evidence:
- the factor is fastest / latency, clamped to [0.85, 1] and computed against the fastest candidate
of this request;
- an unmeasured candidate does not get a perfect score. With no evidence of its own it is treated as
"no faster than the slowest factor actually measured in this request"; only when nothing measurable was
observed does the signal drop out completely (everyone gets 1). Otherwise "we have never looked at it"
would beat "we measured it and it is slow", which is backwards;
- a 100 ms noise floor applies: if the spread between the measured candidates is smaller than that,
the whole latency signal is discarded as warm-up noise (a 4 ms versus 26 ms localhost difference is not
throughput), and every candidate scores as unmeasured.
Because the demotion is capped at 0.85, latency can reorder candidates but cannot overturn the tier's
capability-versus-speed decision.
- A mid-stream failure does not park the provider (one interrupted stream is weak evidence); it only counts
as a failure for the next ranking.
- Attempts are bounded by routing.maxAttempts (default 6) and routing.budgetMs (default 45 000 ms), both
checked before each attempt.
A request that is simply too large
A session can outgrow the candidate the router picked. Upstream answers that with a 400
(maximum context length … However, you requested about N tokens), and the plugin treats it as a per-candidate condition, not a dead end:
- It fails over — but only towards a bigger context window. A candidate whose contextWindow is not wider than the one that just refused the request is skipped, because retrying a too-large request on a smaller model cannot succeed. (Verified: with candidates of 131 072 / 100 000 / 262 144 tokens, the 100 000 one is never contacted.)
- The refusal costs the candidate nothing. It is not parked and does not count as a failure — the model is healthy and would answer a shorter request fine. The useful memory is the window it just reported, and the gate (plus routing.contextSafetyFactor) uses it next time.
- The hard invariant still holds. If any chunk has already reached DSH, there is no failover at all
(STREAM_INTERRUPTED); this change did not touch that rule.
When nothing in the pool is wide enough, the error tells you both numbers you need — how big your request is and how big the biggest reachable model is: text dsh-freellmapi: this request does not fit any model the router can use; it is about 297538 estimated input tokens; the widest model that refused it declares 262144 tokens; the largest context window in the pool is
262144 (kilo/nvidia/nemotron-3-super-120b-a12b:free); compact the session or lower the attached history,
then retry. — [kilo/small-model → CONTEXT_WINDOW_EXCEEDED; kilo/wide-model → CONTEXT_WINDOW_EXCEEDED]
(The line breaks are added for readability here; the real message is one line.) The situation is also a
data question, which is why the context windows are documented in
Limitations.
Failures come back in-band, not as a crash. Every outcome below ends the stream with a terminal
finish chunk carrying a stable DSH code: no exception ever crosses the adapter boundary.
| Code | Meaning |
| NO_CANDIDATES | Nothing usable in the pool — nothing configured and no keyless provider enabled, or every candidate failed a hard gate (vision / tools / context window / cooldown). The message lists key-application entry points. |
| ALL_CANDIDATES_FAILED | Every ranked candidate was tried and failed; the message carries a provider/model → code trail. |
| STREAM_INTERRUPTED | The stream failed after output had already been sent — no failover by design. |
| MODEL_UNAVAILABLE | The provider does not currently serve that model. |
| ABORTED, TIMEOUT, TRANSPORT, AUTH, RATE_LIMIT, QUOTA, CONTEXT_WINDOW_EXCEEDED, UNSUPPORTED_CONTENT, UNSUPPORTED_OPTION, INVALID_REQUEST, SERVER, MISSING_CREDENTIAL, INVALID_CREDENTIAL, EMPTY_RESPONSE, CONTENT_FILTER, UNKNOWN | The usual per-attempt classifications |
For 429/quota_exceeded, the provider's Retry-After (when it sends one) is parsed into DSH's
retry-after field, so the harness backs off for exactly as long as the pool said to.
Two modes: direct and gateway
| | direct (default) | gateway |
| Who routes | This plugin (classifier + ranker + failover) | An external freellmapi gateway |
| Needs an external process | ❌ no | ✅ yes (Docker or node server/dist/index.js on :3001) |
| Credentials | One key per provider, or none at all | One unified freellmapi-… gateway key |
| Model picker | auto, auto:simple/medium/complex, then provider/model | auto, auto:smart/fast/cheap/reliable/balanced, fusion, discovered catalog |
| Log line | dsh-freellmapi: tier=… -> provider/model (…) | dsh-freellmapi: routed via <platform>/<model> (model=…) |
| Use it when | You want the shortest path from install to a working model | You already run the gateway, or you want its bandit scoring, fusion, prompt compression and dashboard |
Switch with one field:
yaml
mode: gateway # default is "direct"; gateway also needs baseURL + a freellmapi-… key
The two modes can coexist in one install, but the freellmapi provider name is owned by exactly one
adapter. freellmapi's official npx freellmapi setup-dsh --url http://localhost:3001 --api-key <key>
writes an llm-pi-ai.providers.freellmapi route, whose DSH provider name is also freellmapi. Provider
route names are unique per process, so whichever adapter registers second is rejected. If you already ran
setup-dsh, remove that route before installing this plugin — the plugin refuses to load with a message naming exactly this conflict, rather than failing silently.
And yes: if the gateway is the only thing you want, setup-dsh needs no plugin at all. It writes
$DSH_HOME/settings.yaml + $DSH_HOME/.env (mode 0600) and sets the default model. This plugin's
gateway mode exists for people who want the routed-via visibility and per-request failure detail on top.
Configuration
Every field is live (a settings edit applies to the next request, no restart) and validated
totally — a malformed value falls back to its default instead of preventing the plugin from loading.
| Field | Type | Default | Meaning |
| mode | direct \| gateway | direct | Self-contained routing, or forward to an external gateway |
| baseURL | string | http://localhost:3001/v1 | Gateway mode only: OpenAI-compatible root of the freellmapi gateway |
| apiKeyEnv | string | FREELLMAPI_API_KEY | Gateway mode only: credential reference name (not the key) |
| apiKey | string (secret) | '' | Gateway mode only: optional inline credential; prefer apiKeyEnv |
| providers | dict or array | {} | Per-provider overrides for the built-in pool (see below) |
| routing | dict | {maxAttempts: 6, budgetMs: 45000} | Routing-engine knobs (see below) |
| logRouting | boolean | true | Log the direct-mode routing decision (tier, winner, candidates tried) |
| logRoutedVia | boolean | true | Log X-Routed-Via / fallback trail (gateway mode) |
| vision | boolean | true | Advertise image input when the chosen model supports it; set false to force text-only |
| contextWindow | number | 131072 | Context window assumed for ids the pool does not describe |
| maxTokens | number | 0 | Output cap declared to DSH; 0 = declare none and let the provider decide |
| timeoutMs | number | 600000 | Per-attempt timeout in milliseconds; 0 disables it |
| includeFusion | boolean | true | Gateway mode: advertise the gateway's fusion route |
| exposeDiscoveredModels | boolean | true | Gateway mode: merge GET /v1/models into the picker |
| discoveryTimeoutMs | number | 5000 | Gateway mode: timeout for that discovery request |
| discoveryCacheMs | number | 60000 | Gateway mode: how long a discovery result stays fresh |
| maxTokensField | max_tokens \| max_completion_tokens | max_tokens | Which wire field carries the cap. max_tokens is the canonical field; the gateway also accepts the other |
| headers | map | {} | Extra request headers. user-agent, authorization, content-type, accept are reserved |
| taskType | '' \| auto \| code \| chat | '' | Gateway mode: optional X-FreeLLM-Task-Type hint (empty sends no header) |
| cacheMode | '' \| on \| off | '' | Gateway mode: optional X-FreeLLM-Cache switch (empty sends no header) |
The schema is a static import of @deepseek-ai/schemastery, declared in both peerDependencies and
devDependencies. There is no fallback: if the schema builder cannot be resolved the plugin fails
loudly at load rather than degrading. That matters because the degradation is invisible downstream — a plugin whose Config lacks toJSON() gets no settings namespace, and the Models page then shows its provider nowhere while it keeps routing traffic. pnpm install in this package during development so a link:-ed install resolves the builder from its own tree.
No configuration value, however malformed, can stop the plugin from loading.
One environment escape hatch worth knowing: FREELLMAPI_BASE_URL overrides the gateway endpoint from
the environment, and it is honoured only while baseURL still holds its built-in default, so an
explicit setting always wins over the environment.
providers.<id> — override one pool entry
| Key | Meaning |
| enabled | Turn a provider on/off. This is how you enable the twelve providers that ship disabled |
| displayName | Label used in the picker and logs |
| baseURL | Endpoint root — fix a hostname without waiting for a release (see zhipu) |
| apiKeyEnv | Your own credential reference name for this provider |
| apiKey | Inline credential for this provider (stored in the profile patch — prefer apiKeyEnv) |
| keyless | Mark a custom endpoint as usable without a credential |
| models | Replaces the built-in model list for that provider. Each entry: id, name, and capability / speed (1–5), contextWindow, maxTokens, vision, toolCalling — either inline or under caps |
A provider that is not in the built-in pool at all is added the same way: give it id/baseURL and a
models list (see Adding keys).
routing — engine knobs
| Key | Default | Meaning |
| maxAttempts | 6 | Maximum candidates tried for one request |
| budgetMs | 45000 | Total failover time budget; 0 = unlimited |
| contextSafetyFactor | 1.15 | Safety margin on the context gate: a candidate must satisfy contextWindow >= estimated tokens × this factor. The token estimate is a heuristic and undercounts, so one model of headroom is cheaper than a 400 from upstream. A value below 1 is rejected and the default applies |
| thresholds.medium | 0.34 | Score at which simple becomes medium |
| thresholds.complex | 0.67 | Score at which medium becomes complex |
| weights | per-tier defaults | {capability, speed} or {simple: {…}, medium: {…}, complex: {…}} |
| preferredOrder | [] | Provider ids you want preferred; a bounded bonus of at most 0.05 |
Three ways to set them
1. Settings page — Settings → Plugins → dsh-freellmapi (every top-level field, with the generated form described above).
2. Profile patch — $DSH_HOME/profiles/<profile>/cordis.patch.yml; the recommended place for providers and routing:
yaml
- id: freellmapi
config:
mode: direct
logRouting: true
providers:
cerebras:
enabled: true
kilo:
enabled: false # if you do not accept its training-recording terms
routing:
maxAttempts: 4
preferredOrder: [cerebras, groq]
3. Environment — the per-provider apiKeyEnv names in Adding keys.
How to see what it did
Direct mode logs one line per request:
text
dsh-freellmapi: tier=complex -> openrouter/nvidia/nemotron-3-ultra-550b-a55b:free (capability=5, speed=3, 2 candidates tried)
dsh-freellmapi: tier=simple -> no candidate (0 candidates tried)
Gateway mode logs its original line:
text
dsh-freellmapi: routed via groq/llama-3.3-70b (model=auto) fallback-attempts=2 trail=groq/… key1=rate_limited; …
The plugin also keeps the last 50 routing observations in a process-wide ring buffer — exported as
getRecentRoutes() (with formatDirectRouteLine() / formatRouteLine()) for tests and future UI. Each
record carries the tier, score, chosen provider/model, every attempt and its failure code, the candidates
that were excluded and why, and the classifier's reasons. It is not a DSH tool or service, so the log line
is what you use day to day.
Just one key? No keys?
No keys at all: works, through kilo and llm7 (both enabled and keyless). "Complexity routing" then
means choosing between two pools' worth of models rather than five — still real routing and still failover-capable, but a much thinner choice.
Exactly one provider configured: works, and the classifier still runs — but with a single provider's models the ranking degenerates into "pick the best model that provider has for this tier". You need at least two providers before routing has anything to route between.
What does not happen: a missing key is never an error. Providers without a usable credential are
dropped from the routing candidates silently — no warning, no "configuration incomplete" banner — and in the model list they appear as a single setup:<provider> signpost instead of rows that cannot answer (see How to read the model list). That is deliberate: a keyless pool must keep working on a machine where nothing is configured.
Architecture
text
┌───────────────────────────── DeepSeek Harness (host process) ────────────────────────────┐
│ │
│ agent loop / chat turn │
│ │ │
│ ▼ │
│ llm service (ctx.llm) route chosen by GenerateOptions.provider │
│ │ │
│ ▼ │
│ LlmAdapter (this plugin, provider = "freellmapi") │
│ │ │
│ │ the model id decides the lane: │
│ │ auto → classify │
│ │ auto:simple → force the fast lane │
│ │ auto:medium → force the balanced lane │
│ │ auto:complex → force the strongest lane │
│ │ provider/model → pin exactly one candidate (strict: no substitution) │
│ ▼ │
│ scoreComplexity() → simple / medium / complex + score + reasons │
│ │ │
│ ▼ │
│ listProviderModels() → rankCandidates() │
│ │ hard gates: vision · tools · context window · cooldown │ │ │ soft: consecutive failures · latency · preferred order │ │ ▼ │ │ dispatchWithFailover() attempt 1 ─▶ 429 / 5xx / model_unavailable / timeout │ │ │ └─▶ park it, take the next ─▶ attempt 2 │ │ │ │ │ │ ══════════════════════════════════════════════════════════════════════ │ │ │ the first chunk reached DSH │ │ │ past this line NO failover happens: a mid-stream failure ends the stream │ │ │ with an error instead of splicing two models into one answer │ │ ▼ │ │ StreamChunk sequence → DSH │ │ │ └──────────┬───────────────┬───────────────┬───────────────┬───────────────┬───────────────┘ ▼ ▼ ▼ ▼ ▼ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ ┌────────────┐ │ groq │ │ cerebras │ │ openrouter │ │ kilo │ │ llm7 │ └────────────┘ └────────────┘ └────────────┘ └────────────┘ └────────────┘ kilo and llm7 answer without any key at all · 18 pools in total
Notes on the seams:
- Every outbound request carries DSH's attribution user-agent and uses redirect: 'error', so a
redirect can never leak a credential to a third host.
- Credentials are resolved per request (one lookup per provider, not per model) and never cached.
- stream() never throws. Every failure is an in-band finish chunk, which is what makes the plugin
immune to module-identity splits — see the red line in Troubleshooting.
- Zero runtime dependencies: no dependencies at all, and the DSH LLM runtime is resolved with a guarded
dynamic import that falls back to local shims outside DSH.
Troubleshooting
1. 401 / 403 — a provider rejects the key
Only one provider is affected; the router parks it (invalid ⇒ 24 h cooldown) and continues with the next
candidate. Check:
- Is the key exported under the exact name in apiKeyEnv (defaults in Adding keys)? You can
override the name per provider if your shell already uses a different convention.
- Does that provider require something else — signup, real-name verification, or a specific site (zhipu is the notorious case: mainland and overseas keys do not interchange)?
- An empty string counts as "not configured", so an exported-but-empty variable looks identical to a
missing one.
2. 429 — rate limited, and what happens next
This is normal weather on free pools, not an outage. The candidate is parked immediately, on the first
failure, and escalates the ladder if it keeps failing (2 m → 10 m → 1 h → 24 h), using the provider's
Retry-After when it sends one; the request continues on the next-ranked candidate. Remedies: add another
provider's key, lower concurrency, or use
preferredOrder to steer to the pool with quota left.
3. Every candidate failed — where do I look?
The terminal chunk says ALL_CANDIDATES_FAILED and carries the trail:
text
dsh-freellmapi: all 3 candidate(s) failed for tier=medium after 2 attempts — groq/qwen/qwen3.8-27b: RATE_LIMIT; …
NO_CANDIDATES instead means the pool was empty before any request went out: nothing configured and no
keyless provider enabled, or every candidate failed a hard gate (vision / tools / context window /
cooldown). Its message lists key-application entry points. STREAM_INTERRUPTED means output had already
been delivered, so by design there was no failover.
4. The model list only shows auto, the tier routes and some setup: entries
That is the intended zero-credential shape: the virtual routes plus whatever is genuinely reachable. A
setup:<provider> entry means that provider is enabled but has no usable credential — set the environment variable in its name and reopen the picker. See How to read the model list.
5. A provider I configured is being skipped
Missing key ⇒ silently dropped from the routing candidates, no warning (the picker shows it as a
setup:<provider> signpost instead). Also check:
- enabled — twelve providers ship disabled: mistral, zhipu, volcengine, longcat, nvidia, ovh, plus the six 0.7.0 additions (modelscope, xfyun, siliconflow, sambanova, huggingface, routeway); turn them on in providers.<id>.enabled.
- Hard gates — a model without vision: true is excluded from image requests, toolCalling: false from tool requests, and anything whose contextWindow is smaller than the estimated prompt size.
- Cooldowns — a recently failed candidate is parked; the exclusions list in the routing record says so
explicitly ("cooling down for another Ns after …").
6. The profile misbehaves or will not start (the dependency red line)
@deepseek-ai/dsh- must never appear in this plugin's dependencies. It ships zero runtime
dependencies, and it does not even import the DSH LLM runtime statically: it resolves
@deepseek-ai/dsh-llm through a guarded dynamic import and falls back to local shims outside DSH. The only
declared peer is the optional @deepseek-ai/cordis.
Why it matters: DSH supplies its runtime through filesystem junctions — $DSH_HOME/profiles/node_modules/<pkg> points into the npx install cache (…\npm-cache\_npx\<hash>\node_modules\…). A plugin that declares, say, @deepseek-ai/dsh-llm as an ordinary dependency makes the package manager hoist a second copy into profiles/<profile>/node_modules/@deepseek-ai/, which then wins module resolution. Class identity breaks across the copies: instanceof fails, error codes silently degrade, and services go missing. The version range makes it worse — npm's @deepseek-ai/dsh-llm only exists in 0.0.1-rc.x–0.1.x, far below the 0.2.0-rc.2 runtime this plugin targets.
Health check — in a healthy profile, the only entries in the profile's own* scope are cosmokit and schemastery: powershell Get-ChildItem "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\@deepseek-ai"
Anything else (dsh-llm, cordis, another dsh- adapter) is a leaked dependency. Find the culprit with
pnpm why @deepseek-ai/dsh-llm inside the profile, remove it, reinstall, restart.
7. 400 model_unavailable keeps appearing
That is catalog drift on an aggregator (LLM7 rotates models), not a broken install. It carries the same
meaning as a 404 model_not_found: the (provider, model) pair is parked (starting at the 2-minute
cooldown step) and the next candidate serves the request. If it happens constantly for one model, remove it
from that provider's models list or set providers.<id>.enabled: false.
8. I picked a specific model and got MISSING_CREDENTIAL
That is intentional — it is the strict-pinning behaviour described in Pinned models are strict. A concrete model id asks for that* model, and if its provider has no credential the plugin refuses rather than quietly answering with another provider's model. The message names the exact environment variable and where to get a free key. Two ways forward:
- set that variable (or paste the key in the DSH settings page), then send the request again; or
- select auto, which uses the providers that need no key at all right now.
9. UNKNOWN_MODEL
The id you asked for is not served by any provider in the pool. The message lists the three closest ids
and tells you when the provider prefix you used is merely disabled. Common causes: a typo, an id copied
from a provider's own docs that is not in this pool, or a model of a provider that ships disabled
(mistral, zhipu, volcengine, longcat, nvidia, ovh, plus the six 0.7.0 additions) — enable it under config.providers.<id> first. Use auto if you just want an answer.
A pinned model that exists but is cooling down reports MODEL_UNAVAILABLE instead, with the exclusion
reason for each candidate; auto or waiting out the cooldown both work.
Limitations and unverified claims
Stated plainly, so you know what you are relying on:
1. The complexity classifier is a heuristic, not understanding. It counts tokens, fenced code, tool
schemas and vocabulary. When it gets it wrong shows
three real misjudgements from this implementation. When you know better, pin auto:simple /
auto:medium / auto:complex, or pin one provider/model.
2. No end-to-end run against real providers. Every test in this repository runs against in-process
mock providers, deliberately, so CI stays offline. The only real external calls ever made were the
maintainer's keyless probe (five requests, four 200s and one 404 documented in
_lead/live-probe-2026-10-13.md) plus the credential-free reachability and catalog probes behind the
reachability section above (_lead/research/reachability-v0.7.md, 2026-10-15). No completion has ever
been requested with a real key, so everything else — every keyed provider, every quota number, every model id except the ones a public catalog publishes — is documentation-level evidence, not measurement.
3. Only credential-free requests were ever made. groq, cerebras, openrouter, mistral,
nvidia and the rest were contacted without credentials (status codes and public catalogs only, never a
completion), so their baseURLs and model ids come from upstream's registry and migrations. Rate-limit
numbers are the 2026-04 baseline and will drift.
4. zhipu is the riskiest entry in the pool, and volcengine / longcat model ids are inferred — all three ship disabled with confidence: 'speculative'. Enabling them is you accepting that risk. The zhipu hostname in particular differs between the mainland and overseas sites and the keys do not interchange: a wrong hostname is a guaranteed 401.
5. Free catalogs drift. The same model id can answer 200 today and 400 model_unavailable tomorrow.
The router treats that as a per-model condition (park + next candidate), so it degrades instead of
failing — but it is normal operation, not a bug.
6. stream_error frame shapes are documentation-level. Upstream does not pin the JSON shape, so the
parser recognises the four plausible forms (type, code, object, nested error) and treats any
error-shaped frame as terminal.
7. The image path is not verified against a real attachments service. Bytes are read through
attachments.imageHostPath(), the only documented way for an adapter to reach them, and inlined as
data: URLs (up to 20 MB each); if that service differs in your build, each affected image degrades to
a placeholder text and the request still succeeds. Only two models in the default pool declare
vision: true.
8. sessionId / purpose from GenerateOptions are not mapped — Chat Completions has no standard field for them.
9. Health state is not persisted. Cooldowns live in the process; after a restart a parked provider is
retried once. HealthTracker can round-trip its state, but nothing writes it to disk.
10. A failed-over answer comes from a different model than the first choice. When candidate 1 fails,
candidate 2 answers, and they are different models. That is safe (nothing was emitted yet) and fully
recorded in the routing record's attempts, but the answer is not from the top-ranked model.
11. nvidia needs parallel_tool_calls: false. Several NIM models reject parallel tool calls, so the
pool declares the flag and it is applied to the request body. NVIDIA still ships disabled by default
(trial-only ToS, minutes-long first token, tight limits).
12. routing.weights / routing.thresholds are advanced fields with no rendered form in the settings
page (they are z.any() payloads). Edit them in the profile patch.
13. Still open from the v0.1.0 verification round (re-checked against this implementation):
maxTokens above Number.MAX_SAFE_INTEGER still makes resolveModel() return metadata DSH refuses
(INVALID_MODEL_MAX_TOKENS); a role: "user" message whose content is a plain string is still sent
with empty content; in gateway mode includeFusion: false can still be undone by the gateway's own
fusion entry; and discover() can still return duplicate ids (DSH de-duplicates them). L1 — the incomplete failure-code list — is fixed.
Context windows: what is measured and what is estimated
Every model's contextWindow decides whether a request is even attempted (see
A request that is simply too large), so a wrong number hurts twice:
too large and the router picks a model that will refuse the request; too small and a model that could have
served it gets filtered out before the first byte.
The four kilo models were wrong in exactly those two directions and were corrected on 2026-10-14 from
kilo's own GET /api/gateway/models:
| Model | Was | Now (measured) |
| kilo/nvidia/nemotron-3-super-120b-a12b:free | 1 000 000 | 262 144 |
| kilo/nvidia/nemotron-3-ultra-550b-a55b:free | 131 072 | 1 000 000 |
| kilo/poolside/laguna-s-2.1:free | 131 072 | 262 144 |
| kilo/cohere/north-mini-code:free | 131 072 | 256 000 |
Every other context window in the pool is either a catalog reading or an estimate. Those providers'
GET /v1/models answers 403 without a credential, and OVH's endpoint — although it answers 200 — does not publish a context field at all. The source note on each entry in lib/providers.js says where its number came from and when; treat anything that does not say "measured" as an estimate. Measured rows include llm7's two (context_window.tokens), sambanova's four and routeway's four (context_length, both catalogs fetched anonymously on 2026-10-15 — two routeway :free routes declare 42 000, not the 62 000 the rest of its :free family declares), and huggingface's two, whose number is the selected backend's context_length and deliberately not the largest one on offer. The six 0.7.0 additions that ship disabled are otherwise conservative defaults: modelscope's catalog publishes no context field at all, and xfyun's and siliconflow's catalogs need a credential nobody here has.
If a provider in your install reports a different window, override it per model — no code change needed: yaml providers: groq: models:
- id: qwen/qwen3.8-27b
name: Llama 3.3 70B
capability: 4
speed: 5
contextWindow: 131072 # your number wins
toolCalling: true
Compatibility
| Component | Supported | Notes |
| DeepSeek Harness runtime | 0.2.0-rc.2 (written and verified against it) | Other 0.2.0-rc.x builds are likely fine; only stable LLM seam APIs are used |
| Node.js | >= 22 | Matches the reference DSH plugins |
| Provider APIs | OpenAI-compatible POST {baseURL}/chat/completions with SSE | Everything in the pool speaks the same wire |
| dsh-freellmapi | 0.7.18 | Zero runtime dependencies; the DSH LLM runtime is resolved at load time; peers are @deepseek-ai/schemastery (the config schema) and the optional @deepseek-ai/cordis |
| freellmapi gateway | v0.13.x protocol surface | Only relevant in gateway mode |
Version-numbering note: the upstream gateway's releases are v0.13.x, while the npm CLI package
freellmapi (which provides setup-dsh) is versioned independently (e.g. 0.8.1). Do not treat the two
numbers as the same series.
Development
bash
npm test # offline test suite — no network, no provider, no API key required
The suite is hermetic on purpose: it must pass on a clean GitHub runner, so it never contacts a real
provider. CI runs the same command on ubuntu-latest with Node 22 (.github/workflows/ci.yml).
text
dsh-freellmapi/
├── lib/
│ ├── index.js # plugin entry: apply(), adapter, listModels/resolveModel/stream
│ ├── config.js # the config schema and its total normalisation (DEFAULTS is authoritative)
│ ├── providers.js # the built-in pool + per-provider overrides + credential resolution
│ ├── complexity.js # the classifier: signals, weights, thresholds
│ ├── router.js # pure candidate ranking (gates, health, latency, preferred order)
│ ├── health.js # cooldowns, failure counts, latency EWMA
│ ├── dispatch.js # one provider attempt + the failover loop
│ ├── wire.js # GenerateOptions → OpenAI-compatible request
│ ├── sse.js / frames.js / translate.js # SSE → DSH StreamChunk
│ ├── errors.js / routes.js / catalog.js / runtime.js / types.js
├── tests/ # offline tests (411 of them)
├── tools/gate.mjs # the project gate — one command, one verdict (npm run gate) ├── docs/ # EXPANSION-v0.7.md (task brief) · NEW-SESSION.md (bootstrap) ├── IMPLEMENTATION.md # design notes, including every deliberate deviation ├── PROJECT-RULES.md # operating manual: PDCA loop, 8 hard rules, lesson ledger ├── README.md / README-zh.md / CHANGELOG.md / NOTICE / LICENSE └── .github/workflows/ci.yml
Upstream and credits
This plugin is a derived work of freellmapi
(Copyright © 2026 Tashfeen Ahmed, MIT). Saying so plainly matters, because the earlier revisions of this
README mentioned that project only as "the external gateway gateway mode can forward to" — which reads as though the plugin were independent of it. It is not:
- The provider catalogue in lib/providers.js is transcribed from freellmapi's own sources — providers/index.ts, server/src/db/migrations/20260101_000000_legacy_baseline.ts and docs/en/providers/. Each entry carries a source: or note: field naming the upstream file and line range it came from; those citations are the provenance record.
- The failover/cooldown policy in lib/health.js follows freellmapi's own escalation ladder.
- The routing model — score the request, pick a tier, fail over across providers — is freellmapi's design.
What is not derived: no upstream file is vendored here (upstream is TypeScript; this package is plain
JavaScript and shares no file with it), and the DSH integration — adapter, provider route, settings schema, browser half — is this package's own, written against the DSH plugin contract. apiKeyEnv names are this package's convention, since DSH resolves credentials by reference name and each provider needed a stable one.
The upstream MIT notice and licence text are reproduced in full in NOTICE, which ships with
the package as its licence requires.
License
MIT © 2026 lion231226 — a derived work of freellmapi; see NOTICE.
Not affiliated with, endorsed by or supported by any provider in the pool, the freellmapi project, or
DeepSeek. Provider names belong to their owners; "DeepSeek Harness" and @deepseek-ai/ are DeepSeek's.
Free tiers have quotas, terms and privacy policies that are yours to comply with — several of them log or train on what you send. Upstream describes itself as "personal experimentation only"; read their terms before deploying either project.预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
lion231226/dsh-freellmapi
为 DeepSeek Harness 提供免费额度多服务商路由。
相关插件 · 模型接入
API 中继安全审计toby-bridges/api-relay-audit
面向 AI API 中转与 LLM 代理的本地安全审计。
★ 897已汉化DSH
69.4
免费搜索插件DDDMUC/dsh-free-search
DeepSeek Harness 免费搜索插件 —— 无需 API key,零成本,多引擎可切换。 一个给…
★ 761已汉化DSH
68.7
免费联网搜索liustack/modsearch
在官方 App 里模型能联网,切到 API 就不能了。ModSearch 把联网补回来:网页搜索、X…
★ 863已汉化DSH
68.5
OpenCode 免费模型FishBottle7/opencode2dsh
在 DSHDeepSeek Harness里原生使用 OpenCode Zen 的免费匿名模型。
★ 379已汉化DSH
67.8
AI 模型网关tingly-dev/tingly-box
Tingly Box Web UI 演示
★ 373已汉化DSH
67.4
WorkBuddy 账号池XDTrees/dsh-xdpool
把你能白嫖的模型额度接进 DSH 同一个池:WorkBuddy 多账号自动容错、十三家扫码登录、76…
★ 359已汉化DSH
67.1