@goodandready/dsh-opencode-provider 0.5.30 → 0.5.31

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.31 — release 8: command localization
4
+
5
+ - **#48 — `/opencode` localization**: new `commandLocale` setting (`en` default, `zh`) localizes status, usage, accounts, models, rules and errors. Exposed in Settings → OpenCode.
6
+ - **#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.
7
+ - 140 tests.
8
+
3
9
  ## 0.5.30 — release 7: pricing accuracy
4
10
 
5
11
  - **#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
@@ -132,12 +132,12 @@ npm pack
132
132
  Install the **packed tarball** into a profile:
133
133
 
134
134
  ```bash
135
- 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.31.tgz
136
136
  # DSH Desktop:
137
- 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.31.tgz
138
138
 
139
139
  # Or install the published package:
140
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.30
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.31
141
141
  ```
142
142
 
143
143
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
@@ -278,6 +278,8 @@ Captured from the exact 0.5.0 release candidate in DSH Desktop in the dark theme
278
278
  /opencode test [model] # minimal real request
279
279
  ```
280
280
 
281
+ Set **Settings → OpenCode → `/opencode` command language** to `中文` to localize this output. The default is English.
282
+
281
283
  ## Verified API facts
282
284
 
283
285
  - `GET /inference/go/v1/models` is public and returns the live Go roster.
package/README.ru.md CHANGED
@@ -131,12 +131,12 @@ npm pack
131
131
  Установка из **упакованного tarball**:
132
132
 
133
133
  ```bash
134
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
134
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
135
135
  # DSH Desktop:
136
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
136
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
137
137
 
138
138
  # Либо установка опубликованного пакета из npm:
139
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.30
139
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.31
140
140
  ```
141
141
 
142
142
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
@@ -208,6 +208,8 @@ modelAccountRules:
208
208
  /opencode test [model] # проверочный запрос к модели
209
209
  ```
210
210
 
211
+ Язык вывода задаётся в **Настройки → OpenCode → `/opencode` command language** (`中文` включает китайский). По умолчанию английский.
212
+
211
213
  ## Безопасность и надежность
212
214
 
213
215
  - Запросы на изменение настроек и ключей разрешены только с loopback (`127.0.0.1` / `::1`) с обязательной проверкой same-origin для защиты от DNS-rebinding атак.
package/README.zh.md CHANGED
@@ -126,12 +126,12 @@ npm pack
126
126
  通过**打包文件 (.tgz)**安装:
127
127
 
128
128
  ```bash
129
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
129
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
130
130
  # DSH 桌面端:
131
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.30.tgz
131
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.31.tgz
132
132
 
133
133
  # 或从 npm 安装已发布版本:
134
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.30
134
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.31
135
135
  ```
136
136
 
137
137
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
@@ -183,6 +183,8 @@ modelAccountRules:
183
183
  /opencode test [model] # 发送最小化测试请求
184
184
  ```
185
185
 
186
+ 在 **设置 → OpenCode → `/opencode` 命令语言** 中选择 `中文` 即可切换输出语言,默认英语。
187
+
186
188
  ## 安全规范
187
189
 
188
190
  - 写入与配置路由均要求 Loopback(`127.0.0.1` / `::1`)与同源检查,防御 DNS 重新绑定。
package/lib/client.js CHANGED
@@ -187,6 +187,8 @@ 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
193
  proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Credentials are not supported.",
192
194
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
@@ -421,6 +423,8 @@ window.__ModuleLoader__.load({
421
423
  requestImagePixelBudget: "图片像素预算",
422
424
  requestImageMaxBytes: "单图最大字节数",
423
425
  catalogRefreshMinutes: "目录刷新间隔(分钟)",
426
+ commandLocale: "/opencode 命令语言",
427
+ commandLocaleHint: "用于 /opencode 的状态、账户、用量、模型和规则输出。",
424
428
  proxyUrl: "代理地址",
425
429
  proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。不支持代理认证。",
426
430
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
@@ -1715,6 +1719,7 @@ window.__ModuleLoader__.load({
1715
1719
  requestImageMaxBytes: String(state.config.requestImageMaxBytes),
1716
1720
  proxyUrl: state.config.proxyUrl,
1717
1721
  catalogRefreshMinutes: String(state.config.catalogRefreshMinutes),
1722
+ commandLocale: state.config.commandLocale,
1718
1723
  maxOutputTokens: state.config.maxOutputTokens === null ? "" : String(state.config.maxOutputTokens),
1719
1724
  contextWindow: state.config.contextWindow === null ? "" : String(state.config.contextWindow),
1720
1725
  contextWindowOverrides: Object.fromEntries(Object.entries(state.config.contextWindowOverrides).map(([id, value]) => [id, String(value)])),
@@ -1768,6 +1773,7 @@ window.__ModuleLoader__.load({
1768
1773
  requestImageMaxBytes: numberOrNull(draft.requestImageMaxBytes) ?? 1048576,
1769
1774
  proxyUrl: draft.proxyUrl.trim(),
1770
1775
  catalogRefreshMinutes: numberOrNull(draft.catalogRefreshMinutes) ?? 10,
1776
+ commandLocale: draft.commandLocale,
1771
1777
  maxOutputTokens: numberOrNull(draft.maxOutputTokens),
1772
1778
  contextWindow: numberOrNull(draft.contextWindow),
1773
1779
  contextWindowOverrides: numberRecordOrNull(draft.contextWindowOverrides),
@@ -3208,6 +3214,31 @@ window.__ModuleLoader__.load({
3208
3214
  value: draft.catalogRefreshMinutes,
3209
3215
  onChange: (value) => update({ catalogRefreshMinutes: value })
3210
3216
  }),
3217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3218
+ className: "oc-field",
3219
+ children: [
3220
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3221
+ className: "oc-label",
3222
+ children: t("commandLocale")
3223
+ }),
3224
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3225
+ className: "oc-select",
3226
+ value: draft.commandLocale,
3227
+ onChange: (event) => update({ commandLocale: event.target.value }),
3228
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3229
+ value: "en",
3230
+ children: "English"
3231
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3232
+ value: "zh",
3233
+ children: "中文"
3234
+ })]
3235
+ }),
3236
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3237
+ className: "oc-hint",
3238
+ children: t("commandLocaleHint")
3239
+ })
3240
+ ]
3241
+ }),
3211
3242
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3212
3243
  label: t("proxyUrl"),
3213
3244
  hint: t("proxyUrlHint"),
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;
@@ -51,6 +52,8 @@ interface Config {
51
52
  proxyUrl?: string;
52
53
  /** Live catalog cache lifetime in minutes; defaults to 10. */
53
54
  catalogRefreshMinutes?: number;
55
+ /** Language for `/opencode` output; defaults to English. */
56
+ commandLocale?: CommandLocale;
54
57
  /** Visible-model allowlist; an empty list means "show everything". */
55
58
  visibleModels?: string[];
56
59
  /** Per-model visibility overrides; wins over `visibleModels`. */
@@ -104,6 +107,7 @@ interface ResolvedConfig {
104
107
  requestImageMaxBytes: number;
105
108
  proxyUrl: string;
106
109
  catalogRefreshMinutes: number;
110
+ commandLocale: CommandLocale;
107
111
  visibleModels: readonly string[];
108
112
  modelVisibility: Readonly<Record<string, boolean>>;
109
113
  filterModelsByPlan: boolean;
@@ -316,6 +320,7 @@ interface CommandDeps {
316
320
  readonly pool: KeyPool;
317
321
  readonly usage: (provider: string, key?: string) => Promise<UsageResult | undefined>;
318
322
  readonly adapter: OpenCodeAdapter;
323
+ readonly locale: () => 'en' | 'zh';
319
324
  }
320
325
  declare function applyCommands(ctx: Context, deps: CommandDeps): void;
321
326
  interface ModelUsageRow {
package/lib/index.js CHANGED
@@ -81,7 +81,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
81
81
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
82
82
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
83
83
  const DEFAULT_AUTH_ENTRY = "opencode-go";
84
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.30";
84
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.31";
85
85
  const DEFAULT_CONTEXT_WINDOW = 262144;
86
86
  const DEFAULT_MAX_TOKENS = 32768;
87
87
  const DEFAULT_GENERATE_MAX_TOKENS = 65536;
@@ -110,6 +110,7 @@ const Config = z.object({
110
110
  requestImageMaxBytes: markVolatile(z.number().min(1).max(1e8).step(1).default(DEFAULT_REQUEST_IMAGE_MAX_BYTES)),
111
111
  proxyUrl: markVolatile(z.string().default("")),
112
112
  catalogRefreshMinutes: markVolatile(z.number().min(1).max(10080).step(1).default(10)),
113
+ commandLocale: markVolatile(z.union(["en", "zh"]).default("en")),
113
114
  visibleModels: markVolatile(z.array(z.string()).default([])),
114
115
  modelVisibility: markVolatile(z.dict(z.boolean()).default({})),
115
116
  filterModelsByPlan: markVolatile(z.boolean().default(true)),
@@ -229,6 +230,7 @@ function resolveConfig(config) {
229
230
  requestImageMaxBytes: positiveInt(config.requestImageMaxBytes, DEFAULT_REQUEST_IMAGE_MAX_BYTES),
230
231
  proxyUrl: normalizeProxyUrl(config.proxyUrl),
231
232
  catalogRefreshMinutes: positiveInt(config.catalogRefreshMinutes, 10),
233
+ commandLocale: config.commandLocale === "zh" ? "zh" : "en",
232
234
  visibleModels: Array.isArray(config.visibleModels) ? config.visibleModels.filter((id) => typeof id === "string" && id !== "") : [],
233
235
  modelVisibility: booleanRecord(config.modelVisibility),
234
236
  filterModelsByPlan: config.filterModelsByPlan !== false,
@@ -3978,22 +3980,66 @@ var OpenCodeAdapter = class extends LlmAdapter {
3978
3980
  return error.code === "RATE_LIMIT" || error.code === "QUOTA" || error.code === "SERVER" || error.code === "TIMEOUT" || error.code === "TRANSPORT" || error.code === "EMPTY_RESPONSE";
3979
3981
  }
3980
3982
  };
3983
+ // src/command-copy.ts
3984
+ const en = {
3985
+ provider: "OpenCode provider",
3986
+ noCredential: "no credential",
3987
+ active: "active",
3988
+ cooling: "cooling",
3989
+ rules: "Rules",
3990
+ auth: "Auth",
3991
+ none: "none",
3992
+ noRules: "No model → account rules.",
3993
+ noModels: "No models.",
3994
+ modelsUnavailable: "Models unavailable",
3995
+ unknown: "Unknown /opencode argument",
3996
+ quotaUnavailable: "Quota unavailable",
3997
+ noWindows: "No quota windows returned.",
3998
+ testFailed: "failed",
3999
+ resets: "resets",
4000
+ fiveHour: "5h",
4001
+ weekly: "weekly",
4002
+ monthly: "monthly"
4003
+ };
4004
+ const zh = {
4005
+ provider: "OpenCode 提供商",
4006
+ noCredential: "未配置凭据",
4007
+ active: "活动",
4008
+ cooling: "冷却中",
4009
+ rules: "规则",
4010
+ auth: "认证",
4011
+ none: "无",
4012
+ noRules: "没有模型 → 账户规则。",
4013
+ noModels: "没有模型。",
4014
+ modelsUnavailable: "模型不可用",
4015
+ unknown: "未知的 /opencode 参数",
4016
+ quotaUnavailable: "配额不可用",
4017
+ noWindows: "未返回配额窗口。",
4018
+ testFailed: "失败",
4019
+ resets: "重置",
4020
+ fiveHour: "5小时",
4021
+ weekly: "每周",
4022
+ monthly: "每月"
4023
+ };
4024
+ function commandCopy(locale) {
4025
+ return locale === "zh" ? zh : en;
4026
+ }
3981
4027
  // src/commands.ts
3982
4028
  function bar(percent, width = 10) {
3983
4029
  const clamped = Math.max(0, Math.min(100, percent));
3984
4030
  const filled = Math.round(clamped / 100 * width);
3985
4031
  return `[${"█".repeat(filled)}${"░".repeat(width - filled)}] ${clamped.toFixed(0)}%`;
3986
4032
  }
3987
- function reset(iso) {
4033
+ function reset(iso, copy) {
3988
4034
  if (iso === void 0) return "";
3989
4035
  const date = new Date(iso);
3990
4036
  if (Number.isNaN(date.getTime())) return "";
3991
- return ` · resets ${date.toISOString().replace("T", " ").slice(0, 16)}Z`;
4037
+ return ` · ${copy.resets} ${date.toISOString().replace("T", " ").slice(0, 16)}Z`;
3992
4038
  }
3993
- function windowLine(label, window) {
4039
+ function windowLine(label, window, copy) {
3994
4040
  if (window === void 0) return void 0;
3995
4041
  const status = window.status === void 0 || window.status === "ok" ? "" : ` (${window.status})`;
3996
- return `${label}: ${bar(window.percentUsed)}${status}${reset(window.resetsAt)}`;
4042
+ return `${label}: ${bar(window.percentUsed)}${status}${reset(window.resetsAt, copy)}`;
3997
4043
  }
3998
4044
  function percent(window) {
3999
4045
  if (window === void 0) return "—";
@@ -4015,18 +4061,18 @@ function relative(until) {
4015
4061
  if (delta >= 36e5) return `${Math.round(delta / 36e5)}h`;
4016
4062
  return `${Math.max(1, Math.round(delta / 6e4))}m`;
4017
4063
  }
4018
- async function accountsText(deps) {
4064
+ async function accountsText(deps, copy) {
4019
4065
  const lines = [];
4020
4066
  for (const provider of [GO_PROVIDER, CONSOLE_PROVIDER]) {
4021
4067
  let keys;
4022
4068
  try {
4023
4069
  keys = await deps.pool.resolveAll(provider);
4024
4070
  } catch {
4025
- lines.push(`${provider}: no credential`);
4071
+ lines.push(`${provider}: ${copy.noCredential}`);
4026
4072
  continue;
4027
4073
  }
4028
4074
  if (keys.length === 0) {
4029
- lines.push(`${provider}: no credential`);
4075
+ lines.push(`${provider}: ${copy.noCredential}`);
4030
4076
  continue;
4031
4077
  }
4032
4078
  const active = keys.find((entry) => deps.adapter.accountHealth(entry.key) === void 0);
@@ -4036,8 +4082,8 @@ async function accountsText(deps) {
4036
4082
  const usage = provider === "opencode-go-direct" ? await deps.usage(provider, entry.key) : void 0;
4037
4083
  const tags = [
4038
4084
  entry.source,
4039
- entry === active ? "active" : "",
4040
- health === void 0 ? "" : `cooling ${health.reason} ${relative(health.coolingUntil)}`,
4085
+ entry === active ? copy.active : "",
4086
+ health === void 0 ? "" : `${copy.cooling} ${health.reason} ${relative(health.coolingUntil)}`,
4041
4087
  usage?.report === void 0 ? usage?.error ?? "" : summary(usage.report)
4042
4088
  ].filter((tag) => tag !== "");
4043
4089
  lines.push(` ${index + 1}. ${entry.label ?? "default"}: ${tags.join(" · ")}`);
@@ -4045,27 +4091,27 @@ async function accountsText(deps) {
4045
4091
  }
4046
4092
  return lines.join("\n");
4047
4093
  }
4048
- function rulesText(deps) {
4094
+ function rulesText(deps, copy) {
4049
4095
  const rules = deps.config().modelAccountRules;
4050
- if (rules.length === 0) return "No model → account rules.";
4096
+ if (rules.length === 0) return copy.noRules;
4051
4097
  return rules.map((rule) => `${rule.pattern} → ${rule.account}`).join("\n");
4052
4098
  }
4053
- async function modelsText(deps) {
4099
+ async function modelsText(deps, copy) {
4054
4100
  const config = deps.config();
4055
4101
  try {
4056
4102
  const models = await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true });
4057
- if (models.length === 0) return "No models.";
4103
+ if (models.length === 0) return copy.noModels;
4058
4104
  return models.map((model) => {
4059
4105
  const meta = effectiveModel(model.id, config);
4060
4106
  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 !== "");
4107
+ 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
4108
  return `- ${model.id}${tags.length === 0 ? "" : ` · ${tags.join(" · ")}`}`;
4063
4109
  }).join("\n");
4064
4110
  } catch (error) {
4065
- return `Models unavailable: ${error instanceof Error ? error.message : String(error)}`;
4111
+ return `${copy.modelsUnavailable}: ${error instanceof Error ? error.message : String(error)}`;
4066
4112
  }
4067
4113
  }
4068
- async function testText(deps, model) {
4114
+ async function testText(deps, model, copy) {
4069
4115
  const started = Date.now();
4070
4116
  let text = "";
4071
4117
  try {
@@ -4088,18 +4134,18 @@ async function testText(deps, model) {
4088
4134
  } catch (error) {
4089
4135
  return {
4090
4136
  ok: false,
4091
- text: `${model}: failed — ${error instanceof Error ? error.message : String(error)}`
4137
+ text: `${model}: ${copy.testFailed} — ${error instanceof Error ? error.message : String(error)}`
4092
4138
  };
4093
4139
  }
4094
4140
  }
4095
- async function statusText(deps) {
4141
+ async function statusText(deps, copy) {
4096
4142
  const config = deps.config();
4097
- const rules = config.modelAccountRules.length === 0 ? "none" : config.modelAccountRules.map((rule) => `${rule.pattern} → ${rule.account}`).join(", ");
4143
+ const rules = config.modelAccountRules.length === 0 ? copy.none : config.modelAccountRules.map((rule) => `${rule.pattern} → ${rule.account}`).join(", ");
4098
4144
  return [
4099
- "OpenCode provider",
4100
- await accountsText(deps),
4101
- `Rules: ${rules}`,
4102
- `Auth: ${config.openCodeAuthEntry} · ${config.apiKeyEnv}`
4145
+ copy.provider,
4146
+ await accountsText(deps, copy),
4147
+ `${copy.rules}: ${rules}`,
4148
+ `${copy.auth}: ${config.openCodeAuthEntry} · ${config.apiKeyEnv}`
4103
4149
  ].filter((line) => line !== "").join("\n");
4104
4150
  }
4105
4151
  function applyCommands(ctx, deps) {
@@ -4108,6 +4154,7 @@ function applyCommands(ctx, deps) {
4108
4154
  description: "OpenCode / OpenCode Go status, accounts, quotas, models and rules",
4109
4155
  input: { hint: "status | usage | accounts | models | rules | test [model]" },
4110
4156
  handler: async (invocation) => {
4157
+ const copy = commandCopy(deps.locale());
4111
4158
  const raw = invocation.rawInput.trim();
4112
4159
  const parts = raw === "" ? ["status"] : raw.split(/\s+/);
4113
4160
  const verb = parts[0] ?? "status";
@@ -4115,39 +4162,39 @@ function applyCommands(ctx, deps) {
4115
4162
  switch (verb.toLowerCase()) {
4116
4163
  case "status": return {
4117
4164
  kind: "success",
4118
- text: await statusText(deps)
4165
+ text: await statusText(deps, copy)
4119
4166
  };
4120
4167
  case "usage": {
4121
4168
  const usage = await deps.usage(GO_PROVIDER);
4122
4169
  if (usage?.report === void 0) return {
4123
4170
  kind: "error",
4124
- text: `Quota unavailable: ${usage?.error ?? "no report"}`
4171
+ text: `${copy.quotaUnavailable}: ${usage?.error ?? "no report"}`
4125
4172
  };
4126
4173
  const windows = usage.report.windows;
4127
4174
  const text = [
4128
- windowLine("5h ", windows.fiveHour),
4129
- windowLine("weekly ", windows.weekly),
4130
- windowLine("monthly", windows.monthly)
4175
+ windowLine(copy.fiveHour, windows.fiveHour, copy),
4176
+ windowLine(copy.weekly, windows.weekly, copy),
4177
+ windowLine(copy.monthly, windows.monthly, copy)
4131
4178
  ].filter((line) => line !== void 0).join("\n");
4132
4179
  return {
4133
4180
  kind: "success",
4134
- text: text === "" ? "No quota windows returned." : text
4181
+ text: text === "" ? copy.noWindows : text
4135
4182
  };
4136
4183
  }
4137
4184
  case "accounts": return {
4138
4185
  kind: "success",
4139
- text: await accountsText(deps)
4186
+ text: await accountsText(deps, copy)
4140
4187
  };
4141
4188
  case "models": return {
4142
4189
  kind: "success",
4143
- text: await modelsText(deps)
4190
+ text: await modelsText(deps, copy)
4144
4191
  };
4145
4192
  case "rules": return {
4146
4193
  kind: "success",
4147
- text: rulesText(deps)
4194
+ text: rulesText(deps, copy)
4148
4195
  };
4149
4196
  case "test": {
4150
- const result = await testText(deps, arg === "" ? "deepseek-v4.1-flash" : arg);
4197
+ const result = await testText(deps, arg === "" ? "deepseek-v4.1-flash" : arg, copy);
4151
4198
  return result.ok ? {
4152
4199
  kind: "success",
4153
4200
  text: result.text
@@ -4158,7 +4205,7 @@ function applyCommands(ctx, deps) {
4158
4205
  }
4159
4206
  default: return {
4160
4207
  kind: "error",
4161
- text: `Unknown /opencode argument "${verb}". Use status, usage, accounts, models, rules, or test [model].`
4208
+ text: `${copy.unknown} "${verb}". ${copy.rules}: status | usage | accounts | models | rules | test [model]`
4162
4209
  };
4163
4210
  }
4164
4211
  }
@@ -4989,6 +5036,7 @@ function publicConfig(config) {
4989
5036
  requestImageMaxBytes: config.requestImageMaxBytes,
4990
5037
  proxyUrl: config.proxyUrl,
4991
5038
  catalogRefreshMinutes: config.catalogRefreshMinutes,
5039
+ commandLocale: config.commandLocale,
4992
5040
  visibleModels: config.visibleModels,
4993
5041
  modelVisibility: config.modelVisibility,
4994
5042
  filterModelsByPlan: config.filterModelsByPlan,
@@ -5191,6 +5239,7 @@ function configOps(patch) {
5191
5239
  if ("requestImageMaxBytes" in patch) set("requestImageMaxBytes", patch.requestImageMaxBytes);
5192
5240
  if ("proxyUrl" in patch) set("proxyUrl", normalizeProxyUrl(typeof patch.proxyUrl === "string" ? patch.proxyUrl : ""));
5193
5241
  if ("catalogRefreshMinutes" in patch) set("catalogRefreshMinutes", patch.catalogRefreshMinutes);
5242
+ if ("commandLocale" in patch) set("commandLocale", patch.commandLocale === "zh" ? "zh" : "en");
5194
5243
  if ("visibleModels" in patch) set("visibleModels", cleanStringArray(patch.visibleModels));
5195
5244
  if ("modelVisibility" in patch) set("modelVisibility", cleanBooleanRecord(patch.modelVisibility));
5196
5245
  if ("filterModelsByPlan" in patch) set("filterModelsByPlan", patch.filterModelsByPlan === true);
@@ -6099,7 +6148,8 @@ function apply(ctx, config) {
6099
6148
  config: current,
6100
6149
  pool: keys,
6101
6150
  usage,
6102
- adapter
6151
+ adapter,
6152
+ locale: () => current().commandLocale
6103
6153
  });
6104
6154
  });
6105
6155
  let refreshTuiSettings;
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.31",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",