@goodandready/dsh-opencode-provider 0.5.20 → 0.5.22

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.22
4
+
5
+ - **Proxy transport**: one live `proxyUrl` setting (HTTP, HTTPS or SOCKS5) now covers inference, catalog discovery and usage checks. Dispatchers are shared by address and released when the plugin unloads; proxy credentials are rejected so the settings API never returns a secret.
6
+ - **Account pool parity**: up to 20 accounts, drag-and-drop priority in Settings, promotion of the account that served the last request (persisted alongside sticky state), and a 5-minute invalid-credential cooldown matching the reference plugin.
7
+ - **Per-model overrides**: per-model context window and output caps are now editable per model; `0` omits the output cap. The output override also applies to request construction, not only the model descriptor.
8
+ - **Tool-call validation**: numeric/null ids and names from OpenAI Chat, Anthropic, Responses and Google streams are normalized to string fallbacks before emitting `tool-call-delta`; a repeated `id:null` no longer overwrites the id from the first fragment. Six regression tests cover streaming, continuation, non-SSE and all transports (GitHub #2).
9
+ - **Docs/TUI**: README/RU/ZH mirrors and the TUI connection section expose the same fields; 124 tests.
10
+ - **Not added**: a separate keyless Free route. A live probe returned HTTP 429 `FreeUsageLimitError`; the existing keyed Console/Zen route remains the reliable path.
11
+
12
+ ## 0.5.21
13
+
14
+ - **Analytics completeness**: projection scans above 5000 files now select the newest files by metadata instead of an arbitrary readdir page; file-limit and unreadable-projection cases set `AnalyticsReport.partial`, surfaced in the dashboard.
15
+ - 113 tests.
16
+
3
17
  ## 0.5.20
4
18
 
5
19
  - **CI/reproducibility**: regenerated `package-lock.json` with the normal npm resolver; clean `npm ci --ignore-scripts` now succeeds and `npm run check` passes.
package/README.md CHANGED
@@ -91,16 +91,18 @@ graph LR
91
91
  - **Replay of provider metadata** where the endpoint requires it:
92
92
  - Anthropic thinking signatures.
93
93
  - Gemini `thoughtSignature` on historical function calls.
94
- - **Multi-account rotation** before a stream starts: 401/403/429 and quota/usage-limit bodies (400/402/403/429). An invalid key cools down for 24h, a quota rejection for 10 minutes or `Retry-After`, a rate limit for 60s or `Retry-After`. A request already streaming is never replayed on another account; the next request uses the next usable key.
94
+ - **Multi-account rotation** before a stream starts: up to 20 accounts in drag-and-drop priority order, 401/403/429 and quota/usage-limit bodies (400/402/403/429). An invalid key cools down for 5 minutes, a quota rejection for 10 minutes or `Retry-After`, a rate limit for 60s or `Retry-After`. The account that served the last request is promoted for following conversations and survives restarts. A request already streaming is never replayed on another account; the next request uses the next usable key.
95
95
  - **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.
96
96
  - **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.
97
97
  - **Model → account rules**: first matching glob pins a preferred account for that model; the other accounts remain fallbacks if it fails or is cooling.
98
+ - **Per-model overrides**: context window and output cap can be changed per model in Settings; `0` omits the output cap for that model.
98
99
  - **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.
99
100
  - **Stable `x-opencode-session`** derived from the durable session id, so retries and every turn of a conversation reuse the same routing key.
100
101
  - **Live catalog** from `/v1/models` with a generated `models.dev` fallback.
102
+ - **Proxy support**: one live `proxyUrl` setting routes inference, model discovery and usage checks through HTTP, HTTPS or SOCKS5; the dispatcher is reused until the address changes.
101
103
  - **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.
102
104
  - **`/opencode` command** for status, usage, accounts, models, rules, and `test [model]`.
103
- - **TUI settings section** for the terminal profile (`/settings → OpenCode`) with the key, endpoints, allowlist, active account, fallback model, sticky routing and timeouts.
105
+ - **TUI settings section** for the terminal profile (`/settings → OpenCode`) with the key, endpoints, proxy, allowlist, active account, fallback model, sticky routing and timeouts.
104
106
  - **Quota fallback**: choose a free model in Settings → OpenCode; a Go quota rejection retries once through Console/Zen.
105
107
  - **Sticky account**: the last successful account is preferred for the same conversation and survives restarts via a local `sticky.json` (hashed session ids and key fingerprints only); explicit model rules and pinned accounts win.
106
108
  - **Routing trace** in the quota panel and per-account heatmap filter.
@@ -130,12 +132,12 @@ npm pack
130
132
  Install the **packed tarball** into a profile:
131
133
 
132
134
  ```bash
133
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.20.tgz
135
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.22.tgz
134
136
  # DSH Desktop:
135
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.20.tgz
137
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.22.tgz
136
138
 
137
139
  # Or install the published package:
138
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.20
140
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.22
139
141
  ```
140
142
 
141
143
  Restart dsh (or DSH Desktop), then pick **OpenCode (plugin)** or **OpenCode Go (plugin)** in the model picker.
package/README.ru.md CHANGED
@@ -93,13 +93,14 @@ graph LR
93
93
  - Google `thoughtSignature` в истории вызовов функций.
94
94
  - **Пул аккаунтов с интеллектуальной авто-ротацией** до старта генерации:
95
95
  - Автоматический переход на следующий аккаунт при 401/403/429 или ответах о превышении квот (400/402/403/429).
96
- - Недействительный ключ охлаждается 24 часа, превышение квоты — 10 минут (или по заголовку `Retry-After`), rate limit — 60 секунд (или по `Retry-After`).
96
+ - До 20 аккаунтов с drag-and-drop приоритетом. Недействительный ключ охлаждается 5 минут, превышение квоты — 10 минут (или по заголовку `Retry-After`), rate limit — 60 секунд (или по `Retry-After`). Аккаунт, обслуживший последний запрос, поднимается первым для следующих диалогов и сохраняется после рестарта.
97
97
  - Если запрос уже начал стриминг, он не переключается посреди ответа; следующий запрос берёт следующий готовый ключ.
98
98
  - **Sidebar-карточка квот**: отдельный переключатель в разделе **Sidebar** (и в TUI); после сохранения карточка появляется/исчезает сразу, без ожидания 60-секундного polling.
99
99
  - **Квоты по каждому аккаунту**: отображение индивидуального расхода за 5 часов, неделю и месяц; активный аккаунт управляет сайдбаром и панелью.
100
100
  - **Runway по каждому аккаунту**: оценка времени до исчерпания по среднему темпу каждого окна; отмечается первое окно, которое закончится раньше сброса. Оценка приблизительная: API отдаёт только текущий процент и время сброса.
101
101
  - **Онбординг аккаунтов**: **Import all from auth.json** переносит все ключи OpenCode в отдельные credential ref и добавляет аккаунты; **Check all accounts** обновляет квоты и health по каждому аккаунту.
102
102
  - **Правила «модель ➔ аккаунт»**: сопоставление по glob-паттернам закрепляет предпочтительный аккаунт за моделью; остальные аккаунты остаются в резерве.
103
+ - **Per-model overrides**: контекст и лимит вывода настраиваются для каждой модели; `0` убирает лимит вывода.
103
104
  - **Квотный fallback**: выберите бесплатную модель в настройках; при ошибке квоты Go тот же запрос один раз повторяется через Console/Zen. Если fallback тоже не сработал, возвращается исходная ошибка квоты.
104
105
  - **Липкий аккаунт**: последний успешный аккаунт предпочитается в том же диалоге и сохраняется между перезапусками в локальном `sticky.json` (только хешированные session id и fingerprint ключей); явные правила модели, закреплённый аккаунт и квотный fallback имеют приоритет.
105
106
  - **Трассировка маршрутизации**: панель квот показывает последние решения (`rule`, `sticky`, `rotation`, `quota-fallback`) без промптов и ключей.
@@ -107,6 +108,7 @@ graph LR
107
108
  - **Почасовой Heatmap запросов**: сетка «день недели × час» в локальном времени плюс суточная полоса агрегации. Фильтры переключают отображение между всеми запросами, конкретной моделью и аккаунтом. Лог `~/.dsh/storages/dsh-opencode/requests.jsonl` хранит только метаданные `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` (промпты и ответы не сохраняются).
108
109
  - **Стабильный заголовок `x-opencode-session`**: вычисляется из устойчивого ID сессии DSH, обеспечивая одинаковый ключ маршрутизации при повторах и диалоге.
109
110
  - **Живой каталог моделей**: автоматическое чтение `/v1/models` с резервным fallback из `models.dev`.
111
+ - **Proxy**: live-параметр `proxyUrl` направляет инференс, каталог и проверку квот через HTTP, HTTPS или SOCKS5; dispatcher переиспользуется до смены адреса.
110
112
  - **Оценки лимитов подписки OpenCode Go**: долларовый лимит и расчетное число запросов для окон 5h, недели и месяца для планов Go и Go Plus.
111
113
  - **Команда `/opencode`**: быстрый просмотр статуса, квот, аккаунтов, моделей, правил и запуск теста связи `test [model]`.
112
114
  - **Настройки в терминале (TUI)**: раздел `/settings → OpenCode` для профиля terminal с ключом, эндпоинтами, allowlist, активным аккаунтом, fallback-моделью, sticky-маршрутизацией и таймаутами.
@@ -129,12 +131,12 @@ npm pack
129
131
  Установка из **упакованного tarball**:
130
132
 
131
133
  ```bash
132
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.20.tgz
134
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.22.tgz
133
135
  # DSH Desktop:
134
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.20.tgz
136
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.22.tgz
135
137
 
136
138
  # Либо установка опубликованного пакета из npm:
137
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.20
139
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.22
138
140
  ```
139
141
 
140
142
  После установки перезапустите dsh (или DSH Desktop) и выберите **OpenCode (plugin)** или **OpenCode Go (plugin)** в списке моделей.
package/README.zh.md CHANGED
@@ -92,12 +92,13 @@ graph LR
92
92
  - Google 历史函数调用中的 `thoughtSignature`。
93
93
  - **多账户智能无缝轮换**:
94
94
  - 在生成流开始前,遇到 401/403/429 或配额用尽响应(400/402/403/429)自动重试下一个账户。
95
- - 无效密钥冷却 24 小时,超额冷却 10 分钟(或遵循 `Retry-After`),速率限制冷却 60 秒。
95
+ - 最多 20 个账户,支持拖拽调整优先级。无效密钥冷却 5 分钟,超额冷却 10 分钟(或遵循 `Retry-After`),速率限制冷却 60 秒。上次成功服务的账户会提升到首位并在重启后保留。
96
96
  - 正在流式传输中的请求不会被打断,下一个请求会自动使用下一个可用密钥。
97
97
  - **侧边栏配额卡片**:独立 **Sidebar** 开关(TUI 也可设置);保存后立即显示/隐藏,无需等待 60 秒轮询。
98
98
  - **多账户额度与健康监控**:每行独立展示 5 小时、每周与每月使用情况,活跃账户驱动侧边栏与配额面板。
99
99
  - **每账户预计可用时长**:按各窗口的平均消耗速度估算耗尽时间,并标记在重置前最先耗尽的窗口;API 仅提供当前百分比与重置时间,因此为近似值。
100
100
  - **模型 ➔ 账户路由规则**:支持 Glob 通配符(如 `*free*`、`deepseek-*`)将模型优先绑定至特定账户,其他账户保留为备用。
101
+ - **按模型覆盖**:可在设置中为每个模型修改上下文窗口和输出上限;`0` 表示该模型省略输出上限。
101
102
  - **配额回退**:在设置中选择免费模型;Go 返回配额错误时,同一请求通过 Console/Zen 重试一次。若回退也失败,返回原始配额错误。
102
103
  - **会话粘性账户**:同一会话优先使用上次成功的账户,并通过本地 `sticky.json` 跨重启保留(仅哈希后的 session id 和密钥指纹);显式模型规则、固定账户和配额回退优先。
103
104
  - **路由追踪**:配额面板显示最近的账户/模型决策(`rule`、`sticky`、`rotation`、`quota-fallback`),不记录提示词或密钥。
@@ -105,6 +106,7 @@ graph LR
105
106
  - **本地时间请求热力图**:星期×小时交互式热力图,排除系统心跳与重复记录,可按模型或账户过滤,仅保存 `{at, provider, model, purpose, account, reason, ok, latencyMs, ttftMs, error}` 元数据,不保存任何提示词与回复。
106
107
  - **稳定的 `x-opencode-session` 标头**:根据 DSH 会话 ID 派生,确保重试与连续对话复用相同的网关路由键。
107
108
  - **实时模型目录**:自动从 `/v1/models` 同步,并以 `models.dev` 生成文件作为回退。
109
+ - **代理**:一个实时 `proxyUrl` 设置即可让推理、模型目录和额度检查走 HTTP、HTTPS 或 SOCKS5;地址变化前复用 dispatcher。
108
110
  - **Go 订阅额度估算**:每月美元额度以及 5 小时、每周和每月的估算请求次数。
109
111
  - **`/opencode` 斜杠指令**:快速查询状态、配额、账户、模型及执行联通性测试。
110
112
  - **TUI 设置**:`/settings → OpenCode` 支持密钥、端点、allowlist、活动账户、回退模型、粘性路由和超时。
@@ -124,12 +126,12 @@ npm pack
124
126
  通过**打包文件 (.tgz)**安装:
125
127
 
126
128
  ```bash
127
- dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.20.tgz
129
+ dsh plugin --profile web add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.22.tgz
128
130
  # DSH 桌面端:
129
- dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.20.tgz
131
+ dsh plugin --profile desktop add /absolute/path/to/goodandready-dsh-opencode-provider-0.5.22.tgz
130
132
 
131
133
  # 或从 npm 安装已发布版本:
132
- dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.20
134
+ dsh plugin --profile web add @goodandready/dsh-opencode-provider@0.5.22
133
135
  ```
134
136
 
135
137
  安装完成后重启 dsh 或 DSH 桌面端,在模型选择器中选择 **OpenCode (plugin)** 或 **OpenCode Go (plugin)** 即可。
package/lib/client.js CHANGED
@@ -53,7 +53,7 @@ window.__ModuleLoader__.load({
53
53
  markInvalid: "Invalid key",
54
54
  markTransport: "Transport cooldown",
55
55
  accounts: "Accounts",
56
- accountsHint: "The first usable account serves requests; a 401/403/429 or an exhausted quota before the stream starts switches to the next one.",
56
+ accountsHint: "The first usable account serves requests; a 401/403/429 or an exhausted quota before the stream starts switches to the next one. Drag the handle to change priority.",
57
57
  accountLabel: "Name",
58
58
  accountRef: "Credential ref",
59
59
  accountSource: "Source",
@@ -62,6 +62,8 @@ window.__ModuleLoader__.load({
62
62
  checkAllAccounts: "Check all accounts",
63
63
  importedAll: "Imported all auth.json keys; review the accounts and save.",
64
64
  accountsImported: "Imported keys",
65
+ dragAccount: "Drag to reorder accounts",
66
+ accountLimit: "At most 20 accounts are allowed.",
65
67
  removeAccount: "Remove",
66
68
  pinAccount: "Pin",
67
69
  activeAccount: "Pinned account",
@@ -138,6 +140,7 @@ window.__ModuleLoader__.load({
138
140
  heatmapByAccount: "Account",
139
141
  heatmapAllAccounts: "All accounts",
140
142
  analyticsHint: "Aggregated from local session metadata by last activity. A session that switched models is counted under its last model; failures are not exposed by the projection. Only plugin routes are counted; legacy opencode sessions belong to the built-in catalog.",
143
+ analyticsPartial: "This report is partial: the projection scan hit its file limit or skipped unreadable files.",
141
144
  analyticsEmpty: "No plugin sessions in this window. Legacy opencode sessions belong to the built-in catalog.",
142
145
  showAll: "Show all",
143
146
  modelSearch: "Search models…",
@@ -171,7 +174,11 @@ window.__ModuleLoader__.load({
171
174
  usageUrl: "Usage endpoint",
172
175
  requestTimeoutMs: "Header timeout (ms)",
173
176
  streamIdleTimeoutMs: "Stream idle timeout (ms)",
177
+ proxyUrl: "Proxy URL",
178
+ proxyUrlHint: "HTTP, HTTPS or SOCKS5 proxy for inference, catalog and usage. Empty disables it. Credentials are not supported.",
174
179
  maxOutputTokens: "Output limit (empty = model default, 0 = omit)",
180
+ contextOverrideShort: "Context",
181
+ maxOutputOverrideShort: "Max output",
175
182
  contextWindow: "Context override",
176
183
  providerMigrationNotice: "Provider ids changed in 0.5.7: use OpenCode (plugin) / OpenCode Go (plugin). Sessions recorded with opencode/opencode-go stay on the built-in pi-ai routes; legacy traffic is excluded from plugin analytics.",
177
184
  sidebar: "Sidebar",
@@ -183,6 +190,7 @@ window.__ModuleLoader__.load({
183
190
  routingRule: "Model rule",
184
191
  routingSticky: "Sticky session",
185
192
  routingPinned: "Pinned account",
193
+ routingPromoted: "Last successful",
186
194
  routingDefault: "Automatic",
187
195
  routingRotation: "Rotation",
188
196
  routingFallback: "Quota fallback",
@@ -258,7 +266,7 @@ window.__ModuleLoader__.load({
258
266
  markInvalid: "密钥无效",
259
267
  markTransport: "传输冷却",
260
268
  accounts: "账户",
261
- accountsHint: "第一个可用账户处理请求;流开始前遇到 401/403/429 或配额耗尽时切换到下一个账户。",
269
+ accountsHint: "第一个可用账户处理请求;流开始前遇到 401/403/429 或配额耗尽时切换到下一个账户。拖动左侧手柄可调整优先级。",
262
270
  accountLabel: "名称",
263
271
  accountRef: "凭据引用",
264
272
  accountSource: "来源",
@@ -267,6 +275,8 @@ window.__ModuleLoader__.load({
267
275
  checkAllAccounts: "检查全部账户",
268
276
  importedAll: "已导入 auth.json 全部密钥;请检查账户并保存。",
269
277
  accountsImported: "已导入密钥",
278
+ dragAccount: "拖动以调整账户顺序",
279
+ accountLimit: "最多允许 20 个账户。",
270
280
  removeAccount: "移除",
271
281
  pinAccount: "固定",
272
282
  activeAccount: "固定账户",
@@ -343,6 +353,7 @@ window.__ModuleLoader__.load({
343
353
  heatmapByAccount: "账户",
344
354
  heatmapAllAccounts: "全部账户",
345
355
  analyticsHint: "按最后活动时间聚合本地会话元数据。切换过模型的会话计入最后使用的模型;本地投影不暴露失败。仅统计插件路由;旧 opencode 会话属于内置目录。",
356
+ analyticsPartial: "此报告不完整:投影扫描达到文件上限或跳过了不可读文件。",
346
357
  analyticsEmpty: "此窗口内没有插件会话。旧 opencode 会话属于内置目录。",
347
358
  showAll: "显示全部",
348
359
  modelSearch: "搜索模型…",
@@ -376,7 +387,11 @@ window.__ModuleLoader__.load({
376
387
  usageUrl: "用量端点",
377
388
  requestTimeoutMs: "请求头超时(毫秒)",
378
389
  streamIdleTimeoutMs: "流空闲超时(毫秒)",
390
+ proxyUrl: "代理地址",
391
+ proxyUrlHint: "用于推理、目录和用量的 HTTP、HTTPS 或 SOCKS5 代理。留空则禁用。不支持代理认证。",
379
392
  maxOutputTokens: "输出上限(空 = 模型默认,0 = 省略)",
393
+ contextOverrideShort: "上下文",
394
+ maxOutputOverrideShort: "最大输出",
380
395
  contextWindow: "上下文覆盖",
381
396
  providerMigrationNotice: "0.5.7 起 provider id 已更改:请选择 OpenCode (plugin) / OpenCode Go (plugin)。使用 opencode/opencode-go 记录的历史会话仍使用内置 pi-ai 路由,旧流量不计入插件分析。",
382
397
  sidebar: "侧边栏",
@@ -388,6 +403,7 @@ window.__ModuleLoader__.load({
388
403
  routingRule: "模型规则",
389
404
  routingSticky: "会话粘性",
390
405
  routingPinned: "固定账户",
406
+ routingPromoted: "上次成功",
391
407
  routingDefault: "自动",
392
408
  routingRotation: "轮换",
393
409
  routingFallback: "配额回退",
@@ -923,6 +939,10 @@ window.__ModuleLoader__.load({
923
939
  })
924
940
  ]
925
941
  }),
942
+ analytics.partial === true ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
943
+ className: "oc-hint",
944
+ children: t("analyticsPartial")
945
+ }) : null,
926
946
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
927
947
  className: "oc-hint",
928
948
  children: t("analyticsHint")
@@ -1154,6 +1174,7 @@ window.__ModuleLoader__.load({
1154
1174
  if (reason === "rule") return t("routingRule");
1155
1175
  if (reason === "sticky") return t("routingSticky");
1156
1176
  if (reason === "pinned") return t("routingPinned");
1177
+ if (reason === "promoted") return t("routingPromoted");
1157
1178
  if (reason === "rotation") return t("routingRotation");
1158
1179
  if (reason === "quota-fallback") return t("routingFallback");
1159
1180
  return t("routingDefault");
@@ -1622,8 +1643,11 @@ window.__ModuleLoader__.load({
1622
1643
  openCodeAuthEntry: state.config.openCodeAuthEntry,
1623
1644
  requestTimeoutMs: String(state.config.requestTimeoutMs),
1624
1645
  streamIdleTimeoutMs: String(state.config.streamIdleTimeoutMs),
1646
+ proxyUrl: state.config.proxyUrl,
1625
1647
  maxOutputTokens: state.config.maxOutputTokens === null ? "" : String(state.config.maxOutputTokens),
1626
1648
  contextWindow: state.config.contextWindow === null ? "" : String(state.config.contextWindow),
1649
+ contextWindowOverrides: Object.fromEntries(Object.entries(state.config.contextWindowOverrides).map(([id, value]) => [id, String(value)])),
1650
+ maxOutputTokensOverrides: Object.fromEntries(Object.entries(state.config.maxOutputTokensOverrides).map(([id, value]) => [id, String(value)])),
1627
1651
  showSidebarQuota: state.config.showSidebarQuota,
1628
1652
  activeAccount: state.config.activeAccount,
1629
1653
  accounts: state.config.accounts.map((account) => ({ ...account })),
@@ -1643,6 +1667,17 @@ window.__ModuleLoader__.load({
1643
1667
  if (!Number.isInteger(value) || value < 0) throw new Error(`Invalid number: ${text}`);
1644
1668
  return value;
1645
1669
  }
1670
+ function numberRecordOrNull(record) {
1671
+ const out = {};
1672
+ for (const [id, text] of Object.entries(record)) {
1673
+ const trimmed = text.trim();
1674
+ if (trimmed === "") continue;
1675
+ const value = Number(trimmed);
1676
+ if (!Number.isInteger(value) || value < 0) throw new Error(`Invalid number: ${text}`);
1677
+ out[id] = value;
1678
+ }
1679
+ return out;
1680
+ }
1646
1681
  function patchOf(draft) {
1647
1682
  return {
1648
1683
  apiKeyEnv: draft.apiKeyEnv.trim(),
@@ -1653,8 +1688,11 @@ window.__ModuleLoader__.load({
1653
1688
  openCodeAuthEntry: draft.openCodeAuthEntry.trim(),
1654
1689
  requestTimeoutMs: numberOrNull(draft.requestTimeoutMs) ?? 3e5,
1655
1690
  streamIdleTimeoutMs: numberOrNull(draft.streamIdleTimeoutMs) ?? 3e5,
1691
+ proxyUrl: draft.proxyUrl.trim(),
1656
1692
  maxOutputTokens: numberOrNull(draft.maxOutputTokens),
1657
1693
  contextWindow: numberOrNull(draft.contextWindow),
1694
+ contextWindowOverrides: numberRecordOrNull(draft.contextWindowOverrides),
1695
+ maxOutputTokensOverrides: numberRecordOrNull(draft.maxOutputTokensOverrides),
1658
1696
  showSidebarQuota: draft.showSidebarQuota,
1659
1697
  activeAccount: draft.activeAccount.trim(),
1660
1698
  accounts: draft.accounts.map((account) => ({
@@ -1896,6 +1934,7 @@ window.__ModuleLoader__.load({
1896
1934
  const [testResult, setTestResult] = (0, react.useState)(null);
1897
1935
  const [importing, setImporting] = (0, react.useState)(false);
1898
1936
  const [checking, setChecking] = (0, react.useState)(null);
1937
+ const [dragFrom, setDragFrom] = (0, react.useState)(null);
1899
1938
  const load = (0, react.useCallback)(async () => {
1900
1939
  setLoading(true);
1901
1940
  try {
@@ -2033,100 +2072,136 @@ window.__ModuleLoader__.load({
2033
2072
  const hasPrice = model.cost.input > 0 || model.cost.output > 0;
2034
2073
  const zen = !model.free && model.limits === null;
2035
2074
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2036
- className: "oc-model",
2037
- children: [
2038
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2039
- type: "checkbox",
2040
- checked,
2041
- onChange: () => toggleModel(model.id)
2042
- }),
2043
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2044
- className: "oc-model-name",
2045
- title: model.name + " · " + model.description,
2046
- children: [
2047
- model.name,
2048
- model.free ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2049
- className: "oc-badge",
2050
- title: t("legendFree"),
2051
- children: [" ", t("free")]
2052
- }) : null,
2053
- !model.free && hasPrice ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2054
- className: "oc-badge",
2055
- title: t("legendPrice"),
2056
- children: [
2057
- " ",
2058
- "$",
2059
- model.cost.input,
2060
- "/",
2061
- "$",
2062
- model.cost.output
2063
- ]
2064
- }) : null,
2065
- !model.free && !hasPrice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2066
- className: "oc-badge",
2067
- title: t("legendUnknownPrice"),
2068
- children: t("priceUnknown")
2069
- }) : null,
2070
- model.limits === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2071
- className: "oc-badge",
2072
- title: describeLimitsFull(model.limits),
2073
- children: [
2074
- "Go ",
2075
- formatRequests(model.limits.go.fiveHour),
2076
- "/5h"
2077
- ]
2078
- }),
2079
- zen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2080
- className: "oc-badge",
2081
- title: t("legendZen"),
2082
- children: t("zenBadge")
2083
- }) : null,
2084
- model.input.includes("image") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2085
- className: "oc-badge",
2086
- title: t("legendImage"),
2087
- children: [" ", t("vision")]
2088
- }) : null
2089
- ]
2090
- }),
2091
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2092
- className: "oc-select",
2093
- value: draft.modelTransports[model.id] ?? "",
2094
- onChange: (event) => {
2095
- const value = event.target.value;
2096
- const modelTransports = { ...draft.modelTransports };
2097
- if (value === "") delete modelTransports[model.id];
2098
- else modelTransports[model.id] = value;
2099
- update({ modelTransports });
2100
- },
2101
- children: [
2102
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2103
- value: "",
2104
- children: [
2105
- t("transportAuto"),
2106
- " (",
2107
- model.transport,
2108
- ")"
2109
- ]
2110
- }),
2111
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2112
- value: "openai-chat",
2113
- children: "openai-chat"
2114
- }),
2115
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2116
- value: "openai-responses",
2117
- children: "openai-responses"
2118
- }),
2119
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2120
- value: "anthropic",
2121
- children: "anthropic"
2122
- }),
2123
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2124
- value: "google",
2125
- children: "google"
2126
- })
2127
- ]
2128
- })
2129
- ]
2075
+ className: "oc-model-entry",
2076
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2077
+ className: "oc-model",
2078
+ children: [
2079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2080
+ type: "checkbox",
2081
+ checked,
2082
+ onChange: () => toggleModel(model.id)
2083
+ }),
2084
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2085
+ className: "oc-model-name",
2086
+ title: model.name + " · " + model.description,
2087
+ children: [
2088
+ model.name,
2089
+ model.free ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2090
+ className: "oc-badge",
2091
+ title: t("legendFree"),
2092
+ children: [" ", t("free")]
2093
+ }) : null,
2094
+ !model.free && hasPrice ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2095
+ className: "oc-badge",
2096
+ title: t("legendPrice"),
2097
+ children: [
2098
+ " ",
2099
+ "$",
2100
+ model.cost.input,
2101
+ "/",
2102
+ "$",
2103
+ model.cost.output
2104
+ ]
2105
+ }) : null,
2106
+ !model.free && !hasPrice ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2107
+ className: "oc-badge",
2108
+ title: t("legendUnknownPrice"),
2109
+ children: t("priceUnknown")
2110
+ }) : null,
2111
+ model.limits === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2112
+ className: "oc-badge",
2113
+ title: describeLimitsFull(model.limits),
2114
+ children: [
2115
+ "Go ",
2116
+ formatRequests(model.limits.go.fiveHour),
2117
+ "/5h"
2118
+ ]
2119
+ }),
2120
+ zen ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2121
+ className: "oc-badge",
2122
+ title: t("legendZen"),
2123
+ children: t("zenBadge")
2124
+ }) : null,
2125
+ model.input.includes("image") ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2126
+ className: "oc-badge",
2127
+ title: t("legendImage"),
2128
+ children: [" ", t("vision")]
2129
+ }) : null
2130
+ ]
2131
+ }),
2132
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
2133
+ className: "oc-select",
2134
+ value: draft.modelTransports[model.id] ?? "",
2135
+ onChange: (event) => {
2136
+ const value = event.target.value;
2137
+ const modelTransports = { ...draft.modelTransports };
2138
+ if (value === "") delete modelTransports[model.id];
2139
+ else modelTransports[model.id] = value;
2140
+ update({ modelTransports });
2141
+ },
2142
+ children: [
2143
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
2144
+ value: "",
2145
+ children: [
2146
+ t("transportAuto"),
2147
+ " (",
2148
+ model.transport,
2149
+ ")"
2150
+ ]
2151
+ }),
2152
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2153
+ value: "openai-chat",
2154
+ children: "openai-chat"
2155
+ }),
2156
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2157
+ value: "openai-responses",
2158
+ children: "openai-responses"
2159
+ }),
2160
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2161
+ value: "anthropic",
2162
+ children: "anthropic"
2163
+ }),
2164
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
2165
+ value: "google",
2166
+ children: "google"
2167
+ })
2168
+ ]
2169
+ })
2170
+ ]
2171
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2172
+ className: "oc-model-overrides",
2173
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2174
+ className: "oc-model-override",
2175
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("contextOverrideShort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2176
+ className: "oc-input",
2177
+ type: "number",
2178
+ min: 1,
2179
+ placeholder: String(model.contextWindow),
2180
+ value: draft.contextWindowOverrides[model.id] ?? "",
2181
+ onChange: (event) => {
2182
+ const next = { ...draft.contextWindowOverrides };
2183
+ if (event.target.value === "") delete next[model.id];
2184
+ else next[model.id] = event.target.value;
2185
+ update({ contextWindowOverrides: next });
2186
+ }
2187
+ })]
2188
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
2189
+ className: "oc-model-override",
2190
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("maxOutputOverrideShort") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2191
+ className: "oc-input",
2192
+ type: "number",
2193
+ min: 0,
2194
+ placeholder: String(model.maxTokens),
2195
+ value: draft.maxOutputTokensOverrides[model.id] ?? "",
2196
+ onChange: (event) => {
2197
+ const next = { ...draft.maxOutputTokensOverrides };
2198
+ if (event.target.value === "") delete next[model.id];
2199
+ else next[model.id] = event.target.value;
2200
+ update({ maxOutputTokensOverrides: next });
2201
+ }
2202
+ })]
2203
+ })]
2204
+ })]
2130
2205
  }, model.id);
2131
2206
  };
2132
2207
  const renderSection = (title, hint, rows) => {
@@ -2456,10 +2531,32 @@ window.__ModuleLoader__.load({
2456
2531
  const draftKey = keyDrafts[index] ?? "";
2457
2532
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2458
2533
  className: "oc-account-block",
2534
+ onDragOver: (event) => {
2535
+ if (dragFrom !== null) event.preventDefault();
2536
+ },
2537
+ onDrop: () => {
2538
+ if (dragFrom === null || dragFrom === index) return;
2539
+ const accounts = [...draft.accounts];
2540
+ const [moved] = accounts.splice(dragFrom, 1);
2541
+ if (moved !== void 0) accounts.splice(index, 0, moved);
2542
+ update({ accounts });
2543
+ setDragFrom(null);
2544
+ },
2459
2545
  children: [
2460
2546
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2461
2547
  className: "oc-account",
2462
2548
  children: [
2549
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2550
+ className: "oc-drag",
2551
+ draggable: true,
2552
+ title: t("dragAccount"),
2553
+ onDragStart: (event) => {
2554
+ setDragFrom(index);
2555
+ event.dataTransfer.effectAllowed = "move";
2556
+ },
2557
+ onDragEnd: () => setDragFrom(null),
2558
+ children: "⋮⋮"
2559
+ }),
2463
2560
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
2464
2561
  className: "oc-input",
2465
2562
  placeholder: t("accountLabel"),
@@ -2580,12 +2677,17 @@ window.__ModuleLoader__.load({
2580
2677
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2581
2678
  type: "button",
2582
2679
  className: "oc-button",
2680
+ disabled: draft.accounts.length >= 20,
2583
2681
  onClick: () => update({ accounts: [...draft.accounts, {
2584
2682
  label: "",
2585
2683
  apiKeyEnv: ""
2586
2684
  }] }),
2587
2685
  children: t("addAccount")
2588
2686
  }),
2687
+ draft.accounts.length >= 20 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2688
+ className: "oc-hint",
2689
+ children: t("accountLimit")
2690
+ }) : null,
2589
2691
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2590
2692
  type: "button",
2591
2693
  className: "oc-button",
@@ -2938,6 +3040,12 @@ window.__ModuleLoader__.load({
2938
3040
  value: draft.streamIdleTimeoutMs,
2939
3041
  onChange: (value) => update({ streamIdleTimeoutMs: value })
2940
3042
  }),
3043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
3044
+ label: t("proxyUrl"),
3045
+ hint: t("proxyUrlHint"),
3046
+ value: draft.proxyUrl,
3047
+ onChange: (value) => update({ proxyUrl: value })
3048
+ }),
2941
3049
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Field, {
2942
3050
  label: t("maxOutputTokens"),
2943
3051
  type: "number",
@@ -3086,6 +3194,10 @@ window.__ModuleLoader__.load({
3086
3194
  .oc-bar-fill-bad { background: var(--oc-danger); opacity: 1; }
3087
3195
  .oc-models { display: flex; flex-direction: column; gap: 6px; max-height: 340px; overflow: auto; padding-right: 4px; }
3088
3196
  .oc-model { display: grid; grid-template-columns: 22px minmax(0,1fr) 120px; gap: 8px; align-items: center; font-size: 12px; padding: 3px 0; }
3197
+ .oc-model-entry { display: flex; flex-direction: column; gap: 2px; }
3198
+ .oc-model-overrides { display: flex; gap: 8px; padding-left: 30px; }
3199
+ .oc-model-override { display: flex; align-items: center; gap: 5px; font-size: 10px; color: var(--oc-text-tertiary); }
3200
+ .oc-model-override .oc-input { width: 92px; padding: 3px 6px; font-size: 11px; }
3089
3201
  .oc-model-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
3090
3202
  .oc-model-legend { display: flex; flex-wrap: wrap; gap: 6px 12px; align-items: center; color: var(--oc-text-secondary); font-size: 11px; }
3091
3203
  .oc-model-legend > span { display: inline-flex; align-items: center; gap: 5px; }
@@ -3094,8 +3206,10 @@ window.__ModuleLoader__.load({
3094
3206
  .oc-model-section-head { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; font-weight: 650; }
3095
3207
  .oc-model-section-head .oc-badge { padding: 0 6px; font-size: 10px; }
3096
3208
  .oc-model-section .oc-hint { margin: 0 0 4px; }
3097
- .oc-account { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr) auto; gap: 8px; align-items: center; }
3209
+ .oc-account { display: grid; grid-template-columns: 18px minmax(0,1fr) minmax(0,1fr) auto; gap: 8px; align-items: center; }
3098
3210
  .oc-account-block { display: flex; flex-direction: column; gap: 6px; padding: 9px; border: 1px solid var(--oc-border-soft); border-radius: 8px; background: var(--oc-fill); }
3211
+ .oc-drag { cursor: grab; color: var(--oc-text-tertiary); user-select: none; font-size: 13px; line-height: 1; }
3212
+ .oc-drag:active { cursor: grabbing; }
3099
3213
  .oc-notice { font-size: 12px; padding: 7px 10px; border-radius: 7px; background: var(--oc-accent-soft); border: 1px solid var(--oc-accent-border); }
3100
3214
  .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); }
3101
3215
  [class*="_footerActions"]:has(.oc-foot), [class*="_footerActions"]:has(.oc-railButton) { flex-direction: column; }