@goodandready/dsh-opencode-provider 0.5.25 → 0.5.27

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.27 — release 4: catalog freshness
4
+
5
+ - **#86 — new-model badge**: the live catalog now keeps `release_date` (or `created`) metadata; Settings marks models released within the last seven UTC days with a NEW badge and date tooltip. Dates are dropped safely when the gateway omits or malforms them.
6
+ - **#85 — output ceiling**: the default generated-output cap is 65,536 tokens (was 32,768), matching the reference plugin while still respecting each model's own `maxTokens` and explicit caller limits.
7
+ - 133 tests.
8
+
9
+ ## 0.5.26 — release 3: quota display and switch notices
10
+
11
+ - **#75 — quota display modes**: new `usageDisplay` setting with `auto`/`always`/`off` (legacy `showSidebarQuota` maps to `always`); `auto` shows the sidebar card when a Go usage report exists. Exposed in Settings and TUI.
12
+ - **#79 — account-switch notice**: routing trace entries now carry the opaque DSH session id; a `conversation.composer.dock` notice shows a non-blocking switch message for the current conversation after rotation or quota fallback. No prompt content or keys are exposed.
13
+ - 130 tests.
14
+
3
15
  ## 0.5.25 — release 2: stale data
4
16
 
5
17
  - **#87 — stale usage**: a failed usage refresh now returns the last successful report with `stale: true` and `lastSuccessAt` instead of discarding it. The quota sidebar/panel and Settings label the stale snapshot; an explicit refresh no longer loses the previous reading.
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.25.tgz
135
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
136
136
  # DSH Desktop:
137
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
137
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
138
138
 
139
139
  # Or install the published package:
140
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.27
141
141
  ```
142
142
 
143
143
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
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.25.tgz
134
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
135
135
  # DSH Desktop:
136
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
136
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
137
137
 
138
138
  # Либо установка опубликованного пакета из npm:
139
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
139
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.27
140
140
  ```
141
141
 
142
142
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
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.25.tgz
129
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
130
130
  # DSH 桌面端:
131
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.25.tgz
131
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.27.tgz
132
132
 
133
133
  # 或从 npm 安装已发布版本:
134
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.25
134
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.27
135
135
  ```
136
136
 
137
137
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
package/lib/client.js CHANGED
@@ -153,6 +153,8 @@ window.__ModuleLoader__.load({
153
153
  transportAuto: "Auto",
154
154
  free: "FREE",
155
155
  vision: "Image",
156
+ newModel: "NEW",
157
+ newModelHint: "Released within the last seven UTC days.",
156
158
  sectionFree: "Free models",
157
159
  sectionFreeHint: "No charge on any plan.",
158
160
  sectionGo: "Go subscription",
@@ -189,6 +191,11 @@ window.__ModuleLoader__.load({
189
191
  sidebar: "Sidebar",
190
192
  showSidebarQuota: "Show the quota card in the sidebar",
191
193
  showSidebarQuotaHint: "The card updates immediately after saving and otherwise every 60 seconds.",
194
+ usageDisplay: "Quota display",
195
+ usageDisplayAuto: "Auto (when Go usage is available)",
196
+ usageDisplayAlways: "Always",
197
+ usageDisplayOff: "Off",
198
+ usageDisplayHint: "Auto shows the sidebar card only when a Go usage report is available; Always keeps it visible; Off stops polling-hidden display.",
192
199
  routing: "Routing",
193
200
  routingHint: "Recent account/model choices. No prompts or keys are recorded.",
194
201
  routingEmpty: "No routing decisions yet.",
@@ -199,6 +206,7 @@ window.__ModuleLoader__.load({
199
206
  routingDefault: "Automatic",
200
207
  routingRotation: "Rotation",
201
208
  routingFallback: "Quota fallback",
209
+ accountSwitchNotice: "Account switched to {account} ({reason}).",
202
210
  routingFailed: "Failed",
203
211
  quotaFallbackModel: "Quota fallback model",
204
212
  quotaFallbackModelHint: "If Go rejects with a quota error, retry once on this free model through Console/Zen.",
@@ -373,6 +381,8 @@ window.__ModuleLoader__.load({
373
381
  transportAuto: "自动",
374
382
  free: "免费",
375
383
  vision: "图片",
384
+ newModel: "NEW",
385
+ newModelHint: "最近七个 UTC 日内发布。",
376
386
  sectionFree: "免费模型",
377
387
  sectionFreeHint: "任何计划均不收费。",
378
388
  sectionGo: "Go 订阅",
@@ -409,6 +419,11 @@ window.__ModuleLoader__.load({
409
419
  sidebar: "侧边栏",
410
420
  showSidebarQuota: "在侧边栏显示配额卡片",
411
421
  showSidebarQuotaHint: "保存后立即更新;其余时间每 60 秒更新一次。",
422
+ usageDisplay: "配额显示",
423
+ usageDisplayAuto: "自动(有 Go 用量时)",
424
+ usageDisplayAlways: "常驻",
425
+ usageDisplayOff: "关闭",
426
+ usageDisplayHint: "自动:仅有 Go 用量报告时显示侧边栏卡片;常驻:始终显示;关闭:隐藏。",
412
427
  routing: "路由",
413
428
  routingHint: "最近使用的账户/模型选择。不会记录提示词或密钥。",
414
429
  routingEmpty: "尚无路由决策。",
@@ -419,6 +434,7 @@ window.__ModuleLoader__.load({
419
434
  routingDefault: "自动",
420
435
  routingRotation: "轮换",
421
436
  routingFallback: "配额回退",
437
+ accountSwitchNotice: "账户已切换为 {account}({reason})。",
422
438
  routingFailed: "失败",
423
439
  quotaFallbackModel: "配额回退模型",
424
440
  quotaFallbackModelHint: "Go 返回配额错误时,通过 Console/Zen 用此免费模型重试一次。",
@@ -469,6 +485,9 @@ window.__ModuleLoader__.load({
469
485
  function fetchState() {
470
486
  return call("/dsh-opencode/state");
471
487
  }
488
+ function fetchNotices() {
489
+ return call("/dsh-opencode/notices");
490
+ }
472
491
  function refreshState() {
473
492
  return call("/dsh-opencode/refresh", {
474
493
  method: "POST",
@@ -1238,7 +1257,10 @@ window.__ModuleLoader__.load({
1238
1257
  const { t } = props;
1239
1258
  const state = useHostState(6e4);
1240
1259
  const expanded = usePanelOpen();
1241
- if (state === null || state.config.showSidebarQuota !== true) return null;
1260
+ if (state === null) return null;
1261
+ const mode = state.config.usageDisplay ?? (state.config.showSidebarQuota === true ? "always" : "auto");
1262
+ if (mode === "off") return null;
1263
+ if (mode === "auto" && state.usage === null) return null;
1242
1264
  const windows = state.usage?.report?.windows;
1243
1265
  const accountLabel = state.accounts.find((account) => account.active)?.label;
1244
1266
  const rows = [
@@ -1681,6 +1703,7 @@ window.__ModuleLoader__.load({
1681
1703
  maxOutputTokensOverrides: Object.fromEntries(Object.entries(state.config.maxOutputTokensOverrides).map(([id, value]) => [id, String(value)])),
1682
1704
  showSidebarQuota: state.config.showSidebarQuota,
1683
1705
  activeAccount: state.config.activeAccount,
1706
+ usageDisplay: state.config.usageDisplay,
1684
1707
  accounts: state.config.accounts.map((account) => ({ ...account })),
1685
1708
  modelAccountRules: state.config.modelAccountRules.map((rule) => ({ ...rule })),
1686
1709
  quotaFallbackModel: state.config.quotaFallbackModel,
@@ -1727,6 +1750,7 @@ window.__ModuleLoader__.load({
1727
1750
  maxOutputTokensOverrides: numberRecordOrNull(draft.maxOutputTokensOverrides),
1728
1751
  showSidebarQuota: draft.showSidebarQuota,
1729
1752
  activeAccount: draft.activeAccount.trim(),
1753
+ usageDisplay: draft.usageDisplay,
1730
1754
  accounts: draft.accounts.map((account) => ({
1731
1755
  label: account.label.trim(),
1732
1756
  apiKeyEnv: account.apiKeyEnv.trim()
@@ -2138,6 +2162,11 @@ window.__ModuleLoader__.load({
2138
2162
  title: t("legendFree"),
2139
2163
  children: [" ", t("free")]
2140
2164
  }) : null,
2165
+ model.isNew === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2166
+ className: "oc-badge oc-badge-ok",
2167
+ title: `${t("newModelHint")} ${model.releaseDate ?? ""}`,
2168
+ children: t("newModel")
2169
+ }) : null,
2141
2170
  !model.free && hasPrice ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2142
2171
  className: "oc-badge",
2143
2172
  title: t("legendPrice"),
@@ -3148,16 +3177,33 @@ window.__ModuleLoader__.load({
3148
3177
  children: t("sidebar")
3149
3178
  }),
3150
3179
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
3151
- className: "oc-row",
3152
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
3153
- type: "checkbox",
3154
- checked: draft.showSidebarQuota,
3155
- onChange: (event) => update({ showSidebarQuota: event.target.checked })
3156
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("showSidebarQuota") })]
3180
+ className: "oc-field",
3181
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3182
+ className: "oc-label",
3183
+ children: t("usageDisplay")
3184
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
3185
+ className: "oc-select",
3186
+ value: draft.usageDisplay,
3187
+ onChange: (event) => update({ usageDisplay: event.target.value }),
3188
+ children: [
3189
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3190
+ value: "auto",
3191
+ children: t("usageDisplayAuto")
3192
+ }),
3193
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3194
+ value: "always",
3195
+ children: t("usageDisplayAlways")
3196
+ }),
3197
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
3198
+ value: "off",
3199
+ children: t("usageDisplayOff")
3200
+ })
3201
+ ]
3202
+ })]
3157
3203
  }),
3158
3204
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
3159
3205
  className: "oc-hint",
3160
- children: t("showSidebarQuotaHint")
3206
+ children: t("usageDisplayHint")
3161
3207
  })
3162
3208
  ]
3163
3209
  }),
@@ -3290,6 +3336,7 @@ window.__ModuleLoader__.load({
3290
3336
  .oc-drag { cursor: grab; color: var(--oc-text-tertiary); user-select: none; font-size: 13px; line-height: 1; }
3291
3337
  .oc-drag:active { cursor: grabbing; }
3292
3338
  .oc-notice { font-size: 12px; padding: 7px 10px; border-radius: 7px; background: var(--oc-accent-soft); border: 1px solid var(--oc-accent-border); }
3339
+ .oc-switch-notice { margin: 4px auto; padding: 4px 10px; border: 1px solid var(--oc-border); border-radius: 999px; background: var(--oc-fill-soft); color: var(--oc-text-secondary); font-size: 11px; line-height: 1.4; text-align: center; }
3293
3340
  .oc-error { font-size: 12px; padding: 7px 10px; border-radius: 7px; background: var(--oc-danger-soft); border: 1px solid var(--oc-danger-border); color: var(--oc-danger); }
3294
3341
  [class*="_footerActions"]:has(.oc-foot), [class*="_footerActions"]:has(.oc-railButton) { flex-direction: column; }
3295
3342
  .oc-foot { box-sizing: border-box; flex: 1 1 auto; width: 100%; min-width: 0; display: flex; flex-direction: column; gap: 6px; margin: 0 0 4px; padding: 8px; border: 0.5px solid transparent; border-radius: var(--dsw-radius-md, 8px); background: transparent; color: var(--oc-text-secondary); font: inherit; text-align: left; cursor: pointer; }
@@ -3321,6 +3368,47 @@ window.__ModuleLoader__.load({
3321
3368
  .oc-table th, .oc-table td { text-align: left; padding: 5px 8px; border-bottom: 1px solid var(--oc-border-soft); vertical-align: top; }
3322
3369
  .oc-table th { font-weight: 600; opacity: 0.8; }
3323
3370
  `;
3371
+ // src/client/switch-notice.tsx
3372
+ /** Ambient notice under the composer when this conversation switched accounts. */
3373
+ function SwitchNotice({ sessionId, t }) {
3374
+ const [notice, setNotice] = (0, react.useState)(null);
3375
+ const seenAt = (0, react.useRef)(Date.now() - 5e3);
3376
+ (0, react.useEffect)(() => {
3377
+ if (sessionId === void 0 || String(sessionId) === "") return;
3378
+ let alive = true;
3379
+ const poll = () => {
3380
+ fetchNotices().then(({ events }) => {
3381
+ if (!alive) return;
3382
+ const event = events.filter((entry) => entry.session === String(sessionId) && entry.at > seenAt.current && entry.ok && (entry.reason === "rotation" || entry.reason === "quota-fallback")).sort((left, right) => right.at - left.at)[0];
3383
+ if (event === void 0) return;
3384
+ seenAt.current = event.at;
3385
+ setNotice({
3386
+ account: event.account,
3387
+ reason: event.reason,
3388
+ at: event.at
3389
+ });
3390
+ }).catch(() => {});
3391
+ };
3392
+ poll();
3393
+ const timer = setInterval(poll, 5e3);
3394
+ return () => {
3395
+ alive = false;
3396
+ clearInterval(timer);
3397
+ };
3398
+ }, [sessionId]);
3399
+ (0, react.useEffect)(() => {
3400
+ if (notice === null) return;
3401
+ const timer = setTimeout(() => setNotice(null), 6e4);
3402
+ return () => clearTimeout(timer);
3403
+ }, [notice]);
3404
+ if (notice === null) return null;
3405
+ const reason = notice.reason === "quota-fallback" ? t("routingFallback") : t("routingRotation");
3406
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3407
+ className: "oc-switch-notice",
3408
+ role: "status",
3409
+ children: t("accountSwitchNotice").replace("{account}", notice.account).replace("{reason}", reason)
3410
+ });
3411
+ }
3324
3412
  // src/client/index.tsx
3325
3413
  function injectCss() {
3326
3414
  if (typeof document === "undefined") return () => {};
@@ -3356,6 +3444,12 @@ window.__ModuleLoader__.load({
3356
3444
  locale: NS,
3357
3445
  inject: () => ({ close: () => ctx.get("layout")?.selectPanel(null) })
3358
3446
  }, QuotaPanel));
3447
+ ctx.slots.inject("conversation.composer.dock", () => slots.register({
3448
+ name: "conversation.composer.dock",
3449
+ id: "opencode-switch-notice",
3450
+ order: 80,
3451
+ locale: NS
3452
+ }, SwitchNotice));
3359
3453
  ctx.inject(["layout"], (layoutCtx) => {
3360
3454
  const layoutSlots = layoutCtx.slots;
3361
3455
  layoutCtx.slots.inject("sidebar.footer.action", () => layoutSlots.register({
package/lib/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import { Context } from "@deepseek-ai/cordis";
4
4
  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
+ type UsageDisplay = 'auto' | 'always' | 'off';
7
8
  interface AccountConfig {
8
9
  label?: string;
9
10
  apiKeyEnv?: string;
@@ -66,7 +67,9 @@ interface Config {
66
67
  stickySession?: boolean;
67
68
  /** Pinned account id; empty means automatic. */
68
69
  activeAccount?: string;
69
- /** Show the quota card in the sidebar. */
70
+ /** Sidebar quota visibility; legacy booleans map to always/auto. */
71
+ usageDisplay?: UsageDisplay;
72
+ /** Legacy boolean; used only when `usageDisplay` is absent. */
70
73
  showSidebarQuota?: boolean;
71
74
  /** Product user agent sent to OpenCode. */
72
75
  userAgent?: string;
@@ -97,6 +100,7 @@ interface ResolvedConfig {
97
100
  autoSwitch: boolean;
98
101
  stickySession: boolean;
99
102
  activeAccount: string;
103
+ usageDisplay: UsageDisplay;
100
104
  showSidebarQuota: boolean;
101
105
  userAgent: string;
102
106
  }
@@ -163,6 +167,8 @@ interface RoutingTraceEntry {
163
167
  readonly provider: string;
164
168
  readonly model: string;
165
169
  readonly account: string;
170
+ /** DSH session id; never prompt content. */
171
+ readonly session?: string;
166
172
  readonly reason: RoutingReason;
167
173
  readonly fallback: boolean;
168
174
  readonly ok: boolean;
package/lib/index.js CHANGED
@@ -81,10 +81,10 @@ 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.25";
84
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.27";
85
85
  const DEFAULT_CONTEXT_WINDOW = 262144;
86
86
  const DEFAULT_MAX_TOKENS = 32768;
87
- const DEFAULT_GENERATE_MAX_TOKENS = 32768;
87
+ const DEFAULT_GENERATE_MAX_TOKENS = 65536;
88
88
  function markVolatile(schema) {
89
89
  const mark = schema.volatile;
90
90
  return typeof mark === "function" ? mark.call(schema) : schema;
@@ -126,6 +126,11 @@ const Config = z.object({
126
126
  autoSwitch: markVolatile(z.boolean().default(true)),
127
127
  stickySession: markVolatile(z.boolean().default(true)),
128
128
  activeAccount: markVolatile(z.string().default("")),
129
+ usageDisplay: markVolatile(z.union([
130
+ "auto",
131
+ "always",
132
+ "off"
133
+ ])),
129
134
  showSidebarQuota: markVolatile(z.boolean().default(false)),
130
135
  userAgent: markVolatile(z.string().default(DEFAULT_USER_AGENT))
131
136
  });
@@ -222,6 +227,7 @@ function resolveConfig(config) {
222
227
  autoSwitch: config.autoSwitch !== false,
223
228
  stickySession: config.stickySession !== false,
224
229
  activeAccount: typeof config.activeAccount === "string" ? config.activeAccount : "",
230
+ usageDisplay: config.usageDisplay === "always" || config.usageDisplay === "off" ? config.usageDisplay : config.usageDisplay === "auto" ? "auto" : config.showSidebarQuota === true ? "always" : "auto",
225
231
  showSidebarQuota: config.showSidebarQuota === true,
226
232
  userAgent: typeof config.userAgent === "string" && config.userAgent !== "" ? config.userAgent : DEFAULT_USER_AGENT
227
233
  };
@@ -1102,6 +1108,24 @@ function modelInGoPlan(id, config) {
1102
1108
  if (GENERATED_MODELS[id] === void 0) return true;
1103
1109
  return effectiveModel(id, config).free || limitsFor(id) !== void 0;
1104
1110
  }
1111
+ function releaseDateOf(entry) {
1112
+ const raw = entry.release_date;
1113
+ if (typeof raw === "string" && raw !== "") {
1114
+ const parsed = Date.parse(raw.length === 10 ? `${raw}T00:00:00Z` : raw);
1115
+ if (Number.isFinite(parsed)) return new Date(parsed).toISOString().slice(0, 10);
1116
+ }
1117
+ if (typeof entry.created === "number" && Number.isFinite(entry.created)) return (/* @__PURE__ */ new Date(entry.created * 1e3)).toISOString().slice(0, 10);
1118
+ }
1119
+ /** Seven UTC calendar days, used by the picker's NEW badge. */
1120
+ function isNewRelease(date, now = Date.now()) {
1121
+ if (date === void 0) return false;
1122
+ const parsed = Date.parse(date.length === 10 ? `${date}T00:00:00Z` : date);
1123
+ return Number.isFinite(parsed) && parsed <= now && now - parsed < 6048e5;
1124
+ }
1125
+ /** Release date learned from the live catalog, if the gateway published it. */
1126
+ function catalogReleaseDate(provider, id, config) {
1127
+ return catalogCache.get(`${provider}|${modelsUrlFor(provider, config)}`)?.releaseDates[id];
1128
+ }
1105
1129
  async function fetchLiveIds(provider, config, apiKey, fetchImpl) {
1106
1130
  const url = modelsUrlFor(provider, config);
1107
1131
  const cacheKey = `${provider}|${url}`;
@@ -1121,9 +1145,16 @@ async function fetchLiveIds(provider, config, apiKey, fetchImpl) {
1121
1145
  const payload = await response.json();
1122
1146
  const ids = Array.isArray(payload.data) ? payload.data.map((entry) => entry.id).filter((id) => typeof id === "string" && id !== "") : Object.keys(payload.models ?? {});
1123
1147
  if (ids.length === 0) return void 0;
1148
+ const releaseDates = {};
1149
+ if (Array.isArray(payload.data)) for (const entry of payload.data) {
1150
+ if (typeof entry.id !== "string" || entry.id === "") continue;
1151
+ const date = releaseDateOf(entry);
1152
+ if (date !== void 0) releaseDates[entry.id] = date;
1153
+ }
1124
1154
  catalogCache.set(cacheKey, {
1125
1155
  at: Date.now(),
1126
- ids
1156
+ ids,
1157
+ releaseDates
1127
1158
  });
1128
1159
  return ids;
1129
1160
  } catch {
@@ -3305,6 +3336,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3305
3336
  const runStartedAt = Date.now();
3306
3337
  const model = effectiveModel(options.model, config);
3307
3338
  const sessionId = openCodeSessionId(options);
3339
+ const traceSession = options.sessionId === void 0 ? "" : String(options.sessionId);
3308
3340
  const rawKeys = orderKeysForModel(this.deps.resolveKeys !== void 0 ? await this.deps.resolveKeys(options.provider) : [await this.deps.resolveKey(options.provider)], options.model, config.modelAccountRules);
3309
3341
  const rule = matchModelAccount(options.model, config.modelAccountRules);
3310
3342
  let keys = rawKeys;
@@ -3404,6 +3436,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3404
3436
  provider: options.provider,
3405
3437
  model: options.model,
3406
3438
  account: entry.label ?? "default",
3439
+ ...traceSession === "" ? {} : { session: traceSession },
3407
3440
  reason: attemptReason,
3408
3441
  fallback,
3409
3442
  ok: true
@@ -3418,6 +3451,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3418
3451
  provider: options.provider,
3419
3452
  model: options.model,
3420
3453
  account: entry.label ?? "default",
3454
+ ...traceSession === "" ? {} : { session: traceSession },
3421
3455
  reason: attemptReason,
3422
3456
  fallback,
3423
3457
  ok: false,
@@ -3446,6 +3480,7 @@ var OpenCodeAdapter = class extends LlmAdapter {
3446
3480
  provider: options.provider,
3447
3481
  model: options.model,
3448
3482
  account: lastAccount,
3483
+ ...traceSession === "" ? {} : { session: traceSession },
3449
3484
  reason,
3450
3485
  fallback,
3451
3486
  ok: false,
@@ -4734,6 +4769,7 @@ function publicConfig(config) {
4734
4769
  autoSwitch: config.autoSwitch,
4735
4770
  stickySession: config.stickySession,
4736
4771
  activeAccount: config.activeAccount,
4772
+ usageDisplay: config.usageDisplay,
4737
4773
  showSidebarQuota: config.showSidebarQuota,
4738
4774
  userAgent: config.userAgent
4739
4775
  };
@@ -4923,6 +4959,10 @@ function configOps(patch) {
4923
4959
  if ("autoSwitch" in patch) set("autoSwitch", patch.autoSwitch === true);
4924
4960
  if ("stickySession" in patch) set("stickySession", patch.stickySession === true);
4925
4961
  if ("activeAccount" in patch) set("activeAccount", patch.activeAccount);
4962
+ if ("usageDisplay" in patch) {
4963
+ const value = patch.usageDisplay;
4964
+ if (value === "auto" || value === "always" || value === "off") set("usageDisplay", value);
4965
+ }
4926
4966
  if ("showSidebarQuota" in patch) set("showSidebarQuota", patch.showSidebarQuota === true);
4927
4967
  if ("userAgent" in patch) set("userAgent", patch.userAgent);
4928
4968
  return ops;
@@ -5015,7 +5055,9 @@ async function buildState(ctx, deps) {
5015
5055
  limits: limitsFor(model.id) ?? null,
5016
5056
  input: meta.input,
5017
5057
  reasoning: meta.reasoning,
5018
- visible: modelVisible(model.id, config)
5058
+ visible: modelVisible(model.id, config),
5059
+ ...catalogReleaseDate("opencode-go-direct", model.id, config) === void 0 ? {} : { releaseDate: catalogReleaseDate(GO_PROVIDER, model.id, config) },
5060
+ isNew: isNewRelease(catalogReleaseDate(GO_PROVIDER, model.id, config))
5019
5061
  };
5020
5062
  });
5021
5063
  } catch {
@@ -5074,6 +5116,21 @@ function applyRoutes(ctx, web, deps) {
5074
5116
  json(res, 500, { error: error instanceof Error ? error.message : "state failed" });
5075
5117
  }
5076
5118
  });
5119
+ route("/dsh-opencode/notices", async (req, res) => {
5120
+ if (req.method !== "GET") {
5121
+ json(res, 405, { error: "GET required" });
5122
+ return;
5123
+ }
5124
+ if (!trustedHost(req)) {
5125
+ json(res, 403, { error: "non-local Host refused" });
5126
+ return;
5127
+ }
5128
+ if (!allowedOrigin(req)) {
5129
+ json(res, 403, { error: "cross-origin request refused" });
5130
+ return;
5131
+ }
5132
+ json(res, 200, { events: deps.adapter.routingTrace() });
5133
+ });
5077
5134
  route("/dsh-opencode/health", async (req, res) => {
5078
5135
  if (req.method !== "GET") {
5079
5136
  json(res, 405, { error: "GET required" });
@@ -5477,6 +5534,32 @@ function buildOpenCodeTuiSection(deps) {
5477
5534
  value: text.trim() === "true"
5478
5535
  })
5479
5536
  },
5537
+ {
5538
+ path: ["usageDisplay"],
5539
+ group: "sidebar",
5540
+ kind: "select",
5541
+ label: "Quota display",
5542
+ hint: "Auto, Always, or Off for the sidebar quota card.",
5543
+ options: [
5544
+ {
5545
+ value: "auto",
5546
+ label: "Auto"
5547
+ },
5548
+ {
5549
+ value: "always",
5550
+ label: "Always"
5551
+ },
5552
+ {
5553
+ value: "off",
5554
+ label: "Off"
5555
+ }
5556
+ ],
5557
+ format: (value) => typeof value === "string" ? value : deps.usageDisplay(),
5558
+ parse: (text) => text === "auto" || text === "always" || text === "off" ? {
5559
+ kind: "set",
5560
+ value: text
5561
+ } : void 0
5562
+ },
5480
5563
  {
5481
5564
  path: ["showSidebarQuota"],
5482
5565
  group: "sidebar",
@@ -5793,6 +5876,7 @@ function apply(ctx, config) {
5793
5876
  quotaFallbackModel: () => current().quotaFallbackModel,
5794
5877
  autoSwitch: () => current().autoSwitch,
5795
5878
  stickySession: () => current().stickySession,
5879
+ usageDisplay: () => current().usageDisplay,
5796
5880
  showSidebarQuota: () => current().showSidebarQuota
5797
5881
  });
5798
5882
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.25",
3
+ "version": "0.5.27",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",