@goodandready/dsh-opencode-provider 0.5.1 β 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +306 -301
- package/README.ru.md +207 -202
- package/README.zh.md +183 -177
- package/lib/client.js +223 -4
- package/lib/index.d.ts +31 -1
- package/lib/index.js +188 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,302 +1,307 @@
|
|
|
1
|
-
# π¦ @goodandready/dsh-opencode-provider
|
|
2
|
-
|
|
3
|
-
<div align="center">
|
|
4
|
-
|
|
5
|
-
<h3>OpenCode Console & OpenCode Go LLM Provider for DeepSeek Harness</h3>
|
|
6
|
-
|
|
7
|
-
<p>
|
|
8
|
-
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/π_DSH_Hub-goodandready.app-ff4500.svg?style=for-the-badge&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
|
|
9
|
-
<a href="https://www.npmjs.com/package/@goodandready/dsh-opencode-provider"><img src="https://img.shields.io/npm/v/@goodandready/dsh-opencode-provider?style=for-the-badge&color=crimson&logo=npm" alt="npm version"></a>
|
|
10
|
-
<a href="https://github.com/GooDAnDReaDY/dsh-opencode-provider/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="License"></a>
|
|
11
|
-
</p>
|
|
12
|
-
|
|
13
|
-
</div>
|
|
14
|
-
|
|
15
|
-
<table align="center">
|
|
16
|
-
<tr>
|
|
17
|
-
<td align="center">
|
|
18
|
-
β <strong>If you like this plugin, please star it on GitHub</strong> β it shows me that the plugin is useful to you and motivates me to keep developing it.
|
|
19
|
-
<br><br>
|
|
20
|
-
π <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language β I will review your proposal and implement useful suggestions in a future plugin version.
|
|
21
|
-
</td>
|
|
22
|
-
</tr>
|
|
23
|
-
</table>
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
OpenCode / OpenCode Go provider plugin for [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/) (dsh).
|
|
28
|
-
|
|
29
|
-
It registers two native LLM routes:
|
|
30
|
-
|
|
31
|
-
| Route | Product | Base |
|
|
32
|
-
|---|---|---|
|
|
33
|
-
| `opencode` | OpenCode Console / Zen (pay-as-you-go) | `https://opencode.ai/inference` |
|
|
34
|
-
| `opencode-go` | OpenCode Go / Go Plus subscription | `https://opencode.ai/inference/go` |
|
|
35
|
-
|
|
36
|
-
Requests resolve the key per call, so a changed credential reaches the next request without a restart.
|
|
37
|
-
|
|
38
|
-
## Architecture
|
|
39
|
-
|
|
40
|
-
```mermaid
|
|
41
|
-
graph LR
|
|
42
|
-
subgraph DSH["DeepSeek Harness Core"]
|
|
43
|
-
Agent["DSH Agent / Chat"]
|
|
44
|
-
Creds["Credentials / auth.json"]
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
subgraph Plugin["dsh-opencode-provider"]
|
|
48
|
-
Router{"Route Selector"}
|
|
49
|
-
Pool["Multi-Account Pool<br/>(Auto-Rotation & Cooldowns)"]
|
|
50
|
-
Heatmap["Heatmap & Quota Telemetry<br/>(5h / Week / Month)"]
|
|
51
|
-
|
|
52
|
-
subgraph Transports["4 Wire Transports"]
|
|
53
|
-
T1["OpenAI Chat"]
|
|
54
|
-
T2["Anthropic Messages<br/>(Thinking Signatures)"]
|
|
55
|
-
T3["OpenAI Responses"]
|
|
56
|
-
T4["Google GenAI<br/>(thoughtSignature Replay)"]
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
subgraph OpenCode["OpenCode Gateway"]
|
|
61
|
-
Console["opencode (Console / Zen)"]
|
|
62
|
-
Go["opencode-go (Go / Go Plus)"]
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
Agent --> Router
|
|
66
|
-
Creds -.-> Pool
|
|
67
|
-
Router -->|opencode| Pool
|
|
68
|
-
Router -->|opencode-go| Pool
|
|
69
|
-
Pool --> Heatmap
|
|
70
|
-
Pool --> Transports
|
|
71
|
-
T1 & T2 & T3 & T4 --> Console & Go
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## What works
|
|
75
|
-
|
|
76
|
-
- **Four wire transports**, chosen per model:
|
|
77
|
-
- OpenAI Chat Completions (DeepSeek, GLM, Kimi, MiMo, LongCat, Hy3, Step, β¦).
|
|
78
|
-
- Anthropic Messages (Claude Haiku, MiniMax, Qwen).
|
|
79
|
-
- OpenAI Responses (GPT, Grok, Muse).
|
|
80
|
-
- Google Generative AI (Gemini).
|
|
81
|
-
- **Text, reasoning, streamed tool calls, usage, finish reasons.**
|
|
82
|
-
- **Images** through the durable attachment service, rendered natively per transport.
|
|
83
|
-
- **
|
|
84
|
-
|
|
85
|
-
-
|
|
86
|
-
-
|
|
87
|
-
- **
|
|
88
|
-
- **
|
|
89
|
-
- **
|
|
90
|
-
- **
|
|
91
|
-
- **
|
|
92
|
-
- **
|
|
93
|
-
-
|
|
94
|
-
-
|
|
95
|
-
- **
|
|
96
|
-
- **
|
|
97
|
-
- **
|
|
98
|
-
- **
|
|
99
|
-
- **
|
|
100
|
-
- **
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
- `/dsh-opencode/
|
|
145
|
-
-
|
|
146
|
-
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
- a
|
|
151
|
-
-
|
|
152
|
-
-
|
|
153
|
-
-
|
|
154
|
-
- a
|
|
155
|
-
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
The
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
- **
|
|
230
|
-
- **
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
/opencode
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
- `
|
|
271
|
-
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
npm run
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
-
|
|
296
|
-
-
|
|
297
|
-
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
1
|
+
# π¦ @goodandready/dsh-opencode-provider
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
<h3>OpenCode Console & OpenCode Go LLM Provider for DeepSeek Harness</h3>
|
|
6
|
+
|
|
7
|
+
<p>
|
|
8
|
+
<a href="https://goodandready.app/"><img src="https://img.shields.io/badge/π_DSH_Hub-goodandready.app-ff4500.svg?style=for-the-badge&labelColor=1a1a2e" alt="GoodAndReady Showcase"></a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/@goodandready/dsh-opencode-provider"><img src="https://img.shields.io/npm/v/@goodandready/dsh-opencode-provider?style=for-the-badge&color=crimson&logo=npm" alt="npm version"></a>
|
|
10
|
+
<a href="https://github.com/GooDAnDReaDY/dsh-opencode-provider/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg?style=for-the-badge" alt="License"></a>
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<table align="center">
|
|
16
|
+
<tr>
|
|
17
|
+
<td align="center">
|
|
18
|
+
β <strong>If you like this plugin, please star it on GitHub</strong> β it shows me that the plugin is useful to you and motivates me to keep developing it.
|
|
19
|
+
<br><br>
|
|
20
|
+
π <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language β I will review your proposal and implement useful suggestions in a future plugin version.
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
23
|
+
</table>
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
OpenCode / OpenCode Go provider plugin for [DeepSeek Harness](https://deepseek-harness.github.io/deepseek-harness/) (dsh).
|
|
28
|
+
|
|
29
|
+
It registers two native LLM routes:
|
|
30
|
+
|
|
31
|
+
| Route | Product | Base |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| `opencode` | OpenCode Console / Zen (pay-as-you-go) | `https://opencode.ai/inference` |
|
|
34
|
+
| `opencode-go` | OpenCode Go / Go Plus subscription | `https://opencode.ai/inference/go` |
|
|
35
|
+
|
|
36
|
+
Requests resolve the key per call, so a changed credential reaches the next request without a restart.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
```mermaid
|
|
41
|
+
graph LR
|
|
42
|
+
subgraph DSH["DeepSeek Harness Core"]
|
|
43
|
+
Agent["DSH Agent / Chat"]
|
|
44
|
+
Creds["Credentials / auth.json"]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
subgraph Plugin["dsh-opencode-provider"]
|
|
48
|
+
Router{"Route Selector"}
|
|
49
|
+
Pool["Multi-Account Pool<br/>(Auto-Rotation & Cooldowns)"]
|
|
50
|
+
Heatmap["Heatmap & Quota Telemetry<br/>(5h / Week / Month)"]
|
|
51
|
+
|
|
52
|
+
subgraph Transports["4 Wire Transports"]
|
|
53
|
+
T1["OpenAI Chat"]
|
|
54
|
+
T2["Anthropic Messages<br/>(Thinking Signatures)"]
|
|
55
|
+
T3["OpenAI Responses"]
|
|
56
|
+
T4["Google GenAI<br/>(thoughtSignature Replay)"]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
subgraph OpenCode["OpenCode Gateway"]
|
|
61
|
+
Console["opencode (Console / Zen)"]
|
|
62
|
+
Go["opencode-go (Go / Go Plus)"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
Agent --> Router
|
|
66
|
+
Creds -.-> Pool
|
|
67
|
+
Router -->|opencode| Pool
|
|
68
|
+
Router -->|opencode-go| Pool
|
|
69
|
+
Pool --> Heatmap
|
|
70
|
+
Pool --> Transports
|
|
71
|
+
T1 & T2 & T3 & T4 --> Console & Go
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What works
|
|
75
|
+
|
|
76
|
+
- **Four wire transports**, chosen per model:
|
|
77
|
+
- OpenAI Chat Completions (DeepSeek, GLM, Kimi, MiMo, LongCat, Hy3, Step, β¦).
|
|
78
|
+
- Anthropic Messages (Claude Haiku, MiniMax, Qwen).
|
|
79
|
+
- OpenAI Responses (GPT, Grok, Muse).
|
|
80
|
+
- Google Generative AI (Gemini).
|
|
81
|
+
- **Text, reasoning, streamed tool calls, usage, finish reasons.**
|
|
82
|
+
- **Images** through the durable attachment service, rendered natively per transport; **file attachments** are projected to DSH's deterministic handle text (the harness supplies the read-only execution path), never inline bytes.
|
|
83
|
+
- **Tool-result images** are inlined by Anthropic, OpenAI Responses and Google; OpenAI Chat keeps the text placeholder because its tool message has no image part.
|
|
84
|
+
- **Replay of provider metadata** where the endpoint requires it:
|
|
85
|
+
- Anthropic thinking signatures.
|
|
86
|
+
- Gemini `thoughtSignature` on historical function calls.
|
|
87
|
+
- **Multi-account rotation** before a stream starts: 401/403/429 and quota/usage-limit bodies (400/402/403/429). An invalid key cools down for 24h, a quota rejection for 10 minutes or `Retry-After`, a rate limit for 60s or `Retry-After`. A request already streaming is never replayed on another account; the next request uses the next usable key.
|
|
88
|
+
- **Per-account quotas**: each account row shows its own 5-hour / weekly / monthly usage, the active account drives the sidebar and quota panel, and the panel lists every account side by side.
|
|
89
|
+
- **Model β account rules**: first matching glob pins a preferred account for that model; the other accounts remain fallbacks if it fails or is cooling.
|
|
90
|
+
- **Request heatmap** in the quota panel: a Monday-first weekday Γ hour grid in local time, plus a 24-hour aggregate strip underneath. Selectors switch between all requests, any single model, and any single account. The adapter appends `{at, provider, model, purpose?, account, reason}` to `~/.dsh/storages/dsh-opencode/requests.jsonl`; no prompt, response or key is stored. Auxiliary `session-title`/`compaction` calls are excluded from the heatmap and exact duplicate records collapse. The log compacts at 2 MB to the newest 5000 lines. Collection starts with this build β the projection cache only has `lastPromptAt`, not per-request timestamps.
|
|
91
|
+
- **Stable `x-opencode-session`** derived from the durable session id, so retries and every turn of a conversation reuse the same routing key.
|
|
92
|
+
- **Live catalog** from `/v1/models` with a generated `models.dev` fallback.
|
|
93
|
+
- **Per-model subscription limits** from the OpenCode Go pricing table: monthly dollar allowance plus estimated requests for the 5-hour, weekly and monthly windows, for both Go and Go Plus. Shown in the model picker and the settings page; estimates only β the gateway remains the authority.
|
|
94
|
+
- **`/opencode` command** for status, usage, accounts, models, rules, and `test [model]`.
|
|
95
|
+
- **TUI settings section** for the terminal profile (`/settings β OpenCode`) with the key, endpoints, allowlist, active account, fallback model, sticky routing and timeouts.
|
|
96
|
+
- **Quota fallback**: choose a free model in Settings β OpenCode; a Go quota rejection retries once through Console/Zen.
|
|
97
|
+
- **Sticky account**: the last successful account is preferred for the same conversation; explicit model rules and pinned accounts win.
|
|
98
|
+
- **Routing trace** in the quota panel and per-account heatmap filter.
|
|
99
|
+
- **One-click plugin update** in Settings β OpenCode: current/latest version, an update badge, and an **Update now** button that installs the exact npm version through the DSH CLI and asks for a restart. The host endpoint is `GET/POST /dsh-opencode/update`.
|
|
100
|
+
- **Plan-aware Go picker**: hides models outside the Go table unless turned off; descriptions carry `Go`, `Console` or `FREE`.
|
|
101
|
+
- **Request-wide image budget** (newest images win, oldest become an omitted placeholder) and **object-root tool-schema normalization** for MCP/third-party tools.
|
|
102
|
+
- **Usage analytics dashboard** in the quota panel: KPI cards (requests, tokens, success, input/output, reasoning, cache, TTFT, latency, errors), a 7/30/90 selector, and Models / Days / Sessions tables. Success and error cards show `β`: the local projection does not expose failed attempts or reasoning tokens.
|
|
103
|
+
- **Usage by model** from the local session projection cache: sessions and tokens per OpenCode model. Only numeric metadata is read; titles and message content stay local.
|
|
104
|
+
- **Browser UI**:
|
|
105
|
+
- **Settings β OpenCode**: key status, paste/import/clear, quota bars, accounts, model allowlist, per-model transport overrides, connection settings.
|
|
106
|
+
- **Sidebar quota card** (off by default): a ring with the active account and the highest window, then 5-hour / weekly / monthly rows with remaining quota, reset countdown and bars; clicking opens the full quota panel in the center column. The panel scrolls internally, so the heatmap and usage table below the fold stay reachable.
|
|
107
|
+
- **Cost** is shown only by DSH's native cost display; the plugin does not add a second composer pill.
|
|
108
|
+
- Model price badges in the settings page; the OpenCode auth-entry field autocompletes from `auth.json`.
|
|
109
|
+
- Host routes under `/dsh-opencode/*` serve state and accept writes only from a loopback Host with same-origin and JSON checks; `GET /dsh-opencode/health` reports provider registration, accounts and model count.
|
|
110
|
+
- The provider keeps core harness tools (`read`, `write`, `edit`, `bash`, `glob`, `grep`, β¦) when a large MCP set is trimmed to OpenCode's 128-tool limit.
|
|
111
|
+
|
|
112
|
+
## Install
|
|
113
|
+
|
|
114
|
+
Build and pack first:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
npm install
|
|
118
|
+
npm run build
|
|
119
|
+
npm pack
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Install the **packed tarball** into a profile:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.2.tgz
|
|
126
|
+
# DSH Desktop:
|
|
127
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.2.tgz
|
|
128
|
+
|
|
129
|
+
# Or install the published package:
|
|
130
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.2
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Restart dsh (or DSH Desktop), then pick `OpenCode` or `OpenCode Go` in the model picker.
|
|
134
|
+
|
|
135
|
+
> **Use the tarball, not the directory.** On dsh 0.2.1-rc.2 the Desktop CLI aliases a local
|
|
136
|
+
> directory passed to `dsh plugin add` as `@deepseek-ai/dsh-desktop-runtime` and overwrites
|
|
137
|
+
> the target's `package.json`. Installing the packed `.tgz` avoids that path and installs
|
|
138
|
+
> the package under its real name.
|
|
139
|
+
|
|
140
|
+
## Verified in DSH Desktop
|
|
141
|
+
|
|
142
|
+
Tested against DSH Desktop **0.2.1-rc.2**:
|
|
143
|
+
|
|
144
|
+
- the plugin loads and `/dsh-opencode/state` returns `200`;
|
|
145
|
+
- the key is picked up from `~/.local/share/opencode/auth.json`;
|
|
146
|
+
- the live catalog returned 37 models with prices and Go limits;
|
|
147
|
+
- quota windows returned `rolling / weekly / monthly`;
|
|
148
|
+
- `/dsh-opencode/analytics` scanned 7 local sessions (0 OpenCode sessions yet, as expected before the first OpenCode chat);
|
|
149
|
+
- `/dsh-opencode/test` against the default flash model returned `{"ok":true,"text":"OK"}`;
|
|
150
|
+
- a second Go account was added, the default key was temporarily replaced with an invalid one, and `/test` rotated to the second account and still returned `OK`;
|
|
151
|
+
- the second account was already at ~98% of its monthly allowance; the rotation test pushed it over, so a later run surfaced `QUOTA`, marked the default as `invalid-credential` and `Go #2` as `quota` until its reset, with no active account;
|
|
152
|
+
- per-account usage returned `default` 1% / 5% / 2% and `Go #2` 9% / 4% / **100%** monthly (`rate-limited`), so the exhausted account is visible without a failed request;
|
|
153
|
+
- two `/test` requests produced `Tue 13:00 = 2` in the heatmap and two lines in `requests.jsonl`; the 24-hour strip shows the same value at 13:00;
|
|
154
|
+
- a third request with a free-tier model added a second per-model series, so the model selector is populated;
|
|
155
|
+
- after restarting Desktop, `Go #2` still showed its persisted `quota` cooldown while the restored default key was active;
|
|
156
|
+
- setting `*free* β Go #2` through `/config` returned the normalized rule from `/state`, and clearing it returned `[]`; the account list stayed unchanged;
|
|
157
|
+
- the redesigned sidebar card was checked in Desktop: `OpenCode Go` with a 5% ring, `5-Hour Window 99% remaining Β· 3h`, `Weekly Window 95% remaining Β· 6d`, `Monthly Window 98% remaining Β· 29d`; it uses the DSH theme variables and matches the meter's row/ring layout;
|
|
158
|
+
- a real chat produced two OpenCode sessions: one standard flash model (4105 in / 243 out, β$0.00079) and one free model (10670 in / 171 out, Free); the heatmap counted 6 main requests (the two title calls and exact duplicates are filtered);
|
|
159
|
+
- the renderer loaded the client bundle (`settings.opencode` is present in its code cache).
|
|
160
|
+
|
|
161
|
+
Open **Settings β OpenCode** and enable *Show the quota card in the sidebar* if you want the sidebar meter.
|
|
162
|
+
|
|
163
|
+
## Configure
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
- id: llm-opencode
|
|
167
|
+
name: "@goodandready/dsh-opencode-provider"
|
|
168
|
+
config:
|
|
169
|
+
apiKeyEnv: OPENCODE_API_KEY # default
|
|
170
|
+
# apiKey: oc_sk_... # composition only, never commit
|
|
171
|
+
openCodeAuthEntry: opencode-go
|
|
172
|
+
requestTimeoutMs: 300000
|
|
173
|
+
streamIdleTimeoutMs: 300000
|
|
174
|
+
visibleModels: []
|
|
175
|
+
modelTransports: {}
|
|
176
|
+
# contextWindowOverrides: { model-id: 65536 }
|
|
177
|
+
# maxOutputTokens: 32768
|
|
178
|
+
accounts:
|
|
179
|
+
- label: Go #2
|
|
180
|
+
apiKeyEnv: OPENCODE_API_KEY_2
|
|
181
|
+
activeAccount: '' # label or credential name to pin
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Every field except `apiKey` is volatile and appears in dsh settings.
|
|
185
|
+
|
|
186
|
+
### Keys
|
|
187
|
+
|
|
188
|
+
1. `config.apiKey` (composition only, `role('secret')`).
|
|
189
|
+
2. The dsh credential seam under `apiKeyEnv`.
|
|
190
|
+
3. `~/.local/share/opencode/auth.json` β entry `openCodeAuthEntry` (default `opencode-go`), written by OpenCode's own `/connect`.
|
|
191
|
+
4. The launch environment (process, project `.env`, `$DSH_HOME/.env`).
|
|
192
|
+
|
|
193
|
+
Extra `accounts` resolve the same way (literal key or credential reference). If you already use OpenCode, the plugin picks up the stored Go key automatically.
|
|
194
|
+
|
|
195
|
+
### Accounts and rotation
|
|
196
|
+
|
|
197
|
+
Add a second account in **Settings β OpenCode β Accounts**: give it a name and a credential reference (for example `OPENCODE_API_KEY_2`), paste its key, then Save. The default account (the key card at the top) always serves first unless `activeAccount` pins another one.
|
|
198
|
+
|
|
199
|
+
Rotation happens **before any content is streamed**. When the serving key is rejected with an invalid credential, a rate limit, or an exhausted quota, the adapter marks that key and retries the same request with the next usable account. An exhausted pinned account still falls back to the others; it returns to service once its cooldown passes. A request that has already started streaming is not replayed on another account.
|
|
200
|
+
|
|
201
|
+
Each account row shows the current cooldown (`Invalid key`, `Quota cooldown Β· 23h`, `Rate limited Β· 30s`) and its own quota chips. The active marker moves to the first account that is not cooling down. The last account is marked too, so a failed turn explains which key ended it. Quota cooldowns honor the provider's reset (`Retry-After`) when present. The quota panel repeats the numbers in an accounts table, and the sidebar meter uses the active account.
|
|
202
|
+
|
|
203
|
+
Cooldowns survive restarts: deadlines and reasons are stored in `~/.dsh/storages/dsh-opencode/cooldowns.json` keyed by a SHA-256 fingerprint β raw keys are never written. Expired entries are dropped on load, and the file keeps at most 50 entries.
|
|
204
|
+
|
|
205
|
+
### Model β account rules
|
|
206
|
+
|
|
207
|
+
Pin a model to a preferred account with a glob. The first matching rule wins; `*` and `?` are supported and matching is case-insensitive. The named account is tried first, and the other accounts remain fallbacks if it is cooling, missing, or rejected β a rule is a preference, not a hard filter. An unknown account label is ignored, and rules apply to both `opencode` and `opencode-go`.
|
|
208
|
+
|
|
209
|
+
Configure them in **Settings β OpenCode β Model β account rules**, or in YAML:
|
|
210
|
+
|
|
211
|
+
```yaml
|
|
212
|
+
modelAccountRules:
|
|
213
|
+
- pattern: "*free*"
|
|
214
|
+
account: "free"
|
|
215
|
+
- pattern: "your-provider-*"
|
|
216
|
+
account: "go"
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The account label is the same one shown in **Accounts** (`default` or the extra account's name).
|
|
220
|
+
|
|
221
|
+
### Request heatmap
|
|
222
|
+
|
|
223
|
+
The quota panel has a **Request heatmap**: rows are MondayβSunday, columns are local-time hours, and each cell counts calls made through this adapter (including tool-loop steps and `Test connection`). Auxiliary `session-title` and `compaction` calls are excluded, and exact duplicate records collapse; tool-loop steps are separate adapter requests, so one chat message is not necessarily one cell. Hover a cell to see the exact count. A 24-hour strip below the grid aggregates the same window by hour across all days, so the daily shape stays readable even when the grid is sparse. When more than one model or account has traffic, selectors switch the grid and strip to a single model or account; each option shows its total. The data is append-only metadata in `~/.dsh/storages/dsh-opencode/requests.jsonl`; it never includes prompts, responses or keys. Because the local projection cache stores only the last prompt time per session, the heatmap starts collecting with this build and grows as you use the plugin.
|
|
224
|
+
|
|
225
|
+
## Settings page
|
|
226
|
+
|
|
227
|
+
Open **Settings β OpenCode**:
|
|
228
|
+
|
|
229
|
+
- **Key** β status, paste a new key, import the key OpenCode already stored, clear it, or run a **Test connection** against a chosen model and see the latency and reply.
|
|
230
|
+
- **Quota** β 5-hour, weekly and monthly bars with reset dates, plus warning banners at 80% and 95%.
|
|
231
|
+
- **Accounts** β add extra accounts by credential reference, set each account's key from the page, see whether it is configured, pin one, or return to automatic rotation. **Import all from auth.json** copies every OpenCode key into its own credential ref and adds an account for it; **Check all accounts** refreshes the per-account quota/health rows.
|
|
232
|
+
- **Models** β search, refresh the live catalog, uncheck models to build an allowlist, override the wire transport per model, and see each model's input/output price plus its Go request estimate (`Go 34.7K/5h`). Hover the estimate for the full Go / Go Plus monthly allowance and 5h/week/month request counts; expand **Subscription limits** for a table of every model with both plans.
|
|
233
|
+
- **Connection** β API bases, usage endpoint, timeouts, output cap, context override, sidebar-card switch. The OpenCode auth-entry field autocompletes from the keys present in `auth.json`.
|
|
234
|
+
- **Plugin update** β current/latest version, update badge, and **Update now**. It installs the exact registry version through the normal `dsh plugin add` path, blocks a second click while installing, and reports when DSH must be restarted.
|
|
235
|
+
- **Routing** β sticky account per conversation and the free fallback model used after a Go quota error. The quota panel shows the last routing decisions with their reason.
|
|
236
|
+
|
|
237
|
+
The sidebar quota card is **off by default**. Enable *Show the quota card in the sidebar* in the settings page; clicking the card opens the full quota panel, which also carries the **Usage by model** table (7/30/90-day window).
|
|
238
|
+
|
|
239
|
+
The Host serves `/dsh-opencode/state`, `/health`, `/analytics`, `/config`, `/key`, `/key/clear`, `/auth-import`, `/refresh`, `/models/refresh`, `/test` and `/update`. Writes require JSON, are refused for cross-origin requests, and are restricted to loopback (`127.0.0.1` / `::1`); reads stay same-origin. This keeps key and configuration writes off a LAN-exposed web server until the host's own auth token can be integrated.
|
|
240
|
+
|
|
241
|
+
## Updates
|
|
242
|
+
|
|
243
|
+
The **Plugin update** card in Settings β OpenCode calls `GET /dsh-opencode/update`, which compares the installed `package.json` with the npm registry. When a newer version exists, **Update now** sends the same-origin request with `x-dsh-plugin-update: 1`; the Host runs the normal DSH CLI:
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
dsh plugin --profile <profile> add --config.minimumReleaseAge=0 @goodandready/dsh-opencode-provider@<exact-version>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
The update is blocked while another update is running, never uses `--force`, and never installs from a worktree or a `file:` path. The card reports that DSH must be restarted after a successful install. If the runtime does not expose the CLI entry, the card says that automatic update is unavailable and points to the DSH plugin manager; the status check still works.
|
|
250
|
+
|
|
251
|
+
## Visual verification
|
|
252
|
+
|
|
253
|
+

|
|
254
|
+
|
|
255
|
+
Captured from the exact 0.5.0 release candidate in DSH Desktop in the dark theme.
|
|
256
|
+
|
|
257
|
+
## Command
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
/opencode status # accounts, per-account quotas, rules
|
|
261
|
+
/opencode usage # quota windows only
|
|
262
|
+
/opencode accounts # labels, source, active/cooling, 5h/wk/mo
|
|
263
|
+
/opencode models # ids, FREE/Go limits
|
|
264
|
+
/opencode rules # model β account rules
|
|
265
|
+
/opencode test [model] # minimal real request
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
## Verified API facts
|
|
269
|
+
|
|
270
|
+
- `GET /inference/go/v1/models` is public and returns the live Go roster.
|
|
271
|
+
- `GET /zen/go/v1/usage` returns `rolling` (5h), `weekly`, `monthly` with `percent` and `resetsAt`. It is undocumented; the plugin treats it as fail-open.
|
|
272
|
+
- The unified `POST /zen/go/v1/chat/completions` accepts OpenAI-compatible **and** Anthropic-backed models, but **not** Responses-only or Google models.
|
|
273
|
+
- Responses-only models and Gemini need their native endpoints, which the plugin uses.
|
|
274
|
+
- Gemini requires replaying `thoughtSignature` on historical function calls; the Google transport stores it in dsh's replay state.
|
|
275
|
+
- `x-opencode-session` is mandatory. A stable opaque value is enough for a Go key; the canonical `ses_*` shape and CLI impersonation are only needed for the anonymous free lane, which this plugin deliberately does not implement.
|
|
276
|
+
- Free-tier anonymous keys are rejected with `403 FreeTierError`; free models work normally with an active Go subscription.
|
|
277
|
+
|
|
278
|
+
## Development
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
npm run typecheck
|
|
282
|
+
npm run build
|
|
283
|
+
npm test
|
|
284
|
+
npm run check # typecheck + tests + build
|
|
285
|
+
npm run release # check + packed tarball
|
|
286
|
+
npm run sync:models # regenerate src/models.generated.ts from models.dev
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
GitHub Actions runs `npm run check` and `npm pack` on push/PR (`.github/workflows/ci.yml`).
|
|
290
|
+
|
|
291
|
+
## Limitations
|
|
292
|
+
|
|
293
|
+
- File attachments are forwarded as DSH handle text, not inline bytes; the harness normally supplies the read-only execution path. Tool-result images are inline on Anthropic, OpenAI Responses and Google; OpenAI Chat uses a placeholder.
|
|
294
|
+
- The usage endpoint is unofficial and may change; chat never depends on it.
|
|
295
|
+
- Cost is displayed only by DSH's native cost display; this plugin has no second composer cost pill.
|
|
296
|
+
- Automatic update requires the DSH CLI entry in the runtime. When it is not exposed, the update card shows the current/latest status and directs the user to the DSH plugin manager.
|
|
297
|
+
- Subscription request counts are OpenCode's published estimates, not enforced quotas. They can change when OpenCode updates the Go pricing table; the gateway is the final authority.
|
|
298
|
+
- Usage-by-model aggregates the local projection cache and attributes a whole session to its last-used model; a session that switched models is therefore approximate. It never reads prompt text or titles.
|
|
299
|
+
- `opencode` (Console) billing shows no per-model allowance; the Go route is the primary target.
|
|
300
|
+
- The anonymous free lane (`Bearer public` + CLI impersonation) is not implemented.
|
|
301
|
+
- The UI requires a web profile (the Host needs `ctx.webServer`); the TUI keeps the `/opencode` command and the model routes.
|
|
302
|
+
- Write routes are loopback-only; a web profile exposed to a LAN can read state but cannot save configuration or keys until the host auth token is integrated.
|
|
303
|
+
|
|
304
|
+
## License
|
|
305
|
+
|
|
306
|
+
MIT Β© [GooDAnDReaDY](https://github.com/GooDAnDReaDY). See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
|
|
302
307
|
|