@goodandready/dsh-opencode-provider 0.5.3 → 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 +10 -0
- package/README.md +7 -6
- package/README.ru.md +6 -4
- package/README.zh.md +6 -4
- package/lib/client.js +124 -67
- package/lib/index.d.ts +39 -28
- package/lib/index.js +93 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
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
|
+
|
|
8
|
+
## 0.5.4
|
|
9
|
+
|
|
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.
|
|
11
|
+
- 85 tests.
|
|
12
|
+
|
|
3
13
|
## 0.5.3
|
|
4
14
|
|
|
5
15
|
- **Per-account runway**: the quota panel estimates time to exhaustion for each account from the 5-hour, weekly and monthly burn rate and flags the first window projected to run out before its reset. The estimate is intentionally approximate; only the current percentage and reset time are exposed by the usage endpoint.
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
@@ -231,11 +231,12 @@ Open **Settings → OpenCode**:
|
|
|
231
231
|
- **Quota** — 5-hour, weekly and monthly bars with reset dates, plus warning banners at 80% and 95%.
|
|
232
232
|
- **Accounts** — add extra accounts by credential reference, set each account's key from the page, see whether it is configured, pin one, or return to automatic rotation. **Import all from auth.json** copies every OpenCode key into its own credential ref and adds an account for it; **Check all accounts** refreshes the per-account quota/health rows.
|
|
233
233
|
- **Models** — search, refresh the live catalog, uncheck models to build an allowlist, override the wire transport per model, and see each model's input/output price plus its Go request estimate (`Go 34.7K/5h`). Hover the estimate for the full Go / Go Plus monthly allowance and 5h/week/month request counts; expand **Subscription limits** for a table of every model with both plans.
|
|
234
|
-
- **Connection** — API bases, usage endpoint, timeouts, output cap, context override
|
|
234
|
+
- **Connection** — API bases, usage endpoint, timeouts, output cap, context override. The OpenCode auth-entry field autocompletes from the keys present in `auth.json`.
|
|
235
|
+
- **Sidebar** — show or hide the quota card. The card refreshes immediately after saving and otherwise every 60 seconds.
|
|
235
236
|
- **Plugin update** — current/latest version, update badge, and **Update now**. It installs the exact registry version through the normal `dsh plugin add` path, blocks a second click while installing, and reports when DSH must be restarted.
|
|
236
237
|
- **Routing** — sticky account per conversation and the free fallback model used after a Go quota error. The quota panel shows the last routing decisions with their reason.
|
|
237
238
|
|
|
238
|
-
The sidebar quota card is **off by default**. Enable *Show the quota card in the sidebar*
|
|
239
|
+
The sidebar quota card is **off by default**. Enable *Show the quota card in the sidebar* under **Sidebar**; the card appears immediately after saving, and clicking it opens the full quota panel with the **Usage by model** table (7/30/90-day window).
|
|
239
240
|
|
|
240
241
|
The Host serves `/dsh-opencode/state`, `/health`, `/analytics`, `/config`, `/key`, `/key/clear`, `/auth-import`, `/refresh`, `/models/refresh`, `/test` and `/update`. Writes require JSON, are refused for cross-origin requests, and are restricted to loopback (`127.0.0.1` / `::1`); reads stay same-origin. This keeps key and configuration writes off a LAN-exposed web server until the host's own auth token can be integrated.
|
|
241
242
|
|
package/README.ru.md
CHANGED
|
@@ -88,6 +88,7 @@ graph LR
|
|
|
88
88
|
- Автоматический переход на следующий аккаунт при 401/403/429 или ответах о превышении квот (400/402/403/429).
|
|
89
89
|
- Недействительный ключ охлаждается 24 часа, превышение квоты — 10 минут (или по заголовку `Retry-After`), rate limit — 60 секунд (или по `Retry-After`).
|
|
90
90
|
- Если запрос уже начал стриминг, он не переключается посреди ответа; следующий запрос берёт следующий готовый ключ.
|
|
91
|
+
- **Sidebar-карточка квот**: отдельный переключатель в разделе **Sidebar** (и в TUI); после сохранения карточка появляется/исчезает сразу, без ожидания 60-секундного polling.
|
|
91
92
|
- **Квоты по каждому аккаунту**: отображение индивидуального расхода за 5 часов, неделю и месяц; активный аккаунт управляет сайдбаром и панелью.
|
|
92
93
|
- **Runway по каждому аккаунту**: оценка времени до исчерпания по среднему темпу каждого окна; отмечается первое окно, которое закончится раньше сброса. Оценка приблизительная: API отдаёт только текущий процент и время сброса.
|
|
93
94
|
- **Онбординг аккаунтов**: **Import all from auth.json** переносит все ключи OpenCode в отдельные credential ref и добавляет аккаунты; **Check all accounts** обновляет квоты и health по каждому аккаунту.
|
|
@@ -95,7 +96,8 @@ graph LR
|
|
|
95
96
|
- **Квотный fallback**: выберите бесплатную модель в настройках; при ошибке квоты Go тот же запрос один раз повторяется через Console/Zen. Если fallback тоже не сработал, возвращается исходная ошибка квоты.
|
|
96
97
|
- **Липкий аккаунт**: последний успешный аккаунт предпочитается в том же диалоге; явные правила модели, закреплённый аккаунт и квотный fallback имеют приоритет.
|
|
97
98
|
- **Трассировка маршрутизации**: панель квот показывает последние решения (`rule`, `sticky`, `rotation`, `quota-fallback`) без промптов и ключей.
|
|
98
|
-
-
|
|
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}` (промпты и ответы не сохраняются).
|
|
99
101
|
- **Стабильный заголовок `x-opencode-session`**: вычисляется из устойчивого ID сессии DSH, обеспечивая одинаковый ключ маршрутизации при повторах и диалоге.
|
|
100
102
|
- **Живой каталог моделей**: автоматическое чтение `/v1/models` с резервным fallback из `models.dev`.
|
|
101
103
|
- **Оценки лимитов подписки OpenCode Go**: долларовый лимит и расчетное число запросов для окон 5h, недели и месяца для планов Go и Go Plus.
|
|
@@ -120,12 +122,12 @@ npm pack
|
|
|
120
122
|
Установка из **упакованного tarball**:
|
|
121
123
|
|
|
122
124
|
```bash
|
|
123
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
125
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
|
|
124
126
|
# DSH Desktop:
|
|
125
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
127
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
|
|
126
128
|
|
|
127
129
|
# Либо установка опубликованного пакета из npm:
|
|
128
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
130
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.5
|
|
129
131
|
```
|
|
130
132
|
|
|
131
133
|
После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode** или **OpenCode Go** в списке моделей.
|
package/README.zh.md
CHANGED
|
@@ -89,13 +89,15 @@ graph LR
|
|
|
89
89
|
- 在生成流开始前,遇到 401/403/429 或配额用尽响应(400/402/403/429)自动重试下一个账户。
|
|
90
90
|
- 无效密钥冷却 24 小时,超额冷却 10 分钟(或遵循 `Retry-After`),速率限制冷却 60 秒。
|
|
91
91
|
- 正在流式传输中的请求不会被打断,下一个请求会自动使用下一个可用密钥。
|
|
92
|
+
- **侧边栏配额卡片**:独立 **Sidebar** 开关(TUI 也可设置);保存后立即显示/隐藏,无需等待 60 秒轮询。
|
|
92
93
|
- **多账户额度与健康监控**:每行独立展示 5 小时、每周与每月使用情况,活跃账户驱动侧边栏与配额面板。
|
|
93
94
|
- **每账户预计可用时长**:按各窗口的平均消耗速度估算耗尽时间,并标记在重置前最先耗尽的窗口;API 仅提供当前百分比与重置时间,因此为近似值。
|
|
94
95
|
- **模型 ➔ 账户路由规则**:支持 Glob 通配符(如 `*free*`、`deepseek-*`)将模型优先绑定至特定账户,其他账户保留为备用。
|
|
95
96
|
- **配额回退**:在设置中选择免费模型;Go 返回配额错误时,同一请求通过 Console/Zen 重试一次。若回退也失败,返回原始配额错误。
|
|
96
97
|
- **会话粘性账户**:同一会话优先使用上次成功的账户;显式模型规则、固定账户和配额回退优先。
|
|
97
98
|
- **路由追踪**:配额面板显示最近的账户/模型决策(`rule`、`sticky`、`rotation`、`quota-fallback`),不记录提示词或密钥。
|
|
98
|
-
-
|
|
99
|
+
- **按账户请求统计**:请求日志记录 `ok`、`latencyMs`、`ttftMs` 和 `error`;热力图表格显示每个账户的请求数、错误数及平均延迟/TTFT。
|
|
100
|
+
- **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` 元数据,不保存任何提示词与回复。
|
|
99
101
|
- **稳定的 `x-opencode-session` 标头**:根据 DSH 会话 ID 派生,确保重试与连续对话复用相同的网关路由键。
|
|
100
102
|
- **实时模型目录**:自动从 `/v1/models` 同步,并以 `models.dev` 生成文件作为回退。
|
|
101
103
|
- **Go 订阅额度估算**:每月美元额度以及 5 小时、每周和每月的估算请求次数。
|
|
@@ -117,12 +119,12 @@ npm pack
|
|
|
117
119
|
通过**打包文件 (.tgz)**安装:
|
|
118
120
|
|
|
119
121
|
```bash
|
|
120
|
-
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
122
|
+
dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
|
|
121
123
|
# DSH 桌面端:
|
|
122
|
-
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.
|
|
124
|
+
dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.5.tgz
|
|
123
125
|
|
|
124
126
|
# 或从 npm 安装已发布版本:
|
|
125
|
-
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.
|
|
127
|
+
dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.5
|
|
126
128
|
```
|
|
127
129
|
|
|
128
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.",
|
|
@@ -151,7 +153,9 @@ window.__ModuleLoader__.load({
|
|
|
151
153
|
streamIdleTimeoutMs: "Stream idle timeout (ms)",
|
|
152
154
|
maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
|
|
153
155
|
contextWindow: "Context override",
|
|
156
|
+
sidebar: "Sidebar",
|
|
154
157
|
showSidebarQuota: "Show the quota card in the sidebar",
|
|
158
|
+
showSidebarQuotaHint: "The card updates immediately after saving and otherwise every 60 seconds.",
|
|
155
159
|
routing: "Routing",
|
|
156
160
|
routingHint: "Recent account/model choices. No prompts or keys are recorded.",
|
|
157
161
|
routingEmpty: "No routing decisions yet.",
|
|
@@ -284,6 +288,8 @@ window.__ModuleLoader__.load({
|
|
|
284
288
|
avgTtft: "平均 TTFT",
|
|
285
289
|
avgLatency: "平均延迟",
|
|
286
290
|
errors: "错误",
|
|
291
|
+
latency: "延迟",
|
|
292
|
+
ttft: "首字延迟",
|
|
287
293
|
requestHeatmap: "请求热力图",
|
|
288
294
|
heatmapPeak: "峰值",
|
|
289
295
|
heatmapEmpty: "尚无请求记录。发送消息或运行测试连接。",
|
|
@@ -331,7 +337,9 @@ window.__ModuleLoader__.load({
|
|
|
331
337
|
streamIdleTimeoutMs: "流空闲超时(毫秒)",
|
|
332
338
|
maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
|
|
333
339
|
contextWindow: "上下文覆盖",
|
|
340
|
+
sidebar: "侧边栏",
|
|
334
341
|
showSidebarQuota: "在侧边栏显示配额卡片",
|
|
342
|
+
showSidebarQuotaHint: "保存后立即更新;其余时间每 60 秒更新一次。",
|
|
335
343
|
routing: "路由",
|
|
336
344
|
routingHint: "最近使用的账户/模型选择。不会记录提示词或密钥。",
|
|
337
345
|
routingEmpty: "尚无路由决策。",
|
|
@@ -454,6 +462,9 @@ window.__ModuleLoader__.load({
|
|
|
454
462
|
});
|
|
455
463
|
}
|
|
456
464
|
//#endregion
|
|
465
|
+
//#region src/client/events.ts
|
|
466
|
+
const CONFIG_CHANGED_EVENT = "dsh-opencode-config-changed";
|
|
467
|
+
//#endregion
|
|
457
468
|
//#region src/client/heatmap.tsx
|
|
458
469
|
const DAYS = [
|
|
459
470
|
"Mon",
|
|
@@ -475,6 +486,11 @@ window.__ModuleLoader__.load({
|
|
|
475
486
|
function pad(hour) {
|
|
476
487
|
return String(hour).padStart(2, "0");
|
|
477
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
|
+
}
|
|
478
494
|
function RequestHeatmapView(props) {
|
|
479
495
|
const { heatmap, t } = props;
|
|
480
496
|
const [selected, setSelected] = (0, react.useState)("all");
|
|
@@ -565,6 +581,26 @@ window.__ModuleLoader__.load({
|
|
|
565
581
|
})
|
|
566
582
|
]
|
|
567
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
|
+
}),
|
|
568
604
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
569
605
|
className: "oc-heat-scroll",
|
|
570
606
|
children: [
|
|
@@ -1014,9 +1050,14 @@ window.__ModuleLoader__.load({
|
|
|
1014
1050
|
};
|
|
1015
1051
|
load();
|
|
1016
1052
|
const timer = setInterval(load, intervalMs);
|
|
1053
|
+
const onConfigChanged = () => {
|
|
1054
|
+
load();
|
|
1055
|
+
};
|
|
1056
|
+
window.addEventListener(CONFIG_CHANGED_EVENT, onConfigChanged);
|
|
1017
1057
|
return () => {
|
|
1018
1058
|
cancelled = true;
|
|
1019
1059
|
clearInterval(timer);
|
|
1060
|
+
window.removeEventListener(CONFIG_CHANGED_EVENT, onConfigChanged);
|
|
1020
1061
|
};
|
|
1021
1062
|
}, [intervalMs]);
|
|
1022
1063
|
return state;
|
|
@@ -1256,8 +1297,13 @@ window.__ModuleLoader__.load({
|
|
|
1256
1297
|
load();
|
|
1257
1298
|
loadAnalytics(days);
|
|
1258
1299
|
const timer = setInterval(load, 6e4);
|
|
1300
|
+
const onConfigChanged = () => {
|
|
1301
|
+
load();
|
|
1302
|
+
};
|
|
1303
|
+
window.addEventListener(CONFIG_CHANGED_EVENT, onConfigChanged);
|
|
1259
1304
|
return () => {
|
|
1260
1305
|
clearInterval(timer);
|
|
1306
|
+
window.removeEventListener(CONFIG_CHANGED_EVENT, onConfigChanged);
|
|
1261
1307
|
analyticsSeq.current += 1;
|
|
1262
1308
|
};
|
|
1263
1309
|
}, [days]);
|
|
@@ -1803,6 +1849,7 @@ window.__ModuleLoader__.load({
|
|
|
1803
1849
|
setError(null);
|
|
1804
1850
|
try {
|
|
1805
1851
|
await saveConfig(patchOf(draft));
|
|
1852
|
+
window.dispatchEvent(new CustomEvent(CONFIG_CHANGED_EVENT));
|
|
1806
1853
|
setNotice(t("saved"));
|
|
1807
1854
|
setState(null);
|
|
1808
1855
|
setDraft(null);
|
|
@@ -2657,78 +2704,84 @@ window.__ModuleLoader__.load({
|
|
|
2657
2704
|
})
|
|
2658
2705
|
]
|
|
2659
2706
|
}),
|
|
2707
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2708
|
+
className: "oc-card",
|
|
2709
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2710
|
+
className: "oc-card-title",
|
|
2711
|
+
children: t("advanced")
|
|
2712
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2713
|
+
className: "oc-grid",
|
|
2714
|
+
children: [
|
|
2715
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2716
|
+
label: t("apiKeyEnv"),
|
|
2717
|
+
hint: t("apiKeyEnvHint"),
|
|
2718
|
+
value: draft.apiKeyEnv,
|
|
2719
|
+
onChange: (value) => update({ apiKeyEnv: value })
|
|
2720
|
+
}),
|
|
2721
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2722
|
+
label: t("openCodeAuthEntry"),
|
|
2723
|
+
hint: t("openCodeAuthEntryHint"),
|
|
2724
|
+
value: draft.openCodeAuthEntry,
|
|
2725
|
+
onChange: (value) => update({ openCodeAuthEntry: value }),
|
|
2726
|
+
list: "oc-auth-entries"
|
|
2727
|
+
}),
|
|
2728
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
2729
|
+
id: "oc-auth-entries",
|
|
2730
|
+
children: state.auth.entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: entry }, entry))
|
|
2731
|
+
}),
|
|
2732
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2733
|
+
label: t("apiBase"),
|
|
2734
|
+
value: draft.apiBase,
|
|
2735
|
+
onChange: (value) => update({ apiBase: value })
|
|
2736
|
+
}),
|
|
2737
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2738
|
+
label: t("goApiBase"),
|
|
2739
|
+
value: draft.goApiBase,
|
|
2740
|
+
onChange: (value) => update({ goApiBase: value })
|
|
2741
|
+
}),
|
|
2742
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2743
|
+
label: t("goZenBase"),
|
|
2744
|
+
value: draft.goZenBase,
|
|
2745
|
+
onChange: (value) => update({ goZenBase: value })
|
|
2746
|
+
}),
|
|
2747
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2748
|
+
label: t("usageUrl"),
|
|
2749
|
+
value: draft.usageUrl,
|
|
2750
|
+
onChange: (value) => update({ usageUrl: value })
|
|
2751
|
+
}),
|
|
2752
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2753
|
+
label: t("requestTimeoutMs"),
|
|
2754
|
+
type: "number",
|
|
2755
|
+
value: draft.requestTimeoutMs,
|
|
2756
|
+
onChange: (value) => update({ requestTimeoutMs: value })
|
|
2757
|
+
}),
|
|
2758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2759
|
+
label: t("streamIdleTimeoutMs"),
|
|
2760
|
+
type: "number",
|
|
2761
|
+
value: draft.streamIdleTimeoutMs,
|
|
2762
|
+
onChange: (value) => update({ streamIdleTimeoutMs: value })
|
|
2763
|
+
}),
|
|
2764
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2765
|
+
label: t("maxOutputTokens"),
|
|
2766
|
+
type: "number",
|
|
2767
|
+
value: draft.maxOutputTokens,
|
|
2768
|
+
onChange: (value) => update({ maxOutputTokens: value })
|
|
2769
|
+
}),
|
|
2770
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2771
|
+
label: t("contextWindow"),
|
|
2772
|
+
type: "number",
|
|
2773
|
+
value: draft.contextWindow,
|
|
2774
|
+
onChange: (value) => update({ contextWindow: value })
|
|
2775
|
+
})
|
|
2776
|
+
]
|
|
2777
|
+
})]
|
|
2778
|
+
}),
|
|
2660
2779
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
2661
2780
|
className: "oc-card",
|
|
2662
2781
|
children: [
|
|
2663
2782
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
2664
2783
|
className: "oc-card-title",
|
|
2665
|
-
children: t("
|
|
2666
|
-
}),
|
|
2667
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2668
|
-
className: "oc-grid",
|
|
2669
|
-
children: [
|
|
2670
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2671
|
-
label: t("apiKeyEnv"),
|
|
2672
|
-
hint: t("apiKeyEnvHint"),
|
|
2673
|
-
value: draft.apiKeyEnv,
|
|
2674
|
-
onChange: (value) => update({ apiKeyEnv: value })
|
|
2675
|
-
}),
|
|
2676
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2677
|
-
label: t("openCodeAuthEntry"),
|
|
2678
|
-
hint: t("openCodeAuthEntryHint"),
|
|
2679
|
-
value: draft.openCodeAuthEntry,
|
|
2680
|
-
onChange: (value) => update({ openCodeAuthEntry: value }),
|
|
2681
|
-
list: "oc-auth-entries"
|
|
2682
|
-
}),
|
|
2683
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("datalist", {
|
|
2684
|
-
id: "oc-auth-entries",
|
|
2685
|
-
children: state.auth.entries.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", { value: entry }, entry))
|
|
2686
|
-
}),
|
|
2687
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2688
|
-
label: t("apiBase"),
|
|
2689
|
-
value: draft.apiBase,
|
|
2690
|
-
onChange: (value) => update({ apiBase: value })
|
|
2691
|
-
}),
|
|
2692
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2693
|
-
label: t("goApiBase"),
|
|
2694
|
-
value: draft.goApiBase,
|
|
2695
|
-
onChange: (value) => update({ goApiBase: value })
|
|
2696
|
-
}),
|
|
2697
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2698
|
-
label: t("goZenBase"),
|
|
2699
|
-
value: draft.goZenBase,
|
|
2700
|
-
onChange: (value) => update({ goZenBase: value })
|
|
2701
|
-
}),
|
|
2702
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2703
|
-
label: t("usageUrl"),
|
|
2704
|
-
value: draft.usageUrl,
|
|
2705
|
-
onChange: (value) => update({ usageUrl: value })
|
|
2706
|
-
}),
|
|
2707
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2708
|
-
label: t("requestTimeoutMs"),
|
|
2709
|
-
type: "number",
|
|
2710
|
-
value: draft.requestTimeoutMs,
|
|
2711
|
-
onChange: (value) => update({ requestTimeoutMs: value })
|
|
2712
|
-
}),
|
|
2713
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2714
|
-
label: t("streamIdleTimeoutMs"),
|
|
2715
|
-
type: "number",
|
|
2716
|
-
value: draft.streamIdleTimeoutMs,
|
|
2717
|
-
onChange: (value) => update({ streamIdleTimeoutMs: value })
|
|
2718
|
-
}),
|
|
2719
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2720
|
-
label: t("maxOutputTokens"),
|
|
2721
|
-
type: "number",
|
|
2722
|
-
value: draft.maxOutputTokens,
|
|
2723
|
-
onChange: (value) => update({ maxOutputTokens: value })
|
|
2724
|
-
}),
|
|
2725
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
|
|
2726
|
-
label: t("contextWindow"),
|
|
2727
|
-
type: "number",
|
|
2728
|
-
value: draft.contextWindow,
|
|
2729
|
-
onChange: (value) => update({ contextWindow: value })
|
|
2730
|
-
})
|
|
2731
|
-
]
|
|
2784
|
+
children: t("sidebar")
|
|
2732
2785
|
}),
|
|
2733
2786
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
2734
2787
|
className: "oc-row",
|
|
@@ -2737,6 +2790,10 @@ window.__ModuleLoader__.load({
|
|
|
2737
2790
|
checked: draft.showSidebarQuota,
|
|
2738
2791
|
onChange: (event) => update({ showSidebarQuota: event.target.checked })
|
|
2739
2792
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("showSidebarQuota") })]
|
|
2793
|
+
}),
|
|
2794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
2795
|
+
className: "oc-hint",
|
|
2796
|
+
children: t("showSidebarQuotaHint")
|
|
2740
2797
|
})
|
|
2741
2798
|
]
|
|
2742
2799
|
}),
|
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.
|
|
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()].
|
|
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,
|
|
@@ -4871,6 +4937,10 @@ function buildOpenCodeTuiSection(deps) {
|
|
|
4871
4937
|
{
|
|
4872
4938
|
id: "advanced",
|
|
4873
4939
|
title: "Advanced"
|
|
4940
|
+
},
|
|
4941
|
+
{
|
|
4942
|
+
id: "sidebar",
|
|
4943
|
+
title: "Sidebar"
|
|
4874
4944
|
}
|
|
4875
4945
|
],
|
|
4876
4946
|
fields: [
|
|
@@ -4957,6 +5027,18 @@ function buildOpenCodeTuiSection(deps) {
|
|
|
4957
5027
|
value: text.trim() === "true"
|
|
4958
5028
|
})
|
|
4959
5029
|
},
|
|
5030
|
+
{
|
|
5031
|
+
path: ["showSidebarQuota"],
|
|
5032
|
+
group: "sidebar",
|
|
5033
|
+
kind: "boolean",
|
|
5034
|
+
label: "Show quota card in sidebar",
|
|
5035
|
+
hint: "Card visibility updates on the next sidebar refresh.",
|
|
5036
|
+
format: (value) => String(value === void 0 ? deps.showSidebarQuota() : value === true),
|
|
5037
|
+
parse: (text) => ({
|
|
5038
|
+
kind: "set",
|
|
5039
|
+
value: text.trim() === "true"
|
|
5040
|
+
})
|
|
5041
|
+
},
|
|
4960
5042
|
{
|
|
4961
5043
|
path: ["activeAccount"],
|
|
4962
5044
|
group: "advanced",
|
|
@@ -5028,7 +5110,8 @@ function applyOpenCodeTuiSettings(ctx, deps) {
|
|
|
5028
5110
|
activeAccount: deps.activeAccount(),
|
|
5029
5111
|
filterModelsByPlan: deps.filterModelsByPlan(),
|
|
5030
5112
|
quotaFallbackModel: deps.quotaFallbackModel(),
|
|
5031
|
-
stickySession: deps.stickySession()
|
|
5113
|
+
stickySession: deps.stickySession(),
|
|
5114
|
+
showSidebarQuota: deps.showSidebarQuota()
|
|
5032
5115
|
});
|
|
5033
5116
|
let lastSignature = signature();
|
|
5034
5117
|
const refresh = () => {
|
|
@@ -5209,7 +5292,8 @@ function apply(ctx, config) {
|
|
|
5209
5292
|
activeAccount: () => current().activeAccount,
|
|
5210
5293
|
filterModelsByPlan: () => current().filterModelsByPlan,
|
|
5211
5294
|
quotaFallbackModel: () => current().quotaFallbackModel,
|
|
5212
|
-
stickySession: () => current().stickySession
|
|
5295
|
+
stickySession: () => current().stickySession,
|
|
5296
|
+
showSidebarQuota: () => current().showSidebarQuota
|
|
5213
5297
|
});
|
|
5214
5298
|
});
|
|
5215
5299
|
ctx.inject(["webServer"], (webCtx) => {
|