<div align="center">
  <p><a href="./README.md">English</a> &nbsp;·&nbsp; <b>简体中文</b></p>

  <img src="icon.svg" alt="OpenCode on DeepSeek Harness" width="112" />
  <h1>dsh-opencode-patch</h1>
  <p><b>OpenCode on DeepSeek Harness</b><br />网关来源头 · 会话亲和 · 免费层工具回退 · 实时双模式额度计量</p>

  <p>
    <a href="https://www.npmjs.com/package/dsh-opencode-patch"><img src="https://img.shields.io/npm/v/dsh-opencode-patch.svg" alt="npm version" /></a>
    <a href="https://www.npmjs.com/package/dsh-opencode-patch"><img src="https://img.shields.io/npm/dm/dsh-opencode-patch.svg" alt="npm downloads" /></a>
    <a href="https://github.com/viztor/dsh-opencode-patch/actions/workflows/ci.yml"><img src="https://github.com/viztor/dsh-opencode-patch/actions/workflows/ci.yml/badge.svg" alt="CI" /></a>
    <a href="https://github.com/viztor/dsh-opencode-patch/actions/workflows/release.yml"><img src="https://github.com/viztor/dsh-opencode-patch/actions/workflows/release.yml/badge.svg" alt="Release" /></a>
    <a href="https://github.com/viztor/dsh-opencode-patch/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/dsh-opencode-patch.svg" alt="license" /></a>
    <a href="https://nodejs.org"><img src="https://img.shields.io/node/v/dsh-opencode-patch.svg" alt="node" /></a>
    <a href="https://www.typescriptlang.org"><img src="https://img.shields.io/badge/TypeScript-strict-3178C6.svg" alt="TypeScript" /></a>
    <a href="#-quick-start"><img src="https://img.shields.io/badge/DSH-host%20plugin-4D6BFE.svg" alt="DSH host plugin" /></a>
    <a href="#-quick-start"><img src="https://img.shields.io/badge/config-zero-8B5CF6.svg" alt="zero-config" /></a>
    <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs welcome" /></a>
  </p>

  <p>
    <a href="#-快速开始">快速开始</a> ·
    <a href="#-修复的问题">修复的问题</a> ·
    <a href="#-界面">界面</a> ·
    <a href="#-配置参考">配置</a> ·
    <a href="#-故障排查">故障排查</a> ·
    <a href="./CHANGELOG.md">更新日志</a> ·
    <a href="./CONTRIBUTING.md">贡献指南</a>
  </p>
</div>

---

`dsh-opencode-patch` 是一个 DeepSeek Harness 宿主插件，让 **OpenCode Zen 与 Go** 模型在 DSH 内持续可用。无需遭遇网络拒绝、Cloudflare 挑战、权益不匹配或隐形限制，即可接入 `claude-sonnet-4-5`、`gpt-5.4`、`gemini-3.8-flash`、`deepseek-v4.1-flash`、`muse-spark-1.3-contributor-free`、`qwen3.8-flash` 以及 OpenCode 目录中的其余模型。

**默认状态。** 把 DSH 直接指向 OpenCode、不装任何插件，模型是**完全不能用**的——不是某一种形状不能用，而是全部：

- **请求不会被识别为"编码 agent"发出的。** DSH 发的是自己的 `User-Agent`，不带 `x-opencode-client`、`x-opencode-project`，也不带 `x-opencode-session`；网关看到的就是一次匿名 SDK 调用：免费层模型返回 `403 FreeTierError`，流量还要受厂商对"不表明身份的客户端"所设的滥用规则约束。
- **没有会话 id 的一轮会被拒、或失去关联。** 每一轮都必须带一个稳定的 `ses_…` 形状的 id，而 DSH 在子代理、后台评估以及 **Auto Review** 这类模式下会完全省略 `sessionId`。
- **免费层的 `/responses` 请求体缺少 `read` 与 `bash` 会被拒**，而 DSH 是刻意不发这两个工具的。
- **目录很单薄。** 裸 `/models` 列表只有 id，几乎没有别的信息，所以选择器里是一堆没有名字、没有价格、没有上下文窗口的行。

**这些能用之后剩下的**是另一个问题，而且与账号无关：OpenCode 的模型分布在**四种不同的 API 形状**上，而 DSH 是按 provider 行选择传输协议的——一条路由只能说一种形状。所以走 Responses / Messages / Mistral 的模型在基础问题解决之后就会返回 `500`。这正是插件**按模型而不是按 provider** 路由的原因。

插件在网络层补齐所有缺失的协议要素——**且仅针对 OpenCode 路由**（`opencode` / `opencode-go` / `opencode-responses` / `opencode-anthropic` / `opencode-mistral`）。其余全部流量（DeepSeek、OpenAI、Anthropic、GitHub）原样通过。

**三条主线。**

1. **最小侵入。** 不做全局补丁：只拦截本插件声明的路由，其他厂商——DeepSeek、OpenAI、Anthropic、GitHub——的请求原样通过。计量器离"彻底消失"只差一个开关，而不是离"勉强忍受"隔着一个页面。
2. **原生 DSH 插件。** 一个 `apply()`、一张绑定自己命名空间的设置卡、用 `ctx.inject` 声明服务、按宿主自己的方式填充插槽，文案随宿主语言切换。没有私有钩子，不改宿主代码。
3. **OpenCode API 兼容。** 网关真正检查的那些特征在网络层还原——与 CLI 同形状的 `ses_…` 会话 id、官方来源头、父会话血统，以及免费层的 `read`/`bash` 工具 schema 回退。

**亮点**

- 🔑 确定性的 `ses_<12hex><14base62>` 会话哈希，跨轮次、子代理与分叉保持 KV 缓存亲和
- 🌐 网关来源恢复——`User-Agent`、`x-opencode-client`、`x-opencode-project`、父会话谱系
- 🧰 免费层 `read` + `bash` 工具 schema 回退，让 Zen 免费模型不再报 `403 FreeTierError`
- 📇 基于 models.dev 的模型目录，带离线预置与后台 SWR 刷新——名称、上下文窗口、价格
- ⭕ 实时双模式停靠栏计量——Go 额度环（5 小时 / 每周 / 每月）或 Zen 按量计费胶囊，外加会话消耗与模型费率
- 🕵️ 严格凭据隔离——Zen 密钥（`oc_sk_…`）绝不查询 Go 额度端点

**目录**

1. [快速开始](#-快速开始)
2. [修复的问题](#-修复的问题)
3. [支持的模型与协议](#-支持的模型与多协议路由)
4. [界面](#-界面)
5. [配置参考](#-配置参考)
6. [故障排查](#-故障排查)
7. [兼容性与验证](#-兼容性与验证)
8. [深入解析](./docs/deep-dive.md) — 协议内部实现，已移出本 README
9. [署名与许可](#-署名与许可)

---

## 🚀 快速开始

**1. 安装**到你的 DSH Web profile（Node 24+）：

```sh
cd ~/.dsh/profiles/web
npm install dsh-opencode-patch
```

包名在所有用到它的地方都是 `dsh-opencode-patch`：你安装的依赖、profile 里的 bundle 条目，以及宿主解析的那一行。

**2. 启用 bundle**——把该包加入 profile 的 `dsh.profile.bundles` 数组：

```jsonc
// ~/.dsh/profiles/web/package.json
{
  "dependencies": {
    "dsh-opencode-patch": "^0.12.0",
  },
  "dsh": {
    "profile": {
      "bundles": ["dsh-opencode-patch"],
      "patchReload": "live",
    },
  },
}
```

**3. 添加凭据**，让额度计量能解析到密钥——在 DSH Credentials 或你的环境变量中存储 `OPENCODE_GO_API_KEY`（Go 订阅，`sk-…`）和/或 `OPENCODE_API_KEY`（Zen 按量计费，`oc_sk_…`）。

**4. 重启 `dsh web`。** 宿主 bundle 只在启动时导入（`hmr root: []`），因此要重启才会加载 `lib/index.mjs`；客户端 UI 变更（`lib/client.js`）只需刷新浏览器。

完成——**OpenCode 补丁设置**卡片会出现在 _Settings → Plugins_ 下，一旦有 OpenCode 模型激活，计量表就会挂载到输入框停靠栏。

---

## 🔌 修复的问题

| 没有补丁时 | 使用 `dsh-opencode-patch` 后 |
| :-- | :-- |
| Zen 免费模型报 `403 FreeTierError` | 自动恢复**网关来源头与工具回退** |
| 一条路由只能说一种形状——基础问题解决后，其余模型返回 `500` | **逐模型路由**——按目录里每个模型自己声明的 SDK 派发到对应端点 |
| 会话 ID 被拒并返回 `400 MissingSessionID` | **确定性的 `ses_…` 会话哈希**与跨轮次亲和 |
| 子代理丢失对话上下文 | **父会话跟踪**（`x-opencode-parent-session-id`、`x-parent-session-id`） |
| Auto Review 调用报 `TRANSPORT: Connection error` | **回退会话轮次捕获**，在评估调用之间保留轮次状态 |
| 额度与余额不可见 | **实时双模式停靠栏计量**，显示 Go 额度或 Zen 按量计费 |
| Zen 密钥在 Go 用量上触发 `403 EntitlementError` | **严格凭据隔离**，让 Zen 密钥不接触 Go 端点 |
| 所有项目共用一个 `"global"` 遥测桶 | **动态工作区归属**，由当前 `session.header.cwd` 解析 |
| 网关 `/models` 返回截断且无名称的列表 | **models.dev 补全**：显示名、上下文窗口与价格 |

---

## 🧭 支持的模型与多协议路由

**两种端点，两种凭据形状。** OpenCode 的目录在两个层级上定义端点：**provider 默认端点**——Zen 是 `https://opencode.ai/zen/v1`，Go 是 `https://opencode.ai/zen/go/v1`——以及**逐模型的推理端点**，目录里设置时会覆盖默认值（CLI 读 `model.api.url`，取不到就回落到 provider 默认）。插件**两个都不钉死**：它匹配的是 `opencode.ai/zen` 这个标记，所以走默认端点的模型和走自己端点的模型都会被补丁覆盖。

这些端点的认证有**两种**，因为它们说三种线格式：OpenAI 那两个平面发 `Authorization: Bearer <key>`，Anthropic 平面发 `x-api-key: <key>`。插件**两种都读**——先 `Authorization`，再 `x-api-key` / `api-key`——所以凭据无论由哪个平面带过来都能被捕获，Go 额度查询也仍然找得到密钥。

网关提供四种线路家族，而 **OpenAI 自己就占了两种**：Chat Completions 与 Responses 是**两种不同的线路格式**，不是同一个东西的两个名字。需要 Responses 的模型在 completions 端点上不工作，而网关**不会回退**——它直接返回 `500`。

DSH 能说其中三种，所以插件路由这三种，**故意不提供第四种**（Google Generative AI）。见下表。

### 1. OpenCode Zen (`provider: opencode`) — 按量计费与免费额度

- **Anthropic Messages** (`https://opencode.ai/zen/v1/messages`)：`claude-sonnet-4-5`、`claude-opus-4-7`、`claude-haiku-4-5`、`qwen3.8-flash`
- **OpenAI Responses** (`https://opencode.ai/zen/v1/responses`)：`gpt-5.4`、`gpt-5.2`、`gpt-5.1-codex-max`、`muse-spark-1.3`、`space-bunny-free`，以及免费层的 `muse-spark-1.3-contributor-free`
- **OpenAI Chat Completions** (`https://opencode.ai/zen/v1/chat/completions`)：`deepseek-v4.1-flash`、`kimi-k2.5`、`kimi-k3`、`minimax-m2.5`、`glm-5.2`，外加免费层的 `nemotron-3-ultra-free`、`ling-3.0-flash-fin-free`、`mimo-v2.6-flash-free`
- **Google Generative AI** (`https://opencode.ai/zen/v1/models/*:streamGenerateContent`)：`gemini-3.8-flash`、`gemini-3.1-pro`、`gemini-3.5-flash-lite`

### 2. OpenCode Go (`provider: opencode-go`) — 订阅额度

- **OpenAI Chat Completions**（`https://opencode.ai/zen/go/v1/chat/completions`）：`deepseek-v4-flash`, `deepseek-v4-flash-vision-exp`, `deepseek-v4-pro`, `deepseek-v4.1-flash`, `glm-5.2`, `glm-5.3`, `glm-5.3-flash`, `hy3`, `hy4-preview`, `kimi-k2.6`, `kimi-k2.7-code`, `kimi-k3`, `longcat-2.0`, `longcat-2.5-preview-free`, `mimo-v2.5`, `mimo-v2.5-pro`, `mimo-v2.6-flash`, `mimo-v2.6-pro`, `qwen3.6-plus`, `qwen3.7-max`, `space-bunny`, `space-bunny-free`, `step-5-preview-free`
- **OpenAI Responses**（`https://opencode.ai/zen/go/v1/responses`）：`gpt-5.6-luna`, `gpt-6-luna`, `grok-4.5`, `grok-4.6`, `grok-4.7`, `muse-spark-1.2-contributor`, `muse-spark-1.3-contributor`
- **Anthropic Messages**（`https://opencode.ai/zen/go/v1/messages`）：`claude-haiku-5-5`, `minimax-m2.7`, `minimax-m3`, `qwen3.7-plus`, `qwen3.8-flash`, `qwen3.8-max`
- 由实时三窗口额度计量监控（5 小时滚动、每周、每月）。完整集合随内置目录发布——参见[权威模型目录](#7-权威模型目录双本地预置--实时-swr-更新)。

### 3. 模型的协议是怎么定的 —— 以及为什么你什么都不用配

Zen 的 provider 级 SDK 是 `@ai-sdk/openai-compatible`。models.dev **只在该模型需要不同 SDK 时**才逐模型标注 `provider.npm` —— 所以这个字段的**存在本身就是信号**，它决定走哪条线路协议。没有任何手工匹配：

| models.dev `provider.npm` | 模型数 | 协议 | 服务自 |
| :-- | --: | :-- | :-- |
| _（缺失）_ | 28 | OpenAI Chat Completions | 你配置的路由 |
| `@ai-sdk/openai` | 30 | OpenAI Responses | `opencode-responses` |
| `@ai-sdk/anthropic` | 18 | Anthropic Messages | `opencode-anthropic` |
| `@ai-sdk/google` | 7 | _（DSH 无此协议）_ | **不提供** |

数量为 `models.dev` 中 `opencode` 的 83 个**在用**模型，统计于 2026-10-05 —— 其余已废弃或下架，补丁只内置网关仍在服务的部分。同样这 83 个模型离线内置在 `src/catalog-data.ts`，因此在第一次 catalog 刷新之前它们每一个路由都是正确的；用 `pnpm run catalog:shim` 重新生成该文件。

插件把这两条内部路由同时挡在模型选择器和 _Settings → Models_ 之外。**三条性质今天就成立，还有一条尚未成立：**

- **你的挑选仍然有效。** 选择器显示的就是你列出的模型，每个模型都会**自动匹配到正确的协议**。OpenCode 的模型分布在三种形状上——OpenAI Chat Completions（默认）、OpenAI Responses、Anthropic Messages——需要后两种的模型会被派发到对应路由。不需要手工声明第二条路由。
- **永远不会提供跑不通的模型。** 那 8 个 `@ai-sdk/google` 模型会从"获取可用模型"**和** `opencode` 路由自己的报告中**双双剔除** —— DSH 没有对应协议，选了只会失败，而且行里没有任何东西能告诉你原因。
- **一份凭据。** 所有路由模型共用你已配置的 `opencode` key，经凭据服务解析，**不会重新问你要**。

**如果你更想自己声明路由**：`opencode-responses`（以及你用上 Anthropic 平面模型后的 `opencode-anthropic`）可以写进 profile 的 `llm-pi-ai` `providers` 块里，并列出它服务的模型。声明是一个选择，不是要求：

```yaml
opencode-responses:
  api: openai-responses
  baseURL: https://opencode.ai/zen/v1
  headers:
    authorization: Bearer unused # 由 fetch 补丁换成你的 key
  models:
    - id: muse-spark-1.3-contributor-free
      name: Muse Spark 1.3 Free
      contextWindow: 1048576
      maxTokens: 131072
      input: [text, image]
```

**这条路由由插件替你注册**，所以上面那段配置是可选的。`responses-provider.ts` 会在进程内挂载宿主自己的 `llm-pi-ai`：隔离 authorization 与 settings 两个接缝，并在一层只转发 adapter 注册的 `llm` 门面之后挂载——因此它既不会重复登记整个 catalog 目录、settings 命名空间，也不会多注册任何一条 sign-in flow。这一版是对着真实 harness 验证的，不是桩：路由注册成功，宿主自己的 41 条 catalog 与 41 条 sign-in flow 原封不动，卸载时该路由被撤回。

这些路由继承**你的**凭据：每一条都沿用你 `opencode` 路由已经声明的同一个 `apiKeyEnv`，所以自定义的 key 引用不需要你再说一遍。你在 profile 里已经声明过的路由会被尊重——插件不会覆盖你的模型列表。

若宿主没有已加载的 `llm-pi-ai` 条目，插件不会注册任何东西，只会在日志里说明；此时请在 profile 里显式声明该路由。

### 4. 覆盖的执行模式

| 模式 | 补丁做什么 |
| :-- | :-- |
| **交互式多轮对话** | 通过每会话稳定的 `ses_…` id 提供 KV 提示缓存亲和 |
| **子代理与分叉** | 子、父会话都参与哈希；谱系由父级请求头携带 |
| **Agent 团队** | 跨编排轮次保留共享工作区的归属信息 |
| **实验性 Auto Review** | 即使后台审计调用省略 `sessionId`，仍会得到确定性的轮次状态、请求头与工具回退 |

---

### 5. 目录：合并了什么，谁说了算

模型清单由四个来源拼成，每个来源回答的是不同的问题：

| 来源             | 回答                                           |
| :--------------- | :--------------------------------------------- |
| 内置 shim        | 一次网络请求都还没有时能提供什么——冷启动、离线 |
| models.dev       | 规范参数、价格与显示名                         |
| 网关的 `/models` | **这个账号**实际能用什么                       |
| 发现装饰         | `discoverModels` 该回答什么                    |

合并是**增量式**的：适配器给的行保留，规范行只在缺失时追加，只有当按 provider 记录的退役名单点名某个模型时才省略它——那是事实，不是猜测。显示名与价格取自 models.dev，因为网关列表只给 id，别的几乎没有。

有两点你在使用中会注意到。**冷启动永远不为空**——内置 shim 在第一次刷新前就能回答，选择器立刻有模型可选。而且**models.dev 里没有条目的模型照样出现**，费率位置显示 `—`：既不猜，也不会退回去显示上一个模型的名字。

两个生成文件都在 CI 里校验（`catalog:shim`、`limits:shim`）；厂商数据变动时它们以非零码退出——这是唯一能发现 models.dev 在你背后变了的办法。

→ 协议路由、合并与界面的技术细节： [`docs/protocol-routing-and-merge.md`](./docs/protocol-routing-and-merge.md) · [English](./docs/protocol-routing-and-merge.md)

## 🖥 界面

两个界面，同一个读数。两者都是纯视图，数据来自宿主。

| 界面 | 位置 | 显示什么 |
| :-- | :-- | :-- |
| **计量表触发按钮** | 输入框停靠栏，模型选择器旁 | Go：圆环 + 该窗口的百分比。Zen：本会话消费额，**不画圆环** |
| **计量表面板** | 点击触发按钮 | 三个窗口、月度额度、本会话消耗、Zen 余额、操作链接 |
| **设置卡片** | _Settings → Plugins → OpenCode 补丁设置_ | 八个控件，上方另有一份实时 Go 用量摘要 |

悬停触发按钮解释它自己的数字；点击打开面板。下面逐节展示每个界面，逐状态的行为见 [docs/quota-meter.zh-CN.md](./docs/quota-meter.zh-CN.md)。

### 设置卡片 — _Settings → Plugins → OpenCode 补丁设置_

三个分区共八个控件——都是用户真正会做出的决定。所有控件都基于平台自身的原语渲染（`Switch`、`Tag`、`Button`、宿主 token），每个配置项都带提示以及恢复默认的操作。

| 分区 | 控件 | 默认 | 作用 |
| :-- | :-- | :-: | :-- |
| **网关请求** | 恢复 User-Agent（默认开启） | `on` | 恢复官方 OpenCode CLI 的 `User-Agent`，使 Cloudflare WAF 校验通过 |
| **网关请求** | 注入客户端来源头（默认开启） | `on` | 在网关流量上注入 `x-opencode-client`（以及来源请求头集合） |
| **网关请求** | 附带工作区项目标识（默认开启） | `on` | 用当前文件夹名标记 `x-opencode-project`；关闭则省略该请求头 |
| **模型与免费额度** | 使用 models.dev 补全模型列表（默认开启） | `on` | 将规范参数、显示名、价格与当前免费模型合并进列表**以及**原生 DSH 模型发现 |
| **模型与免费额度** | 自动补全核心工具（默认开启） | `on` | 补上免费层 `/responses` 请求体所需的 `read` + `bash` schema |
| **配额计量** | 开启输入区用量计量（默认开启） | `on` | 在输入框停靠栏挂载实时计量表——Go 额度环，或 Zen 会话消耗 |
| **配额计量** | 显示会话消耗与模型费率（默认开启） | `on` | 追加显示本会话累计花费与当前模型每百万 Token 费率 |
| **配额计量** | 凭据来源 | `auto` | 多个密钥同时可用时以哪个为准：**自动** · **优先实时请求** · **优先已声明密钥** |

覆盖类配置项（字面字符串、标记、路由列表）刻意只留在配置里，让默认值适用于所有有文档记载的配置——参见[配置参考](#-配置参考)。

卡片在控件上方还带一份**实时的 Go 用量摘要**——和输入框计量表同一个读数，但用官方控制台的排版：每个窗口一行，**剩余**单独放在右侧。

```
Go 用量                                             状态正常
● 5 小时   0% 已用 · 4小时58分钟后重置              100% 剩余
● 每周    28% 已用 · 3天2小时后重置                  72% 剩余
● 每月    14% 已用 · 11月7日8点55分重置              86% 剩余
月度额度  mimo-v2.6-pro · Go $60 · Plus $120              ⓘ
```

「剩余」是百分比，不是美元数：`/usage` 每个窗口只给一个百分比、完全没有余额接口，而套餐档位也无法探测——任何美元余额都会是猜的。这一点由 ⓘ 说明。读不到配额时它明说读不到，并且不打印任何数字，而不是打一排会被当成真实读数的 `0%`。

### 输入框停靠栏计量表

计量表挂载在 `conversation.composer.dock` 中 DSH 原生 `ContextMeter` 旁。逐状态的行为说明（触发按钮的三种状态、两个面板、每一行的含义、三种「没有数字」的区别）见 [`docs/quota-meter.zh-CN.md`](./docs/quota-meter.zh-CN.md)。

```
┌─────────────────────────────────────────────────────────────────┐
│ Type a message...                                               │
│                                                                 │
│ [+] Attach                            [DeepSeek V4.1 Flash ⌄] [⬆]│
└─────────────────────────────────────────────────────────────────┘
   [ ⭕ 73% Context ]   [ ⭕ 42% Go Quota ]   ← when OpenCode Go is active
   [ ⭕ 73% Context ]   [ ⭕ $0.00 ]            ← 使用 OpenCode Zen 时
```

#### 模式 A — OpenCode Go (`opencode-go`)

- **自适应环**：实时 SVG 环，显示能回答问题的那个窗口。**已经用尽**的窗口——受限或到达上限——优先显示，从宽到窄（每月 → 每周 → 5 小时），因为月上限能解释一次拒绝，而 5 小时窗口解释不了。否则显示 **5 小时**窗口：它最早重置，是还能行动的那个。**免费模型**上环形以灰色空心显示——套餐上限与一笔无法产生的账单无关，对着 `$0.00` 亮红色会被读成"你的钱用完了"。
- **语义色**：低于 80% 为绿色（`--dsw-alias-state-success-primary`），≥80% 为琥珀色（`--dsw-alias-state-warn-primary`），达到上限为红色（`--dsw-alias-state-error-primary`）。
- **点击面板**：三行窗口，各带实时重置倒计时与一条进度条；一行会话消耗，写的是**当前选中的**模型；一行 Zen 余额；一条受限告警；以及可直接处理的链接。悬停触发器显示 `Tooltip`，**列出全部三个窗口**（`5 小时 11% · 每周 33% · 每月 16%`）——触发器只能印一个数字，而下一个问题永远是"另外两个呢？"。
- **目录里没有价格的模型显示 `—`，不是 `Free`。** 面板始终显示选择器上的模型名，即使 models.dev 还没有它的条目（目录按计划同步）——显示上一个模型的名字比显示没有价格更糟。

```
┌──────────────────────────────────────────────┐
│  OpenCode Go                          [Go Plan]│
├──────────────────────────────────────────────┤
│  • 5 hours        ███████░░░░░░░░░    42% │
│    Resets 3h 12m                             │
│  • Weekly         ███░░░░░░░░░░░░░    18% │
│    Resets 5d 8h                              │
│  • Monthly        █████████████░░░    65% │
│    Resets 22d 4h                             │
├──────────────────────────────────────────────┤
│  月度额度                                    │
│  mimo-v2.6-flash              Go $60 · Plus $120│
├──────────────────────────────────────────────┤
│  当前会话消耗                                │
│  deepseek-v4.1-flash · $0.15 / $0.6 per 1M   │
│                                        $0.42 │
│  可用 Zen 余额                               │
│  请求将自动从 Zen 余额中扣除            就绪 │
├──────────────────────────────────────────────┤
│  ⟳ 更新于 08:30            升级套餐  控制台 ›│
└──────────────────────────────────────────────┘
```

**月度额度，以及为什么它是总额而不是余额。** 三个窗口是某个模型月度**美元**额度的比例——官方原话：_"Usage limits are defined as monthly dollar amounts… 5-hour — 20% of the monthly limit; weekly — 50%; and monthly — 100%."_ 所以计量表把这笔额度打印在窗口下面：`mimo-v2.6-flash` 是 `Go $60 · Plus $120`。它由官方 Go 文档生成（`pnpm run limits:shim`，CI 定时校验），因为页面明说*"usage limits may change"*——手写表一个版本内就会过期，而过期的美元数字看起来很权威。

它同时显示**两个档位**而不是只显示一个，因为档位查不到：`/limits`、`/plan`、`/subscription`、`/account`、`/credits` 全部 404，`/models` 也没有额度字段。猜一个档位，金额就会差 2–3 倍。

它**刻意不是剩余额度**。`GET /zen/go/v1/usage` 不接受 model 参数，所以返回的百分比是账号级的，而额度是按模型的——两者相乘得到的数字没有指代对象。真实余额在控制台，那里是准确的。

#### 模式 B — OpenCode Zen (`opencode`)

Zen 路线上不出现任何 Go 的数字：根本不画圆环（没有可量的量规就是装饰），标签是消费额而不是百分比，Go 套餐被限流也不会把 Zen 按钮染成告警色，悬停则带上价格（`当前会话消耗 $0.00 · 按量计费`）。Go 的窗口是关于一条本路线永不结算的套餐的事实，所以在这里不出现。

- **Zen 触发器**：标签是本会话累计花费（未计价时为 `$0.00`，计价后如 `$0.42`），且**不画圆环**——圆环是量规，而 Zen 没有可量的窗口。这个数字是唯一能拿到的真实数字：OpenCode 的 Zen 余额只能通过 console 的 server action 读取，API 密钥读不到。
- **按量计费面板**：带 `Pay-as-you-go` 徽标的头部、会话消耗行（价格开关开启时），以及一个链接——[OpenCode 控制台](https://opencode.ai/console)，标为**充值**：按量计费的人点进去就是要充钱，而充值本身没有独立的地址。

```
┌───────────────────────────────────────────────┐
│  OpenCode Zen                [Pay-as-you-go]  │
├───────────────────────────────────────────────┤
│  Session Spend                                │
│  Space Bunny Free · 免费               $0.00│
├───────────────────────────────────────────────┤
│  ⟳ 更新于 08:30                      控制台 ›│
└───────────────────────────────────────────────┘
```

**Zen 余额与溢出。** 若配置了 `OPENCODE_API_KEY`（或 `oc_sk_…`），Zen 按量计费会被自动检测。Zen 面板**不显示余额行**：该余额只能通过 console 的 server action 读取，需要浏览器会话，因此面板直接链接到 [OpenCode 控制台](https://opencode.ai/console)，而不是冻结一个无法保持最新的数字。余额行放在 **Go** 面板上——那里它回答的是一个面板真能回答的问题：超出配额的 Go 请求是否真的会从该余额扣费。

---

## ⚙ 配置参考

### 仅配置项 (`cordis.patch.yml`)

这些项存在于 schema 中，却不渲染任何控件——每一项都是字面值、标记或引用，其默认值适用于所有有文档记载的配置。它们仍可在该行的 `config` 中编辑；[`cordis.patch.yml`](./cordis.patch.yml) 是参考。

| 配置项 | 默认值 | 保留在配置中的原因 |
| :-- | :-- | :-- |
| `providers` | `opencode`, `opencode-go`, `opencode-responses`, `opencode-anthropic` | 要拦截的路由 id；必须覆盖本层声明的每一条路由 |
| `gatewayUrls` | `opencode.ai/zen` | 标记网关流量的 URL 子串；只有镜像或中继才会改它们 |
| `userAgent` | 空（= 规范 CLI UA） | 字面覆盖；默认值就是网关所期望的值 |
| `originClient` | `cli` | `x-opencode-client` 的字面值 |
| `sessionIdEnv` | `OPENCODE_SESSION_ID` | 指定一个仅当调用方提供会话 id 时才使用的环境变量名 |
| `freeModelMarker` | `free` | 模型 id 子串；`*` 强制启用回退，`''` 关闭回退 |
| `usageBaseURL` | `https://opencode.ai/zen/go/v1` | 端点覆盖；从组合中自动发现 |
| `debug` / `debugFile` | `false` / — | 诊断用 JSONL 日志，不是谁会在 UI 里调节的行为 |

```yaml
# cordis.patch.yml — the plugin's row; every key is optional.
- insert:
    - id: dsh-opencode-patch
      name: "dsh-opencode-patch"
      config:
        providers:
          - opencode
          - opencode-go
          - opencode-responses
          - opencode-anthropic
        gatewayUrls:
          - opencode.ai/zen
        sessionIdEnv: "OPENCODE_SESSION_ID"
        freeModelMarker: "free"
        usageBaseURL: "https://opencode.ai/zen/go/v1"
        keySource: "auto" # auto | request | configured
        injectUserAgent: true
        injectOriginHeaders: true
        originClient: "cli"
        injectProject: true # attach workspace folder (or 'global'); false omits the header
        injectCoreTools: true
        enrichModels: true # merge models.dev specs + active free models into listings
        usageEnabled: true # off = no meter and no price row
        # File-level only debug options:
        debug: false
        debugFile: "/tmp/dsh-opencode-debug.jsonl"
```

> **宿主重载规则：** 任何宿主变更（`lib/index.mjs`）之后都要重启 `dsh web`；客户端 UI 变更（`lib/client.js`）刷新浏览器即可。

---

## 🛠 故障排查

| 症状 | 可能原因 | 解决方法 |
| :-- | :-- | :-- |
| 免费模型报 `403 FreeTierError` | 网关请求头被剥离，或缺少工具定义 | 保持**恢复 User-Agent**、**注入客户端来源头**与**自动补全核心工具**开启 |
| `400 MissingSessionID` | 未附带会话请求头 | 确认 `dsh-opencode-patch` 已列入 profile 的 `dsh.profile.bundles` |
| Auto Review 报 `TRANSPORT: Connection error` | 更新后宿主进程未重启 | 停止并重启 `dsh web`，让新的 `lib/index.mjs` 加载 |
| Go 模型始终不显示额度环 | 解析不到 Go 凭据，或当前激活的网关不是 `opencode-go` | 在 DSH Credentials 中存储 `OPENCODE_GO_API_KEY`，并经 `opencode-go` 路由 |
| 目录中的模型在 Settings → Fetch 里缺失，或已下线的模型仍然存在 | 宿主模块过期、补全关闭，或适配器用自带目录作答 | `pnpm run build`、重启 `dsh web`、保持**使用 models.dev 补全模型列表**开启、从匹配的路由获取、搜索精确 id（如 `space-bunny-free`） |
| 弹出面板以红色显示“已达限额” | 滚动 / 每月窗口已用到 100% | 打开 [OpenCode 控制台](https://opencode.ai/console)并启用 _使用余额（Use balance）_，以溢出到 Zen 额度 |
| 非 OpenCode 模型行为异常 | 与本补丁无关 | 发往其他网关的流量原样通过 |

---

## 📜 兼容性与验证

**已在 DeepSeek Harness 0.2.0-rc.2（Node 24+）上验证**

| 层面 | 目标 |
| :-- | :-- |
| **插件包** | [`dsh-opencode-patch`](https://www.npmjs.com/package/dsh-opencode-patch) |
| **宿主 profile** | DSH Web profile（`patchReload: live`） |
| **声明的路由** | `opencode`、`opencode-go`、`opencode-responses`、`opencode-anthropic` |
| **网关** | `opencode.ai/zen/v1`（`/responses`、`/chat/completions`、`/messages`、`:streamGenerateContent`）、`zen/go/v1`（`/chat/completions`） |
| **支持的模型** | `claude-sonnet-4-5`、`gpt-5.4`、`gemini-3.8-flash`、`deepseek-v4.1-flash`、`muse-spark-1.3-contributor-free`、`qwen3.8-flash` |
| **验证关卡** | `vp check` 干净、**266** 个确定性测试全绿、完整 schema 校验、消费者安装 + 加载（[`scripts/check.ts`](./scripts/check.ts)） |

---

## 🔍 深入解析

请求头注入矩阵、会话谱系、工作区归属、API 层级与目录内部实现，都在 **[docs/deep-dive.md](./docs/deep-dive.md)**。本 README 只讲你看得见的界面和要配的东西。

## 👥 署名与许可

灵感来自 [@nobu121](https://github.com/nobu121) 的 [**`nobu121/dsh-opencode-session`**](https://github.com/nobu121/dsh-opencode-session)，该项目开创了 OpenCode on DSH 的会话 ID 处理。本插件由 [@viztor](https://github.com/viztor) **独立实现**：沿用了这个思路，其余部分都是重写的——Zen 免费层网关兼容、分层子代理谱系、动态工作区项目归属、实时双模式 Go 额度与 Zen 余额监控，以及原生 Web UI 集成。

**链接：** [npm](https://www.npmjs.com/package/dsh-opencode-patch) · [仓库](https://github.com/viztor/dsh-opencode-patch) · [问题](https://github.com/viztor/dsh-opencode-patch/issues) · [更新日志](./CHANGELOG.md) · [贡献指南](./CONTRIBUTING.md) · [OpenCode](https://opencode.ai) · [models.dev](https://models.dev)

以 [MIT License](LICENSE) 授权。
