@goodandready/dsh-opencode-provider 0.5.30 → 0.5.32

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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.32 — release 9: proxy auth and route switches
4
+
5
+ - **#71 — authenticated proxy**: `proxyUsername` plus a password stored in the credential service enable Basic auth for HTTP/HTTPS and SOCKS5 proxies. The authenticated URL exists only in memory; `/state` returns the credential-free address and a `configured` flag, never the password.
6
+ - **#80 — route switches**: `goEnabled` and `consoleEnabled` are exposed in Settings. Disabling a route removes it from the provider directory, the picker and request handling at runtime; at least one route stays enabled.
7
+ - 144 tests.
8
+
9
+ ## 0.5.31 — release 8: command localization
10
+
11
+ - **#48 — `/opencode` localization**: new `commandLocale` setting (`en` default, `zh`) localizes status, usage, accounts, models, rules and errors. Exposed in Settings → OpenCode.
12
+ - **#72 — thinking budgets**: DSH exposes selectable `reasoningEffort` ids only; there is no numeric per-model token-budget seam, and the Anthropic transport uses `output_config.effort`. No synthetic mapping is added; limitation recorded in DESIGN and the issue is closed with evidence.
13
+ - 140 tests.
14
+
3
15
  ## 0.5.30 — release 7: pricing accuracy
4
16
 
5
17
  - **#74 — peak/off-peak multipliers**: analytics now reads live gateway `cost.multipliers` and evaluates UTC weekday/hour/date conditions at the session timestamp.
package/README.md CHANGED
@@ -99,7 +99,8 @@ graph LR
99
99
  - **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. A per-account table shows requests, errors and average latency/TTFT. The adapter appends `{at, provider, model, purpose?, account, reason, ok, latencyMs, ttftMs?, error?}` 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.
100
100
  - **Stable `x-opencode-session`** derived from the durable session id, so retries and every turn of a conversation reuse the same routing key.
101
101
  - **Live catalog** from `/v1/models` with a generated `models.dev` fallback.
102
- - **Proxy support**: one live `proxyUrl` setting routes inference, model discovery and usage checks through HTTP, HTTPS or SOCKS5; the dispatcher is reused until the address changes.
102
+ - **Proxy support**: one live `proxyUrl` setting routes inference, model discovery and usage checks through HTTP, HTTPS or SOCKS5; the dispatcher is reused until the address changes. `proxyUsername` and a password stored in the credential service add Basic auth for both HTTP and SOCKS5. Passwords are never returned to the browser.
103
+ - **Route switches**: enable or disable the Console/Zen and Go/Go Plus routes independently; a disabled route is removed from the picker and refuses requests. At least one route stays enabled.
103
104
  - **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.
104
105
  - **`/opencode` command** for status, usage, accounts, models, rules, and `test [model]`.
105
106
  - **TUI settings section** for the terminal profile (`/settings → OpenCode`) with the key, endpoints, proxy, allowlist, active account, fallback model, sticky routing and timeouts.
@@ -132,12 +133,12 @@ npm pack
132
133
  Install the **packed tarball** into a profile:
133
134
 
134
135
  ```bash
135
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
136
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
136
137
  # DSH Desktop:
137
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
138
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
138
139
 
139
140
  # Or install the published package:
140
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.30
141
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.32
141
142
  ```
142
143
 
143
144
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
@@ -278,6 +279,8 @@ Captured from the exact 0.5.0 release candidate in DSH Desktop in the dark theme
278
279
  /opencode test [model] # minimal real request
279
280
  ```
280
281
 
282
+ Set **Settings → OpenCode → `/opencode` command language** to `中文` to localize this output. The default is English.
283
+
281
284
  ## Verified API facts
282
285
 
283
286
  - `GET /inference/go/v1/models` is public and returns the live Go roster.
package/README.ru.md CHANGED
@@ -108,7 +108,8 @@ graph LR
108
108
  - **Почасовой Heatmap запросов**: сетка «день недели × час» в локальном времени плюс суточная полоса агрегации. Фильтры переключают отображение между всеми запросами, конкретной моделью и аккаунтом. Лог `~/.dsh/storages/dsh-opencode/requests.jsonl` хранит только метаданные `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` (промпты и ответы не сохраняются).
109
109
  - **Стабильный заголовок `x-opencode-session`**: вычисляется из устойчивого ID сессии DSH, обеспечивая одинаковый ключ маршрутизации при повторах и диалоге.
110
110
  - **Живой каталог моделей**: автоматическое чтение `/v1/models` с резервным fallback из `models.dev`.
111
- - **Proxy**: live-параметр `proxyUrl` направляет инференс, каталог и проверку квот через HTTP, HTTPS или SOCKS5; dispatcher переиспользуется до смены адреса.
111
+ - **Proxy**: live-параметр `proxyUrl` направляет инференс, каталог и проверку квот через HTTP, HTTPS или SOCKS5; dispatcher переиспользуется до смены адреса. `proxyUsername` и пароль из credential-сервиса включают Basic auth для HTTP и SOCKS5. Пароль не возвращается в браузер.
112
+ - **Переключатели маршрутов**: Console/Zen и Go/Go Plus включаются и отключаются независимо; отключённый маршрут исчезает из выбора и отклоняет запросы. Хотя бы один маршрут остаётся включённым.
112
113
  - **Оценки лимитов подписки OpenCode Go**: долларовый лимит и расчетное число запросов для окон 5h, недели и месяца для планов Go и Go Plus.
113
114
  - **Команда `/opencode`**: быстрый просмотр статуса, квот, аккаунтов, моделей, правил и запуск теста связи `test [model]`.
114
115
  - **Настройки в терминале (TUI)**: раздел `/settings → OpenCode` для профиля terminal с ключом, эндпоинтами, allowlist, активным аккаунтом, fallback-моделью, sticky-маршрутизацией и таймаутами.
@@ -131,12 +132,12 @@ npm pack
131
132
  Установка из **упакованного tarball**:
132
133
 
133
134
  ```bash
134
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
135
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
135
136
  # DSH Desktop:
136
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
137
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
137
138
 
138
139
  # Либо установка опубликованного пакета из npm:
139
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.30
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.32
140
141
  ```
141
142
 
142
143
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
@@ -208,6 +209,8 @@ modelAccountRules:
208
209
  /opencode test [model] # проверочный запрос к модели
209
210
  ```
210
211
 
212
+ Язык вывода задаётся в **Настройки → OpenCode → `/opencode` command language** (`中文` включает китайский). По умолчанию английский.
213
+
211
214
  ## Безопасность и надежность
212
215
 
213
216
  - Запросы на изменение настроек и ключей разрешены только с loopback (`127.0.0.1` / `::1`) с обязательной проверкой same-origin для защиты от DNS-rebinding атак.
package/README.zh.md CHANGED
@@ -106,7 +106,8 @@ graph LR
106
106
  - **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` 元数据,不保存任何提示词与回复。
107
107
  - **稳定的 `x-opencode-session` 标头**:根据 DSH 会话 ID 派生,确保重试与连续对话复用相同的网关路由键。
108
108
  - **实时模型目录**:自动从 `/v1/models` 同步,并以 `models.dev` 生成文件作为回退。
109
- - **代理**:一个实时 `proxyUrl` 设置即可让推理、模型目录和额度检查走 HTTP、HTTPS 或 SOCKS5;地址变化前复用 dispatcher。
109
+ - **代理**:一个实时 `proxyUrl` 设置即可让推理、模型目录和额度检查走 HTTP、HTTPS 或 SOCKS5;地址变化前复用 dispatcher。`proxyUsername` 与凭据服务中的密码可为 HTTP 和 SOCKS5 启用 Basic 认证,密码不会返回浏览器。
110
+ - **路由开关**:可独立启用或禁用 Console/Zen 与 Go/Go Plus 路由;禁用的路由会从选择器移除并拒绝请求,至少保留一个路由。
110
111
  - **Go 订阅额度估算**:每月美元额度以及 5 小时、每周和每月的估算请求次数。
111
112
  - **`/opencode` 斜杠指令**:快速查询状态、配额、账户、模型及执行联通性测试。
112
113
  - **TUI 设置**:`/settings → OpenCode` 支持密钥、端点、allowlist、活动账户、回退模型、粘性路由和超时。
@@ -126,12 +127,12 @@ npm pack
126
127
  通过**打包文件 (.tgz)**安装:
127
128
 
128
129
  ```bash
129
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
130
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
130
131
  # DSH 桌面端:
131
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
132
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.32.tgz
132
133
 
133
134
  # 或从 npm 安装已发布版本:
134
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.30
135
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.32
135
136
  ```
136
137
 
137
138
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
@@ -183,6 +184,8 @@ modelAccountRules:
183
184
  /opencode test [model] # 发送最小化测试请求
184
185
  ```
185
186
 
187
+ 在 **设置 → OpenCode → `/opencode` 命令语言** 中选择 `中文` 即可切换输出语言,默认英语。
188
+
186
189
  ## 安全规范
187
190
 
188
191
  - 写入与配置路由均要求 Loopback(`127.0.0.1` / `::1`)与同源检查,防御 DNS 重新绑定。
package/lib/client.js CHANGED
@@ -187,8 +187,21 @@ window.__ModuleLoader__.load({
187
187
  requestImagePixelBudget: "Image pixel budget",
188
188
  requestImageMaxBytes: "Per-image max bytes",
189
189
  catalogRefreshMinutes: "Catalog refresh (minutes)",
190
+ commandLocale: "/opencode command language",
191
+ commandLocaleHint: "Language for /opencode status, accounts, usage, models and rules.",
190
192
  proxyUrl: "Proxy URL",
191
- proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Credentials are not supported.",
193
+ proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Do not put credentials in this URL.",
194
+ proxyUsername: "Proxy username",
195
+ proxyPassword: "Proxy password",
196
+ proxyPasswordSet: "configured — enter a new one to replace it",
197
+ proxyPasswordUnset: "not configured",
198
+ proxyPasswordHint: "Stored in the credential service, never returned to the browser.",
199
+ saveProxyPassword: "Save proxy password",
200
+ clearProxyPassword: "Clear proxy password",
201
+ enabledRoutes: "Enabled routes",
202
+ routeConsole: "OpenCode Console / Zen",
203
+ routeGo: "OpenCode Go / Go Plus",
204
+ enabledRoutesHint: "Disabled routes are removed from the picker; at least one route stays enabled.",
192
205
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
193
206
  contextOverrideShort: "Context",
194
207
  maxOutputOverrideShort: "Max output",
@@ -421,8 +434,21 @@ window.__ModuleLoader__.load({
421
434
  requestImagePixelBudget: "图片像素预算",
422
435
  requestImageMaxBytes: "单图最大字节数",
423
436
  catalogRefreshMinutes: "目录刷新间隔(分钟)",
437
+ commandLocale: "/opencode 命令语言",
438
+ commandLocaleHint: "用于 /opencode 的状态、账户、用量、模型和规则输出。",
424
439
  proxyUrl: "代理地址",
425
- proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。不支持代理认证。",
440
+ proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。请勿在此 URL 中填写凭据。",
441
+ proxyUsername: "代理用户名",
442
+ proxyPassword: "代理密码",
443
+ proxyPasswordSet: "已配置 — 输入新密码可替换",
444
+ proxyPasswordUnset: "未配置",
445
+ proxyPasswordHint: "保存在凭据服务中,不会返回给浏览器。",
446
+ saveProxyPassword: "保存代理密码",
447
+ clearProxyPassword: "清除代理密码",
448
+ enabledRoutes: "启用的路由",
449
+ routeConsole: "OpenCode Console / Zen",
450
+ routeGo: "OpenCode Go / Go Plus",
451
+ enabledRoutesHint: "禁用的路由会从选择器中移除;至少保留一个启用的路由。",
426
452
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
427
453
  contextOverrideShort: "上下文",
428
454
  maxOutputOverrideShort: "最大输出",
@@ -552,6 +578,18 @@ window.__ModuleLoader__.load({
552
578
  body: JSON.stringify({ ref })
553
579
  });
554
580
  }
581
+ function saveProxyPassword(password) {
582
+ return call("/dsh-opencode/proxy-password", {
583
+ method: "POST",
584
+ body: JSON.stringify({ password })
585
+ });
586
+ }
587
+ function clearProxyPassword() {
588
+ return call("/dsh-opencode/proxy-password/clear", {
589
+ method: "POST",
590
+ body: "{}"
591
+ });
592
+ }
555
593
  function importAuth(entry, ref) {
556
594
  return call("/dsh-opencode/auth-import", {
557
595
  method: "POST",
@@ -1714,7 +1752,11 @@ window.__ModuleLoader__.load({
1714
1752
  requestImagePixelBudget: String(state.config.requestImagePixelBudget),
1715
1753
  requestImageMaxBytes: String(state.config.requestImageMaxBytes),
1716
1754
  proxyUrl: state.config.proxyUrl,
1755
+ proxyUsername: state.config.proxyUsername,
1756
+ goEnabled: state.config.goEnabled,
1757
+ consoleEnabled: state.config.consoleEnabled,
1717
1758
  catalogRefreshMinutes: String(state.config.catalogRefreshMinutes),
1759
+ commandLocale: state.config.commandLocale,
1718
1760
  maxOutputTokens: state.config.maxOutputTokens === null ? "" : String(state.config.maxOutputTokens),
1719
1761
  contextWindow: state.config.contextWindow === null ? "" : String(state.config.contextWindow),
1720
1762
  contextWindowOverrides: Object.fromEntries(Object.entries(state.config.contextWindowOverrides).map(([id, value]) => [id, String(value)])),
@@ -1767,7 +1809,11 @@ window.__ModuleLoader__.load({
1767
1809
  requestImagePixelBudget: numberOrNull(draft.requestImagePixelBudget) ?? 2458624,
1768
1810
  requestImageMaxBytes: numberOrNull(draft.requestImageMaxBytes) ?? 1048576,
1769
1811
  proxyUrl: draft.proxyUrl.trim(),
1812
+ proxyUsername: draft.proxyUsername.trim(),
1813
+ goEnabled: draft.goEnabled,
1814
+ consoleEnabled: draft.consoleEnabled,
1770
1815
  catalogRefreshMinutes: numberOrNull(draft.catalogRefreshMinutes) ?? 10,
1816
+ commandLocale: draft.commandLocale,
1771
1817
  maxOutputTokens: numberOrNull(draft.maxOutputTokens),
1772
1818
  contextWindow: numberOrNull(draft.contextWindow),
1773
1819
  contextWindowOverrides: numberRecordOrNull(draft.contextWindowOverrides),
@@ -2008,6 +2054,8 @@ window.__ModuleLoader__.load({
2008
2054
  const [error, setError] = (0, react.useState)(null);
2009
2055
  const [notice, setNotice] = (0, react.useState)(null);
2010
2056
  const [keyDraft, setKeyDraft] = (0, react.useState)("");
2057
+ const [proxyPassword, setProxyPassword] = (0, react.useState)("");
2058
+ const [proxyBusy, setProxyBusy] = (0, react.useState)(false);
2011
2059
  const [keyDrafts, setKeyDrafts] = (0, react.useState)({});
2012
2060
  const [search, setSearch] = (0, react.useState)("");
2013
2061
  const [testModel, setTestModel] = (0, react.useState)("deepseek-v4.1-flash");
@@ -2071,6 +2119,34 @@ window.__ModuleLoader__.load({
2071
2119
  setSaving(false);
2072
2120
  }
2073
2121
  };
2122
+ const storeProxyPassword = async () => {
2123
+ if (proxyPassword === "") return;
2124
+ setProxyBusy(true);
2125
+ setError(null);
2126
+ try {
2127
+ await saveProxyPassword(proxyPassword);
2128
+ setProxyPassword("");
2129
+ setNotice(t("saved"));
2130
+ await load();
2131
+ } catch (caught) {
2132
+ setError(caught instanceof Error ? caught.message : t("failed"));
2133
+ } finally {
2134
+ setProxyBusy(false);
2135
+ }
2136
+ };
2137
+ const removeProxyPassword = async () => {
2138
+ setProxyBusy(true);
2139
+ setError(null);
2140
+ try {
2141
+ await clearProxyPassword();
2142
+ setNotice(t("saved"));
2143
+ await load();
2144
+ } catch (caught) {
2145
+ setError(caught instanceof Error ? caught.message : t("failed"));
2146
+ } finally {
2147
+ setProxyBusy(false);
2148
+ }
2149
+ };
2074
2150
  const checkKey = async (label) => {
2075
2151
  setChecking(label);
2076
2152
  setError(null);
@@ -3208,12 +3284,115 @@ window.__ModuleLoader__.load({
3208
3284
  value: draft.catalogRefreshMinutes,
3209
3285
  onChange: (value) => update({ catalogRefreshMinutes: value })
3210
3286
  }),
3287
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3288
+ className: "oc-field",
3289
+ children: [
3290
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3291
+ className: "oc-label",
3292
+ children: t("commandLocale")
3293
+ }),
3294
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3295
+ className: "oc-select",
3296
+ value: draft.commandLocale,
3297
+ onChange: (event) => update({ commandLocale: event.target.value }),
3298
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3299
+ value: "en",
3300
+ children: "English"
3301
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3302
+ value: "zh",
3303
+ children: "中文"
3304
+ })]
3305
+ }),
3306
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3307
+ className: "oc-hint",
3308
+ children: t("commandLocaleHint")
3309
+ })
3310
+ ]
3311
+ }),
3211
3312
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3212
3313
  label: t("proxyUrl"),
3213
3314
  hint: t("proxyUrlHint"),
3214
3315
  value: draft.proxyUrl,
3215
3316
  onChange: (value) => update({ proxyUrl: value })
3216
3317
  }),
3318
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3319
+ label: t("proxyUsername"),
3320
+ value: draft.proxyUsername,
3321
+ onChange: (value) => update({ proxyUsername: value })
3322
+ }),
3323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3324
+ className: "oc-field",
3325
+ children: [
3326
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3327
+ className: "oc-label",
3328
+ children: t("proxyPassword")
3329
+ }),
3330
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3331
+ className: "oc-input",
3332
+ type: "password",
3333
+ value: proxyPassword,
3334
+ placeholder: state.proxy.configured ? t("proxyPasswordSet") : t("proxyPasswordUnset"),
3335
+ onChange: (event) => setProxyPassword(event.target.value)
3336
+ }),
3337
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3338
+ className: "oc-hint",
3339
+ children: t("proxyPasswordHint")
3340
+ }),
3341
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3342
+ className: "oc-actions",
3343
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3344
+ type: "button",
3345
+ className: "oc-button",
3346
+ disabled: proxyBusy || proxyPassword === "",
3347
+ onClick: () => {
3348
+ storeProxyPassword();
3349
+ },
3350
+ children: t("saveProxyPassword")
3351
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3352
+ type: "button",
3353
+ className: "oc-button",
3354
+ disabled: proxyBusy || !state.proxy.configured,
3355
+ onClick: () => {
3356
+ removeProxyPassword();
3357
+ },
3358
+ children: t("clearProxyPassword")
3359
+ })]
3360
+ })
3361
+ ]
3362
+ }),
3363
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3364
+ className: "oc-field",
3365
+ children: [
3366
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3367
+ className: "oc-label",
3368
+ children: t("enabledRoutes")
3369
+ }),
3370
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3371
+ className: "oc-actions",
3372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
3373
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3374
+ type: "checkbox",
3375
+ checked: draft.consoleEnabled,
3376
+ onChange: (event) => update({ consoleEnabled: event.target.checked })
3377
+ }),
3378
+ " ",
3379
+ t("routeConsole")
3380
+ ] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", { children: [
3381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3382
+ type: "checkbox",
3383
+ checked: draft.goEnabled,
3384
+ onChange: (event) => update({ goEnabled: event.target.checked })
3385
+ }),
3386
+ " ",
3387
+ t("routeGo")
3388
+ ] })]
3389
+ }),
3390
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3391
+ className: "oc-hint",
3392
+ children: t("enabledRoutesHint")
3393
+ })
3394
+ ]
3395
+ }),
3217
3396
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3218
3397
  label: t("maxOutputTokens"),
3219
3398
  type: "number",
package/lib/index.d.ts CHANGED
@@ -5,6 +5,7 @@ import { AttachmentStore } from "@deepseek-ai/dsh-attachment";
5
5
  import { WebServer } from "@deepseek-ai/dsh-host-webserver";
6
6
  type TransportId = 'openai-chat' | 'openai-responses' | 'anthropic' | 'google';
7
7
  type UsageDisplay = 'auto' | 'always' | 'off';
8
+ type CommandLocale = 'en' | 'zh';
8
9
  interface AccountConfig {
9
10
  label?: string;
10
11
  apiKeyEnv?: string;
@@ -49,8 +50,18 @@ interface Config {
49
50
  requestImageMaxBytes?: number;
50
51
  /** HTTP/HTTPS/SOCKS5 proxy for inference, catalog and usage requests. */
51
52
  proxyUrl?: string;
53
+ /** Proxy username; the password is resolved from `proxyPasswordRef`. */
54
+ proxyUsername?: string;
55
+ /** Credential reference for the proxy password. */
56
+ proxyPasswordRef?: string;
57
+ /** Enable the Go / Go Plus subscription route. */
58
+ goEnabled?: boolean;
59
+ /** Enable the Console / Zen pay-as-you-go route. */
60
+ consoleEnabled?: boolean;
52
61
  /** Live catalog cache lifetime in minutes; defaults to 10. */
53
62
  catalogRefreshMinutes?: number;
63
+ /** Language for `/opencode` output; defaults to English. */
64
+ commandLocale?: CommandLocale;
54
65
  /** Visible-model allowlist; an empty list means "show everything". */
55
66
  visibleModels?: string[];
56
67
  /** Per-model visibility overrides; wins over `visibleModels`. */
@@ -103,7 +114,12 @@ interface ResolvedConfig {
103
114
  requestImagePixelBudget: number;
104
115
  requestImageMaxBytes: number;
105
116
  proxyUrl: string;
117
+ proxyUsername: string;
118
+ proxyPasswordRef: string;
119
+ goEnabled: boolean;
120
+ consoleEnabled: boolean;
106
121
  catalogRefreshMinutes: number;
122
+ commandLocale: CommandLocale;
107
123
  visibleModels: readonly string[];
108
124
  modelVisibility: Readonly<Record<string, boolean>>;
109
125
  filterModelsByPlan: boolean;
@@ -316,6 +332,7 @@ interface CommandDeps {
316
332
  readonly pool: KeyPool;
317
333
  readonly usage: (provider: string, key?: string) => Promise<UsageResult | undefined>;
318
334
  readonly adapter: OpenCodeAdapter;
335
+ readonly locale: () => 'en' | 'zh';
319
336
  }
320
337
  declare function applyCommands(ctx: Context, deps: CommandDeps): void;
321
338
  interface ModelUsageRow {
@@ -403,6 +420,7 @@ interface RouteDeps {
403
420
  readonly analytics?: (days: number) => Promise<AnalyticsReport>;
404
421
  readonly heatmap?: (days: number) => Promise<RequestHeatmap>;
405
422
  readonly refreshTui?: () => void;
423
+ readonly syncRoutes?: () => void;
406
424
  /** Test seam; defaults to the local managed-credentials registry. */
407
425
  readonly managedPath?: string;
408
426
  }
package/lib/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { credentialRef } from "@deepseek-ai/dsh-credentials";
1
2
  import { createHash } from "node:crypto";
2
3
  import { existsSync, readFileSync, statSync } from "node:fs";
3
4
  import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
@@ -6,15 +7,15 @@ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
7
  import { LlmAdapter, LlmError, ReasoningEffortId, ToolCallId, attributionHeaders, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
7
8
  import z from "@deepseek-ai/schemastery";
8
9
  import { ProxyAgent, Socks5ProxyAgent } from "undici";
9
- import { credentialRef } from "@deepseek-ai/dsh-credentials";
10
10
  import { launchEnvironmentOf } from "@deepseek-ai/dsh-launch-environment";
11
11
  import { spawn } from "node:child_process";
12
12
  import { fileURLToPath } from "node:url";
13
13
  import { isIP } from "node:net";
14
14
  // src/proxy.ts
15
15
  /**
16
- * Proxy addresses are deliberately credential-free: the settings API returns
17
- * the configured address to the browser, so embedding a password would leak it.
16
+ * Proxy addresses stay credential-free: the settings API returns the configured
17
+ * address to the browser. Authentication is supplied separately from the
18
+ * credential service and is only assembled in memory.
18
19
  */
19
20
  function normalizeProxyUrl(raw) {
20
21
  const value = raw?.trim() ?? "";
@@ -31,7 +32,24 @@ function normalizeProxyUrl(raw) {
31
32
  "socks5:"
32
33
  ].includes(url.protocol)) throw new Error("proxy address must use http://, https:// or socks5://");
33
34
  if (url.hostname === "" || url.port === "0" || url.pathname !== "" && url.pathname !== "/" || url.search !== "" || url.hash !== "") throw new Error("proxy address must contain a host and optional port, without a path, query or fragment");
34
- if (url.username !== "" || url.password !== "") throw new Error("proxy credentials are not supported; use a local proxy without authentication");
35
+ if (url.username !== "" || url.password !== "") throw new Error("proxy credentials must be set through the proxy username and password fields");
36
+ return url.href;
37
+ }
38
+ /** Validate a proxy username; the password lives in the credential service. */
39
+ function normalizeProxyUsername(raw) {
40
+ const value = raw?.trim() ?? "";
41
+ if (value === "") return "";
42
+ if (value.length > 256 || value.includes(":") || /[\u0000-\u001f\u007f]/.test(value)) throw new Error("proxy username must be 1–256 characters without a colon or control characters");
43
+ return value;
44
+ }
45
+ /** Assemble the authenticated proxy URL in memory; never persist or return it. */
46
+ function buildProxyUrl(address, username, password) {
47
+ const base = normalizeProxyUrl(address);
48
+ const user = normalizeProxyUsername(username);
49
+ if (base === "" || user === "") return base;
50
+ const url = new URL(base);
51
+ url.username = encodeURIComponent(user);
52
+ url.password = encodeURIComponent(password ?? "");
35
53
  return url.href;
36
54
  }
37
55
  /**
@@ -43,7 +61,7 @@ function createProxyFetch(address, baseFetch = fetch) {
43
61
  let disposed = false;
44
62
  const wrapped = (async (input, init) => {
45
63
  if (disposed) throw new Error("proxy transport is disposed");
46
- const proxy = normalizeProxyUrl(address());
64
+ const proxy = normalizeProxyUrl(await address());
47
65
  if (proxy === "") return baseFetch(input, init);
48
66
  let agent = agents.get(proxy);
49
67
  if (agent === void 0) {
@@ -81,7 +99,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
81
99
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
82
100
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
83
101
  const DEFAULT_AUTH_ENTRY = "opencode-go";
84
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.30";
102
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.32";
85
103
  const DEFAULT_CONTEXT_WINDOW = 262144;
86
104
  const DEFAULT_MAX_TOKENS = 32768;
87
105
  const DEFAULT_GENERATE_MAX_TOKENS = 65536;
@@ -89,6 +107,7 @@ const DEFAULT_MAX_REQUEST_IMAGE_BYTES = 25165824;
89
107
  const DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET = 2458624;
90
108
  const DEFAULT_REQUEST_IMAGE_MAX_BYTES = 1048576;
91
109
  const DEFAULT_REQUEST_PREPARATION_TIMEOUT_MS = 6e4;
110
+ const DEFAULT_PROXY_PASSWORD_REF = "OPENCODE_PROXY_PASSWORD";
92
111
  function markVolatile(schema) {
93
112
  const mark = schema.volatile;
94
113
  return typeof mark === "function" ? mark.call(schema) : schema;
@@ -109,7 +128,12 @@ const Config = z.object({
109
128
  requestImagePixelBudget: markVolatile(z.number().min(1).max(1e8).step(1).default(DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET)),
110
129
  requestImageMaxBytes: markVolatile(z.number().min(1).max(1e8).step(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES)),
111
130
  proxyUrl: markVolatile(z.string().default("")),
131
+ proxyUsername: markVolatile(z.string().default("")),
132
+ proxyPasswordRef: markVolatile(z.string().role("credential-ref").default(DEFAULT_PROXY_PASSWORD_REF)),
133
+ goEnabled: markVolatile(z.boolean().default(true)),
134
+ consoleEnabled: markVolatile(z.boolean().default(true)),
112
135
  catalogRefreshMinutes: markVolatile(z.number().min(1).max(10080).step(1).default(10)),
136
+ commandLocale: markVolatile(z.union(["en", "zh"]).default("en")),
113
137
  visibleModels: markVolatile(z.array(z.string()).default([])),
114
138
  modelVisibility: markVolatile(z.dict(z.boolean()).default({})),
115
139
  filterModelsByPlan: markVolatile(z.boolean().default(true)),
@@ -228,7 +252,21 @@ function resolveConfig(config) {
228
252
  requestImagePixelBudget: positiveInt(config.requestImagePixelBudget, DEFAULT_REQUEST_IMAGE_PIXEL_BUDGET),
229
253
  requestImageMaxBytes: positiveInt(config.requestImageMaxBytes, DEFAULT_REQUEST_IMAGE_MAX_BYTES),
230
254
  proxyUrl: normalizeProxyUrl(config.proxyUrl),
255
+ proxyUsername: normalizeProxyUsername(config.proxyUsername),
256
+ proxyPasswordRef: typeof config.proxyPasswordRef === "string" && config.proxyPasswordRef.trim() !== "" ? config.proxyPasswordRef.trim() : DEFAULT_PROXY_PASSWORD_REF,
257
+ ...(() => {
258
+ const goEnabled = config.goEnabled !== false;
259
+ const consoleEnabled = config.consoleEnabled !== false;
260
+ return goEnabled || consoleEnabled ? {
261
+ goEnabled,
262
+ consoleEnabled
263
+ } : {
264
+ goEnabled: true,
265
+ consoleEnabled: false
266
+ };
267
+ })(),
231
268
  catalogRefreshMinutes: positiveInt(config.catalogRefreshMinutes, 10),
269
+ commandLocale: config.commandLocale === "zh" ? "zh" : "en",
232
270
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : [],
233
271
  modelVisibility: booleanRecord(config.modelVisibility),
234
272
  filterModelsByPlan: config.filterModelsByPlan !== false,
@@ -3499,6 +3537,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3499
3537
  }
3500
3538
  async listModels(provider, opts) {
3501
3539
  const config = this.deps.config();
3540
+ if (isGoProvider(provider) ? !config.goEnabled : !config.consoleEnabled) return [];
3502
3541
  let apiKey;
3503
3542
  try {
3504
3543
  apiKey = (await this.deps.resolveKey(provider)).key;
@@ -3522,6 +3561,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3522
3561
  }
3523
3562
  async resolveModel(provider, model) {
3524
3563
  const config = this.deps.config();
3564
+ if (isGoProvider(provider) ? !config.goEnabled : !config.consoleEnabled) throw new Error(`OpenCode route "${provider}" is disabled`);
3525
3565
  const meta = effectiveModel(model, config);
3526
3566
  const maxTokens = effectiveMaxTokens(meta, config);
3527
3567
  return {
@@ -3544,6 +3584,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3544
3584
  async *run(options, fallback) {
3545
3585
  const record = this.deps.recordRequest ?? ((provider, model, purpose, account, reason, outcome) => recordRequest(provider, model, outcome?.at ?? Date.now(), purpose, account, reason, outcome));
3546
3586
  const config = this.deps.config();
3587
+ if (isGoProvider(options.provider) ? !config.goEnabled : !config.consoleEnabled) throw new Error(`OpenCode route "${options.provider}" is disabled`);
3547
3588
  const runStartedAt = Date.now();
3548
3589
  const model = effectiveModel(options.model, config);
3549
3590
  const sessionId = openCodeSessionId(options);
@@ -3978,22 +4019,66 @@ var OpenCodeAdapter = class extends LlmAdapter {
3978
4019
  return error.code === "RATE_LIMIT" || error.code === "QUOTA" || error.code === "SERVER" || error.code === "TIMEOUT" || error.code === "TRANSPORT" || error.code === "EMPTY_RESPONSE";
3979
4020
  }
3980
4021
  };
4022
+ // src/command-copy.ts
4023
+ const en = {
4024
+ provider: "OpenCode provider",
4025
+ noCredential: "no credential",
4026
+ active: "active",
4027
+ cooling: "cooling",
4028
+ rules: "Rules",
4029
+ auth: "Auth",
4030
+ none: "none",
4031
+ noRules: "No model → account rules.",
4032
+ noModels: "No models.",
4033
+ modelsUnavailable: "Models unavailable",
4034
+ unknown: "Unknown /opencode argument",
4035
+ quotaUnavailable: "Quota unavailable",
4036
+ noWindows: "No quota windows returned.",
4037
+ testFailed: "failed",
4038
+ resets: "resets",
4039
+ fiveHour: "5h",
4040
+ weekly: "weekly",
4041
+ monthly: "monthly"
4042
+ };
4043
+ const zh = {
4044
+ provider: "OpenCode 提供商",
4045
+ noCredential: "未配置凭据",
4046
+ active: "活动",
4047
+ cooling: "冷却中",
4048
+ rules: "规则",
4049
+ auth: "认证",
4050
+ none: "无",
4051
+ noRules: "没有模型 → 账户规则。",
4052
+ noModels: "没有模型。",
4053
+ modelsUnavailable: "模型不可用",
4054
+ unknown: "未知的 /opencode 参数",
4055
+ quotaUnavailable: "配额不可用",
4056
+ noWindows: "未返回配额窗口。",
4057
+ testFailed: "失败",
4058
+ resets: "重置",
4059
+ fiveHour: "5小时",
4060
+ weekly: "每周",
4061
+ monthly: "每月"
4062
+ };
4063
+ function commandCopy(locale) {
4064
+ return locale === "zh" ? zh : en;
4065
+ }
3981
4066
  // src/commands.ts
3982
4067
  function bar(percent, width = 10) {
3983
4068
  const clamped = Math.max(0, Math.min(100, percent));
3984
4069
  const filled = Math.round(clamped / 100 * width);
3985
4070
  return `[${"█".repeat(filled)}${"░".repeat(width - filled)}] ${clamped.toFixed(0)}%`;
3986
4071
  }
3987
- function reset(iso) {
4072
+ function reset(iso, copy) {
3988
4073
  if (iso === void 0) return "";
3989
4074
  const date = new Date(iso);
3990
4075
  if (Number.isNaN(date.getTime())) return "";
3991
- return ` · resets ${date.toISOString().replace("T", " ").slice(0, 16)}Z`;
4076
+ return ` · ${copy.resets} ${date.toISOString().replace("T", " ").slice(0, 16)}Z`;
3992
4077
  }
3993
- function windowLine(label, window) {
4078
+ function windowLine(label, window, copy) {
3994
4079
  if (window === void 0) return void 0;
3995
4080
  const status = window.status === void 0 || window.status === "ok" ? "" : ` (${window.status})`;
3996
- return `${label}: ${bar(window.percentUsed)}${status}${reset(window.resetsAt)}`;
4081
+ return `${label}: ${bar(window.percentUsed)}${status}${reset(window.resetsAt, copy)}`;
3997
4082
  }
3998
4083
  function percent(window) {
3999
4084
  if (window === void 0) return "—";
@@ -4015,18 +4100,18 @@ function relative(until) {
4015
4100
  if (delta >= 36e5) return `${Math.round(delta / 36e5)}h`;
4016
4101
  return `${Math.max(1, Math.round(delta / 6e4))}m`;
4017
4102
  }
4018
- async function accountsText(deps) {
4103
+ async function accountsText(deps, copy) {
4019
4104
  const lines = [];
4020
4105
  for (const provider of [GO_PROVIDER, CONSOLE_PROVIDER]) {
4021
4106
  let keys;
4022
4107
  try {
4023
4108
  keys = await deps.pool.resolveAll(provider);
4024
4109
  } catch {
4025
- lines.push(`${provider}: no credential`);
4110
+ lines.push(`${provider}: ${copy.noCredential}`);
4026
4111
  continue;
4027
4112
  }
4028
4113
  if (keys.length === 0) {
4029
- lines.push(`${provider}: no credential`);
4114
+ lines.push(`${provider}: ${copy.noCredential}`);
4030
4115
  continue;
4031
4116
  }
4032
4117
  const active = keys.find((entry) => deps.adapter.accountHealth(entry.key) === void 0);
@@ -4036,8 +4121,8 @@ async function accountsText(deps) {
4036
4121
  const usage = provider === "opencode-go-direct" ? await deps.usage(provider, entry.key) : void 0;
4037
4122
  const tags = [
4038
4123
  entry.source,
4039
- entry === active ? "active" : "",
4040
- health === void 0 ? "" : `cooling ${health.reason} ${relative(health.coolingUntil)}`,
4124
+ entry === active ? copy.active : "",
4125
+ health === void 0 ? "" : `${copy.cooling} ${health.reason} ${relative(health.coolingUntil)}`,
4041
4126
  usage?.report === void 0 ? usage?.error ?? "" : summary(usage.report)
4042
4127
  ].filter((tag) => tag !== "");
4043
4128
  lines.push(` ${index + 1}. ${entry.label ?? "default"}: ${tags.join(" · ")}`);
@@ -4045,27 +4130,27 @@ async function accountsText(deps) {
4045
4130
  }
4046
4131
  return lines.join("\n");
4047
4132
  }
4048
- function rulesText(deps) {
4133
+ function rulesText(deps, copy) {
4049
4134
  const rules = deps.config().modelAccountRules;
4050
- if (rules.length === 0) return "No model → account rules.";
4135
+ if (rules.length === 0) return copy.noRules;
4051
4136
  return rules.map((rule) => `${rule.pattern} → ${rule.account}`).join("\n");
4052
4137
  }
4053
- async function modelsText(deps) {
4138
+ async function modelsText(deps, copy) {
4054
4139
  const config = deps.config();
4055
4140
  try {
4056
4141
  const models = await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true });
4057
- if (models.length === 0) return "No models.";
4142
+ if (models.length === 0) return copy.noModels;
4058
4143
  return models.map((model) => {
4059
4144
  const meta = effectiveModel(model.id, config);
4060
4145
  const limits = limitsFor(model.id);
4061
- const tags = [meta.free ? "FREE" : limits === void 0 ? "Console" : "Go", limits === void 0 ? "" : `5h ${formatRequests(limits.go.fiveHour)} / wk ${formatRequests(limits.go.weekly)} / mo ${formatRequests(limits.go.monthly)}`].filter((tag) => tag !== "");
4146
+ const tags = [meta.free ? "FREE" : limits === void 0 ? "Console" : "Go", limits === void 0 ? "" : `${copy.fiveHour} ${formatRequests(limits.go.fiveHour)} / ${copy.weekly} ${formatRequests(limits.go.weekly)} / ${copy.monthly} ${formatRequests(limits.go.monthly)}`].filter((tag) => tag !== "");
4062
4147
  return `- ${model.id}${tags.length === 0 ? "" : ` · ${tags.join(" · ")}`}`;
4063
4148
  }).join("\n");
4064
4149
  } catch (error) {
4065
- return `Models unavailable: ${error instanceof Error ? error.message : String(error)}`;
4150
+ return `${copy.modelsUnavailable}: ${error instanceof Error ? error.message : String(error)}`;
4066
4151
  }
4067
4152
  }
4068
- async function testText(deps, model) {
4153
+ async function testText(deps, model, copy) {
4069
4154
  const started = Date.now();
4070
4155
  let text = "";
4071
4156
  try {
@@ -4088,18 +4173,18 @@ async function testText(deps, model) {
4088
4173
  } catch (error) {
4089
4174
  return {
4090
4175
  ok: false,
4091
- text: `${model}: failed — ${error instanceof Error ? error.message : String(error)}`
4176
+ text: `${model}: ${copy.testFailed} — ${error instanceof Error ? error.message : String(error)}`
4092
4177
  };
4093
4178
  }
4094
4179
  }
4095
- async function statusText(deps) {
4180
+ async function statusText(deps, copy) {
4096
4181
  const config = deps.config();
4097
- const rules = config.modelAccountRules.length === 0 ? "none" : config.modelAccountRules.map((rule) => `${rule.pattern} → ${rule.account}`).join(", ");
4182
+ const rules = config.modelAccountRules.length === 0 ? copy.none : config.modelAccountRules.map((rule) => `${rule.pattern} → ${rule.account}`).join(", ");
4098
4183
  return [
4099
- "OpenCode provider",
4100
- await accountsText(deps),
4101
- `Rules: ${rules}`,
4102
- `Auth: ${config.openCodeAuthEntry} · ${config.apiKeyEnv}`
4184
+ copy.provider,
4185
+ await accountsText(deps, copy),
4186
+ `${copy.rules}: ${rules}`,
4187
+ `${copy.auth}: ${config.openCodeAuthEntry} · ${config.apiKeyEnv}`
4103
4188
  ].filter((line) => line !== "").join("\n");
4104
4189
  }
4105
4190
  function applyCommands(ctx, deps) {
@@ -4108,6 +4193,7 @@ function applyCommands(ctx, deps) {
4108
4193
  description: "OpenCode / OpenCode Go status, accounts, quotas, models and rules",
4109
4194
  input: { hint: "status | usage | accounts | models | rules | test [model]" },
4110
4195
  handler: async (invocation) => {
4196
+ const copy = commandCopy(deps.locale());
4111
4197
  const raw = invocation.rawInput.trim();
4112
4198
  const parts = raw === "" ? ["status"] : raw.split(/\s+/);
4113
4199
  const verb = parts[0] ?? "status";
@@ -4115,39 +4201,39 @@ function applyCommands(ctx, deps) {
4115
4201
  switch (verb.toLowerCase()) {
4116
4202
  case "status": return {
4117
4203
  kind: "success",
4118
- text: await statusText(deps)
4204
+ text: await statusText(deps, copy)
4119
4205
  };
4120
4206
  case "usage": {
4121
4207
  const usage = await deps.usage(GO_PROVIDER);
4122
4208
  if (usage?.report === void 0) return {
4123
4209
  kind: "error",
4124
- text: `Quota unavailable: ${usage?.error ?? "no report"}`
4210
+ text: `${copy.quotaUnavailable}: ${usage?.error ?? "no report"}`
4125
4211
  };
4126
4212
  const windows = usage.report.windows;
4127
4213
  const text = [
4128
- windowLine("5h ", windows.fiveHour),
4129
- windowLine("weekly ", windows.weekly),
4130
- windowLine("monthly", windows.monthly)
4214
+ windowLine(copy.fiveHour, windows.fiveHour, copy),
4215
+ windowLine(copy.weekly, windows.weekly, copy),
4216
+ windowLine(copy.monthly, windows.monthly, copy)
4131
4217
  ].filter((line) => line !== void 0).join("\n");
4132
4218
  return {
4133
4219
  kind: "success",
4134
- text: text === "" ? "No quota windows returned." : text
4220
+ text: text === "" ? copy.noWindows : text
4135
4221
  };
4136
4222
  }
4137
4223
  case "accounts": return {
4138
4224
  kind: "success",
4139
- text: await accountsText(deps)
4225
+ text: await accountsText(deps, copy)
4140
4226
  };
4141
4227
  case "models": return {
4142
4228
  kind: "success",
4143
- text: await modelsText(deps)
4229
+ text: await modelsText(deps, copy)
4144
4230
  };
4145
4231
  case "rules": return {
4146
4232
  kind: "success",
4147
- text: rulesText(deps)
4233
+ text: rulesText(deps, copy)
4148
4234
  };
4149
4235
  case "test": {
4150
- const result = await testText(deps, arg === "" ? "deepseek-v4.1-flash" : arg);
4236
+ const result = await testText(deps, arg === "" ? "deepseek-v4.1-flash" : arg, copy);
4151
4237
  return result.ok ? {
4152
4238
  kind: "success",
4153
4239
  text: result.text
@@ -4158,7 +4244,7 @@ function applyCommands(ctx, deps) {
4158
4244
  }
4159
4245
  default: return {
4160
4246
  kind: "error",
4161
- text: `Unknown /opencode argument "${verb}". Use status, usage, accounts, models, rules, or test [model].`
4247
+ text: `${copy.unknown} "${verb}". ${copy.rules}: status | usage | accounts | models | rules | test [model]`
4162
4248
  };
4163
4249
  }
4164
4250
  }
@@ -4988,7 +5074,12 @@ function publicConfig(config) {
4988
5074
  requestImagePixelBudget: config.requestImagePixelBudget,
4989
5075
  requestImageMaxBytes: config.requestImageMaxBytes,
4990
5076
  proxyUrl: config.proxyUrl,
5077
+ proxyUsername: config.proxyUsername,
5078
+ proxyPasswordRef: config.proxyPasswordRef,
5079
+ goEnabled: config.goEnabled,
5080
+ consoleEnabled: config.consoleEnabled,
4991
5081
  catalogRefreshMinutes: config.catalogRefreshMinutes,
5082
+ commandLocale: config.commandLocale,
4992
5083
  visibleModels: config.visibleModels,
4993
5084
  modelVisibility: config.modelVisibility,
4994
5085
  filterModelsByPlan: config.filterModelsByPlan,
@@ -5190,7 +5281,12 @@ function configOps(patch) {
5190
5281
  if ("requestImagePixelBudget" in patch) set("requestImagePixelBudget", patch.requestImagePixelBudget);
5191
5282
  if ("requestImageMaxBytes" in patch) set("requestImageMaxBytes", patch.requestImageMaxBytes);
5192
5283
  if ("proxyUrl" in patch) set("proxyUrl", normalizeProxyUrl(typeof patch.proxyUrl === "string" ? patch.proxyUrl : ""));
5284
+ if ("proxyUsername" in patch) set("proxyUsername", normalizeProxyUsername(typeof patch.proxyUsername === "string" ? patch.proxyUsername : ""));
5285
+ if ("proxyPasswordRef" in patch) set("proxyPasswordRef", patch.proxyPasswordRef);
5286
+ if ("goEnabled" in patch) set("goEnabled", patch.goEnabled === true);
5287
+ if ("consoleEnabled" in patch) set("consoleEnabled", patch.consoleEnabled === true);
5193
5288
  if ("catalogRefreshMinutes" in patch) set("catalogRefreshMinutes", patch.catalogRefreshMinutes);
5289
+ if ("commandLocale" in patch) set("commandLocale", patch.commandLocale === "zh" ? "zh" : "en");
5194
5290
  if ("visibleModels" in patch) set("visibleModels", cleanStringArray(patch.visibleModels));
5195
5291
  if ("modelVisibility" in patch) set("modelVisibility", cleanBooleanRecord(patch.modelVisibility));
5196
5292
  if ("filterModelsByPlan" in patch) set("filterModelsByPlan", patch.filterModelsByPlan === true);
@@ -5228,6 +5324,12 @@ async function buildState(ctx, deps) {
5228
5324
  ...info.source === void 0 ? {} : { source: info.source }
5229
5325
  };
5230
5326
  } catch {}
5327
+ let proxyConfigured = false;
5328
+ if (config.proxyUsername !== "" && credentials !== void 0) try {
5329
+ proxyConfigured = (await credentials.describe(credentialRef(config.proxyPasswordRef))).configured;
5330
+ } catch {
5331
+ proxyConfigured = false;
5332
+ }
5231
5333
  const resolved = await deps.pool.resolveAll(GO_PROVIDER).catch(() => []);
5232
5334
  if (!keyInfo.configured) keyInfo = {
5233
5335
  ...keyInfo,
@@ -5285,9 +5387,10 @@ async function buildState(ctx, deps) {
5285
5387
  managed: slot.ref !== "" && managedRefs.has(slot.ref)
5286
5388
  });
5287
5389
  }
5390
+ const listingProvider = config.goEnabled ? GO_PROVIDER : CONSOLE_PROVIDER;
5288
5391
  let models = [];
5289
5392
  try {
5290
- models = (await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true })).map((model) => {
5393
+ models = (await deps.adapter.listModels(listingProvider, { unfiltered: true })).map((model) => {
5291
5394
  const meta = effectiveModel(model.id, config);
5292
5395
  return {
5293
5396
  id: model.id,
@@ -5302,8 +5405,8 @@ async function buildState(ctx, deps) {
5302
5405
  input: meta.input,
5303
5406
  reasoning: meta.reasoning,
5304
5407
  visible: modelVisible(model.id, config),
5305
- ...catalogReleaseDate("opencode-go-direct", model.id, config) === void 0 ? {} : { releaseDate: catalogReleaseDate(GO_PROVIDER, model.id, config) },
5306
- isNew: isNewRelease(catalogReleaseDate(GO_PROVIDER, model.id, config))
5408
+ ...catalogReleaseDate(listingProvider, model.id, config) === void 0 ? {} : { releaseDate: catalogReleaseDate(listingProvider, model.id, config) },
5409
+ isNew: isNewRelease(catalogReleaseDate(listingProvider, model.id, config))
5307
5410
  };
5308
5411
  });
5309
5412
  } catch {
@@ -5314,9 +5417,14 @@ async function buildState(ctx, deps) {
5314
5417
  return {
5315
5418
  config: publicConfig(config),
5316
5419
  key: keyInfo,
5420
+ proxy: {
5421
+ configured: proxyConfigured,
5422
+ username: config.proxyUsername,
5423
+ passwordRef: config.proxyPasswordRef
5424
+ },
5317
5425
  accounts,
5318
5426
  models,
5319
- catalog: catalogStatus(GO_PROVIDER, config, models.length),
5427
+ catalog: catalogStatus(listingProvider, config, models.length),
5320
5428
  usage: usage ?? null,
5321
5429
  service: {
5322
5430
  ...usage?.checkedAt === void 0 ? {} : { usageCheckedAt: usage.checkedAt },
@@ -5452,6 +5560,7 @@ function applyRoutes(ctx, web, deps) {
5452
5560
  await settings.mutate(NS, ops, revision);
5453
5561
  deps.usage.clear();
5454
5562
  deps.refreshTui?.();
5563
+ deps.syncRoutes?.();
5455
5564
  json(res, 200, { ok: true });
5456
5565
  } catch (error) {
5457
5566
  const message = error instanceof Error ? error.message : "config write failed";
@@ -5503,6 +5612,50 @@ function applyRoutes(ctx, web, deps) {
5503
5612
  json(res, 500, { error: error instanceof Error ? error.message : "key clear failed" });
5504
5613
  }
5505
5614
  });
5615
+ route("/dsh-opencode/proxy-password", async (req, res) => {
5616
+ if (!requirePost(req, res)) return;
5617
+ const credentials = getCredentials(ctx);
5618
+ if (credentials === void 0) {
5619
+ json(res, 503, { error: "credentials service unavailable" });
5620
+ return;
5621
+ }
5622
+ try {
5623
+ const body = await readJson(req);
5624
+ const password = typeof body.password === "string" ? body.password : "";
5625
+ if (password === "") {
5626
+ json(res, 400, { error: "proxy password is required" });
5627
+ return;
5628
+ }
5629
+ const refName = deps.config().proxyPasswordRef;
5630
+ await credentials.set(credentialRef(refName), password);
5631
+ await addManagedCredentialRef(refName, deps.managedPath);
5632
+ json(res, 200, {
5633
+ ok: true,
5634
+ ref: refName
5635
+ });
5636
+ } catch (error) {
5637
+ json(res, 500, { error: error instanceof Error ? error.message : "proxy password write failed" });
5638
+ }
5639
+ });
5640
+ route("/dsh-opencode/proxy-password/clear", async (req, res) => {
5641
+ if (!requirePost(req, res)) return;
5642
+ const credentials = getCredentials(ctx);
5643
+ if (credentials === void 0) {
5644
+ json(res, 503, { error: "credentials service unavailable" });
5645
+ return;
5646
+ }
5647
+ try {
5648
+ const refName = deps.config().proxyPasswordRef;
5649
+ await credentials.unset(credentialRef(refName));
5650
+ await removeManagedCredentialRef(refName, deps.managedPath);
5651
+ json(res, 200, {
5652
+ ok: true,
5653
+ ref: refName
5654
+ });
5655
+ } catch (error) {
5656
+ json(res, 500, { error: error instanceof Error ? error.message : "proxy password clear failed" });
5657
+ }
5658
+ });
5506
5659
  route("/dsh-opencode/auth-import", async (req, res) => {
5507
5660
  if (!requirePost(req, res)) return;
5508
5661
  const credentials = getCredentials(ctx);
@@ -6065,7 +6218,19 @@ const inject = ["llm"];
6065
6218
  function apply(ctx, config) {
6066
6219
  const current = () => resolveConfig(unwrapVolatileConfig(config));
6067
6220
  const keys = createKeyPool(ctx, current);
6068
- const proxy = createProxyFetch(() => current().proxyUrl);
6221
+ const proxy = createProxyFetch(async () => {
6222
+ const config = current();
6223
+ let password = "";
6224
+ if (config.proxyUsername !== "") {
6225
+ const credentials = ctx.get("credentials");
6226
+ if (credentials?.resolve !== void 0) try {
6227
+ password = (await credentials.resolve(credentialRef(config.proxyPasswordRef)))?.value ?? "";
6228
+ } catch {
6229
+ password = "";
6230
+ }
6231
+ }
6232
+ return buildProxyUrl(config.proxyUrl, config.proxyUsername, password);
6233
+ });
6069
6234
  const usage = createUsageReader(current, (provider) => keys.resolve(provider), proxy.fetch);
6070
6235
  const adapter = new OpenCodeAdapter({
6071
6236
  config: current,
@@ -6078,19 +6243,30 @@ function apply(ctx, config) {
6078
6243
  ctx.effect(() => () => {
6079
6244
  proxy.dispose();
6080
6245
  });
6081
- ctx.llm.registerConfigurableProviders([{
6246
+ const providersFor = () => {
6247
+ const config = current();
6248
+ return [...config.consoleEnabled ? [CONSOLE_PROVIDER] : [], ...config.goEnabled ? [GO_PROVIDER] : []];
6249
+ };
6250
+ const entriesFor = (providers) => [...providers.includes("opencode-direct") ? [{
6082
6251
  provider: CONSOLE_PROVIDER,
6083
6252
  displayName: "OpenCode (plugin)",
6084
6253
  settingsNs: NS,
6085
6254
  settingsPath: []
6086
- }, {
6255
+ }] : [], ...providers.includes("opencode-go-direct") ? [{
6087
6256
  provider: GO_PROVIDER,
6088
6257
  displayName: "OpenCode Go (plugin)",
6089
6258
  settingsNs: NS,
6090
6259
  settingsPath: []
6091
- }]);
6092
- ctx.llm.registerAdapter([CONSOLE_PROVIDER, GO_PROVIDER], adapter);
6093
- ctx.logger("llm-opencode").info(`registered providers: ${CONSOLE_PROVIDER}, ${GO_PROVIDER}`);
6260
+ }] : []];
6261
+ const directory = ctx.llm.registerConfigurableProviders(entriesFor(providersFor()));
6262
+ const registration = ctx.llm.registerAdapter(providersFor(), adapter);
6263
+ const syncRoutes = () => {
6264
+ const providers = providersFor();
6265
+ registration.replace(providers);
6266
+ directory.replace(entriesFor(providers));
6267
+ ctx.logger("llm-opencode").info(`enabled providers: ${providers.join(", ")}`);
6268
+ };
6269
+ ctx.logger("llm-opencode").info(`registered providers: ${providersFor().join(", ")}`);
6094
6270
  ctx.inject(["settings"], (settingsCtx) => {
6095
6271
  settingsCtx.settings.configure({ auto: false }, ctx.fiber);
6096
6272
  });
@@ -6099,7 +6275,8 @@ function apply(ctx, config) {
6099
6275
  config: current,
6100
6276
  pool: keys,
6101
6277
  usage,
6102
- adapter
6278
+ adapter,
6279
+ locale: () => current().commandLocale
6103
6280
  });
6104
6281
  });
6105
6282
  let refreshTuiSettings;
@@ -6133,7 +6310,8 @@ function apply(ctx, config) {
6133
6310
  pool: keys,
6134
6311
  usage,
6135
6312
  heatmap: readRequestHeatmap,
6136
- refreshTui: () => refreshTuiSettings?.()
6313
+ refreshTui: () => refreshTuiSettings?.(),
6314
+ syncRoutes
6137
6315
  });
6138
6316
  webCtx.effect(() => registerPluginUpdater(webCtx, {
6139
6317
  endpoint: "/dsh-opencode/update",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.30",
3
+ "version": "0.5.32",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",