@goodandready/dsh-opencode-provider 0.5.4 → 0.5.5

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,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.5
4
+
5
+ - **Per-account request stats**: every upstream attempt now records its outcome, latency and TTFT in the local request log; the heatmap adds a per-account table with requests, errors and average latency/TTFT. No prompt or response content is stored.
6
+ - 87 tests.
7
+
3
8
  ## 0.5.4
4
9
 
5
10
  - **Sidebar settings**: the quota-card switch moved out of Connection into its own **Sidebar** section in Settings → OpenCode and into the TUI; saving now refreshes the sidebar card immediately instead of waiting for the 60-second poll.
package/README.md CHANGED
@@ -88,7 +88,7 @@ graph LR
88
88
  - **Per-account quotas**: each account row shows its own 5-hour / weekly / monthly usage, the active account drives the sidebar and quota panel, and the panel lists every account side by side.
89
89
  - **Per-account runway**: estimates time to exhaustion from the average burn rate of each window and marks the first window projected to run out before its reset; approximate because the API exposes only the current percentage and reset time.
90
90
  - **Model → account rules**: first matching glob pins a preferred account for that model; the other accounts remain fallbacks if it fails or is cooling.
91
- - **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. The adapter appends `{at, provider, model, purpose?, account, reason}` 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.
91
+ - **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.
92
92
  - **Stable `x-opencode-session`** derived from the durable session id, so retries and every turn of a conversation reuse the same routing key.
93
93
  - **Live catalog** from `/v1/models` with a generated `models.dev` fallback.
94
94
  - **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.
@@ -123,12 +123,12 @@ npm pack
123
123
  Install the **packed tarball** into a profile:
124
124
 
125
125
  ```bash
126
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.4.tgz
126
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
127
127
  # DSH Desktop:
128
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.4.tgz
128
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
129
129
 
130
130
  # Or install the published package:
131
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.4
131
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.5
132
132
  ```
133
133
 
134
134
  Restart dsh (or DSH Desktop), then pick `OpenCode` or `OpenCode Go` in the model picker.
package/README.ru.md CHANGED
@@ -96,7 +96,8 @@ graph LR
96
96
  - **Квотный fallback**: выберите бесплатную модель в настройках; при ошибке квоты Go тот же запрос один раз повторяется через Console/Zen. Если fallback тоже не сработал, возвращается исходная ошибка квоты.
97
97
  - **Липкий аккаунт**: последний успешный аккаунт предпочитается в том же диалоге; явные правила модели, закреплённый аккаунт и квотный fallback имеют приоритет.
98
98
  - **Трассировка маршрутизации**: панель квот показывает последние решения (`rule`, `sticky`, `rotation`, `quota-fallback`) без промптов и ключей.
99
- - **Почасовой Heatmap запросов**: сетка «день недели × час» в локальном времени плюс суточная полоса агрегации. Фильтры переключают отображение между всеми запросами, конкретной моделью и аккаунтом. Лог `~/.dsh/storages/dsh-opencode/requests.jsonl` хранит только метаданные `{at, provider, model, purpose, account, reason}` (промпты и ответы не сохраняются).
99
+ - **Статистика по аккаунтам**: в request log пишутся `ok`, `latencyMs`, `ttftMs` и `error`; heatmap показывает таблицу запросов, ошибок и средних latency/TTFT по каждому аккаунту.
100
+ - **Почасовой Heatmap запросов**: сетка «день недели × час» в локальном времени плюс суточная полоса агрегации. Фильтры переключают отображение между всеми запросами, конкретной моделью и аккаунтом. Лог `~/.dsh/storages/dsh-opencode/requests.jsonl` хранит только метаданные `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` (промпты и ответы не сохраняются).
100
101
  - **Стабильный заголовок `x-opencode-session`**: вычисляется из устойчивого ID сессии DSH, обеспечивая одинаковый ключ маршрутизации при повторах и диалоге.
101
102
  - **Живой каталог моделей**: автоматическое чтение `/v1/models` с резервным fallback из `models.dev`.
102
103
  - **Оценки лимитов подписки OpenCode Go**: долларовый лимит и расчетное число запросов для окон 5h, недели и месяца для планов Go и Go Plus.
@@ -121,12 +122,12 @@ npm pack
121
122
  Установка из **упакованного tarball**:
122
123
 
123
124
  ```bash
124
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.4.tgz
125
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
125
126
  # DSH Desktop:
126
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.4.tgz
127
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
127
128
 
128
129
  # Либо установка опубликованного пакета из npm:
129
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.4
130
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.5
130
131
  ```
131
132
 
132
133
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode** или **OpenCode Go** в списке моделей.
package/README.zh.md CHANGED
@@ -96,7 +96,8 @@ graph LR
96
96
  - **配额回退**:在设置中选择免费模型;Go 返回配额错误时,同一请求通过 Console/Zen 重试一次。若回退也失败,返回原始配额错误。
97
97
  - **会话粘性账户**:同一会话优先使用上次成功的账户;显式模型规则、固定账户和配额回退优先。
98
98
  - **路由追踪**:配额面板显示最近的账户/模型决策(`rule`、`sticky`、`rotation`、`quota-fallback`),不记录提示词或密钥。
99
- - **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason}` 元数据,不保存任何提示词与回复。
99
+ - **按账户请求统计**:请求日志记录 `ok`、`latencyMs`、`ttftMs` 和 `error`;热力图表格显示每个账户的请求数、错误数及平均延迟/TTFT。
100
+ - **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` 元数据,不保存任何提示词与回复。
100
101
  - **稳定的 `x-opencode-session` 标头**:根据 DSH 会话 ID 派生,确保重试与连续对话复用相同的网关路由键。
101
102
  - **实时模型目录**:自动从 `/v1/models` 同步,并以 `models.dev` 生成文件作为回退。
102
103
  - **Go 订阅额度估算**:每月美元额度以及 5 小时、每周和每月的估算请求次数。
@@ -118,12 +119,12 @@ npm pack
118
119
  通过**打包文件 (.tgz)**安装:
119
120
 
120
121
  ```bash
121
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.4.tgz
122
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
122
123
  # DSH 桌面端:
123
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.4.tgz
124
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
124
125
 
125
126
  # 或从 npm 安装已发布版本:
126
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.4
127
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.5
127
128
  ```
128
129
 
129
130
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode** 或 **OpenCode Go** 即可。
package/lib/client.js CHANGED
@@ -104,6 +104,8 @@ window.__ModuleLoader__.load({
104
104
  avgTtft: "Avg TTFT",
105
105
  avgLatency: "Avg latency",
106
106
  errors: "Errors",
107
+ latency: "Latency",
108
+ ttft: "TTFT",
107
109
  requestHeatmap: "Request heatmap",
108
110
  heatmapPeak: "Peak",
109
111
  heatmapEmpty: "No requests recorded yet. Send a message or run Test connection.",
@@ -286,6 +288,8 @@ window.__ModuleLoader__.load({
286
288
  avgTtft: "平均 TTFT",
287
289
  avgLatency: "平均延迟",
288
290
  errors: "错误",
291
+ latency: "延迟",
292
+ ttft: "首字延迟",
289
293
  requestHeatmap: "请求热力图",
290
294
  heatmapPeak: "峰值",
291
295
  heatmapEmpty: "尚无请求记录。发送消息或运行测试连接。",
@@ -482,6 +486,11 @@ window.__ModuleLoader__.load({
482
486
  function pad(hour) {
483
487
  return String(hour).padStart(2, "0");
484
488
  }
489
+ function formatMs(value) {
490
+ if (value === null || !Number.isFinite(value)) return "—";
491
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}s`;
492
+ return `${Math.round(value)}ms`;
493
+ }
485
494
  function RequestHeatmapView(props) {
486
495
  const { heatmap, t } = props;
487
496
  const [selected, setSelected] = (0, react.useState)("all");
@@ -572,6 +581,26 @@ window.__ModuleLoader__.load({
572
581
  })
573
582
  ]
574
583
  }),
584
+ accounts.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
585
+ className: "oc-table-scroll",
586
+ style: { marginTop: 8 },
587
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
588
+ className: "oc-table",
589
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
590
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("accountLabel") }),
591
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("requests") }),
592
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("errors") }),
593
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("latency") }),
594
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", { children: t("ttft") })
595
+ ] }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: accounts.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", { children: [
596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: entry.account }),
597
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: entry.total }),
598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: entry.errors }),
599
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatMs(entry.avgLatencyMs) }),
600
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", { children: formatMs(entry.avgTtftMs) })
601
+ ] }, entry.account)) })]
602
+ })
603
+ }),
575
604
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
576
605
  className: "oc-heat-scroll",
577
606
  children: [
package/lib/index.d.ts CHANGED
@@ -116,6 +116,44 @@ declare function createKeyPool(ctx: Context, config: () => ResolvedConfig): KeyP
116
116
  /** Backwards-compatible single-key facade over the pool. */
117
117
  declare function createKeyResolver(ctx: Context, config: () => ResolvedConfig): KeyResolver;
118
118
  //#endregion
119
+ //#region src/request-log.d.ts
120
+ interface RequestOutcome {
121
+ ok: boolean;
122
+ latencyMs: number;
123
+ /** Request start; the log uses it as the event timestamp. */
124
+ at?: number;
125
+ ttftMs?: number;
126
+ error?: string;
127
+ }
128
+ interface ModelRequestHeatmap {
129
+ provider: string;
130
+ model: string;
131
+ total: number;
132
+ hours: number[];
133
+ grid: number[][];
134
+ }
135
+ interface AccountRequestHeatmap {
136
+ account: string;
137
+ total: number;
138
+ errors: number;
139
+ avgLatencyMs: number | null;
140
+ avgTtftMs: number | null;
141
+ hours: number[];
142
+ grid: number[][];
143
+ }
144
+ interface RequestHeatmap {
145
+ since: number;
146
+ total: number;
147
+ /** Requests per hour of day, local time. */
148
+ hours: number[];
149
+ /** Monday-first weekday × hour grid, local time. */
150
+ grid: number[][];
151
+ firstAt: number | null;
152
+ lastAt: number | null;
153
+ models: ModelRequestHeatmap[];
154
+ accounts: AccountRequestHeatmap[];
155
+ }
156
+ //#endregion
119
157
  //#region src/adapter.d.ts
120
158
  type RoutingReason = 'rule' | 'sticky' | 'pinned' | 'default' | 'rotation' | 'quota-fallback';
121
159
  interface RoutingTraceEntry {
@@ -135,7 +173,7 @@ interface OpenCodeAdapterDeps {
135
173
  readonly resolveAttachments?: () => AttachmentStore | undefined;
136
174
  readonly fetchImpl?: typeof fetch;
137
175
  /** Test seam; defaults to the local append-only request log. */
138
- readonly recordRequest?: (provider: string, model: string, purpose?: string, account?: string, reason?: string) => void;
176
+ readonly recordRequest?: (provider: string, model: string, purpose?: string, account?: string, reason?: string, outcome?: RequestOutcome) => void;
139
177
  /** Test seam; defaults to ~/.dsh/storages/dsh-opencode/cooldowns.json. */
140
178
  readonly cooldownPath?: string;
141
179
  /** Receives a one-time warning when the provider tool limit drops tools. */
@@ -307,33 +345,6 @@ interface AnalyticsReport {
307
345
  sessions: SessionUsageRow[];
308
346
  }
309
347
  //#endregion
310
- //#region src/request-log.d.ts
311
- interface ModelRequestHeatmap {
312
- provider: string;
313
- model: string;
314
- total: number;
315
- hours: number[];
316
- grid: number[][];
317
- }
318
- interface AccountRequestHeatmap {
319
- account: string;
320
- total: number;
321
- hours: number[];
322
- grid: number[][];
323
- }
324
- interface RequestHeatmap {
325
- since: number;
326
- total: number;
327
- /** Requests per hour of day, local time. */
328
- hours: number[];
329
- /** Monday-first weekday × hour grid, local time. */
330
- grid: number[][];
331
- firstAt: number | null;
332
- lastAt: number | null;
333
- models: ModelRequestHeatmap[];
334
- accounts: AccountRequestHeatmap[];
335
- }
336
- //#endregion
337
348
  //#region src/routes.d.ts
338
349
  interface RouteDeps {
339
350
  readonly config: () => ResolvedConfig;
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@ const DEFAULT_GO_API_BASE = "https://opencode.ai/inference/go";
17
17
  const DEFAULT_GO_ZEN_BASE = "https://opencode.ai/zen/go/v1";
18
18
  const DEFAULT_USAGE_URL = "https://opencode.ai/zen/go/v1/usage";
19
19
  const DEFAULT_AUTH_ENTRY = "opencode-go";
20
- const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.4";
20
+ const DEFAULT_USER_AGENT = "dsh-opencode-provider/0.5.5";
21
21
  const DEFAULT_CONTEXT_WINDOW = 262144;
22
22
  const DEFAULT_MAX_TOKENS = 32768;
23
23
  const DEFAULT_GENERATE_MAX_TOKENS = 32768;
@@ -1526,14 +1526,20 @@ function requestLogPath() {
1526
1526
  let queue = Promise.resolve();
1527
1527
  let readyDirectory = "";
1528
1528
  let appends = 0;
1529
- function recordRequest(provider, model, at = Date.now(), purpose, account, reason) {
1529
+ function recordRequest(provider, model, at = Date.now(), purpose, account, reason, outcome) {
1530
1530
  const line = `${JSON.stringify({
1531
1531
  at,
1532
1532
  provider,
1533
1533
  model,
1534
1534
  ...purpose === void 0 ? {} : { purpose },
1535
1535
  ...account === void 0 ? {} : { account },
1536
- ...reason === void 0 ? {} : { reason }
1536
+ ...reason === void 0 ? {} : { reason },
1537
+ ...outcome === void 0 ? {} : {
1538
+ ok: outcome.ok,
1539
+ latencyMs: outcome.latencyMs,
1540
+ ...outcome.ttftMs === void 0 ? {} : { ttftMs: outcome.ttftMs },
1541
+ ...outcome.error === void 0 ? {} : { error: outcome.error }
1542
+ }
1537
1543
  })}\n`;
1538
1544
  queue = queue.then(async () => {
1539
1545
  try {
@@ -1565,6 +1571,7 @@ async function readRequestHeatmap(days, now = Date.now()) {
1565
1571
  const grid = Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0));
1566
1572
  const models = /* @__PURE__ */ new Map();
1567
1573
  const accounts = /* @__PURE__ */ new Map();
1574
+ const accountMetrics = /* @__PURE__ */ new Map();
1568
1575
  const seen = /* @__PURE__ */ new Set();
1569
1576
  let total = 0;
1570
1577
  let firstAt = null;
@@ -1632,12 +1639,37 @@ async function readRequestHeatmap(days, now = Date.now()) {
1632
1639
  accountEntry = {
1633
1640
  account,
1634
1641
  total: 0,
1642
+ errors: 0,
1643
+ avgLatencyMs: null,
1644
+ avgTtftMs: null,
1635
1645
  hours: Array.from({ length: 24 }, () => 0),
1636
1646
  grid: Array.from({ length: 7 }, () => Array.from({ length: 24 }, () => 0))
1637
1647
  };
1638
1648
  accounts.set(account, accountEntry);
1639
1649
  }
1640
1650
  accountEntry.total += 1;
1651
+ let metrics = accountMetrics.get(account);
1652
+ if (metrics === void 0) {
1653
+ metrics = {
1654
+ errors: 0,
1655
+ latencySum: 0,
1656
+ latencyCount: 0,
1657
+ ttftSum: 0,
1658
+ ttftCount: 0
1659
+ };
1660
+ accountMetrics.set(account, metrics);
1661
+ }
1662
+ if (parsed.ok === false) metrics.errors += 1;
1663
+ const latency = parsed.latencyMs;
1664
+ if (typeof latency === "number" && Number.isFinite(latency) && latency >= 0) {
1665
+ metrics.latencySum += latency;
1666
+ metrics.latencyCount += 1;
1667
+ }
1668
+ const ttft = parsed.ttftMs;
1669
+ if (typeof ttft === "number" && Number.isFinite(ttft) && ttft >= 0) {
1670
+ metrics.ttftSum += ttft;
1671
+ metrics.ttftCount += 1;
1672
+ }
1641
1673
  accountEntry.hours[hour] = (accountEntry.hours[hour] ?? 0) + 1;
1642
1674
  const accountRow = accountEntry.grid[day];
1643
1675
  if (accountRow !== void 0) accountRow[hour] = (accountRow[hour] ?? 0) + 1;
@@ -1653,7 +1685,15 @@ async function readRequestHeatmap(days, now = Date.now()) {
1653
1685
  firstAt,
1654
1686
  lastAt,
1655
1687
  models: [...models.values()].sort((a, b) => b.total - a.total || a.model.localeCompare(b.model)),
1656
- accounts: [...accounts.values()].sort((a, b) => b.total - a.total || a.account.localeCompare(b.account))
1688
+ accounts: [...accounts.values()].map((entry) => {
1689
+ const metrics = accountMetrics.get(entry.account);
1690
+ return {
1691
+ ...entry,
1692
+ errors: metrics?.errors ?? 0,
1693
+ avgLatencyMs: metrics !== void 0 && metrics.latencyCount > 0 ? metrics.latencySum / metrics.latencyCount : null,
1694
+ avgTtftMs: metrics !== void 0 && metrics.ttftCount > 0 ? metrics.ttftSum / metrics.ttftCount : null
1695
+ };
1696
+ }).sort((a, b) => b.total - a.total || a.account.localeCompare(b.account))
1657
1697
  };
1658
1698
  }
1659
1699
  //#endregion
@@ -3122,8 +3162,9 @@ var OpenCodeAdapter = class extends LlmAdapter {
3122
3162
  return this.run(options, false);
3123
3163
  }
3124
3164
  async *run(options, fallback) {
3125
- const record = this.deps.recordRequest ?? ((provider, model, purpose, account, reason) => recordRequest(provider, model, Date.now(), purpose, account, reason));
3165
+ const record = this.deps.recordRequest ?? ((provider, model, purpose, account, reason, outcome) => recordRequest(provider, model, outcome?.at ?? Date.now(), purpose, account, reason, outcome));
3126
3166
  const config = this.deps.config();
3167
+ const runStartedAt = Date.now();
3127
3168
  const model = effectiveModel(options.model, config);
3128
3169
  const sessionId = openCodeSessionId(options);
3129
3170
  const rawKeys = orderKeysForModel(this.deps.resolveKeys !== void 0 ? await this.deps.resolveKeys(options.provider) : [await this.deps.resolveKey(options.provider)], options.model, config.modelAccountRules);
@@ -3143,7 +3184,6 @@ var OpenCodeAdapter = class extends LlmAdapter {
3143
3184
  }
3144
3185
  }
3145
3186
  const initial = keys.find((entry) => fallback || !this.coolingDown(entry.key)) ?? keys[0];
3146
- if (initial !== void 0) record(options.provider, options.model, options.purpose, initial.label ?? "default", reason);
3147
3187
  const url = endpointFor(options.provider, model.transport, options.model, config);
3148
3188
  let images;
3149
3189
  if (hasImages(options)) {
@@ -3166,18 +3206,35 @@ var OpenCodeAdapter = class extends LlmAdapter {
3166
3206
  const body = model.transport === "openai-chat" ? buildChatBody(bodyOptions, model, config, images) : model.transport === "anthropic" ? buildAnthropicBody(bodyOptions, model, config, images) : model.transport === "openai-responses" ? buildResponsesBody(bodyOptions, model, config, images) : buildGoogleBody(bodyOptions, model, config, images);
3167
3207
  let lastError;
3168
3208
  let lastAccount = initial?.label ?? "default";
3209
+ let loggedAny = false;
3169
3210
  for (const [index, entry] of keys.entries()) {
3170
3211
  if (!fallback && this.coolingDown(entry.key)) continue;
3171
3212
  lastAccount = entry.label ?? "default";
3172
3213
  const attemptReason = index === 0 ? reason : "rotation";
3214
+ const attemptStartedAt = Date.now();
3173
3215
  let started = false;
3174
3216
  let responseFailure = false;
3217
+ let firstChunkAt;
3218
+ let logged = false;
3219
+ const logAttempt = (ok, errorCode) => {
3220
+ if (logged) return;
3221
+ logged = true;
3222
+ loggedAny = true;
3223
+ record(options.provider, options.model, options.purpose, entry.label ?? "default", attemptReason, {
3224
+ at: attemptStartedAt,
3225
+ ok,
3226
+ latencyMs: Math.max(0, Date.now() - attemptStartedAt),
3227
+ ...firstChunkAt === void 0 ? {} : { ttftMs: Math.max(0, firstChunkAt - attemptStartedAt) },
3228
+ ...errorCode === void 0 ? {} : { error: errorCode }
3229
+ });
3230
+ };
3175
3231
  try {
3176
3232
  const response = await this.send(url, entry.key, sessionId, model.transport, body, config, options.signal);
3177
3233
  if (!response.ok) {
3178
3234
  const text = await response.text().catch(() => "");
3179
3235
  const error = classifyResponseError(response.status, text, response.headers);
3180
3236
  lastError = error;
3237
+ logAttempt(false, error.code);
3181
3238
  const canRotate = index < keys.length - 1;
3182
3239
  if (error.code === "INVALID_CREDENTIAL" || error.code === "RATE_LIMIT" || error.code === "QUOTA") {
3183
3240
  if (!fallback) this.markCooldown(entry.key, response.status, response.headers, error.code);
@@ -3197,13 +3254,16 @@ var OpenCodeAdapter = class extends LlmAdapter {
3197
3254
  ok: true
3198
3255
  });
3199
3256
  for await (const chunk of this.parse(response, model.transport, config, options.signal)) {
3257
+ if (firstChunkAt === void 0) firstChunkAt = Date.now();
3200
3258
  started = true;
3201
3259
  yield chunk;
3202
3260
  }
3261
+ logAttempt(true);
3203
3262
  return;
3204
3263
  } catch (error) {
3205
- if (started || options.signal?.aborted === true) throw error;
3206
3264
  const classified = error instanceof LlmError ? error : new LlmError(error instanceof Error ? error.message : "OpenCode request failed", "TRANSPORT", { cause: error });
3265
+ logAttempt(false, classified.code);
3266
+ if (started || options.signal?.aborted === true) throw error;
3207
3267
  lastError = classified;
3208
3268
  const transient = this.isTransient(classified);
3209
3269
  if (transient && !responseFailure && !fallback) this.markCooldown(entry.key, 0);
@@ -3214,6 +3274,12 @@ var OpenCodeAdapter = class extends LlmAdapter {
3214
3274
  }
3215
3275
  }
3216
3276
  const finalError = lastError ?? new LlmError("llm-opencode: every configured account is cooling down after provider rejections", "RATE_LIMIT");
3277
+ if (!loggedAny) record(options.provider, options.model, options.purpose, lastAccount, reason, {
3278
+ at: runStartedAt,
3279
+ ok: false,
3280
+ latencyMs: 0,
3281
+ error: finalError.code
3282
+ });
3217
3283
  this.pushTrace({
3218
3284
  at: Date.now(),
3219
3285
  provider: options.provider,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-opencode-provider",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
4
4
  "description": "OpenCode / OpenCode Go LLM provider plugin for DeepSeek Harness (dsh).",
5
5
  "license": "MIT",
6
6
  "type": "module",