@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 +12 -0
- package/README.md +7 -4
- package/README.ru.md +7 -4
- package/README.zh.md +7 -4
- package/lib/client.js +181 -2
- package/lib/index.d.ts +18 -0
- package/lib/index.js +230 -52
- package/package.json +1 -1
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
17
|
-
*
|
|
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
|
|
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.
|
|
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}:
|
|
4110
|
+
lines.push(`${provider}: ${copy.noCredential}`);
|
|
4026
4111
|
continue;
|
|
4027
4112
|
}
|
|
4028
4113
|
if (keys.length === 0) {
|
|
4029
|
-
lines.push(`${provider}:
|
|
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 ?
|
|
4040
|
-
health === void 0 ? "" :
|
|
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
|
|
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
|
|
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 ? "" :
|
|
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
|
|
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}:
|
|
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 ?
|
|
4182
|
+
const rules = config.modelAccountRules.length === 0 ? copy.none : config.modelAccountRules.map((rule) => `${rule.pattern} → ${rule.account}`).join(", ");
|
|
4098
4183
|
return [
|
|
4099
|
-
|
|
4100
|
-
await accountsText(deps),
|
|
4101
|
-
|
|
4102
|
-
|
|
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:
|
|
4210
|
+
text: `${copy.quotaUnavailable}: ${usage?.error ?? "no report"}`
|
|
4125
4211
|
};
|
|
4126
4212
|
const windows = usage.report.windows;
|
|
4127
4213
|
const text = [
|
|
4128
|
-
windowLine(
|
|
4129
|
-
windowLine(
|
|
4130
|
-
windowLine(
|
|
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 === "" ?
|
|
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:
|
|
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(
|
|
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(
|
|
5306
|
-
isNew: isNewRelease(catalogReleaseDate(
|
|
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(
|
|
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(() =>
|
|
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
|
-
|
|
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.
|
|
6093
|
-
ctx.
|
|
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",
|