@goodandready/dsh-opencode-provider 0.5.5 → 0.5.7
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 +20 -13
- package/README.ru.md +18 -11
- package/README.zh.md +16 -11
- package/lib/client.js +5 -2
- package/lib/index.d.ts +9 -0
- package/lib/index.js +99 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.7
|
|
4
|
+
|
|
5
|
+
- **Provider-id conflict fix**: the plugin now registers `opencode-direct` and `opencode-go-direct` instead of `opencode`/`opencode-go`. The built-in `llm-pi-ai` catalog owns those ids; reusing them triggered `DUPLICATE_DIRECTORY`, dropped the built-in catalog, hid other providers and disabled the add button. Existing sessions recorded under the old ids stay on the built-in routes; select **OpenCode (plugin)** or **OpenCode Go (plugin)** for this plugin's features.
|
|
6
|
+
- 89 tests.
|
|
7
|
+
|
|
8
|
+
## 0.5.6
|
|
9
|
+
|
|
10
|
+
- **Persistent sticky routing**: the last successful account/model per conversation survives adapter and Desktop restarts. `~/.dsh/storages/dsh-opencode/sticky.json` stores hashed session ids and key fingerprints only (no keys or prompt text), keeps 500 entries for 14 days.
|
|
11
|
+
- 88 tests.
|
|
12
|
+
|
|
3
13
|
## 0.5.5
|
|
4
14
|
|
|
5
15
|
- **Per-account request stats**: every upstream attempt now records its outcome, latency and TTFT in the local request log; the heatmap adds a per-account table with requests, errors and average latency/TTFT. No prompt or response content is stored.
|
package/README.md
CHANGED
|
@@ -30,11 +30,18 @@ It registers two native LLM routes:
|
|
|
30
30
|
|
|
31
31
|
| Route | Product | Base |
|
|
32
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` |
|
|
33
|
+
| `opencode-direct` | OpenCode Console / Zen (pay-as-you-go) | `https://opencode.ai/inference` |
|
|
34
|
+
| `opencode-go-direct` | OpenCode Go / Go Plus subscription | `https://opencode.ai/inference/go` |
|
|
35
35
|
|
|
36
36
|
Requests resolve the key per call, so a changed credential reaches the next request without a restart.
|
|
37
37
|
|
|
38
|
+
The ids use the `-direct` suffix because the built-in `llm-pi-ai` catalog already owns
|
|
39
|
+
`opencode` and `opencode-go`. Reusing those ids makes the core registry reject the
|
|
40
|
+
duplicate (`DUPLICATE_DIRECTORY`) and drop the built-in catalog, which hides the other
|
|
41
|
+
providers and disables the add button. Existing sessions recorded under the old ids stay on
|
|
42
|
+
the built-in routes; select **OpenCode (plugin)** or **OpenCode Go (plugin)** for this
|
|
43
|
+
plugin's accounts, quotas, fallback and telemetry.
|
|
44
|
+
|
|
38
45
|
## Architecture
|
|
39
46
|
|
|
40
47
|
```mermaid
|
|
@@ -58,14 +65,14 @@ graph LR
|
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
subgraph OpenCode["OpenCode Gateway"]
|
|
61
|
-
Console["opencode (Console / Zen)"]
|
|
62
|
-
Go["opencode-go (Go / Go Plus)"]
|
|
68
|
+
Console["opencode-direct (Console / Zen)"]
|
|
69
|
+
Go["opencode-go-direct (Go / Go Plus)"]
|
|
63
70
|
end
|
|
64
71
|
|
|
65
72
|
Agent --> Router
|
|
66
73
|
Creds -.-> Pool
|
|
67
|
-
Router -->|opencode| Pool
|
|
68
|
-
Router -->|opencode-go| Pool
|
|
74
|
+
Router -->|opencode-direct| Pool
|
|
75
|
+
Router -->|opencode-go-direct| Pool
|
|
69
76
|
Pool --> Heatmap
|
|
70
77
|
Pool --> Transports
|
|
71
78
|
T1 & T2 & T3 & T4 --> Console & Go
|
|
@@ -95,7 +102,7 @@ graph LR
|
|
|
95
102
|
- **`/opencode` command** for status, usage, accounts, models, rules, and `test [model]`.
|
|
96
103
|
- **TUI settings section** for the terminal profile (`/settings → OpenCode`) with the key, endpoints, allowlist, active account, fallback model, sticky routing and timeouts.
|
|
97
104
|
- **Quota fallback**: choose a free model in Settings → OpenCode; a Go quota rejection retries once through Console/Zen.
|
|
98
|
-
- **Sticky account**: the last successful account is preferred for the same conversation; explicit model rules and pinned accounts win.
|
|
105
|
+
- **Sticky account**: the last successful account is preferred for the same conversation and survives restarts via a local `sticky.json` (hashed session ids and key fingerprints only); explicit model rules and pinned accounts win.
|
|
99
106
|
- **Routing trace** in the quota panel and per-account heatmap filter.
|
|
100
107
|
- **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`.
|
|
101
108
|
- **Plan-aware Go picker**: hides models outside the Go table unless turned off; descriptions carry `Go`, `Console` or `FREE`.
|
|
@@ -123,15 +130,15 @@ npm pack
|
|
|
123
130
|
Install the **packed tarball** into a profile:
|
|
124
131
|
|
|
125
132
|
```bash
|
|
126
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
133
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.7.tgz
|
|
127
134
|
# DSH Desktop:
|
|
128
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
135
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.7.tgz
|
|
129
136
|
|
|
130
137
|
# Or install the published package:
|
|
131
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
138
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.7
|
|
132
139
|
```
|
|
133
140
|
|
|
134
|
-
Restart dsh (or DSH Desktop), then pick
|
|
141
|
+
Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
|
|
135
142
|
|
|
136
143
|
> **Use the tarball, not the directory.** On dsh 0.2.1-rc.2 the Desktop CLI aliases a local
|
|
137
144
|
> directory passed to `dsh plugin add` as `@deepseek-ai/dsh-desktop-runtime` and overwrites
|
|
@@ -205,7 +212,7 @@ Cooldowns survive restarts: deadlines and reasons are stored in `~/.dsh/storages
|
|
|
205
212
|
|
|
206
213
|
### Model → account rules
|
|
207
214
|
|
|
208
|
-
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`.
|
|
215
|
+
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-direct` and `opencode-go-direct`.
|
|
209
216
|
|
|
210
217
|
Configure them in **Settings → OpenCode → Model → account rules**, or in YAML:
|
|
211
218
|
|
|
@@ -298,7 +305,7 @@ GitHub Actions runs `npm run check` and `npm pack` on push/PR (`.github/workflow
|
|
|
298
305
|
- 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.
|
|
299
306
|
- 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.
|
|
300
307
|
- 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.
|
|
301
|
-
- `opencode` (Console) billing shows no per-model allowance; the Go route is the primary target.
|
|
308
|
+
- `opencode-direct` (Console) billing shows no per-model allowance; the Go route is the primary target.
|
|
302
309
|
- The anonymous free lane (`Bearer public` + CLI impersonation) is not implemented.
|
|
303
310
|
- The UI requires a web profile (the Host needs `ctx.webServer`); the TUI keeps the `/opencode` command and the model routes.
|
|
304
311
|
- 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.
|
package/README.ru.md
CHANGED
|
@@ -30,11 +30,18 @@
|
|
|
30
30
|
|
|
31
31
|
| Маршрут | Продукт / Тариф | Базовый эндпоинт | Назначение |
|
|
32
32
|
|---|---|---|---|
|
|
33
|
-
| `opencode` | OpenCode Console / Zen | `https://opencode.ai/inference` | Поминутная тарификация (pay-as-you-go) |
|
|
34
|
-
| `opencode-go` | OpenCode Go / Go Plus | `https://opencode.ai/inference/go` | Фиксированная подписка с лимитами 5h / недели / месяца |
|
|
33
|
+
| `opencode-direct` | OpenCode Console / Zen | `https://opencode.ai/inference` | Поминутная тарификация (pay-as-you-go) |
|
|
34
|
+
| `opencode-go-direct` | OpenCode Go / Go Plus | `https://opencode.ai/inference/go` | Фиксированная подписка с лимитами 5h / недели / месяца |
|
|
35
35
|
|
|
36
36
|
Ключи разрешаются динамически на каждый вызов: смена токена в настройках или в файле окружения вступает в силу мгновенно без перезапуска сервера.
|
|
37
37
|
|
|
38
|
+
Суффикс `-direct` нужен потому, что встроенный каталог `llm-pi-ai` уже объявляет
|
|
39
|
+
`opencode` и `opencode-go`. Повтор этих id даёт `DUPLICATE_DIRECTORY`, из-за чего
|
|
40
|
+
ядро отбрасывает встроенный каталог, исчезают остальные провайдеры и отключается
|
|
41
|
+
кнопка добавления. Старые сессии с прежними id остаются на встроенных маршрутах;
|
|
42
|
+
для возможностей плагина выбирайте **OpenCode (plugin)** или
|
|
43
|
+
**OpenCode Go (plugin)**.
|
|
44
|
+
|
|
38
45
|
## Архитектура
|
|
39
46
|
|
|
40
47
|
```mermaid
|
|
@@ -58,14 +65,14 @@ graph LR
|
|
|
58
65
|
end
|
|
59
66
|
|
|
60
67
|
subgraph OpenCode["Шлюз OpenCode Gateway"]
|
|
61
|
-
Console["opencode (Console / Zen)"]
|
|
62
|
-
Go["opencode-go (Go / Go Plus)"]
|
|
68
|
+
Console["opencode-direct (Console / Zen)"]
|
|
69
|
+
Go["opencode-go-direct (Go / Go Plus)"]
|
|
63
70
|
end
|
|
64
71
|
|
|
65
72
|
Agent --> Router
|
|
66
73
|
Creds -.-> Pool
|
|
67
|
-
Router -->|opencode| Pool
|
|
68
|
-
Router -->|opencode-go| Pool
|
|
74
|
+
Router -->|opencode-direct| Pool
|
|
75
|
+
Router -->|opencode-go-direct| Pool
|
|
69
76
|
Pool --> Heatmap
|
|
70
77
|
Pool --> Transports
|
|
71
78
|
T1 & T2 & T3 & T4 --> Console & Go
|
|
@@ -94,7 +101,7 @@ graph LR
|
|
|
94
101
|
- **Онбординг аккаунтов**: **Import all from auth.json** переносит все ключи OpenCode в отдельные credential ref и добавляет аккаунты; **Check all accounts** обновляет квоты и health по каждому аккаунту.
|
|
95
102
|
- **Правила «модель ➔ аккаунт»**: сопоставление по glob-паттернам закрепляет предпочтительный аккаунт за моделью; остальные аккаунты остаются в резерве.
|
|
96
103
|
- **Квотный fallback**: выберите бесплатную модель в настройках; при ошибке квоты Go тот же запрос один раз повторяется через Console/Zen. Если fallback тоже не сработал, возвращается исходная ошибка квоты.
|
|
97
|
-
- **Липкий аккаунт**: последний успешный аккаунт предпочитается в том же
|
|
104
|
+
- **Липкий аккаунт**: последний успешный аккаунт предпочитается в том же диалоге и сохраняется между перезапусками в локальном `sticky.json` (только хешированные session id и fingerprint ключей); явные правила модели, закреплённый аккаунт и квотный fallback имеют приоритет.
|
|
98
105
|
- **Трассировка маршрутизации**: панель квот показывает последние решения (`rule`, `sticky`, `rotation`, `quota-fallback`) без промптов и ключей.
|
|
99
106
|
- **Статистика по аккаунтам**: в request log пишутся `ok`, `latencyMs`, `ttftMs` и `error`; heatmap показывает таблицу запросов, ошибок и средних latency/TTFT по каждому аккаунту.
|
|
100
107
|
- **Почасовой Heatmap запросов**: сетка «день недели × час» в локальном времени плюс суточная полоса агрегации. Фильтры переключают отображение между всеми запросами, конкретной моделью и аккаунтом. Лог `~/.dsh/storages/dsh-opencode/requests.jsonl` хранит только метаданные `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` (промпты и ответы не сохраняются).
|
|
@@ -122,15 +129,15 @@ npm pack
|
|
|
122
129
|
Установка из **упакованного tarball**:
|
|
123
130
|
|
|
124
131
|
```bash
|
|
125
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
132
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.7.tgz
|
|
126
133
|
# DSH Desktop:
|
|
127
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
134
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.7.tgz
|
|
128
135
|
|
|
129
136
|
# Либо установка опубликованного пакета из npm:
|
|
130
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
137
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.7
|
|
131
138
|
```
|
|
132
139
|
|
|
133
|
-
После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode** или **OpenCode Go** в списке моделей.
|
|
140
|
+
После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
|
|
134
141
|
|
|
135
142
|
## Конфигурация
|
|
136
143
|
|
package/README.zh.md
CHANGED
|
@@ -30,11 +30,16 @@
|
|
|
30
30
|
|
|
31
31
|
| 路由 | 产品 / 方案 | 基础接口 | 特性说明 |
|
|
32
32
|
|---|---|---|---|
|
|
33
|
-
| `opencode` | OpenCode Console / Zen | `https://opencode.ai/inference` | 按量计费(pay-as-you-go) |
|
|
34
|
-
| `opencode-go` | OpenCode Go / Go Plus | `https://opencode.ai/inference/go` | 订阅专属,含 5小时/每周/每月配额估算 |
|
|
33
|
+
| `opencode-direct` | OpenCode Console / Zen | `https://opencode.ai/inference` | 按量计费(pay-as-you-go) |
|
|
34
|
+
| `opencode-go-direct` | OpenCode Go / Go Plus | `https://opencode.ai/inference/go` | 订阅专属,含 5小时/每周/每月配额估算 |
|
|
35
35
|
|
|
36
36
|
密钥在每次调用时动态解析:在设置中或环境文件中更换凭据无需重启服务器即可立即生效。
|
|
37
37
|
|
|
38
|
+
`-direct` 后缀用于避免与内置 `llm-pi-ai` 已声明的 `opencode` / `opencode-go` 冲突。
|
|
39
|
+
重复 id 会触发 `DUPLICATE_DIRECTORY`,导致核心丢弃内置目录、其他提供商消失并禁用添加按钮。
|
|
40
|
+
旧会话继续使用内置路由;要使用本插件的账户、配额、回退和遥测,请选择
|
|
41
|
+
**OpenCode (plugin)** 或 **OpenCode Go (plugin)**。
|
|
42
|
+
|
|
38
43
|
## 架构示意
|
|
39
44
|
|
|
40
45
|
```mermaid
|
|
@@ -58,14 +63,14 @@ graph LR
|
|
|
58
63
|
end
|
|
59
64
|
|
|
60
65
|
subgraph OpenCode["OpenCode 网关"]
|
|
61
|
-
Console["opencode (Console / Zen)"]
|
|
62
|
-
Go["opencode-go (Go / Go Plus)"]
|
|
66
|
+
Console["opencode-direct (Console / Zen)"]
|
|
67
|
+
Go["opencode-go-direct (Go / Go Plus)"]
|
|
63
68
|
end
|
|
64
69
|
|
|
65
70
|
Agent --> Router
|
|
66
71
|
Creds -.-> Pool
|
|
67
|
-
Router -->|opencode| Pool
|
|
68
|
-
Router -->|opencode-go| Pool
|
|
72
|
+
Router -->|opencode-direct| Pool
|
|
73
|
+
Router -->|opencode-go-direct| Pool
|
|
69
74
|
Pool --> Heatmap
|
|
70
75
|
Pool --> Transports
|
|
71
76
|
T1 & T2 & T3 & T4 --> Console & Go
|
|
@@ -94,7 +99,7 @@ graph LR
|
|
|
94
99
|
- **每账户预计可用时长**:按各窗口的平均消耗速度估算耗尽时间,并标记在重置前最先耗尽的窗口;API 仅提供当前百分比与重置时间,因此为近似值。
|
|
95
100
|
- **模型 ➔ 账户路由规则**:支持 Glob 通配符(如 `*free*`、`deepseek-*`)将模型优先绑定至特定账户,其他账户保留为备用。
|
|
96
101
|
- **配额回退**:在设置中选择免费模型;Go 返回配额错误时,同一请求通过 Console/Zen 重试一次。若回退也失败,返回原始配额错误。
|
|
97
|
-
-
|
|
102
|
+
- **会话粘性账户**:同一会话优先使用上次成功的账户,并通过本地 `sticky.json` 跨重启保留(仅哈希后的 session id 和密钥指纹);显式模型规则、固定账户和配额回退优先。
|
|
98
103
|
- **路由追踪**:配额面板显示最近的账户/模型决策(`rule`、`sticky`、`rotation`、`quota-fallback`),不记录提示词或密钥。
|
|
99
104
|
- **按账户请求统计**:请求日志记录 `ok`、`latencyMs`、`ttftMs` 和 `error`;热力图表格显示每个账户的请求数、错误数及平均延迟/TTFT。
|
|
100
105
|
- **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` 元数据,不保存任何提示词与回复。
|
|
@@ -119,15 +124,15 @@ npm pack
|
|
|
119
124
|
通过**打包文件 (.tgz)**安装:
|
|
120
125
|
|
|
121
126
|
```bash
|
|
122
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
127
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.7.tgz
|
|
123
128
|
# DSH 桌面端:
|
|
124
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
129
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.7.tgz
|
|
125
130
|
|
|
126
131
|
# 或从 npm 安装已发布版本:
|
|
127
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
132
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.7
|
|
128
133
|
```
|
|
129
134
|
|
|
130
|
-
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode** 或 **OpenCode Go** 即可。
|
|
135
|
+
安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
|
|
131
136
|
|
|
132
137
|
## 配置示例
|
|
133
138
|
|
package/lib/client.js
CHANGED
|
@@ -379,6 +379,9 @@ window.__ModuleLoader__.load({
|
|
|
379
379
|
}
|
|
380
380
|
};
|
|
381
381
|
//#endregion
|
|
382
|
+
//#region src/provider-ids.ts
|
|
383
|
+
const GO_PROVIDER = "opencode-go-direct";
|
|
384
|
+
//#endregion
|
|
382
385
|
//#region src/client/api.ts
|
|
383
386
|
async function call(path, init) {
|
|
384
387
|
const response = await fetch(path, {
|
|
@@ -452,7 +455,7 @@ window.__ModuleLoader__.load({
|
|
|
452
455
|
function fetchAnalytics(days = 30) {
|
|
453
456
|
return call(`/dsh-opencode/analytics?days=${encodeURIComponent(String(days))}`);
|
|
454
457
|
}
|
|
455
|
-
function testConnection(model, provider =
|
|
458
|
+
function testConnection(model, provider = GO_PROVIDER) {
|
|
456
459
|
return call("/dsh-opencode/test", {
|
|
457
460
|
method: "POST",
|
|
458
461
|
body: JSON.stringify({
|
|
@@ -2205,7 +2208,7 @@ window.__ModuleLoader__.load({
|
|
|
2205
2208
|
testConnection(testModel).then(setTestResult).catch((caught) => setTestResult({
|
|
2206
2209
|
ok: false,
|
|
2207
2210
|
model: testModel,
|
|
2208
|
-
provider:
|
|
2211
|
+
provider: GO_PROVIDER,
|
|
2209
2212
|
latencyMs: 0,
|
|
2210
2213
|
error: caught instanceof Error ? caught.message : t("failed")
|
|
2211
2214
|
})).finally(() => setTesting(false));
|
package/lib/index.d.ts
CHANGED
|
@@ -176,6 +176,8 @@ interface OpenCodeAdapterDeps {
|
|
|
176
176
|
readonly recordRequest?: (provider: string, model: string, purpose?: string, account?: string, reason?: string, outcome?: RequestOutcome) => void;
|
|
177
177
|
/** Test seam; defaults to ~/.dsh/storages/dsh-opencode/cooldowns.json. */
|
|
178
178
|
readonly cooldownPath?: string;
|
|
179
|
+
/** Test seam; defaults to ~/.dsh/storages/dsh-opencode/sticky.json. */
|
|
180
|
+
readonly stickyPath?: string;
|
|
179
181
|
/** Receives a one-time warning when the provider tool limit drops tools. */
|
|
180
182
|
readonly warn?: (message: string) => void;
|
|
181
183
|
}
|
|
@@ -183,7 +185,9 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
183
185
|
private readonly deps;
|
|
184
186
|
private readonly fetchImpl;
|
|
185
187
|
private readonly cooldownPath;
|
|
188
|
+
private readonly stickyPath;
|
|
186
189
|
private cooldownWrites;
|
|
190
|
+
private stickyWrites;
|
|
187
191
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
188
192
|
private readonly cooldowns;
|
|
189
193
|
private readonly sticky;
|
|
@@ -201,6 +205,11 @@ declare class OpenCodeAdapter extends LlmAdapter {
|
|
|
201
205
|
stream(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
202
206
|
private run;
|
|
203
207
|
private rememberSticky;
|
|
208
|
+
/** Await pending sticky persistence writes (tests and shutdown). */
|
|
209
|
+
flushSticky(): Promise<void>;
|
|
210
|
+
private loadSticky;
|
|
211
|
+
private pruneSticky;
|
|
212
|
+
private persistSticky;
|
|
204
213
|
private pushTrace;
|
|
205
214
|
private parse;
|
|
206
215
|
private send;
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
|
|
|
17
17
|
const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
|
|
18
18
|
const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
|
|
19
19
|
const DEFAULT_AUTH_ENTRY = "opencode-go";
|
|
20
|
-
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.
|
|
20
|
+
const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.7";
|
|
21
21
|
const DEFAULT_CONTEXT_WINDOW = 262144;
|
|
22
22
|
const DEFAULT_MAX_TOKENS = 32768;
|
|
23
23
|
const DEFAULT_GENERATE_MAX_TOKENS = 32768;
|
|
@@ -791,9 +791,24 @@ const GENERATED_MODELS = {
|
|
|
791
791
|
}
|
|
792
792
|
};
|
|
793
793
|
//#endregion
|
|
794
|
+
//#region src/provider-ids.ts
|
|
795
|
+
/**
|
|
796
|
+
* Provider ids owned by this plugin. The built-in llm-pi-ai catalog already
|
|
797
|
+
* declares `opencode` and `opencode-go`; the core registry rejects duplicate
|
|
798
|
+
* configurable provider ids and would drop the whole catalog when they clash.
|
|
799
|
+
*/
|
|
800
|
+
const CONSOLE_PROVIDER = "opencode-direct";
|
|
801
|
+
const GO_PROVIDER = "opencode-go-direct";
|
|
802
|
+
const PLUGIN_AND_LEGACY_PROVIDERS = /* @__PURE__ */ new Set([
|
|
803
|
+
CONSOLE_PROVIDER,
|
|
804
|
+
GO_PROVIDER,
|
|
805
|
+
"opencode",
|
|
806
|
+
"opencode-go"
|
|
807
|
+
]);
|
|
808
|
+
//#endregion
|
|
794
809
|
//#region src/routing.ts
|
|
795
810
|
function isGoProvider(provider) {
|
|
796
|
-
return provider === "opencode-go";
|
|
811
|
+
return provider === "opencode-go-direct" || provider === "opencode-go";
|
|
797
812
|
}
|
|
798
813
|
function rootFor(provider, config) {
|
|
799
814
|
return isGoProvider(provider) ? config.goApiBase : config.apiBase;
|
|
@@ -1059,7 +1074,7 @@ async function discoverModelIds(provider, config, apiKey, fetchImpl = fetch) {
|
|
|
1059
1074
|
return Object.keys(GENERATED_MODELS);
|
|
1060
1075
|
}
|
|
1061
1076
|
function sortForPicker(ids, config, unfiltered = false, provider) {
|
|
1062
|
-
return ids.filter((id) => unfiltered || modelVisible(id, config) && (provider
|
|
1077
|
+
return ids.filter((id) => unfiltered || modelVisible(id, config) && (!isGoProvider(provider) || modelInGoPlan(id, config))).sort((left, right) => {
|
|
1063
1078
|
const a = effectiveModel(left, config);
|
|
1064
1079
|
const b = effectiveModel(right, config);
|
|
1065
1080
|
if (a.free !== b.free) return a.free ? -1 : 1;
|
|
@@ -1072,7 +1087,7 @@ function describeModel(meta, provider) {
|
|
|
1072
1087
|
else if (meta.cost.input > 0 || meta.cost.output > 0) parts.push(`$${meta.cost.input}/$${meta.cost.output} per 1M`);
|
|
1073
1088
|
if (meta.input.includes("image")) parts.push("Image");
|
|
1074
1089
|
if (meta.reasoning.length > 0) parts.push(`reasoning: ${meta.reasoning.join("/")}`);
|
|
1075
|
-
if (provider
|
|
1090
|
+
if (isGoProvider(provider)) {
|
|
1076
1091
|
const limits = limitsFor(meta.id);
|
|
1077
1092
|
if (!meta.free) parts.unshift(limits === void 0 ? "Console" : "Go");
|
|
1078
1093
|
if (limits !== void 0) parts.push(describeLimitsShort(limits));
|
|
@@ -3076,11 +3091,17 @@ function defaultCooldownPath() {
|
|
|
3076
3091
|
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
3077
3092
|
return join(home, "storages", "dsh-opencode", "cooldowns.json");
|
|
3078
3093
|
}
|
|
3094
|
+
function defaultStickyPath() {
|
|
3095
|
+
const home = process.env.DSH_HOME?.trim() || join(homedir(), ".dsh");
|
|
3096
|
+
return join(home, "storages", "dsh-opencode", "sticky.json");
|
|
3097
|
+
}
|
|
3079
3098
|
var OpenCodeAdapter = class extends LlmAdapter {
|
|
3080
3099
|
deps;
|
|
3081
3100
|
fetchImpl;
|
|
3082
3101
|
cooldownPath;
|
|
3102
|
+
stickyPath;
|
|
3083
3103
|
cooldownWrites = Promise.resolve();
|
|
3104
|
+
stickyWrites = Promise.resolve();
|
|
3084
3105
|
/** key fingerprint → cooldown deadline and the rejection that caused it. */
|
|
3085
3106
|
cooldowns = /* @__PURE__ */ new Map();
|
|
3086
3107
|
sticky = /* @__PURE__ */ new Map();
|
|
@@ -3091,12 +3112,14 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3091
3112
|
this.deps = deps;
|
|
3092
3113
|
this.fetchImpl = deps.fetchImpl ?? fetch;
|
|
3093
3114
|
this.cooldownPath = deps.cooldownPath ?? defaultCooldownPath();
|
|
3115
|
+
this.stickyPath = deps.stickyPath ?? defaultStickyPath();
|
|
3094
3116
|
this.loadCooldowns();
|
|
3117
|
+
this.loadSticky();
|
|
3095
3118
|
}
|
|
3096
3119
|
providerInfo(provider) {
|
|
3097
3120
|
return {
|
|
3098
3121
|
id: provider,
|
|
3099
|
-
name: isGoProvider(provider) ? "OpenCode Go" : "OpenCode"
|
|
3122
|
+
name: isGoProvider(provider) ? "OpenCode Go (plugin)" : "OpenCode (plugin)"
|
|
3100
3123
|
};
|
|
3101
3124
|
}
|
|
3102
3125
|
providerRetryPolicy(_provider) {
|
|
@@ -3268,7 +3291,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3268
3291
|
const transient = this.isTransient(classified);
|
|
3269
3292
|
if (transient && !responseFailure && !fallback) this.markCooldown(entry.key, 0);
|
|
3270
3293
|
if (!(index < keys.length - 1 && transient)) {
|
|
3271
|
-
if (!fallback && config.quotaFallbackModel !== "" && options.provider
|
|
3294
|
+
if (!fallback && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && classified.code === "QUOTA") break;
|
|
3272
3295
|
throw classified;
|
|
3273
3296
|
}
|
|
3274
3297
|
}
|
|
@@ -3290,10 +3313,10 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3290
3313
|
ok: false,
|
|
3291
3314
|
error: finalError.code
|
|
3292
3315
|
});
|
|
3293
|
-
if (!fallback && config.quotaFallbackModel !== "" && options.provider
|
|
3316
|
+
if (!fallback && config.quotaFallbackModel !== "" && isGoProvider(options.provider) && (lastError === void 0 || lastError.code === "QUOTA")) try {
|
|
3294
3317
|
yield* this.run({
|
|
3295
3318
|
...options,
|
|
3296
|
-
provider:
|
|
3319
|
+
provider: CONSOLE_PROVIDER,
|
|
3297
3320
|
model: config.quotaFallbackModel
|
|
3298
3321
|
}, true);
|
|
3299
3322
|
return;
|
|
@@ -3315,6 +3338,55 @@ var OpenCodeAdapter = class extends LlmAdapter {
|
|
|
3315
3338
|
model: options.model,
|
|
3316
3339
|
at: Date.now()
|
|
3317
3340
|
});
|
|
3341
|
+
this.persistSticky();
|
|
3342
|
+
}
|
|
3343
|
+
/** Await pending sticky persistence writes (tests and shutdown). */
|
|
3344
|
+
async flushSticky() {
|
|
3345
|
+
await this.stickyWrites;
|
|
3346
|
+
}
|
|
3347
|
+
loadSticky() {
|
|
3348
|
+
try {
|
|
3349
|
+
const entries = JSON.parse(readFileSync(this.stickyPath, "utf8")).sticky;
|
|
3350
|
+
if (typeof entries !== "object" || entries === null || Array.isArray(entries)) return;
|
|
3351
|
+
for (const [sessionId, value] of Object.entries(entries)) {
|
|
3352
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
|
|
3353
|
+
const record = value;
|
|
3354
|
+
const fingerprint = record.fingerprint;
|
|
3355
|
+
const label = record.label;
|
|
3356
|
+
const provider = record.provider;
|
|
3357
|
+
const model = record.model;
|
|
3358
|
+
const at = record.at;
|
|
3359
|
+
if (typeof fingerprint !== "string" || !/^[0-9a-f]{16}$/.test(fingerprint)) continue;
|
|
3360
|
+
if (typeof label !== "string" || label === "") continue;
|
|
3361
|
+
if (typeof provider !== "string" || provider === "") continue;
|
|
3362
|
+
if (typeof model !== "string" || model === "") continue;
|
|
3363
|
+
if (typeof at !== "number" || !Number.isFinite(at)) continue;
|
|
3364
|
+
this.sticky.set(sessionId, {
|
|
3365
|
+
fingerprint,
|
|
3366
|
+
label,
|
|
3367
|
+
provider,
|
|
3368
|
+
model,
|
|
3369
|
+
at
|
|
3370
|
+
});
|
|
3371
|
+
}
|
|
3372
|
+
this.pruneSticky();
|
|
3373
|
+
} catch {}
|
|
3374
|
+
}
|
|
3375
|
+
pruneSticky() {
|
|
3376
|
+
const cutoff = Date.now() - 12096e5;
|
|
3377
|
+
for (const [sessionId, entry] of this.sticky) if (entry.at < cutoff) this.sticky.delete(sessionId);
|
|
3378
|
+
const overflow = this.sticky.size - 500;
|
|
3379
|
+
if (overflow <= 0) return;
|
|
3380
|
+
const oldest = [...this.sticky.entries()].sort((a, b) => a[1].at - b[1].at).slice(0, overflow);
|
|
3381
|
+
for (const [sessionId] of oldest) this.sticky.delete(sessionId);
|
|
3382
|
+
}
|
|
3383
|
+
persistSticky() {
|
|
3384
|
+
this.pruneSticky();
|
|
3385
|
+
const payload = JSON.stringify({ sticky: Object.fromEntries(this.sticky) });
|
|
3386
|
+
this.stickyWrites = this.stickyWrites.then(async () => {
|
|
3387
|
+
await mkdir(dirname(this.stickyPath), { recursive: true });
|
|
3388
|
+
await writeFile(this.stickyPath, payload, "utf8");
|
|
3389
|
+
}).catch(() => {});
|
|
3318
3390
|
}
|
|
3319
3391
|
pushTrace(entry) {
|
|
3320
3392
|
this.routing.unshift(entry);
|
|
@@ -3492,7 +3564,7 @@ function relative(until) {
|
|
|
3492
3564
|
}
|
|
3493
3565
|
async function accountsText(deps) {
|
|
3494
3566
|
const lines = [];
|
|
3495
|
-
for (const provider of [
|
|
3567
|
+
for (const provider of [GO_PROVIDER, CONSOLE_PROVIDER]) {
|
|
3496
3568
|
let keys;
|
|
3497
3569
|
try {
|
|
3498
3570
|
keys = await deps.pool.resolveAll(provider);
|
|
@@ -3508,7 +3580,7 @@ async function accountsText(deps) {
|
|
|
3508
3580
|
lines.push(`${provider}:`);
|
|
3509
3581
|
for (const [index, entry] of keys.entries()) {
|
|
3510
3582
|
const health = deps.adapter.accountHealth(entry.key);
|
|
3511
|
-
const usage = provider === "opencode-go" ? await deps.usage(provider, entry.key) : void 0;
|
|
3583
|
+
const usage = provider === "opencode-go-direct" ? await deps.usage(provider, entry.key) : void 0;
|
|
3512
3584
|
const tags = [
|
|
3513
3585
|
entry.source,
|
|
3514
3586
|
entry === active ? "active" : "",
|
|
@@ -3528,7 +3600,7 @@ function rulesText(deps) {
|
|
|
3528
3600
|
async function modelsText(deps) {
|
|
3529
3601
|
const config = deps.config();
|
|
3530
3602
|
try {
|
|
3531
|
-
const models = await deps.adapter.listModels(
|
|
3603
|
+
const models = await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true });
|
|
3532
3604
|
if (models.length === 0) return "No models.";
|
|
3533
3605
|
return models.map((model) => {
|
|
3534
3606
|
const meta = effectiveModel(model.id, config);
|
|
@@ -3545,7 +3617,7 @@ async function testText(deps, model) {
|
|
|
3545
3617
|
let text = "";
|
|
3546
3618
|
try {
|
|
3547
3619
|
for await (const chunk of deps.adapter.stream({
|
|
3548
|
-
provider:
|
|
3620
|
+
provider: GO_PROVIDER,
|
|
3549
3621
|
model,
|
|
3550
3622
|
messages: [{
|
|
3551
3623
|
role: "user",
|
|
@@ -3593,7 +3665,7 @@ function applyCommands(ctx, deps) {
|
|
|
3593
3665
|
text: await statusText(deps)
|
|
3594
3666
|
};
|
|
3595
3667
|
case "usage": {
|
|
3596
|
-
const usage = await deps.usage(
|
|
3668
|
+
const usage = await deps.usage(GO_PROVIDER);
|
|
3597
3669
|
if (usage?.report === void 0) return {
|
|
3598
3670
|
kind: "error",
|
|
3599
3671
|
text: `Quota unavailable: ${usage?.error ?? "no report"}`
|
|
@@ -4138,7 +4210,7 @@ function aggregateSessions(sessions, days, now = Date.now()) {
|
|
|
4138
4210
|
for (const session of sessions) {
|
|
4139
4211
|
const provider = session.lastUsed?.provider;
|
|
4140
4212
|
const model = session.lastUsed?.model;
|
|
4141
|
-
if (provider !== "
|
|
4213
|
+
if (typeof provider !== "string" || !PLUGIN_AND_LEGACY_PROVIDERS.has(provider) || model === void 0 || model === "") continue;
|
|
4142
4214
|
const usage = session.usage;
|
|
4143
4215
|
if (usage === void 0) continue;
|
|
4144
4216
|
const input = count(usage.uncachedInputTokens);
|
|
@@ -4563,14 +4635,14 @@ async function buildState(ctx, deps) {
|
|
|
4563
4635
|
...info.source === void 0 ? {} : { source: info.source }
|
|
4564
4636
|
};
|
|
4565
4637
|
} catch {}
|
|
4566
|
-
const resolved = await deps.pool.resolveAll(
|
|
4638
|
+
const resolved = await deps.pool.resolveAll(GO_PROVIDER).catch(() => []);
|
|
4567
4639
|
if (!keyInfo.configured) keyInfo = {
|
|
4568
4640
|
...keyInfo,
|
|
4569
4641
|
configured: resolved.length > 0
|
|
4570
4642
|
};
|
|
4571
4643
|
const usageByKey = /* @__PURE__ */ new Map();
|
|
4572
4644
|
await Promise.all([...new Set(resolved.map((entry) => entry.key))].map(async (key) => {
|
|
4573
|
-
const result = await deps.usage(
|
|
4645
|
+
const result = await deps.usage(GO_PROVIDER, key);
|
|
4574
4646
|
usageByKey.set(key, result ?? null);
|
|
4575
4647
|
}));
|
|
4576
4648
|
const usageByLabel = /* @__PURE__ */ new Map();
|
|
@@ -4617,7 +4689,7 @@ async function buildState(ctx, deps) {
|
|
|
4617
4689
|
}
|
|
4618
4690
|
let models = [];
|
|
4619
4691
|
try {
|
|
4620
|
-
models = (await deps.adapter.listModels(
|
|
4692
|
+
models = (await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true })).map((model) => {
|
|
4621
4693
|
const meta = effectiveModel(model.id, config);
|
|
4622
4694
|
return {
|
|
4623
4695
|
id: model.id,
|
|
@@ -4690,10 +4762,10 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4690
4762
|
}
|
|
4691
4763
|
try {
|
|
4692
4764
|
const config = deps.config();
|
|
4693
|
-
const models = await deps.adapter.listModels(
|
|
4765
|
+
const models = await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true }).catch(() => []);
|
|
4694
4766
|
json(res, 200, {
|
|
4695
4767
|
ok: true,
|
|
4696
|
-
providers: [
|
|
4768
|
+
providers: [CONSOLE_PROVIDER, GO_PROVIDER],
|
|
4697
4769
|
accounts: config.accounts.length + 1,
|
|
4698
4770
|
models: models.length
|
|
4699
4771
|
});
|
|
@@ -4847,7 +4919,7 @@ function applyRoutes(ctx, web, deps) {
|
|
|
4847
4919
|
return;
|
|
4848
4920
|
}
|
|
4849
4921
|
const model = typeof body.model === "string" && body.model !== "" ? body.model : "deepseek-v4.1-flash";
|
|
4850
|
-
const provider = body.provider === "opencode" ?
|
|
4922
|
+
const provider = body.provider === "opencode-direct" ? CONSOLE_PROVIDER : GO_PROVIDER;
|
|
4851
4923
|
const started = Date.now();
|
|
4852
4924
|
const controller = new AbortController();
|
|
4853
4925
|
const timer = setTimeout(() => controller.abort(), 6e4);
|
|
@@ -5170,7 +5242,7 @@ function fingerprint(key) {
|
|
|
5170
5242
|
function createUsageReader(config, resolveKey, fetchImpl = fetch) {
|
|
5171
5243
|
const cache = /* @__PURE__ */ new Map();
|
|
5172
5244
|
const read = async (provider, explicitKey) => {
|
|
5173
|
-
if (provider !== "opencode-go") return void 0;
|
|
5245
|
+
if (provider !== "opencode-go-direct" && provider !== "opencode-go") return void 0;
|
|
5174
5246
|
const current = config();
|
|
5175
5247
|
let key;
|
|
5176
5248
|
if (explicitKey !== void 0 && explicitKey !== "") key = explicitKey;
|
|
@@ -5252,18 +5324,18 @@ function apply(ctx, config) {
|
|
|
5252
5324
|
warn: (message) => ctx.logger("llm-opencode").warn(message)
|
|
5253
5325
|
});
|
|
5254
5326
|
ctx.llm.registerConfigurableProviders([{
|
|
5255
|
-
provider:
|
|
5256
|
-
displayName: "OpenCode",
|
|
5327
|
+
provider: CONSOLE_PROVIDER,
|
|
5328
|
+
displayName: "OpenCode (plugin)",
|
|
5257
5329
|
settingsNs: NS,
|
|
5258
5330
|
settingsPath: []
|
|
5259
5331
|
}, {
|
|
5260
|
-
provider:
|
|
5261
|
-
displayName: "OpenCode Go",
|
|
5332
|
+
provider: GO_PROVIDER,
|
|
5333
|
+
displayName: "OpenCode Go (plugin)",
|
|
5262
5334
|
settingsNs: NS,
|
|
5263
5335
|
settingsPath: []
|
|
5264
5336
|
}]);
|
|
5265
|
-
ctx.llm.registerAdapter([
|
|
5266
|
-
ctx.logger("llm-opencode").info(
|
|
5337
|
+
ctx.llm.registerAdapter([CONSOLE_PROVIDER, GO_PROVIDER], adapter);
|
|
5338
|
+
ctx.logger("llm-opencode").info(`registered providers: ${CONSOLE_PROVIDER}, ${GO_PROVIDER}`);
|
|
5267
5339
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
5268
5340
|
settingsCtx.settings.configure({ auto: false }, ctx.fiber);
|
|
5269
5341
|
});
|