@goodandready/dsh-opencode-provider 0.5.29 → 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,17 @@
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
+
9
+ ## 0.5.30 — release 7: pricing accuracy
10
+
11
+ - **#74 — peak/off-peak multipliers**: analytics now reads live gateway `cost.multipliers` and evaluates UTC weekday/hour/date conditions at the session timestamp.
12
+ - **#84 — tiered pricing**: live gateway context overrides are normalized into tiers and selected from billed input (`input + cacheRead`) before multipliers. Malformed pricing metadata fails open to the generated flat cost.
13
+ - 139 tests.
14
+
3
15
  ## 0.5.29 — release 6: catalog refresh and Free-lane decision
4
16
 
5
17
  - **#77 — catalog refresh**: `catalogRefreshMinutes` (1–10,080, default 10) controls the live `/models` cache; explicit `/models/refresh` still bypasses it. Exposed in Settings.
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.29.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.29.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.29
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.29.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.29.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.29
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.29.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.29.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.29
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.29";
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,
@@ -1067,6 +1069,182 @@ function formatRequests(value) {
1067
1069
  function describeLimitsShort(limits) {
1068
1070
  return `Go ${formatRequests(limits.go.fiveHour)}/5h · ${formatRequests(limits.go.weekly)}/wk`;
1069
1071
  }
1072
+ // src/pricing.ts
1073
+ const DAYS = [
1074
+ "sun",
1075
+ "mon",
1076
+ "tue",
1077
+ "wed",
1078
+ "thu",
1079
+ "fri",
1080
+ "sat"
1081
+ ];
1082
+ const HOURS = /^(?:[01]\d|2[0-3]):[0-5]\d$|^24:00$/;
1083
+ const DATE = /^\d{4}-\d{2}-\d{2}$/;
1084
+ function record(value) {
1085
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1086
+ }
1087
+ function rate(value) {
1088
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
1089
+ }
1090
+ function comparison(value, validate) {
1091
+ const raw = record(value);
1092
+ if (raw === void 0) return void 0;
1093
+ const out = {};
1094
+ for (const [key, entry] of Object.entries(raw)) if (key === "in") {
1095
+ if (!Array.isArray(entry) || entry.length === 0 || !entry.every((item) => typeof item === "string" && validate(item))) return void 0;
1096
+ out.in = entry;
1097
+ } else {
1098
+ if (typeof entry !== "string" || !validate(entry)) return void 0;
1099
+ out[key] = entry;
1100
+ }
1101
+ return out;
1102
+ }
1103
+ function conditionOf(value, depth = 0) {
1104
+ if (depth > 12) return void 0;
1105
+ const raw = record(value);
1106
+ if (raw === void 0) return void 0;
1107
+ const out = {};
1108
+ for (const [key, entry] of Object.entries(raw)) if (key === "all" || key === "any") {
1109
+ if (!Array.isArray(entry) || entry.length === 0 || entry.length > 64) return void 0;
1110
+ const nested = entry.map((item) => conditionOf(item, depth + 1));
1111
+ if (nested.some((item) => item === void 0)) return void 0;
1112
+ out[key] = nested;
1113
+ } else if (key === "utc_days") {
1114
+ const value = comparison(entry, (text) => DAYS.includes(text));
1115
+ if (value === void 0) return void 0;
1116
+ out[key] = value;
1117
+ } else if (key === "utc_hours") {
1118
+ const value = comparison(entry, (text) => HOURS.test(text));
1119
+ if (value === void 0) return void 0;
1120
+ out[key] = value;
1121
+ } else if (key === "date") {
1122
+ const value = comparison(entry, (text) => DATE.test(text));
1123
+ if (value === void 0) return void 0;
1124
+ out[key] = value;
1125
+ } else if (key === "context") {
1126
+ const context = record(entry);
1127
+ if (context === void 0) return void 0;
1128
+ const bounds = {};
1129
+ for (const [op, size] of Object.entries(context)) {
1130
+ if (![
1131
+ "gt",
1132
+ "gte",
1133
+ "lt",
1134
+ "lte"
1135
+ ].includes(op) || typeof size !== "number" || !Number.isFinite(size)) return void 0;
1136
+ bounds[op] = size;
1137
+ }
1138
+ out[key] = bounds;
1139
+ } else return;
1140
+ return Object.keys(out).length === 0 ? void 0 : out;
1141
+ }
1142
+ function compare(actual, rule) {
1143
+ return Object.entries(rule).every(([op, expected]) => {
1144
+ if (op === "in") return Array.isArray(expected) && expected.includes(actual);
1145
+ if (op === "eq") return actual === expected;
1146
+ if (op === "gt") return actual > String(expected);
1147
+ if (op === "gte") return actual >= String(expected);
1148
+ if (op === "lt") return actual < String(expected);
1149
+ if (op === "lte") return actual <= String(expected);
1150
+ return false;
1151
+ });
1152
+ }
1153
+ function matches(condition, at, context) {
1154
+ if (condition.all !== void 0 && !condition.all.every((entry) => matches(entry, at, context))) return false;
1155
+ if (condition.any !== void 0 && !condition.any.some((entry) => matches(entry, at, context))) return false;
1156
+ const date = new Date(at);
1157
+ if (condition.utc_days !== void 0 && !compare(DAYS[date.getUTCDay()], condition.utc_days)) return false;
1158
+ if (condition.utc_hours !== void 0) {
1159
+ if (!compare(`${String(date.getUTCHours()).padStart(2, "0")}:${String(date.getUTCMinutes()).padStart(2, "0")}`, condition.utc_hours)) return false;
1160
+ }
1161
+ if (condition.date !== void 0 && !compare(date.toISOString().slice(0, 10), condition.date)) return false;
1162
+ if (condition.context !== void 0) {
1163
+ if (condition.context.gt !== void 0 && !(context > condition.context.gt)) return false;
1164
+ if (condition.context.gte !== void 0 && !(context >= condition.context.gte)) return false;
1165
+ if (condition.context.lt !== void 0 && !(context < condition.context.lt)) return false;
1166
+ if (condition.context.lte !== void 0 && !(context <= condition.context.lte)) return false;
1167
+ }
1168
+ return true;
1169
+ }
1170
+ function billingContext(tokens) {
1171
+ return tokens.input + tokens.cacheRead;
1172
+ }
1173
+ /** Estimate USD from gateway cost metadata, including context tiers and UTC multipliers. */
1174
+ function priceUsage(pricing, tokens, at) {
1175
+ const context = billingContext(tokens);
1176
+ let rates = pricing.base;
1177
+ for (const tier of pricing.tiers) if (tier.at <= context) rates = tier.rates;
1178
+ else break;
1179
+ const factor = pricing.multipliers.reduce((value, entry) => matches(entry.when, at, context) ? value * entry.factor : value, 1);
1180
+ return (tokens.input * rates.input + tokens.output * rates.output + tokens.cacheRead * rates.cacheRead) / 1e6 * factor;
1181
+ }
1182
+ function overridesOf(raw, field) {
1183
+ if (!Array.isArray(raw)) return [];
1184
+ const out = [];
1185
+ for (const entry of raw) {
1186
+ const value = record(entry);
1187
+ const context = record(record(value?.when)?.context);
1188
+ const at = rate(context?.gt) ?? rate(context?.gte);
1189
+ const price = rate(value?.price);
1190
+ const cache = record(value?.cache);
1191
+ const cacheRead = field === "input" ? rate(cache?.read) : void 0;
1192
+ if (at === void 0 || price === void 0 && cacheRead === void 0) continue;
1193
+ out.push({
1194
+ at,
1195
+ ...price === void 0 ? {} : { [field]: price },
1196
+ ...cacheRead === void 0 ? {} : { cacheRead }
1197
+ });
1198
+ }
1199
+ return out.sort((left, right) => left.at - right.at);
1200
+ }
1201
+ /** Normalize the gateway's `cost` object; malformed metadata is ignored. */
1202
+ function parseGatewayPricing(raw) {
1203
+ const cost = record(raw);
1204
+ const text = record(cost?.text);
1205
+ const input = record(text?.input);
1206
+ const output = record(text?.output);
1207
+ const cache = record(input?.cache);
1208
+ const baseInput = rate(input?.price);
1209
+ const baseOutput = rate(output?.price);
1210
+ if (baseInput === void 0 || baseOutput === void 0) return void 0;
1211
+ const base = {
1212
+ input: baseInput,
1213
+ output: baseOutput,
1214
+ cacheRead: rate(cache?.read) ?? 0
1215
+ };
1216
+ const inputOverrides = overridesOf(input?.overrides, "input");
1217
+ const outputOverrides = overridesOf(output?.overrides, "output");
1218
+ const thresholds = [.../* @__PURE__ */ new Set([...inputOverrides.map((entry) => entry.at), ...outputOverrides.map((entry) => entry.at)])].sort((left, right) => left - right);
1219
+ const tiers = [];
1220
+ for (const at of thresholds) {
1221
+ const inputOverride = inputOverrides.filter((entry) => entry.at <= at).at(-1);
1222
+ const outputOverride = outputOverrides.filter((entry) => entry.at <= at).at(-1);
1223
+ tiers.push({
1224
+ at,
1225
+ rates: {
1226
+ input: inputOverride?.input ?? base.input,
1227
+ output: outputOverride?.output ?? base.output,
1228
+ cacheRead: inputOverride?.cacheRead ?? base.cacheRead
1229
+ }
1230
+ });
1231
+ }
1232
+ const multipliers = [];
1233
+ if (Array.isArray(cost?.multipliers)) for (const entry of cost.multipliers) {
1234
+ const value = record(entry);
1235
+ const factor = rate(value?.factor);
1236
+ const when = conditionOf(value?.when);
1237
+ if (factor !== void 0 && factor > 0 && when !== void 0) multipliers.push({
1238
+ when,
1239
+ factor
1240
+ });
1241
+ }
1242
+ return {
1243
+ base,
1244
+ tiers,
1245
+ multipliers
1246
+ };
1247
+ }
1070
1248
  // src/catalog.ts
1071
1249
  const CATALOG_TIMEOUT_MS = 15e3;
1072
1250
  const catalogCache = /* @__PURE__ */ new Map();
@@ -1141,6 +1319,14 @@ function isNewRelease(date, now = Date.now()) {
1141
1319
  function catalogReleaseDate(provider, id, config) {
1142
1320
  return catalogCache.get(`${provider}|${modelsUrlFor(provider, config)}`)?.releaseDates[id];
1143
1321
  }
1322
+ /** Gateway pricing metadata for a live model, if the catalog published it. */
1323
+ function livePricingFor(provider, id) {
1324
+ for (const [key, entry] of catalogCache) {
1325
+ if (!key.startsWith(`${provider}|`)) continue;
1326
+ const pricing = entry.pricing[id];
1327
+ if (pricing !== void 0) return pricing;
1328
+ }
1329
+ }
1144
1330
  async function fetchLiveIds(provider, config, apiKey, fetchImpl) {
1145
1331
  const url = modelsUrlFor(provider, config);
1146
1332
  const cacheKey = `${provider}|${url}`;
@@ -1161,15 +1347,19 @@ async function fetchLiveIds(provider, config, apiKey, fetchImpl) {
1161
1347
  const ids = Array.isArray(payload.data) ? payload.data.map((entry) => entry.id).filter((id) => typeof id === "string" && id !== "") : Object.keys(payload.models ?? {});
1162
1348
  if (ids.length === 0) return void 0;
1163
1349
  const releaseDates = {};
1350
+ const pricing = {};
1164
1351
  if (Array.isArray(payload.data)) for (const entry of payload.data) {
1165
1352
  if (typeof entry.id !== "string" || entry.id === "") continue;
1166
1353
  const date = releaseDateOf(entry);
1167
1354
  if (date !== void 0) releaseDates[entry.id] = date;
1355
+ const parsed = parseGatewayPricing(entry.cost);
1356
+ if (parsed !== void 0) pricing[entry.id] = parsed;
1168
1357
  }
1169
1358
  catalogCache.set(cacheKey, {
1170
1359
  at: Date.now(),
1171
1360
  ids,
1172
- releaseDates
1361
+ releaseDates,
1362
+ pricing
1173
1363
  });
1174
1364
  return ids;
1175
1365
  } catch {
@@ -3790,22 +3980,66 @@ var OpenCodeAdapter = class extends LlmAdapter {
3790
3980
  return error.code === "RATE_LIMIT" || error.code === "QUOTA" || error.code === "SERVER" || error.code === "TIMEOUT" || error.code === "TRANSPORT" || error.code === "EMPTY_RESPONSE";
3791
3981
  }
3792
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
+ }
3793
4027
  // src/commands.ts
3794
4028
  function bar(percent, width = 10) {
3795
4029
  const clamped = Math.max(0, Math.min(100, percent));
3796
4030
  const filled = Math.round(clamped / 100 * width);
3797
4031
  return `[${"█".repeat(filled)}${"░".repeat(width - filled)}] ${clamped.toFixed(0)}%`;
3798
4032
  }
3799
- function reset(iso) {
4033
+ function reset(iso, copy) {
3800
4034
  if (iso === void 0) return "";
3801
4035
  const date = new Date(iso);
3802
4036
  if (Number.isNaN(date.getTime())) return "";
3803
- return ` · resets ${date.toISOString().replace("T", " ").slice(0, 16)}Z`;
4037
+ return ` · ${copy.resets} ${date.toISOString().replace("T", " ").slice(0, 16)}Z`;
3804
4038
  }
3805
- function windowLine(label, window) {
4039
+ function windowLine(label, window, copy) {
3806
4040
  if (window === void 0) return void 0;
3807
4041
  const status = window.status === void 0 || window.status === "ok" ? "" : ` (${window.status})`;
3808
- return `${label}: ${bar(window.percentUsed)}${status}${reset(window.resetsAt)}`;
4042
+ return `${label}: ${bar(window.percentUsed)}${status}${reset(window.resetsAt, copy)}`;
3809
4043
  }
3810
4044
  function percent(window) {
3811
4045
  if (window === void 0) return "—";
@@ -3827,18 +4061,18 @@ function relative(until) {
3827
4061
  if (delta >= 36e5) return `${Math.round(delta / 36e5)}h`;
3828
4062
  return `${Math.max(1, Math.round(delta / 6e4))}m`;
3829
4063
  }
3830
- async function accountsText(deps) {
4064
+ async function accountsText(deps, copy) {
3831
4065
  const lines = [];
3832
4066
  for (const provider of [GO_PROVIDER, CONSOLE_PROVIDER]) {
3833
4067
  let keys;
3834
4068
  try {
3835
4069
  keys = await deps.pool.resolveAll(provider);
3836
4070
  } catch {
3837
- lines.push(`${provider}: no credential`);
4071
+ lines.push(`${provider}: ${copy.noCredential}`);
3838
4072
  continue;
3839
4073
  }
3840
4074
  if (keys.length === 0) {
3841
- lines.push(`${provider}: no credential`);
4075
+ lines.push(`${provider}: ${copy.noCredential}`);
3842
4076
  continue;
3843
4077
  }
3844
4078
  const active = keys.find((entry) => deps.adapter.accountHealth(entry.key) === void 0);
@@ -3848,8 +4082,8 @@ async function accountsText(deps) {
3848
4082
  const usage = provider === "opencode-go-direct" ? await deps.usage(provider, entry.key) : void 0;
3849
4083
  const tags = [
3850
4084
  entry.source,
3851
- entry === active ? "active" : "",
3852
- 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)}`,
3853
4087
  usage?.report === void 0 ? usage?.error ?? "" : summary(usage.report)
3854
4088
  ].filter((tag) => tag !== "");
3855
4089
  lines.push(` ${index + 1}. ${entry.label ?? "default"}: ${tags.join(" · ")}`);
@@ -3857,27 +4091,27 @@ async function accountsText(deps) {
3857
4091
  }
3858
4092
  return lines.join("\n");
3859
4093
  }
3860
- function rulesText(deps) {
4094
+ function rulesText(deps, copy) {
3861
4095
  const rules = deps.config().modelAccountRules;
3862
- if (rules.length === 0) return "No model → account rules.";
4096
+ if (rules.length === 0) return copy.noRules;
3863
4097
  return rules.map((rule) => `${rule.pattern} → ${rule.account}`).join("\n");
3864
4098
  }
3865
- async function modelsText(deps) {
4099
+ async function modelsText(deps, copy) {
3866
4100
  const config = deps.config();
3867
4101
  try {
3868
4102
  const models = await deps.adapter.listModels(GO_PROVIDER, { unfiltered: true });
3869
- if (models.length === 0) return "No models.";
4103
+ if (models.length === 0) return copy.noModels;
3870
4104
  return models.map((model) => {
3871
4105
  const meta = effectiveModel(model.id, config);
3872
4106
  const limits = limitsFor(model.id);
3873
- 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 !== "");
3874
4108
  return `- ${model.id}${tags.length === 0 ? "" : ` · ${tags.join(" · ")}`}`;
3875
4109
  }).join("\n");
3876
4110
  } catch (error) {
3877
- return `Models unavailable: ${error instanceof Error ? error.message : String(error)}`;
4111
+ return `${copy.modelsUnavailable}: ${error instanceof Error ? error.message : String(error)}`;
3878
4112
  }
3879
4113
  }
3880
- async function testText(deps, model) {
4114
+ async function testText(deps, model, copy) {
3881
4115
  const started = Date.now();
3882
4116
  let text = "";
3883
4117
  try {
@@ -3900,18 +4134,18 @@ async function testText(deps, model) {
3900
4134
  } catch (error) {
3901
4135
  return {
3902
4136
  ok: false,
3903
- text: `${model}: failed — ${error instanceof Error ? error.message : String(error)}`
4137
+ text: `${model}: ${copy.testFailed} — ${error instanceof Error ? error.message : String(error)}`
3904
4138
  };
3905
4139
  }
3906
4140
  }
3907
- async function statusText(deps) {
4141
+ async function statusText(deps, copy) {
3908
4142
  const config = deps.config();
3909
- 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(", ");
3910
4144
  return [
3911
- "OpenCode provider",
3912
- await accountsText(deps),
3913
- `Rules: ${rules}`,
3914
- `Auth: ${config.openCodeAuthEntry} · ${config.apiKeyEnv}`
4145
+ copy.provider,
4146
+ await accountsText(deps, copy),
4147
+ `${copy.rules}: ${rules}`,
4148
+ `${copy.auth}: ${config.openCodeAuthEntry} · ${config.apiKeyEnv}`
3915
4149
  ].filter((line) => line !== "").join("\n");
3916
4150
  }
3917
4151
  function applyCommands(ctx, deps) {
@@ -3920,6 +4154,7 @@ function applyCommands(ctx, deps) {
3920
4154
  description: "OpenCode / OpenCode Go status, accounts, quotas, models and rules",
3921
4155
  input: { hint: "status | usage | accounts | models | rules | test [model]" },
3922
4156
  handler: async (invocation) => {
4157
+ const copy = commandCopy(deps.locale());
3923
4158
  const raw = invocation.rawInput.trim();
3924
4159
  const parts = raw === "" ? ["status"] : raw.split(/\s+/);
3925
4160
  const verb = parts[0] ?? "status";
@@ -3927,39 +4162,39 @@ function applyCommands(ctx, deps) {
3927
4162
  switch (verb.toLowerCase()) {
3928
4163
  case "status": return {
3929
4164
  kind: "success",
3930
- text: await statusText(deps)
4165
+ text: await statusText(deps, copy)
3931
4166
  };
3932
4167
  case "usage": {
3933
4168
  const usage = await deps.usage(GO_PROVIDER);
3934
4169
  if (usage?.report === void 0) return {
3935
4170
  kind: "error",
3936
- text: `Quota unavailable: ${usage?.error ?? "no report"}`
4171
+ text: `${copy.quotaUnavailable}: ${usage?.error ?? "no report"}`
3937
4172
  };
3938
4173
  const windows = usage.report.windows;
3939
4174
  const text = [
3940
- windowLine("5h ", windows.fiveHour),
3941
- windowLine("weekly ", windows.weekly),
3942
- 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)
3943
4178
  ].filter((line) => line !== void 0).join("\n");
3944
4179
  return {
3945
4180
  kind: "success",
3946
- text: text === "" ? "No quota windows returned." : text
4181
+ text: text === "" ? copy.noWindows : text
3947
4182
  };
3948
4183
  }
3949
4184
  case "accounts": return {
3950
4185
  kind: "success",
3951
- text: await accountsText(deps)
4186
+ text: await accountsText(deps, copy)
3952
4187
  };
3953
4188
  case "models": return {
3954
4189
  kind: "success",
3955
- text: await modelsText(deps)
4190
+ text: await modelsText(deps, copy)
3956
4191
  };
3957
4192
  case "rules": return {
3958
4193
  kind: "success",
3959
- text: rulesText(deps)
4194
+ text: rulesText(deps, copy)
3960
4195
  };
3961
4196
  case "test": {
3962
- const result = await testText(deps, arg === "" ? "deepseek-v4.1-flash" : arg);
4197
+ const result = await testText(deps, arg === "" ? "deepseek-v4.1-flash" : arg, copy);
3963
4198
  return result.ok ? {
3964
4199
  kind: "success",
3965
4200
  text: result.text
@@ -3970,7 +4205,7 @@ function applyCommands(ctx, deps) {
3970
4205
  }
3971
4206
  default: return {
3972
4207
  kind: "error",
3973
- 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]`
3974
4209
  };
3975
4210
  }
3976
4211
  }
@@ -4540,10 +4775,15 @@ function aggregateSessions(sessions, days, now = Date.now()) {
4540
4775
  if (at < since) continue;
4541
4776
  matched += 1;
4542
4777
  const meta = GENERATED_MODELS[model];
4543
- const priced = meta !== void 0;
4778
+ const livePricing = livePricingFor(provider, model);
4779
+ const priced = meta !== void 0 || livePricing !== void 0;
4544
4780
  const free = meta?.free === true;
4545
4781
  const partial = cacheWrite > 0 || !priced;
4546
- const cost = priced && !free && meta !== void 0 ? (input * meta.cost.input + output * meta.cost.output + cacheRead * meta.cost.cacheRead) / 1e6 : 0;
4782
+ const cost = !free && priced ? livePricing !== void 0 ? priceUsage(livePricing, {
4783
+ input,
4784
+ output,
4785
+ cacheRead
4786
+ }, at) : meta !== void 0 ? (input * meta.cost.input + output * meta.cost.output + cacheRead * meta.cost.cacheRead) / 1e6 : 0 : 0;
4547
4787
  const key = `${provider}/${model}`;
4548
4788
  let row = models.get(key);
4549
4789
  if (row === void 0) {
@@ -4796,6 +5036,7 @@ function publicConfig(config) {
4796
5036
  requestImageMaxBytes: config.requestImageMaxBytes,
4797
5037
  proxyUrl: config.proxyUrl,
4798
5038
  catalogRefreshMinutes: config.catalogRefreshMinutes,
5039
+ commandLocale: config.commandLocale,
4799
5040
  visibleModels: config.visibleModels,
4800
5041
  modelVisibility: config.modelVisibility,
4801
5042
  filterModelsByPlan: config.filterModelsByPlan,
@@ -4998,6 +5239,7 @@ function configOps(patch) {
4998
5239
  if ("requestImageMaxBytes" in patch) set("requestImageMaxBytes", patch.requestImageMaxBytes);
4999
5240
  if ("proxyUrl" in patch) set("proxyUrl", normalizeProxyUrl(typeof patch.proxyUrl === "string" ? patch.proxyUrl : ""));
5000
5241
  if ("catalogRefreshMinutes" in patch) set("catalogRefreshMinutes", patch.catalogRefreshMinutes);
5242
+ if ("commandLocale" in patch) set("commandLocale", patch.commandLocale === "zh" ? "zh" : "en");
5001
5243
  if ("visibleModels" in patch) set("visibleModels", cleanStringArray(patch.visibleModels));
5002
5244
  if ("modelVisibility" in patch) set("modelVisibility", cleanBooleanRecord(patch.modelVisibility));
5003
5245
  if ("filterModelsByPlan" in patch) set("filterModelsByPlan", patch.filterModelsByPlan === true);
@@ -5906,7 +6148,8 @@ function apply(ctx, config) {
5906
6148
  config: current,
5907
6149
  pool: keys,
5908
6150
  usage,
5909
- adapter
6151
+ adapter,
6152
+ locale: () => current().commandLocale
5910
6153
  });
5911
6154
  });
5912
6155
  let refreshTuiSettings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.29",
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",
@@ -70,7 +70,7 @@
70
70
  "build": "tsdown",
71
71
  "prepare": "tsdown 1>&2",
72
72
  "typecheck": "tsc --noEmit",
73
- "test": "node --import tsx --test tests/core.test.ts tests/routes.test.ts tests/limits.test.ts tests/analytics.test.ts tests/adapter.test.ts tests/usage.test.ts tests/request-log.test.ts tests/account-rules.test.ts tests/commands.test.ts tests/tool-schema.test.ts tests/tui-settings.test.ts tests/plan-filter.test.ts tests/image-budget.test.ts tests/tools.test.ts tests/plugin-updater.test.ts tests/client-render.test.tsx tests/sse.test.ts tests/proxy.test.ts tests/tool-call-validation.test.ts",
73
+ "test": "node --import tsx --test tests/core.test.ts tests/routes.test.ts tests/limits.test.ts tests/analytics.test.ts tests/adapter.test.ts tests/usage.test.ts tests/request-log.test.ts tests/account-rules.test.ts tests/commands.test.ts tests/tool-schema.test.ts tests/tui-settings.test.ts tests/plan-filter.test.ts tests/image-budget.test.ts tests/tools.test.ts tests/plugin-updater.test.ts tests/client-render.test.tsx tests/sse.test.ts tests/proxy.test.ts tests/tool-call-validation.test.ts tests/pricing.test.ts",
74
74
  "check": "npm run typecheck && npm test && npm run build",
75
75
  "release": "npm run check && npm pack",
76
76
  "sync:models": "node scripts/sync-models.mjs"