@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 +14 -0
- package/README.md +7 -5
- package/README.ru.md +6 -4
- package/README.zh.md +6 -4
- package/lib/client.js +211 -97
- package/lib/index.d.ts +13 -1
- package/lib/index.js +188 -30
- package/package.json +128 -125
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
- Недействительный ключ охлаждается
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
model.name,
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
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; }
|