🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

模型路由面板 crackerghost/DSH-GOD

DeepSeek Harnessv2.1.0
crackerghost4 天前有提交已提供中文文档扫描:含敏感能力

免登录免密钥选用免费模型并实时探测可用性

37.1/100
综合分
7
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/31 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

自动兼容检查未通过:仓库 package.json 标记 private,未发布到 npm,需从源码安装。以下命令仍可尝试,但可能失败。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add crackerghost/DSH-GOD
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:需留意
是什么
dsh 原生插件 · ui
装得上吗
静态安装检查未通过,可能需要源码安装
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 4 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查需源码安装

仓库 package.json 标记 private,未发布到 npm,需从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✓npm 包dsh-our-free-model @ 2.1.0
✓Node 引擎要求 ^22.19.0 || >=24.0.0 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

源码安装时若 pnpm 拦截 prepare 脚本,按其提示在 pnpm-workspace.yaml 的 allowBuilds 中放行后重跑。

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 04:49:20

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 42 处,展示前 39 条)
  • 发起网络请求
    src/forward.js:581
    const target = http.request({
  • 发起网络请求
    src/chan-relay.js:146
    const target = http.request({
  • 发起网络请求
    src/egress.js:127
    if (relay === null) return fetch(url, init)
  • 发起网络请求
    src/egress.js:140
    return fetch(`http://${RELAY_HOST}:${relay.port}${target.pathname}${target.search}`, { ...outgoing, headers })
  • 执行外部命令来自配置/变量
    src/egress.js:189
    child = spawn(binary, ['-d', dir, '-f', configPath], { windowsHide: true, stdio: 'ignore' })
  • 发起网络请求
    src/egress.js:315
    const outgoing = http.request({
  • 发起网络请求
    src/egress.js:899
    const request = http.request(
  • 发起网络请求
    src/eac.js:86
    if (typeof lane.fetch === 'function') return lane.fetch(url, { method, headers, body, signal })
  • 发起网络请求
    src/eac.js:90
    const request = transport.request({
  • 发起网络请求
    src/eac-login.js:26
    response = await fetch(`${root}/auth/poll?link=${encodeURIComponent(link)}&retain=1`, {
  • 发起网络请求
    src/eac-login.js:47
    await fetch(`${credential.base.replace(/ v1 ?$/, '')}/auth/ack?link=${encodeURIComponent(link)}`, {
  • 发起网络请求
    src/eac-login.js:68
    const response = await fetch(`${credential.base.replace(/ v1 ?$/, '')}/auth/github/start?link=${encodeURIComponent(link)}`, {
  • 执行外部命令来自配置/变量
    src/subscriptions.js:239
    const user = execFileSync(path.join(system32, 'whoami.exe'), [],
  • 执行外部命令来自配置/变量
    src/subscriptions.js:241
    execFileSync(path.join(system32, 'icacls.exe'),
  • 读取本地文件
    src/magpie-engine.js:18
    return `data:image/svg+xml;base64,${readFileSync(new URL('../icon.svg', import.meta.url)).toString('base64')}`
  • 写入本地文件
    src/magpie-engine.js:239
    await writeFile(temp, JSON.stringify({
  • 删除本地文件
    src/magpie-engine.js:248
    } finally { await unlink(temp).catch(() => {}) }
  • 读取本地文件
    src/magpie-engine.js:254
    try { return JSON.parse(await readFile(file, 'utf8')) } catch (error) {
  • 写入本地文件
    src/magpie-engine.js:263
    await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
  • 写入本地文件
    src/magpie-engine.js:264
    await rename(temp, file)
  • 删除本地文件
    src/magpie-engine.js:265
    } finally { await unlink(temp).catch(() => {}) }
  • 读取本地文件
    src/magpie-engine.js:633
    try { metadata = JSON.parse(readFileSync(path.join(vendor, 'engine.json'), 'utf8')) } catch {}
  • 读取本地文件
    src/magpie-engine.js:655
    state, available: existsSync(executable) || (!binary && hasEnginePayload(fileURLToPath(new URL('../', import.meta.url)), entry)), version: metadata.ve
  • 读取本地文件
    src/magpie-engine.js:688
    if (!binary && !existsSync(executable) && entry?.compressed) {
  • 读取本地文件
    src/magpie-engine.js:692
    if (!existsSync(executable)) throw engineError('Magpie engine binary is unavailable')
  • 写入本地文件
    src/magpie-engine.js:703
    await mkdir(home, { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/magpie-engine.js:711
    await mkdir(dir, { recursive: true, mode: 0o700 })
  • 执行外部命令来自配置/变量
    src/magpie-engine.js:757
    child = spawn(executable, ['web', '--addr', `127.0.0.1:${webPort}`, '--no-open'], {
  • 发起网络请求
    src/magpie-engine.js:971
    const upstream = http.request(webURL + destination, { method: req.method, headers, agent: false })
  • 写入本地文件
    src/bun-fetch.js:45
    writeFileSync(file, WORKER_SOURCE, { mode: 0o600 });
  • 读取本地文件
    src/bun-fetch.js:62
    if (!statSync(path).isFile()) continue;
  • 读取本地文件
    src/bun-fetch.js:63
    accessSync(path, constants.X_OK);
  • 执行外部命令来自配置/变量
    src/bun-fetch.js:104
    try { child = spawn(executable, [bunWorkerPath()], { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true }); }
  • 发起网络请求
    src/loopback-fetch.js:41
    const outgoing = http.request(target, {
  • 写入本地文件
    src/engine-payload.js:62
    await mkdir(path.dirname(executable), { recursive: true, mode: 0o700 })
  • 写入本地文件
    src/engine-payload.js:88
    try { await rename(temporary, executable) } catch (error) {
  • 删除本地文件
    src/engine-payload.js:94
    await unlink(temporary).catch(error => { if (error.code !== 'ENOENT') throw error })
  • 发起网络请求
    src/sponsor.js:43
    try { return locks?.request ? await locks.request('ofm-sponsor-daily', claim) : claim() }
  • 写入剪贴板
    src/sponsor.js:62
    await navigator.clipboard.writeText(value)
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-commands@deepseek-ai/dsh-credentials@deepseek-ai/dsh-llm@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

README

由 DeepSeek 最新模型翻译生成
DSH GOD 与 NotMyBugs 合作 Fork 说明: DSH GOD 是 Ebony-Vinyl/dsh-our-free-model 的公开 fork。它保留上游的 MIT 协议与第三方声明。签名更新器、公告源和 EAC star 授权仍配置为上游项目;发布这个 fork 不会把这些服务重定向。 你只需要在 dsh 里安装这个插件——不用登录,不用注册,不用 API key,不用任何其他步骤。前沿模型就在那里,DeepSeek V4.1 Flash 和 Kimi K3 都在其中。完全免费,没有用量上限。 模型名单跟随上游,可用性从你自己的网络出口实测,思考强度控制发送的是真实预算,而不是提示词暗示,还附带一个本地 OpenAI 兼容转发端口。 它以纯插件方式挂载:不改核心,没有构建步骤,零依赖。 亮点 - 无需任何配置——安装、重启、选模型。不用账号,不用 key,不用注册配额面板。 - 上游点名说清——免费通道是 OpenCode 的 Zen 网关,地址 https://opencode.ai;Kilo 通道是 Kilo AI 的公共网关,地址 https://kilo.ai;两者都直连,没有第三方中转你的流量。谁在服务你的请求、你的数据去了哪里,都在模型从哪来和免责声明里写明了。
展开完整文档(README 剩余 1 节)
DSH GOD

与 NotMyBugs 合作

Fork 说明: DSH GOD 是 Ebony-Vinyl/dsh-our-free-model 的公开 fork。它保留上游的 MIT 协议与第三方声明。签名更新器、公告源和 EAC star 授权仍配置为上游项目;发布这个 fork 不会把这些服务重定向。

你只需要在 dsh 里安装这个插件——不用登录,不用注册,不用 API key,不用任何其他步骤。前沿模型就在那里,DeepSeek V4.1 Flash 和 Kimi K3 都在其中。完全免费,没有用量上限。

模型名单跟随上游,可用性从你自己的网络出口实测,思考强度控制发送的是真实预算,而不是提示词暗示,还附带一个本地 OpenAI 兼容转发端口。

它以纯插件方式挂载:不改核心,没有构建步骤,零依赖。

亮点

- 无需任何配置——安装、重启、选模型。不用账号,不用 key,不用注册配额面板。
- 上游点名说清——免费通道是 OpenCode 的 Zen 网关,地址 https://opencode.ai;Kilo 通道是 Kilo AI 的公共网关,地址 https://kilo.ai;两者都直连,没有第三方中转你的流量。谁在服务你的请求、你的数据去了哪里,都在模型从哪来和免责声明里写明了。
- 名单跟随上游——模型集合、上下文长度和能力在每次刷新时重新拉取,而不是冻结在插件里。
- 选择器只列出真正可用的模型 —— 上游列表里点名、但网关直接拒绝路由的模型(报 Model is unavailable,或该 id 返回 404),会从下拉菜单里消失,但仍留在设置页,并记录下拒绝原因。凡不属于「对模型本身的判定」的情况,模型都保持可达:网关返回 5xx、429 配额窗口、超时、连接中断。受地域限制的模型会移到单独的 region-limited 分组。如果一整轮探测全部被拒,也不会隐藏任何东西:选择器永远不会变空。
- 公告中心,实时推送 —— 仓库所有者改一个 JSON 文件并推送;每个安装实例在一个轮询周期内收到。正文是 HTML,经严格白名单渲染;urgent 条目弹出全屏模态框;可选系统级通知。
- 应用内升级 —— 在设置页一键完成:下载 → SHA-256 校验 → 备份 → 原子替换 → 回读校验 → 热重载,任一步失败自动回滚。
- 热重载 —— 升级和代码改动立即生效,无需重启应用;也可以手动点按钮触发,或开启可选的文件监听。
- 按出口区分地域 —— 受地理位置限制的模型单独归入 region-limited 分组,而不是在对话中途失败。切换网络出口后,下一次探测会自动重新分类。
- 由正文决定类型,不看响应头 —— 高负载下,这个网关会用 JSON 的 Content-Type 返回 200,而正文其实是一段完全正常的 SSE 帧流。插件嗅探开头几个字节,再把它们回放进流里,于是这一轮继续流式输出,而不是被丢弃 —— 一个能用的模型绝不会因为某个响应头撒谎就被降级为 unavailable。
- 真正生效的思考强度 —— 两类模型,两种控制方式。声明了思考档位的模型(其中有 DeepSeek V4.1 Flash、GLM 5.3 Flash)有自己的档位菜单(Off / Low / High / Max …),选中的档位原样写入上游 reasoning_effort 字段 —— 这个网关会校验该字段,所以超出集合的值会返回 400。没有档位菜单的模型(其中有 MiMo V2.6)走预算阶梯:Light / Balanced / Deep 分别对应输出 token 预算 2 048 / 8 192 / 模型全部容量;无法关闭思考的模型,整条阶梯翻倍为 4 096 / 16 384 / 容量,因为这类模型里思考和可见回答争用同一个上限。两者都按调用记录,设置页每张模型卡片都会打印档位,或它实际会发送的数值(见 思考强度)。
- 没有浏览器 UI 也能用 —— 硬依赖只有 llm,所以插件在 dsh-tui 这类无头组合里也能激活,照常提供模型。仪表盘那一半跑在自己的 fiber 上,等 webServer 出现时自行挂载;因此即使某个组合先加载插件、后才有 HTTP 服务器,设置页照样能出来。可用性探测和后台循环都跑在普通定时器上。
- 用量仪表盘,仅本地 —— token 热力图、按总量或按模型绘制的累计曲线、每次调用采样的输出速度和首 token 延迟。不上传任何数据。
- OpenAI 兼容转发端口 —— 用一个 base URL 加生成的 API key,把这些模型暴露给本机任何其他工具。Chat 历史里助手消息的字符串 reasoning_content 字段会保留下来,供工具后续调用使用,包括显式的空字符串;字段不存在时不会凭空补造。
- EAC 通道 —— 一条共同付费通道,面向 DSHEAC AIO 以及内核声明的 desktop、tauri、web、web-desktop 配置档,模型带 EAC 前缀。它的凭据是密封的;主机准入和服务器端 GitHub 授权(登录并给仓库点 star)仍是两项独立要求。只靠命令行配置档参数拿不到访问权限。
- Kilo 渠道(免密钥免费池) —— 内置 Kilo AI 公共网关的免费模型池(即列表里的 isFree 切片,实时拉取,包含 kilo-auto/free 自动路由);不用账号,不用密钥。模型卡片带 Kilo 徽标。思考强度的工作方式和 EAC 通道一样:用模型自己的档位菜单(Off / Low / Medium / High,默认 High),通过网关统一的 reasoning 参数真实下发 —— Off 已逐家族验证能把思考归零(nemotron、ling、dots、poolside、apodex、cohere);stepfun 和 liquid 端点拒绝关闭思考(HTTP 400),两个自动路由则忽略该参数,所以这些模型的菜单里没有 Off 档。这个池是上游提供的免费额度,其模型卡片写明提示词可能被记录用于改进服务 —— 绝不要发送敏感内容;见免责声明。
- 聚合通道 —— 自带密钥,76 个服务商 —— 新增设置页签,把第三方端点纳入同一份名单:注册表内置 76 个服务商(NVIDIA NIM、OpenRouter、Groq、Cerebras、SambaNova、Gemini、DeepSeek、Mistral、Together、Fireworks、SiliconFlow、Nebius、Chutes、Hugging Face、Cloudflare、Z.ai、Kimi、MiniMax、QwenCloud/Alibaba、Bedrock、Vertex、Vercel AI Gateway、Azure OpenAI、FleetRelay Gateway,外加免账号的 Ollama、LM Studio 和 llama.cpp)。配置某个服务商(需要密钥的填密钥;本地免账号的不用填),它的每个模型就会变成选择器和本地网关里的一行 provider/model。密钥只存在本地 0600 权限的 settings.json 里;API 只回显 hasKey 和 …abcd 指纹。每个服务商支持:启用/停用、Test(列表探测 —— 只验证密钥,不消耗生成)、Discover、base-URL 覆盖、模型列表覆盖和 Remove。OAuth/ADC 类服务商只列出,标注为此处不可驱动。OpenCode 的免密钥免费池刻意不作为注册表条目:它要求每次请求现签一个 session id、v2 客户端的 User-Agent,以及一组官方工具声明,本通道的通用传输层没地方承载这些 —— 列出来只会多一行永远答不出的选择器条目,而那十个模型由插件自带的免费通道原生提供。
- 省 token —— 三件事:工具输出压缩(RTK)在请求发出前就地改写 tool_result 内容(git diff / status / log、grep、find、ls、tree、构建日志、带行号的文件转储、搜索结果列表、重复日志行)—— 实测一个 40 KB 的 git diff 掉了 90% 的字节,模型照样能就它作答;提示词整形提供 caveman 和 ponytail 两个系列,所有档位默认关闭;还有五个本地答案,完全不调用服务商。设置页会报告压缩前后字节数、节省百分比、压缩块数和实测调用数,数据来自用量图表读取的同一批统计行。
- 路由(回退链) —— 把任意通道的模型串成一条命名链,在选择器和网关上以虚拟模型 combo:<name> 出现。fallback 总是从链头开始;round-robin 轮换起点,并在 N 轮内停在那里。某个环节在任何回答增量之前失败,就整条丢弃,调用方察觉不到切换 —— 推理增量刻意不算内容,所以「思考了一分钟,然后撞上配额墙」正是故障转移存在的场景。
- 本地网关原生支持 Anthropic 协议 — 新增 POST /v1/messages(支持流式与非流式,带 thinking/tool_use 块;一轮对话在响应头发出后失败时,会给出真实的 stop_reason 和流内 error 事件)和 POST /v1/messages/count_tokens(本地估算,插件不带 tokenizer);/v1/models 现在会列出所有通道,包括聚合行和链。网关页面新增 Connect a client 区块,按实际绑定的端口和实际要求的密钥,生成可直接复制的配置:Claude Code、Codex config.toml、OpenAI 与 Anthropic SDK、curl,中继开启时还会给出局域网版本。
- 可选视觉效果 — 设置页头部有「视觉效果」开关,默认关闭。水族箱和模型功能照常可用;打开后恢复背景光晕、玻璃模糊和装饰性动效。该偏好只保存在当前浏览器。
- 六个标签页变十一个 — Aggregate、Token savers、Routing、Plugin upgrade、Recommended projects 加入 Free models、EAC models、Free channels、Dashboard、Request log、Gateway 之列;动效(卡片挂载时升起、切换标签页时页面淡入、储蓄表动画、悬停抬升)遵循 prefers-reduced-motion,面板宽度下收为单列。Recommended projects 页面收录三个配套仓库 —— EAC-Desktop、EAC-Pack 和 EAC-design-world —— 每个都有描述、直达链接和 star 按钮,star 数、fork 数和最近推送时间实时读自 GitHub(GitHub 无法访问时留空,绝不编造)。
- 界面名称干净 — 没有乱码,没有上游厂商字符串混进你的模型选择器。

你能得到什么

编辑器模型选择器

| 分组 | 内容 |
| Our Free Model | 从你当前网络出口可用的模型 |
| Our Free Model · region-limited | 上游拒绝为该地区提供的模型,仍然可见,但单独分组 |

网关点名却不路由的模型不会出现在任何一组。它会留在设置页的 Not in the picker 下,附上拒绝原因和探测时间;一旦探测通过,它会自动回到选择器。

设置页 —— Settings → Our Free Model,共七节:

1. 模型清单 — 每个模型的可用性、视觉还是纯文本、上下文窗口、最大输出、每档 effort 实际发送的输出上限、实测首 token 时间,以及按需触发的单次调用基准测试。
2. EAC 通道授权 — 一键 GitHub 登录(浏览器完成,无需粘贴任何东西)、登录名和 star 判定、重新检查按钮和退出登录;授权前模型卡片带锁。免费通道不受影响。
2. 公告中心 — 所有者推送的信息流:未读计数、紧急程度徽章、标记已读(单条/全部)、立即检查按钮、系统通知开关。正文经允许列表渲染 HTML。
3. 用量看板 —— 顶部计数器、17 周 token 热力图、累计曲线(可在 token 与请求数之间切换,也可在总量与任一单模型之间切换)、速度迷你折线图,以及按模型分列的数据表。
4. 本地转发 —— 开启/关闭、绑定主机与端口、复制 base URL、查看 / 复制 / 轮换 API key,还有可直接运行的 curl 示例。
5. 插件设置 —— 总开关、是否暴露区域受限模型、探测间隔、默认输出上限,以及探测到的出口 IP 与国家。
6. 插件升级 —— 已安装/最新版本、检查更新、一键升级(带进度与失败原因)、上次升级记录、热重载按钮与文件监听开关。

首次运行公告 —— 五页引导(前言、模型清单、使用方法、功能说明、动态与升级),确认一次后不再出现,直到文案版本号提升。

安装

⚠️ 不要从 npm 安装这个插件。

npm 上的 dsh-our-free-model 包并非由本仓库发布。
它是第三方重新打包的产物,内容与本仓库不一致: index.js 和 client.js 被改动过,还附带一个本仓库从未有过的 src/channel-visibility.js。安装它会得到一个本项目未审查过的构建,同时失去本仓库提供的签名清单校验与应用内升级。

只按下面记录的方式安装:本仓库的发布包,或本地源码目录。其他渠道的同名包,一律以本仓库为准。

命令行(纯 dsh web)

dsh plugin --profile web add /absolute/path/to/dsh-our-free-model

重启应用一次。--profile 要写你实际使用的配置档名。

DSHEAC AIO / 桌面版 —— 先读这里

部分 DSHEAC AIO 和较旧的桌面包会在启动 web 应用前执行配置档校验,并限制符号链接或目录联接。带这项校验的主机可能报出:

PROFILE_UPGRADE_REQUIRED: offline dependency migration is not yet available

遇到这项校验时,不要用 link: 依赖和目录联接。改用应用内插件管理器,或下面的本地 tarball 流程。以实际主机的日志和安装约定为准,不要假设每个桌面外壳都有这项校验。

要以真实目录手动安装,请使用可信的本地 tarball:

1. 创建 <DSH_HOME>/profiles/<profile>/vendor/。在已审查的仓库里运行 npm pack --ignore-scripts --pack-destination "<DSH_HOME>/profiles/<profile>/vendor",或把本仓库同版本的官方发布 tarball 放到该目录。本地打包不会发布到 npm。
2. 把 "dsh-our-free-model": "file:./vendor/dsh-our-free-model-2.7.3.tgz" 加到配置档的 package.json dependencies 里,再用宿主自带的安装流程解析它。文件名里的版本号要和本仓库 package.json 保持一致;不要用 npm registry 上同名的版本替代。
3. 把 "dsh-our-free-model" 追加到 dsh.profile.bundles。

如果安装后的字节由包管理器掌管,就在 bundle 配置或插件 settings.json 里设 distribution: 'managed'。之后更新交给包管理器处理;应用内升级、公告轮询和热重载都会停用,模型访问不受影响。后续更新时,替换受信任的 tarball 并更新它的依赖路径,这样旧的 lockfile 就无法还原出旧副本。

如果手动复制发布文件,要复制整个包,包括 adapter/、src/、locale/、vendor/magpie/ 的元数据和许可证,以及 vendor/channel-pack/ 的 pack.js、WASM、LICENSE 和 NOTICE。只复制入口脚本会导致频道加载不完整。

不要再往 cordis.patch.yml 里加条目。从 dsh.profile.bundles 引用的 bundle 已经会应用自己的补丁层,重复注册会报错 duplicate loader entry id: our-free-model。

集成包(托管安装)

当插件通过集成包(EAC pack、Mojobox 等)送达时,更新时机和字节都由集成包掌管:安装时用 bundle 配置 distribution: 'managed'(或把同一字段写进 settings.json),应用内更新器、公告源和热重载都会停用——两个写入方同时操作同一个安装目录会把它弄坏;模型通道不受影响。验收测试在 scripts/offline-test.mjs;可入目录的记录(manifest 0.15 / provenance / license / integrity)在 catalog/ 下。

启动桌面应用前先验证

用桌面宿主自带的校验检查你的配置档,不要盲目重试:

node -e "
const g = require('<APP_ROOT>/sidecar/dist/lib/profile-upgrade.js');
const app = '<APP_ROOT>', profile = '<DSH_HOME>/profiles/<profile>';
console.log(g.planProfileUpgrade(app, profile));
g.assertProfileStartup(app, profile);
console.log('startup gate: PASS');
"

预期输出 status: 'compatible'、空的 mismatches 数组,然后是 PASS。
如果只想检查 bundle 组成、不启动应用:

DSH_HOME=<DSH_HOME> dsh --profile <profile> --dump-config | grep our-free-model

你应该只看到一个 id: our-free-model 条目。

安装失败:ERR_PNPM_VIRTUAL_STORE_DIR_MAX_LENGTH_DIFF

这是目标配置档里的 pnpm 状态问题,不是插件仓库的问题(它在插件下载之前就会触发):配置档里现有的 node_modules 由旧版 pnpm 创建,dsh 更新后自带的 pnpm 拒绝继续使用它。关闭 dsh,删掉配置档的 node_modules 和 pnpm-lock.yaml,让 pnpm 重建,然后重新安装:

bat
rd /s /q "%DSH_HOME%\profiles\web\node_modules"
del "%DSH_HOME%\profiles\web\pnpm-lock.yaml"

重新构建后,伴随出现的 Ignoring broken lockfile 警告也会消失。

运行独立服务

仓库还包含一个独立本地服务,不依赖 DSH 就能运行。请使用 Node.js ^22.19.0 或 >=24.0.0。在仓库根目录安装依赖并启动服务: bash
npm ci
npm run start:standalone

首次启动时,服务会打印一个一次性链接,指向本地管理页面。在浏览器中打开该链接即可配置服务。默认端口是 18900,数据存放在 ~/.our-free-model。要改用其他端口和数据目录: bash
npm run start:standalone -- --port 18900 --data-dir ./ofm-data

独立服务有自己的 API key 和账号数据,不会导入插件的凭据或设置。局域网访问、数据迁移和 API 细节见 packages/standalone/README.md。

桌面安装包(macOS,社区构建)

dsh 插件本身无需构建。桌面安装包(Our Free Model 桌面外壳)官方只为 Windows 发布;macOS 安装包是社区构建。源码改动列在 PR #421,可逐行完整审计。该 PR 及其社区发布页提供现成的 Apple Silicon(arm64)构建,你也可以在 Mac 上自行构建(npm --prefix packages/desktop run build;完整流水线在 .github/workflows/build-macos.yml)。

下载后校验校验和(SHA256 列在 PR / 发布页中): bash shasum -a 256 ~/Downloads/Our.Free.Model_<version>_aarch64.dmg

提示「应用已损坏」或无法打开?

社区构建没有用 Apple Developer 账号签名或公证,所以 Gatekeeper 会拦截首次启动。文件其实没损坏——任选下面一种做法:

1. 右键打开:在 Finder 中按住 Control 点击应用图标,选择 打开,在弹窗中再次确认 打开。
2. 移除隔离属性(终端):

bash
xattr -cr /Applications/Our\ Free\ Model.app

3. 如果仍提示已损坏,用 sudo 移除隔离属性(较新的 macOS 版本对未签名应用更严格):

bash
sudo xattr -rd com.apple.quarantine "/Applications/Our Free Model.app"

4. 在系统设置中放行:打开 系统设置 → 隐私与安全性,如果出现 仍要打开 按钮就点击它。

注意:

- 当前社区构建是 Apple Silicon(arm64);Intel Mac 请从源码构建。
- 安装包自带 Node 运行时和独立服务——无需登录,无需 API key。
- 安装包未经 Apple 签名或公证。请只依据上面可审计的源码改动和可复现的 CI 构建记录来判断是否信任,不要依据其他任何东西。

用法选择模型。 打开编辑器里的模型选择器,在 Our Free Model 下任选一项。选择按会话持久保存。

调整思考深度。 同一个菜单里有 Effort,可选 Light、Balanced 和 Deep。级别越高,输出预算里用于思考的部分越多;上限在请求上强制执行,所以差别可测量,不是表面功夫。思考与可见回答共用同一个上限,因此无法关闭思考的模型,整条阶梯的数值都会翻倍(Settings → Our Free Model 里的模型卡片会印出每一档实际发送的数字)。如果回答总被截断,换到 Deep,或者调高设置里每次调用的输出上限。

为其他本地工具提供服务。 打开 Settings → Our Free Model → Local forward,启用它,然后复制 base URL 并生成密钥。支持的路径: text GET /v1/models POST /v1/chat/completions streaming and non-streaming POST /v1/responses POST /v1/messages streaming and non-streaming (Anthropic Messages) POST /v1/messages/count_tokens local estimate; the plugin ships no tokenizer

如果端口被别的程序占用,监听器不会直接退出:它会先在同一端口重试几轮(刚关闭的监听器,或刚移除的 portproxy 规则,会在几百毫秒内释放端口),然后走到下一个空闲端口,并在设置页上说明——requested 18899 is not available, listening on 18900——写回设置的是真实端口。在 Windows 上,最常见的占用者是 netsh interface portproxy 规则(由 IP Helper 提供服务):它在 0.0.0.0 上的监听会让回环绑定失败,报 EACCES 而不是 EADDRINUSE。netsh interface portproxy show all 列出规则,netsh interface portproxy reset 清除它们。

转发端点完整流式输出:除了 data: 帧,正在思考的通道还会周期性发送 SSE 注释帧(一行 :),这样客户端的空闲超时就不会把「上游还在思考」误读成「套接字已死」。思考内容会在 reasoning、reasoning_content 和 reasoning_text 下被识别——网关把同一段思考重复放在其中两个字段下时只计一次——reasoning_details 数组同样识别。思考从不向上游流式输出的模型,在这里也不会发送推理帧;见已知限制。

为网络上的其他设备提供服务。 同一个面板里有 Network access 区块。默认关闭;打开后,中继会绑定一个可路由地址(默认 0.0.0.0),并要求它自己的密钥——与本地密钥分开,这样任何一侧泄露只需轮换那一侧,已经接在本地端口上的工具完全不受影响。中继会把请求重新发到本地监听器,所以模型清单、流式输出和错误语义都与本地端口一致。端口填 0 表示「随便挑一个」(18899 经常(已在一台运行其他服务的机器上占用);面板随后会显示它最终选定的端口和网络地址。开启期间,任何能访问这台机器的人都能用该密钥消耗它的免费额度——只在可信网络上启用,条件允许时用防火墙收窄来源。

通过代理出口发送请求。 Settings → Our Free Model → Egress proxy。可选,默认关闭——关闭时请求直连。两种模式:

- 订阅模式:粘贴 Clash/V2Ray 订阅 URL;插件会在本地拉起 mihomo(自动探测——例如 Clash Verge 的 verge-mihomo.exe——也可以自己设置路径),并内置一个 url-test 组,每五分钟重新测速,把流量发给最快的节点,健康检查会剔除失效节点。订阅 URL 按凭据对待——订阅链接的路径就是它的 token——所以它和两个转发密钥遵循同一标准:只存在本地设置文件里,绝不随请求发送,也绝不在设置载荷里回显。面板只显示打码后的主机名(https://host/…);「Show」和「Copy」从设置页本身取值(GET /egress/url),输入框留空表示「保留已存地址」,「Clear」会删除地址并一并关闭出口。
- 单代理模式:填入 http:// / https:// / socks5:// / socks5h:// 代理地址,所有经手的请求都走它拨号。

静态存储。 mihomo 必须从配置文件里读取该地址(插件会往自己的数据目录写一份一次性的 mihomo.yaml,并从那里拉起 mihomo),所以磁盘上的副本仍是明文。这套处理去掉的是载荷回显、日志行和旁人窥屏——不是同机上的另一个进程。

只接管三类流量:模型推理、模型列表拉取、出口 IP 探测;公告、升级和 EAC 通道保持直连,订阅身份永远不会混进发布通道。

范围。 出口只服务本插件的 opencode 流量,不碰这台机器上的其他东西。它拉起的 mihomo 用插件自己的一次性配置运行:bind-address: 127.0.0.1、allow-lan: false,没有 tun: 段,不设系统代理,不做系统级代理注册,也不建虚拟网卡——监听的是一个临时空闲的环回端口。你的浏览器、shell 工具和其他 IDE 扩展都不会走它;只有本插件发往 opencode 网关的请求会被改道。单代理模式同理:改写发生在插件内部。

实时状态。 出口开启时,设置页会在下方多出三行:当前出口(打码主机 · 模式)、最佳节点(url-test 选中的节点,以及让它胜出的延迟)、opencode 访问(最近一次模型列表往返,单位秒)。节点和延迟是读数,会刷新每 15 秒向 mihomo 自己的控制器查询一次;读不到数据时,这一行显示「measuring…」,出站流量不受影响。

能买到什么、买不到什么:按 IP 的限流和地区限制会跟着出口走;按会话的限流和指纹限制则不管你用哪个出口。切换出口会自动重新探测——冷出口需要几秒才能承载流量,插件会等新出口应答后再重新分组,所以受地区限制的模型会跟着新出口走。下面的 Reprobe 按需做同样的事。

重新检查地理位置。 Reprobe availability(Reprobe)会针对当前出口重新跑一遍可用性检查。切换 VPN 再重新探测,受地区限制的模型会自动在两组之间移动。

接收公告。 一切都是自动的:所有者推送之后,运行中的安装会在一个轮询周期内取到条目(默认 30 分钟,也可以用 Check for new announcements 立即检查)。普通条目弹 toast,urgent 条目打开全屏模态框,两者都会进入公告中心并带未读标记。在那里打开 OS notifications,还能收到系统级 toast。

升级插件。 Settings → Our Free Model → Plugin upgrade → Check for updates → Upgrade now。整个流程都在应用内完成(下载 → 校验 → 备份 → 替换 → 热重载);不用重装,不用重启。升级失败会恢复上一版本,并说明原因。

聚合通道、token 节省器、路由与本地网关

聚合通道:自带密钥接入 76 个服务商

Aggregate 标签页把第三方端点拉进同一份名单。服务商注册表自带 76 个条目(src/providers/registry.json,分类为 free | free-tier | apikey | oauth | local):NVIDIA NIM、OpenRouter、Groq、Cerebras、SambaNova、Gemini、DeepSeek、Mistral、Together、Fireworks、SiliconFlow、Nebius、Chutes、Hugging Face、Cloudflare、Z.ai、Kimi、MiniMax、QwenCloud/Alibaba、Bedrock、Vertex、Vercel AI Gateway、Azure OpenAI、FleetRelay Gateway,以及本地免账号的几个——Ollama、LM Studio 和 llama.cpp。数据来自 free-claude-code 的服务商目录、9router 的服务商注册表,以及本项目新增的自定义网关。FleetRelay Gateway 目前提供 deepseek-v4.1-flash-free 和 gpt-6-astra;它的第三方 T1 候选因等待分发授权而停用。

Once a provider is configured (a key for the ones that need one; the local no-account ones need none), each of its models becomes a catalog row named provider/model: selectable in the model picker and dialable through the local gateway. A provider's own wire format decides how it is served — OpenAI Chat Completions, OpenAI Responses, or Anthropic Messages (the last two for the providers that only speak those).

Keys live only in the plugin's own 0600 settings.json, and the API never echoes them: the page shows hasKey plus a …abcd fingerprint. An absent field keeps the stored key; an empty string clears it. Per provider the page offers enable/disable, a Test button, a Discover button, a base-URL override (needed for Azure, Cloudflare and Vertex, which have no usable default), a model-list override and Remove. Test is a listing probe — it verifies a key through the provider's model listing without spending a generation, and says so when the provider declares no listing. Providers whose auth is OAuth or ADC (Google ADC) are listed but marked not drivable here, because this lane cannot complete those flows.

Token savers: tool-output compaction, prompt shaping and local answers

Tool-output compaction (RTK) rewrites tool_result / role:"tool" content in place before the request is sent: git diff, git status, git log, grep, find, ls, tree, build and package-manager logs, numbered file dumps, Cursor search listings, and runs of duplicate log lines. Measured live: a 40 KB git diff lost 90% of its bytes and the model still answered about the diff. The safety rules: error tool results are never touched, bodies below a size floor are left alone, the replacement is never larger than the input, a filter that throws passes the original through, and the pass is idempotent. It applies to every lane, the local gateway included, and is on by default.

Prompt shaping offers two families with levels — caveman (terse replies: off/lite/full/ultra/wenyan-lite/wenyan/wenyan-ultra) and ponytail (YAGNI-minimal replies: off/lite/full/ultra). They change how the assistant talks, so every level starts off. Injection is idempotent and format-aware (a system message on chat, Anthropic's system including the block-array form and inserted before the last cache_control, instructions/input item on Responses), and it is reversible.

The savings dashboard shows bytes before → after, bytes saved, percent, blocks compacted and the number of calls measured — read from the same stats rows the usage charts use.

Five local answers (ported from free-claude-code, each individually switchable, all on by default) never call a provider: a quota probe (max_tokens:1 plus "quota" → Quota check passed.), shell command-prefix detection (<policy_spec> plus Command: → the prefix, refusing command injection), session-title generation (→ Conversation), suggestion mode (→ the empty string) and filepath extraction (→ a <filepaths> block). The local gateway answers them in-process.

Routing: named fallback chains (combos)

A named, ordered list of model ids across every lane, which appears in the picker and on the gateway as combo:<name> — a virtual model. Two strategies: fallback (always start at the head) and round-robin (rotate the starting point, sticky for N turns). The chain walks its links until one produces output; a link that fails before any content delta is discarded whole, so the switch is invisible to the caller. Reasoning deltas deliberately do not count as content — "thought for a minute, then hit a quota wall" is the highest-value case to fail over. Once an answer delta has been delivered, a later failure ends the turn normally (the single-model checkpoint recovery still gets its continuation attempt first). Capacities are the intersection of the links (a chain advertises vision only if every link has it, and the smallest context window), and a chain whose links are all currently unavailable is stored and marked not usable rather than silently dropped.

Agent Auto is a built-in virtual model with id auto:agent. Select it in the model picker or set it as the model in a compatible client. For each request, it filters out models that cannot handle attached images, the requested reasoning level or the estimated context size, then ranks the remaining models using quota and usage routing. When tools are available, it favors models that have successfully returned tool calls before; these observations are saved locally and expire after 180 days. If the client requires a tool call and a model answers with prose, Agent Auto holds that answer, records the miss and tries another model, so the agent receives either a valid tool call or a clear exhausted-route error. Tool capability observations are shared across requests and kept in the local stats file.

The local gateway: a native Anthropic surface and connect recipes

Two new endpoints: POST /v1/messages (streaming and non-streaming, with correct message_start / content_block_ / message_delta / message_stop framing, thinking blocks, tool_use blocks, a real stop_reason, and an in-stream error event when a turn fails after the headers went out) and POST /v1/messages/count_tokens (a documented local estimate — the plugin ships no tokenizer). Claude Code and the Anthropic SDKs can therefore point straight at the plugin. The existing /v1/chat/completions and /v1/responses are unchanged, and /v1/models now advertises every lane, the aggregate rows and the chains included. A new Connect a client* section on the gateway page renders ready-to-copy recipes generated from the port that actually bound and the key that is actually required: Claude Code environment variables, Codex config.toml, the OpenAI SDK, the Anthropic SDK, curl, and a LAN variant when the relay is up.

One false success was fixed along the way: a turn whose only output was a tool call the caller never declared used to answer as a clean empty success; it now reports what happened, because the token saver's own [full diff: rtk git diff --no-compact] hint can make a model ask for a tool the caller does not have.

How it works
text
index.js host half: adapter registration, catalog + availability probes,
settings/stats stores, webServer API routes, forward lifecycle,
announcement / upgrade / hot-reload wiring
adapter/ kernel seam: the only module in the package allowed to import
@deepseek-ai/* (kernel.js: attribution User-Agent with a literal fallback)
src/adapter.js structural LlmAdapter: providerInfo, listModels, resolveModel,
prepareCall, stream, providerRetryPolicy
src/upstream.js gateway identity: credentials, session/request id minting,
tool fingerprint, endpoint selection per wire
src/stream.js three wire decoders (chat / messages / responses) normalised to
harness StreamChunks, with disjoint token accounting
src/messages.js harness messages -> wire shapes, plus tool-pairing repair
src/effort.js effort level -> output budget
src/forward.js standalone OpenAI-compatible listener (plus the Anthropic Messages endpoints)
src/providers/registry.json aggregate-lane registry: 76 providers, category, auth, wire
format and endpoints
src/providers/index.js registry access and provider/model composite id parsing
src/aggregate.js aggregate lane: catalog rows, credential resolution, key redaction,
listing probes, per-wire posting
src/anthropic.js Anthropic Messages codec for /v1/messages, plus the local token estimate
src/token-saver/index.js compaction pipeline: prompt injection -> tool-output compaction,
byte accounting, fail-open
src/token-saver/inject.js idempotent injection and stripping on the chat / messages /
responses wires
src/token-saver/prompts.js the caveman and ponytail prompt families and their levels
src/token-saver/rtk/ tool-output filters: git diff/status/log, grep, find, ls, tree,
build logs, numbered dumps, search listings, duplicate log lines
src/local-optimize.js the five local answers: quota, prefix, title, suggestion, filepath
src/combos.js fallback chains and Agent Auto: virtual catalog rows, quota-aware ordering, request capability filters and learned tool-call support
src/route/classify.js one failure vocabulary and cooldown policy for every lane
src/trust.js request trust fence for the plugin's routes (connection bridge
+ structural fallback)
src/push.js SSE push hub: arrivals, update availability, upgrade done
src/feed.js remote announcement feed: multi-source fetch, validation, cache,
arrival detection
src/updater.js in-app upgrade: manifest validation, SHA-256 checks, backup,
atomic replace, rollback
src/reload.js self hot reload: mirrors the kernel HMR sequence (cache purge,
re-import, re-register, rollback)
client.js browser half: hand-written ModuleLoader bundle, no build step

Design decisions worth knowing:

- One adapter, two provider routes. The harness groups the model picker strictly by provider route, and the catalog wire has no group/tag/badge field. Publishing a second route is therefore the only way to render a separate region-limited heading — and because the client drops empty groups, the two collapse into one automatically once geography stops blocking.
- Structural adapter, no @deepseek-ai/dsh-llm import. The kernel never checks instanceof, so the adapter is duck-typed. This keeps the plugin from pinning itself to one kernel version and is what lets the same code run on both 0.1.5 and 0.1.7.
- Own JSON store instead of the settings seam. The settings registration API differs between kernels; a private JSON store under DSH_HOME behaves identically on both and keeps the forward key in a 0600 file that never enters any shared settings document.
- The token savers run on the single choke point, and they fail open. Prompt injection and tool-output compaction run after every other mutation has been applied — the fingerprint rename, the effort patch, the sealed lane's pacing hint — at payloadFor(), the one place both the adapter's own dispatch and the local gateway (which dials through the adapter) pass through, so every lane and every recovery segment is compacted. Every step fails open: a saver that throws is skipped, a filter that throws passes the original text through, an unrecognised body shape is left alone — a request must never fail over the thing that was supposed to make it cheaper.
- A combo is a virtual catalog row, so failover is resolved in the adapter. A chain joins the merged catalog as channel:'combo' plus a list of real model ids, which is why the picker advertises it and the gateway dials it by name like any other row; the chain is expanded and advanced inside the adapter's runStream(), so neither caller has to know about failover.

Thinking effort: a level menu, or a budget ladder

Two kinds of model, two kinds of control.

A model that declares thinking levels (its listing carries reasoning_options
with effort.values) gets a level menu: the selected level is sent as-is
in the upstream reasoning_effort field, the same shape the EAC and Kilo lanes
use. That is the 2026-10-17 re-probe result — the lane did ignore the field back on 2026-09-24, but it now validates it: a value outside the set answers 400 invalid_value naming the accepted set (none|minimal|low|medium|high|xhigh|max), and reasoning volume really does move with the level (deepseek-v4.1-flash-free, medians over three samples: none 0, low 793, high 700, xhigh 1405, max 1906). So the menu offers only the levels that model declares; a model whose set has no off rung (GLM 5.3 Flash, whose none does not actually stop it thinking) is not given one.

A model with no level menu (its listing offers only toggle or
budget_tokens, or no reasoning_options at all) keeps the budget ladder:
effort is a hard output-token ceiling, which does bind — recorded reasoning tokens rise monotonically with the level. Inventing a rung the upstream cannot express would claim a capability the model never had.

Window and output ceilings follow the 9router / models.dev convention
(limit.context / limit.output, modalities.input); the upstream listing
publishes an id and nothing else.

Three kernel behaviours that cost real debugging time

These are recorded here because they will bite any provider plugin:

1. providerRetryPolicy() is stored verbatim. Neither kernel resolves it, and
the backoff scheduler reads initialDelayMs / maxDelayMs / jitterRatio off the
top level. Returning them nested under backoff yields undefined * 2ⁿ =
NaN, and the durable session log rejects non-finite numbers — so a recoverable transient failure becomes an aborted turn. Return an already-resolved, flat policy.
2. One interrupted tool call poisons the whole session. A tool call with no
matching result replays as 400 invalid_request_error, after which every
request in that session fails. The plugin repairs pairing before sending, on all
three wires.
3. A service you did not name in inject throws when read as a property, and
ctx.get() answers undefined for one that is merely not provided yet. Both
bit this round: after inject was reduced to ['llm'], typeof ctx.interval ===
'function' threw cannot get property "timer" without inject (ctx.interval is
a mixin over the timer service) and the plugin stopped activating on every
composition. Switching that one to ctx.get('webServer') then answered
undefined — not because the composition had no web server, but because plugins load before the browser half publishes it — so the settings routes were never registered and the dashboard had no data source behind a server that was busy serving the app. The shape that works is ctx.inject(deps, callback): give the services you need their own fiber and let it wait, instead of guessing once at load time.

Why the response is read by body shape, not by Content-Type

Under load this lane answers 200 with application/json over a normal SSE frame
stream. Trusting the header meant await response.text() swallowed the live stream,
JSON.parse failed, and the turn was lost — and because that error carried status: 200, the availability probe read the perfectly working model as unroutable and dropped it from the picker for a round. The first bytes (≤4 KB) are now sniffed and classified, then replayed into the stream, so nothing is buffered and no token arrives late; sniffBody in src/http.js is the only discriminator.

Why the speed panel can say —

An early build published 2 941 tok/s for a lane really doing ~40. The gateway was
not at fault — a raw-read probe shows 64 frames spread over 5.6 s — but the numerator and the denominator described different intervals: one call billed 422 output tokens of which 291 were reasoning that never streamed a single frame, and the window began at the first visible token anyway. Dividing the whole completion by the seconds the answer text took is not a decode rate.

So a rate is published only over a window that can carry it: windowTokens()
removes unstreamed reasoning tokens from the numerator, decodeWindow() rejects
windows too short to time and rates too fast to be real, and both the panel and
the per-model table sum tokens over sum seconds instead of averaging per-call
ratios — one 1 ms window was enough to move a 26-call average by three orders of magnitude. A model whose answer lands in two frames therefore has no measurable output speed, and says so.

Automatic recovery after a long reasoning stream is cut (issue #12)

Every model enables one bounded recovery attempt by default, across the Chat
Completions, Messages and Responses upstream protocols. It requires a first
stream that delivered nonempty reasoning, no answer text or tool call, and then
either reached EOF without a normal terminal frame, or received a normal stop
terminal carrying reasoning only with no answer text — a turn the host would classify as an empty response. Cancellation, a normal ending that already delivered answer text or a tool call, an output ceiling and an explicit upstream error do not trigger recovery.

The plugin sends one new request containing the original input and the
received reasoning as a text checkpoint, asking for the answer directly. This is
not a native upstream resume, and already displayed reasoning is not replayed.
Tools are disabled for the recovery request. A cut after answer text or a tool
call has started is not recovered, to avoid duplicate content or execution.

To avoid another long reasoning pass, the continuation prompt asks for the
conclusion first and at most 800 words. This is a prompt instruction, not a hard
token limit, and the model may ignore it. Recovery is a degraded answer: an
original request for a long or step-by-step analysis may be shortened.

One logical turn makes at most two physical requests: the original and one
recovery. The default total deadline is 900 seconds (15 minutes); recovery gets
at most 300 seconds, bounded by the remaining total time. Recovery output is
capped at 8192 tokens and still respects the user's and model's ceilings. Known
output tokens from the first segment are subtracted from the original budget;
recovery does not start if fewer than 512 tokens remain. The checkpoint is
limited to 131072 characters and must pass a conservative text-byte estimate of
context capacity.
That estimate does not count image tokens and is not an exact tokenizer check.
Exceeding a limit stops recovery. Success requires a normal ending with answer
text that is not only whitespace. Recovery failure returns STREAM_CUT, which
the harness does not resend as a whole turn; user cancellation keeps
aborted / ABORTED.

Set streamRecovery: false in the local settings.json to disable it. An object
can also supply an enabled switch and lower numeric limits; limits cannot
exceed the defaults. There is no model allowlist.

The local dashboard keeps physical requests and logical turns separate.
Upstream requests and request failures count each HTTP request actually sent;
conversation turns, turn failures and recovered turns count one adapter
invocation by its final outcome. A first segment cut short and a successful
continuation therefore show 2 upstream requests and 1 request failure, while
remaining at 1 conversation turn with 0 turn failures and 1 recovered turn.
Each segment still has its own sample with recovery identity, attempt index and
noUsage markers. A shared recoveryId associates those markers to identify
missing usage. Final usage sent to the harness sums only counts the upstream
actually reported; if one segment has no usage report, this is not the full
turn's token total. Reasoning checkpoints are not written to the statistics
file.

Pre-upgrade history has only physical request records, so migrated turn and
failure counts are estimates and the dashboard labels them. Turns recorded
after the upgrade use the final outcome and are exact.
Requests, turns and token totals are lifetime values; speed, first-frame and
heatmap details use the locally retained history window.

One live MiMo V2.6 Flash · Deep request recovered successfully; an earlier attempt failed at the 480-second deadline. This does not establish live coverage of every model or guarantee completion of every long reasoning turn. The v1.3.1 records below describe behaviour before recovery was added.

Verification

Tested on Windows against the live upstream. The table is kept per release round,
and each row says which way it was checked — only the rows marked live upstream or hands-on are behaviour a user sees in the interface.

Issue #12: verification status

Implementation, offline regression and live results are tracked separately in
docs/issue-12-recovery.md. Recovery 99/99,
truncation 35/35, 11 new fingerprint checks and typecheck have passed. Twelve
checks use the real plugin over local HTTP to verify forward success, failures,
client disconnection and stored statistics. The technical record carries the
final full-suite result. In a live MiMo Deep run, the first
stream reached natural EOF at 304.161 seconds; recovery took 32.777 seconds and
produced 4264 characters of answer text, ending with stop, usage and [DONE].
There were two requests over 336.942 seconds. The first segment had no usage
report, so the summary contains only known counts. The earlier failed
480-second attempt remains in the technical record. Real dsh UI and other
models have not been individually accepted.

Historical: v1.3.2 (issues #11, #13, #19, #20, #21)

| Item | Fix | Verification |
| #19 self-update trust chain | Ed25519-signed manifests (public key pinned in the plugin, private key out of the repo), the update channel fully decoupled from feedUrl, manifest base restricted to relative paths | updater-test: four new cases (signature, tamper, foreign key, feedUrl not followed); release-e2e's negative control now refuses at the signature |
| #19 forward surface | localhost binds resolve first and require all-loopback answers; a 1 MB cap (413) on plugin API bodies; the forward endpoint answers 400 for a non-JSON body and 404 for an unknown model | forward-test resolveLoopbackBind cases; offline-test 404 case |
| #20/#21 tools unusable through the forward port (#21's Unknown tool 'bash'/'read'/'grep' is exactly the fingerprint decoy quartet: the lane forces those names to be declared, the model dials them, the forwarded client never registered them) | streaming tool_calls[].index renumbered from 0 (no longer shares the block counter with reasoning); fingerprint-decoy calls suppressed per block on the forward wire (unnamed blocks have their arguments held); a ceiling-cut turn finishes as length on the stream too | forward-test: four streaming/non-streaming cases against the real listener with a scripted lane |
| #20 dsh-side decoy hazard | stream.js keys parallel tool calls without an index by call id; bare argument continuations join the preceding call | existing truncation/fingerprint suites stay green |
| #13 quota experience | 429 removed from the retryable codes (no more three automatic retries of the same wall); a probe round answered by nothing but 429s backs the next periodic round off 30→120 minutes (manual reprobe, egress change and the boot round are exempt); the probe recognizes in-stream error frames inside a 200; the bench button uses the default effort and a realistic client timeout | retry-safety-test assertion updated; probe logic covered by the offline suites |
| #13 concentrated forward quota | 429 no longer auto-retried (above); the forward session key stays derived from user/conversation (deliberate: the lane accounts per session) | behavior unchanged, no new spend |
| #11 heatmap | cell gap removed and corners tightened for a GitHub contribution-graph ribbon; the layout was already column-major week-aligned | client-lint green |
| Other | per-route client timeouts (bench 240 s, upgrade 600 s — the 8 s guard now covers only fast routes); EventSource reconnects with backoff (30 s → 5 min) instead of closing forever; a completed upgrade clears the stale error banner; the speed-scope copy now says what the panel really averages (last 40 calls); a failed store flush is logged once | client-lint; speed-stat green |

All 20 offline suites (including the new forward-test) and tsc --noEmit pass; host-selftest costs real network and real free-lane quota, so it did not run with this round.

Historical: v1.3.1 (issues #8, #9, #10)

| Item | How | Result |
| Both tool-result vocabularies really reach the model | live upstream, host-selftest.mjs step 2 | Same model, same get_weather answer, sent once in the pre-V4 shape (user carrying a tool-result block) and once in the V4 shape (role:'tool'): both finish=stop, and both times the model repeated the 22°C / sunny from the result. Before the fix the plugin did not recognise the first shape at all (src/messages.js never mentioned tool-result), so that request carried neither the call nor its answer |
| The shapes are taken from records, not imagined | every session root on this machine — 40 session.jsonl.zstd across four homes (zstd, which needs splitting frame by frame) | tool/result events appear in exactly two shapes: 362 with role:'user' carrying a tool-result block (V1/V3 format — 336 written by AIO 6.9.3's v3 home, 26 by ~/.dsh's v1) and 27 V4 role:'tool' from the v4 session copies. Both generations are in live use and the plugin read only the latter — issue #9 was right, about a kernel older than the reporter's |
| Projection and pairing repair | offline projection-test.mjs (new, 40 assertions) | Three wires × two vocabularies: each call and its answer reach the wire, keyed by call id; three parallel calls each get their own answer, merged into one user turn on the Messages wire with tool_result first (the same rule the kernel's own adapter uses); images nested in a result survive instead of vanishing (Chat follows with a user turn, Messages nests them inside tool_result); each answer appears exactly once on each wire — counted in the bytes that go out, not by block type, because a block-type assertion cannot see a duplicate; answers with no call id, dangling calls and orphan results are still removed; already-valid history is returned byte for byte |
| pwsh filling the bash slot is accepted upstream | live upstream, probes/shell-slot-promotion.mjs (new) | Declares bash, glob, grep, read, where bash carries pwsh's real schema and no decoy is added: the gateway does not answer FreeTierError, the turn finishes tool-calls, and the call comes back renamed to pwsh ({"command":"Get-Date"}) — a tool the kernel can actually run. Offline, fingerprint-test.mjs (new, 20 assertions) pins no-duplicate declarations, a real bash winning over the donor, and the decoy surviving only when there is nothing to promote |
| A cut stream is no longer a completed turn | offline truncation-test.mjs (new, 22 assertions) | A local HTTP server really does res.end() a stream that never carries a finish token → kind=error, recorded ok=false and flagged truncated; checked on each of the three wires; genuine endings such as message_stop and response.completed are not caught; a turn that finishes without a usage frame is flagged noUsage, so 0/0 stops being indistinguishable from "produced nothing". Re-sending now depends on what had already arrived: a cut that delivered no token at all stays TRANSPORT (retryable), while one that had streamed content becomes STREAM_CUT (outside the retryable set, so the turn fails on the spot) — the reason is the next row |
| What retries cost, measured in a real kernel session | real dsh web kernel, one turn sent from the browser on MiMo V2.6 Flash · Deep | Taken event by event out of the durable session log: llm/retry retry=1 delay=702ms code=TRANSPORT at 304 s, retry=2 delay=1124ms at 608 s, turn/end {kind:"error",code:"TRANSPORT"} at 912 s; stats.json holds three ok:false truncated:true rows of 0/0, 304 s apart; the interface showed turn failed* with the message and the code. The cut on this lane is a function of how long the turn thinks, so a retryable code only billed the same five minutes three times over — which is why content that already streamed is not re-sent |
| Re-reviewing this release found two defects in the above | offline projection-test.mjs section 7 and truncation-test.mjs section 12, plus one forwarded round trip in tui-test.mjs | (1) Once the Messages wire recognised V4's role:'tool', it put the same answer inside tool_result and left it in the ordinary blocks after it — the text twice, and a returned image with its whole base64 payload twice. The message is now skipped once its result is emitted, and reverting that turns the assertion red. (2) response.incomplete, response.failed and response.done end that wire on purpose too; reading only response.completed as terminal turned a turn that hit its output ceiling into a cut stream and burned two retries on it. The status still decides the finish token, and a terminal frame naming no reason no longer erases one a previous frame gave. (3) The forwarded /v1/responses route had never been exercised with tool history; the outbound messages are now compared byte for byte against a stub gateway, call and result both present |
| What a normal ending looks like on this lane | live upstream, probes/stream-terminal-frames.mjs (new) | Raw SSE captured frame by frame: finish_reason:"stop" → the usage frame → data: [DONE] → {"choices":[],"cost":"0"}, identically on two models. So "no finish token ever arrived" is genuinely an anomaly rather than a second normal ending — which is the basis for #10's check |
| No false positives against the real lane | live upstream, host-selftest.mjs, full run | 11 models probed plus plain chat, two tool rounds, three effort rungs and vision input: every call landed on its own normal ending (stop / tool-calls / max-tokens), none read as truncated; the region-gated model still returns REGION_BLOCKED |
| The upstream is documented (#8) | every outbound destination in the code, checked one by one | New section Where the models come from: opencode.ai/inference/v1/ (inference and /models; /zen/v1 before 2026-10-14), this repository's feed/.json (raw first, jsDelivr as fallback), and api.ipify.org / ipinfo.io / ipapi.co (only to read back this machine's egress IP and country code). No account pool, no relay |
| Offline suite | npm test (18 suites) | Green; projection and fingerprint added, still no network and no free-lane quota spent |

v1.2.2 (issues #1–#4, #6)

Each row names how it was checked: offline fake kernel never leaves the machine,
live upstream means the plugin really calls the gateway but runs on a
hand-built cordis context, and real kernel means the plugin was installed and
started inside dsh. The distinction cost this round a ship-blocker — see the last row.

| Check | How | Result |
| Manifest describes the shipped files | offline, build-manifest.mjs --check + release-e2e.mjs | All 26 published files match. Before the fix, main's published 1.2.1 manifest had drifted on 6 files (index.js, both READMEs, src/catalog.js, src/store.js, src/upstream.js) — one more recurrence than the 2 issue #1 reported, after the issue was filed. Regenerated with the version; --check and the new release-e2e.mjs (it installs the real release end to end, with a negative control proving a manifest that lies about one byte is refused) now run in npm test and in CI. This round closed two holes of the same family: digests are now computed over LF-normalised bytes (.gitattributes says * text=auto eol=lf, and an editor on Windows can leave the working tree CRLF while git status stays clean — which is exactly how issue #1 came back), and listed directories are walked recursively (the old one-level scan skipped src/lib/x.js: npm ships it, the manifest does not name it, and installStaged deletes whatever the manifest does not name from every installed copy). Both carry standing assertions, the first of which rewrites all 26 files to CRLF and runs --check |
| Offline suites | npm test (14 suites) | 14/14 pass; no network, no free-lane quota spent. Suite ports come from the ephemeral range (a hard-coded port loses a race with any other process: occupying the one tui used to write down made its own fetch hit somebody else's listener and hang to the runner's timeout, printing six ok lines as the "failure detail"), and the runner puts a 60 s deadline on every suite and says so when one hangs |
| The new tests actually bite | old behaviour patched back in, suite re-run | Reverting the computeMembership guard → picker fails with Cannot read properties of undefined (reading 'state') (2 checks); reverting the response sniff → sniff fails 7 checks. Restoring each fix turns both green again. Every assertion added this round went through the same treatment as a mutation check: 9 fixes reverted one at a time in throwaway copies, 9/9 turned their suite red |
| Probe verdicts → what the picker advertises | live upstream, host-selftest.mjs | 10 listed models → 7 on the main route plus 2 under region-limited, and the forward port's /v1/models follows at 9; deepseek-v4-flash-free (Model is unavailable) left the dropdown with its refusal recorded in the settings page. 5xx / 429 / dropped connections all keep their model |
| A model with no verdict is survivable | offline fake kernel, picker-test.mjs | With a newly listed model whose probe the test holds open, listModels and /summary both answer, the model is advertised and reads availability=unknown |
| Rungs still enforced | live upstream, MiMo V2.6 Flash | light now sends 4096 and the same prompt finished stop at 2 980 tokens; before the change light (2048) ended that same prompt with length |
| Long answers stop being cut | live upstream, probes/long-answer.mjs | balanced sent max_tokens=16384; the request produced 10 164 output tokens (19 914 characters, 194 s) and finished stop. The old 8192 ceiling cut the same turn with length — the symptom in issue #2. The model cards print each rung's real number (default output limit 16K), read in the browser |
| The body decides, not the header (issue #6) | offline fake gateway, sniff-test.mjs | A 200 with application/json over SSE frames streams normally with no error; a Chinese character split across chunks, a 400-frame stream past the 4 K sniff window, a single JSON body, an empty body and an HTML junk body all route by shape. On the old code that same response also made the probe report a working model as unavailable |
| Composition with no web server | offline fake kernel, scripts/tui-test.mjs | With only llm mounted, apply() does not throw, both routes register, a full streamed turn completes, the forward port comes up and rejects keyless requests, and the background loops run on plain unref'd timers. The dashboard half waits, and mounts both routes the moment webServer appears. Not yet verified on a real dsh-tui: neither kernel on this machine (source build 0.1.7-rc.1, AIO 6.9.3) ships a tui profile |
| The plugin really installs and runs | real kernel, dsh 0.1.7-rc.1 web on port 3099 | No did not activate in the startup log; /api/our-free-model/summary 200 (10 models: 6 available, 2 region-blocked, 1 unknown, 1 unavailable), /events streams its hello; in the browser Settings → Our Free Model renders the 10 cards, the Not in the picker group, the thinking cannot be disabled and default output limit 16K tags, with an empty console. The first cut of this round failed here: with inject reduced to ['llm'], reading ctx.interval threw cannot get property "timer" without inject and the plugin stopped activating on every composition |
| Two new problems the review itself caught | targeted repro + standing assertions | (1) Aborting during the head sniff threw the raw AbortError (numeric code 20), which toFailure cannot recognise and downgrades to TRANSPORT — a retryable code, i.e. a hole where a turn the user cancelled could be retried. All three read paths now share classifyStreamFailure, and sniff carries two abort assertions. (2) After committing, another src/http.js edit drifted the manifest, and --check plus the new release-e2e.mjs failed it on the spot - the gate works, and it is also the reminder that any edit means re-running it |
| This round, on the request path: two ways to lose or double-pay a turn | local fake gateway, targeted repro + standing assertions | (1) readHead filled its whole 4 KB window before deciding what the body was, so a short answer the gateway had already finished typing sat in the sniff until the deadline and was then discarded with the cancel — a completed turn reported as a retryable TIMEOUT, which the harness sends again and the lane pays for twice (the repro prints frames delivered: 0). The body is now allowed to declare itself a stream on the first frame, which also stops withholding the first 4 KB of every answer. (2) In-stream errors were classified into llmCode, and toFailure reads code — so every refusal inside a stream arrived as retryable TRANSPORT (re-sending a turn whose partial answer had already been forwarded), and a mid-turn RegionError could never reach the egress re-probe. In-stream errors now go through the same classifyFailure as an error envelope |
| This round, at the settings boundary | offline, picker-test.mjs + effort-test.mjs | probeIntervalMinutes:'abc' made Math.max(1,'abc') = NaN, and Node treats setTimeout(fn, NaN) as 1 ms — a full catalog probe every second. defaultMaxTokens:0 (which is what clearing the settings-page field posts) became min(capacity, 0), cutting every turn to the 512-token floor while the picker went on printing its 4 K/16 K/32 K ladder. Numbers are now coerced both where they are written and where they are read, and a non-positive value means "unset". The same assertions pin the forward listener to a loopback bind (0.0.0.0 is refused with a 400 that says why) and pin that connection admission is read per request: patch the snapshot back in and the late-mount 401 check goes red on the spot |
| This round, three suites that only looked like tests | mutation check (revert one fix per throwaway copy) | retry-safety-test.mjs's four cases all landed on the "model not on this route" early return — it passed model:"our-free-model/test-model-free", and baseModelId strips a label, not a route — so the suite had never sent a request; against real calls its 7 cases now pin the code, whether it may be retried, and whether the region re-probe fired. tui-test.mjs compared the counts of two different populations, so deleting the 120 s loop's unref() stayed green; it now checks each period individually. And the hard-coded port in tui became an ephemeral one after it hung for 180 s against somebody else's listener |
| Usage accounting | offline, retry-safety-test.mjs | A usage object without prompt_tokens_details no longer computes inputTokens: NaN; the forward port answers in prompt_tokens/completion_tokens and reports a refused turn as an error instead of an empty 200. On the Messages wire message_delta carries only the output side, and the old whole-record overwrite zeroed the prompt tokens of every Claude turn; records are now merged field by field |
| This round: a 5xx reason phrase is no longer read as a verdict (the same regression issue #3 was about) | offline, sniff-test.mjs + revert check | stateOf matched its message fallback without regard for the status, and "Service Unavailable" is the reason phrase every reverse proxy answers a 503 with — so an overloaded gateway was read as the gateway naming each model refused, and those models left the picker one at a time (the cost issue #3 removed, back through the message fallback). The fallback now only speaks when the status has not already answered for the gateway; a body that names the model — including type: ModelError under a 5xx — is still a refusal. Four new assertions, two of which go red the moment the old implementation is restored |

v1.1.2 (announcements, in-app upgrades, hot reload, trust fence)

Every v1.1.2 capability was operated for real, including click-through in a
browser and inside the DSHEAC AIO desktop window:

| Check | Result |
| dsh 0.1.7-rc.1 (source build) | Boots clean; picker shows both groups; multi-round tool calling completes |
| dsh 0.1.5-rc.2 (DSHEAC AIO 6.9.3 kernel) | Boots clean alongside the other installed third-party plugins |
| EAC startup gate | Run at install time and after the in-app upgrade: compatible / PASS |
| Model reachability | All 10 catalog models stayed invocable (behaviour at that round: even probe-failed ones remained in the picker; since v1.2.2 a model judged unroutable is not advertised); real chat, multi-round tools and vision input pass on both kernels |
| Real harness conversation | One real turn completed and answered in both dsh web and the AIO desktop window |
| Announcement feed | New items pushed on a local "repository server" arrived within one poll cycle on both surfaces |
| Announcement center UI | 4 items rendered (bold/code/links/lists), urgency badge, unread dots, mark-read single/all |
| Urgent announcement | An urgent push opened the full-screen modal; acknowledging persisted |
| Toast + OS notifications | Live toast on arrival; the desktop window.Notification channel exists (when the WebView2 permission policy denies the request, the UI says so) |
| In-app upgrade | 1.1.0 → 1.1.2 completed on both dsh web and AIO: 23 files downloaded, SHA-256 verified, backed up, replaced, hot-reloaded; /meta reported the new version immediately |
| Upgrade safety | A hash mismatch discards staging, leaves the installed package untouched and logs the failure; upgraded artifacts pass the EAC gate and a full restart |
| Hot reload | Button and API entry points; ESM cache purge + re-register + rollback sequence; generation increments, client notices once via localStorage |
| Client bundle hot swap | Replacing client.js reloaded the browser bundle automatically via the kernel's client-hmr (observed twice) |
| Trust fence | Non-loopback Host / cross-site sec-fetch-site / foreign Origin all 403; cookieless loopback requests 401 (same as the kernel's /api) |
| SSE push | hello/announcements/update/upgraded events verified; EventSource reconnects after a hot reload |
| Effort propagation | light/balanced/deep measured live: reasoning 2048 (budget-truncated) / 3386 / 3522, output rising monotonically |
| Region gating | Region-blocked model surfaces as REGION_BLOCKED and stays in its own group |
| Forward listener | /v1/models, streaming and non-streaming /v1/chat/completions, unauthenticated requests rejected 401 |
| UI strings | No mojibake; the upstream vendor name appears in exactly two places — the repository docs and the announcement body, since #8's disclosure needs a place a user can actually see. The model picker, the settings page and error copy still never name it |
Not verified, so stated plainly: the final OS-level notification rendering
was not visually confirmed — the AIO build's WebView2 permission policy denies
Notification.requestPermission() (the plugin's Tauri notification channel is
present, the plain-browser path works, and the settings page says plainly that
permission was denied). The AIO guard also runs a report-only heuristic scan
that logs one finding for the env-adjacent-URL pattern in src/upstream.js;
it never touches files.

Known limitations

- "No usage cap" means no cap to buy. There is no balance, no plan and no per-token billing; the lane is metered by session rate, though, and hammering it surfaces as 429. The plugin marks the model quota reached rather than hiding it, and the next probe clears the state.
- Some upstream models are slow. nemotron-3.5-lightning-free measured over 30 s to first token in one run. That is upstream latency, and the dashboard reports it rather than hiding it.
- Output speed is sometimes —. A model that answers in one or two large frames, or whose thinking never streams, has no window worth dividing. The panel says so instead of publishing the model's thinking time as decoding speed.
- Thinking is a silent wait, and it shares the output budget. mimo-v2.6-flash-free measured 60–70 s of silence while the lane billed 3024 reasoning tokens and streamed not one reasoning frame; such a turn ends as stop with nothing visible, which clients report as an empty response. The forward endpoint and the LAN relay keep the connection alive with heartbeats — but that protects the connection, not the timeout: the SSE parser skips comment frames, and pi-ai's idle watchdog (streamIdleTimeoutMs, default 300 s) resets only when real content frames arrive, so a stall past 300 s on the wire still fails (issue #34). Only the caller can give it budget: raise the per-call output ceiling past 16k, or use a model the catalogue marks reasoning: false.
- Automatic recovery has request, time and context limits. It handles only reasoning-only EOF and reasoning-only silent stops (a normal stop ending with no answer text) and adds at most one request. Answer text, tool calls, cancellation and explicit errors exclude recovery. A checkpoint request cannot preserve internal upstream state that was never sent, and success is not guaranteed. Usage with a missing report is only the known part.
- Capabilities are what probes can confirm. Anything the public listing and a live probe do not evidence is left unlabelled.
- Source is plain JavaScript. It has to be, to load as a local plugin. Anyone with the folder can read the gateway logic; treat that as an accepted property of this distribution form, not as something obfuscation would fix.
- Desktop installs need a real directory, for the reason given in Install.
- Upgrade and hot-reload trust boundary: as of v1.3.2 the in-app upgrader's trust root is the Ed25519 public key pinned inside the plugin, not "HTTPS to the repository" — a manifest must carry the release key's signature before anything is installed, so a poisoned mirror (jsDelivr included) fails the upgrade instead of executing code. Whoever holds the release private key can push arbitrary code, the same trust model as whoever can push the repository, but a repository account takeover is now a failed-upgrade outage for every user rather than a direct RCE. File integrity is enforced by signature + SHA-256 manifest; content safety by the client-side allowlist renderer and the host's plugin isolation.
- The AIO build's WebView2 permission policy may deny notification permission (measured denied on this machine). The announcement center says so plainly; plain-browser access to dsh web is unaffected.
- The forward port is not required to be 18899. When the port is taken the listener moves to the next free one and writes that port back into the settings (the settings page carries the note). That is deliberate: a port change beats a forward listener that silently stays down. Which process holds the port is the operating system's answer to give; the plugin only reports what it said.
- The plugin routes' auth depends on the composition: with a connection service mounted (dsh web, the AIO desktop) it matches the kernel's /api (the app's own cookie/token); in minimal compositions without one, a structural fence applies (loopback + same-origin), and other local processes can still reach the routes — the same behaviour the kernel has in those compositions. This covers /forward/key and /forward/lan/key too: in such a composition any local process can fetch both keys with one request and spend the machine's free quota from another machine. Treat the two keys as local-process-readable files there (same trust level as settings.json), or mount a connection service at deploy time when you need more isolation.

Development
bash
npm test # every offline suite below, plus the manifest check
node scripts/client-lint.mjs # browser half: copy/style key coverage, bundle executes
node scripts/sanitize-test.mjs # announcement HTML allowlist renderer vs an XSS corpus
node scripts/trust-test.mjs # request trust fence for the plugin's routes
node scripts/feed-test.mjs # announcement feed: parsing, failover, cache, arrivals
node scripts/updater-test.mjs # in-app upgrade: manifests, SHA-256, backup/rollback
node scripts/build-manifest.mjs # release: regenerate feed/manifest.json
node scripts/retry-safety-test.mjs # failures, retry policy and usage counts are durable-log safe
node scripts/speed-stat-test.mjs # no call can average its way into a fake tok/s
node scripts/effort-test.mjs # a rung is the max_tokens that goes out, and is recorded as itself
node scripts/sniff-test.mjs # a 2xx body is read by shape: SSE frames, single JSON, empty, split mid-codepoint, abort
node scripts/release-e2e.mjs # upgrade against the real manifest, and prove a lying one is refused
node scripts/picker-test.mjs # only usable models are advertised, and the picker never goes empty
node scripts/tui-test.mjs # the plugin activates and serves with no web server in the composition
node scripts/host-selftest.mjs # host half end to end against the live upstream

Release manifests must be signed with the release private key (--key <pem-path>
or the OFM_MANIFEST_KEY environment variable); the builder refuses to write an
unsigned one — the in-app upgrader installs only manifests whose signature matches the public key pinned in the plugin, as of v1.3.2. The private key lives in neither the repository nor any artifact; rotating it means rotating the trust root: change the pinned key in src/updater.js and cut a full release.

scripts/probes/ holds the one-off evidence scripts behind the findings report — capability matrix, region gate, the reasoning_effort no-op sampling, budget dialects, dangling tool calls, tool-name charset rules, raw read timestamps (batch-delivery), per-frame arrival against final usage (decode-window), and whether a long answer survives its ceiling (long-answer). Six of them exercise this plugin's own code and run from the repo root (node scripts/probes/pairing-repair.mjs); the rest reach the upstream through a third-party SSE client and assume that checkout's module paths, so they are recorded as evidence rather than offered as a test suite. None of them is wired into npm test, which runs the offline checks above — no network, no free-lane quota spent.

Requires Node ^22.19.0 || >=24.0.0. No install step, no dependencies.

Where the models come from

Two public keyless upstreams, both dialled directly with no third party in the
middle; the fourth destination (the EAC lane) is your own or the maintainer's
deployed gateway, covered in its own section.

The free lane: OpenCode's key-free gateway, https://opencode.ai/inference/v1/
(moved there from /zen/v1 on 2026-10-14; the old path answers 403 FreeTierError
for every free model). Every fact lives in src/upstream.js, and each one was
checked by direct request against the live gateway (paths and fingerprint:
2026-10-14 / 10-16):

| What | Where | Credentials sent |
| Inference | POST …/inference/v1/chat/completions, …/inference/v1/responses, …/inference/v1/messages (per model, see endpointFor) | Authorization: Bearer public — this lane is a public, key-free allowance; the plugin holds no secret of yours |
| Model list | GET …/inference/v1/models | same |
| Announcements and the update manifest | this repository's feed/.json: raw.githubusercontent.com first, cdn.jsdelivr.net as fallback | none |
| Egress region check | api.ipify.org / ipinfo.io / ipapi.co, only to read back your own public IP and country code | none |

The Kilo channel: Kilo AI's public gateway, https://api.kilo.ai/api/gateway
(added 2026-10-06). The code is src/kilo.js; each row was checked by direct
request on the day it was wired in:

| What | Where | Credentials sent |
| Inference | POST …/api/gateway/chat/completions (OpenAI Chat Completions compatible, SSE streaming) | none — the gateway's free pool (the listing's isFree: true models, including the kilo-auto/free auto-router) needs no account, key or login state |
| Model list | GET …/api/gateway/models | none |

On privacy and trust, plainly:

- No account pool, no relay, no reseller. The tables above are the complete
set of destinations the plugin can contact. npm test touches no network at
all — the shared test scaffolding points both keyless upstreams at closed loopback ports by default, so no suite can "incidentally" reach a real gateway — and the only things that do are scripts/host-selftest.mjs and scripts/probes/, which you run by hand. If another source is ever added, this section is updated before the feature is.
- Your prompts, tool results and any attached images go to the upstream that
serves the model you picked, as an ordinary inference request — the same as calling any model API. Nothing else leaves the machine: the usage dashboard's data, settings and the forwarding key all stay in DSH_HOME/our-free-model/.
- Key-free is not unmanaged. The free lane fingerprints clients through
x-opencode- headers, accounts free usage per session, answers 403 for a
disallowed region and 429 once the allowance is spent. Kilo's free pool
states on its model cards that prompts may be logged by the upstream provider
and used to improve their services* (verbatim from the kilo-auto/free card:
"Prompts may be logged by the upstream provider and used to improve their
services. Not suitable for production or sensitive data workloads."). The model
set and the quota policy belong to the upstream and can change at any time; all
the plugin can do is withdraw an unavailable model from the picker and say why.
- This section is repository documentation. Inside the app — picker, settings page, error copy — the upstreams' names still do not appear (the convention is recorded under Verification), except that the Kilo badge's tooltip does say prompts may be logged — that is information you need while choosing a model.

Disclaimer

- This plugin is a client, not a model provider. Every model response is
generated and delivered by the third-party upstreams named above (OpenCode's
Zen gateway, Kilo AI's public gateway, and the EAC gateway you or the
maintainer deploy); the plugin hosts, modifies and filters none of it. Models
can produce wrong, outdated or biased content — verify before you rely on it.
- Free does not mean unlimited, and it does not mean private. The free
allowances are the upstreams' unilateral offer and can be throttled, priced or
withdrawn at any time; Kilo's free pool states that prompts may be logged and
used to improve their services. Never send passwords, keys, personal data,
trade secrets, or anything you would not hand to a third party through this
plugin's free lanes; for production and sensitive work, use your own paid
API.
- You agree to the upstreams' terms by using them. Using a channel means you
accept that upstream's terms of service and usage policy; throttling, bans or
other consequences of violating them are yours to own. The plugin makes no
promise about any upstream's availability, accuracy or continuity.
- The EAC lane is a shared, co-paid offer. Its gateway is deployed by the
repository maintainer and unlocked by a GitHub login plus a star; it is for
authorized users only. Do not try to bypass the gate, resell, or share access — abuse tightens the lane for everyone.
- The plugin is not affiliated with, nor endorsed by, any of the upstreams.
Vendor names and trademarks belong to their owners and are cited only to name
the source.

Security and privacy

- All state lives in DSH_HOME/our-free-model/; usage and settings stay local, nothing is uploaded.
- The aggregate lane's keys never leave the machine. They are written only into the plugin's own 0600 settings.json, requests go only to the provider you configured, and the plugin's API echoes hasKey plus a …abcd fingerprint rather than the key itself; the Test and Discover probes redact the credential out of anything a provider answers back.
- The forward listener binds a loopback address only, 127.0.0.1 by default, and rejects keyless requests. Widening it to a routable interface is refused: POST /settings answers 400 with the reason, and on a composition with no web server a hand-written settings.json simply does not start the listener. That traffic is spent from this machine's free lane; one string in a settings file should not put a whole subnet on it.
- Network access is a second door, not a relaxation of the rule above. The local listener still binds loopback only and still refuses a routable address; reaching another machine takes an explicit switch, and then every request must carry the network key — / and /health included, unlike the local listener, because an unauthenticated liveness answer tells the whole subnet that this machine is here and proxying. The relay carries five whitelisted paths (/v1/models, /v1/chat/completions, /v1/responses, /v1/messages, /v1/messages/count_tokens, each also accepted without its /v1 prefix) and is not a general proxy for whatever else answers on loopback; it swaps the network key for the local one at the door, so the two are never interchangeable; a request that already carries the relay's hop marker is refused with 508, so a relay port equal to the local one cannot spin. The network key is minted by crypto, compared with timingSafeEqual, stored in the same 0600 file, and rotated on its own from the panel.

- Network access is a second door, not a relaxation of the rule above. The local listener still binds loopback only and still refuses a routable address; reaching another machine takes an explicit switch, and then every request must carry the network key — / and /health included, unlike the local listener, because an unauthenticated liveness answer tells the whole subnet that this machine is here and proxying. The relay carries three whitelisted paths (/v1/models, /v1/chat/completions, /v1/responses) and is not a general proxy for whatever else answers on loopback; it swaps the network key for the local one at the door, so the two are never interchangeable; a request that already carries the relay's hop marker is refused with 508, so a relay port equal to the local one cannot spin. The network key is minted by crypto, compared with timingSafeEqual, stored in the same 0600 file, and rotated on its own from the panel. The address to dial is re-read from this machine's interfaces — the panel polls it every 15 s while the relay is on, so a laptop that changed networks, woke from sleep or brought a VPN up is not told where it used to be. A machine commonly holds several private addresses at once (the router's 192.168.x.y, plus whatever a virtual switch, tunnel or VPN adapter has), so the physical interface leads and is the address the panel offers to copy, with the others listed below it; the relay binds every interface, but whether a given address answers from another machine depends on the network that adapter is on.
- The EAC lane's per-user gate lives server-side. The plugin carries only a shared signing secret, and any install can extract it — so the one place a per-person gate can actually hold is the gateway in front of the relay credential. A chat turn needs a token minted after a GitHub sign-in verified to have starred the repository; the star is re-checked on a configurable window (12 h by default), a removed star revokes at the next sweep, tokens are revocable at any time, and per-account rate and concurrency ceilings bound a shared token. The relay key never reaches the plugin, and the token never reaches the browser — the settings page sees a login name and a verdict.
- The forward key is minted at runtime by crypto, compared with timingSafeEqual, and stored in a 0600 file. No hardcoded credential ships in this repository. / and /health answer ahead of the key check because they are liveness probes — they answer only "is it there"; the model roster requires the key.
- The plugin's HTTP routes carry a request trust fence (fixed in v1.1): the plugin's /api/our-free-model prefix outranks the kernel's /api in webServer's longest-prefix dispatch and used to bypass kernel auth. Every request now goes through the composition's connection admission first (exactly the kernel's /api check: cookie/token); compositions without a connection service fall back to a structural fence — loopback Host, cross-site sec-fetch-site refused, Origin/Referer must match the Host authority and port, and a missing or empty Host is refused too (fail closed; there is no fallback to the socket's local address). Measured: foreign Host/Origin 403, cookieless loopback 401. connection is resolved per request, because the browser half provides it only after plugins load — reading it once at apply time silently degrades the fence to its structural layer for the life of the process.
- Announcement HTML renders through a strict client-side allowlist: scripts/sanitize-test.mjs runs an XSS corpus (script injection, event handlers, javascript:/data: URLs, iframe/svg/form, style injection, mangled tags) and asserts all of it is dropped; nothing ever reaches an innerHTML sink. The feed URL is user-overridable, so the renderer treats feed content as untrusted.
- The in-app upgrade integrity chain (signed as of v1.3.2): manifest Ed25519 signature verification (public key pinned in src/updater.js; unsigned or unverified manifests are refused outright, so no mirror can serve an installable forgery) → manifest validation (semver, path traversal, hash shape, base restricted to manifest-relative paths) → per-file SHA-256 + byte size on download → read-back verification of staging → read-back verification after install → backup restore on any failure. The manifest is re-fetched immediately before installing so a stale one can never vouch for different bytes. Boundaries in Known limitations.
- The update channel is fully decoupled from feedUrl (v1.3.2): the setting redirects the announcement feed only, never the upgrade manifest — previously one settings value could point the update channel at any server with self-consistent hashes, turning "wrote a config field" into "executed arbitrary code in the host process". The announcement override itself is now restricted to https (loopback http excepted, so a locally-hosted mirror and the test suites still work) and may not carry credentials. The target host is not restricted: any https address may serve as feedUrl, and the plugin polls it on the feedPollMinutes period — so in a composition without a connection service (see the auth note above), a local caller who can edit settings can point the process at any external address indefinitely. This is trusted the same way as the plugin's other outbound calls: someone who can write settings.json can already install code.
- The forward listener binds loopback by resolution, not by spelling (v1.3.2): a hostname such as localhost is resolved with dns.lookup first and every answer must be loopback; the listener binds the resolved IP — so a hosts file or enterprise DNS pointing localhost at a routable interface can no longer pass the check while the listener hands the lane to the subnet.
- Uninstalling removes the bundle entry; the plugin leaves no patches behind. Its data directory is plain JSON you can delete.

License

MIT — see LICENSE.

This project is an independent plugin and is not affiliated with, endorsed by, or
sponsored by any model provider. Using it to reach free tiers is subject to those
providers' own terms; check them before deploying anywhere beyond your own machine.
预览分享卡片长什么样 ↓crackerghost/DSH-GOD
分享 · DeepSeek Harness Hub
crackerghost/DSH-GOD

免登录免密钥选用免费模型并实时探测可用性

这是你的仓库?认领条目

回到顶部