dsh-plugin-subscriptions 0.5.3 → 0.6.0
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/README.md +45 -6
- package/README.zh.md +43 -4
- package/lib/auth/rpc.d.ts +36 -2
- package/lib/auth/rpc.js +47 -5
- package/lib/client/ImageGenerateToolview.d.ts +1 -1
- package/lib/client/SpeedSelect.d.ts +25 -2
- package/lib/client/SpeedSelect.js +10 -6
- package/lib/client/SubscriptionsSection.d.ts +74 -0
- package/lib/client/SubscriptionsSection.js +325 -4
- package/lib/client/VideoGenerateToolview.d.ts +1 -1
- package/lib/client/index.d.ts +1 -9
- package/lib/client/index.js +7 -4
- package/lib/client/locales.d.ts +28 -0
- package/lib/client/locales.js +28 -0
- package/lib/client.js +458 -10
- package/lib/client.js.map +1 -1
- package/lib/compat.d.ts +36 -0
- package/lib/compat.js +20 -0
- package/lib/index.d.ts +5 -1
- package/lib/index.js +865 -111
- package/lib/model-defaults.d.ts +23 -0
- package/lib/model-defaults.js +237 -0
- package/lib/providers/claude.d.ts +24 -3
- package/lib/providers/claude.js +35 -24
- package/lib/providers/codex.d.ts +21 -0
- package/lib/providers/codex.js +37 -10
- package/lib/providers/common.d.ts +70 -6
- package/lib/providers/common.js +118 -19
- package/lib/providers/copilot.d.ts +10 -0
- package/lib/providers/copilot.js +21 -8
- package/lib/providers/grok.d.ts +21 -0
- package/lib/providers/grok.js +37 -7
- package/lib/providers/pool-usage.d.ts +23 -2
- package/lib/providers/pool-usage.js +70 -15
- package/lib/providers/rate-limit.d.ts +192 -0
- package/lib/providers/rate-limit.js +338 -0
- package/lib/translate/anthropic.js +5 -4
- package/lib/translate/chat-completions.js +5 -4
- package/lib/translate/responses.js +5 -4
- package/package.json +21 -21
- package/lib/providers/antigravity.d.ts +0 -90
- package/lib/providers/antigravity.js +0 -392
- package/lib/translate/antigravity.d.ts +0 -110
- package/lib/translate/antigravity.js +0 -303
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
Use your **ChatGPT (Codex)**, **Claude**,
|
|
5
|
+
Use your **ChatGPT (Codex)**, **Claude**, **Grok (X Premium)**, and **GitHub Copilot** subscriptions as LLM providers in [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) — no API keys. Codex and Grok log in via OAuth in the dsh web UI (Settings → Subscriptions), while Copilot uses the GitHub OAuth device flow; Claude imports credentials from an existing Claude Code session when there is one (macOS Keychain or `~/.claude/.credentials.json`) and otherwise falls back to the same browser OAuth flow, so the Claude Code CLI is not required. Tokens live at `~/.dsh/plugins/subscriptions/auth.json` (mode 0600) and refresh automatically.
|
|
6
6
|
|
|
7
7
|
## Demo
|
|
8
8
|
|
|
@@ -106,8 +106,8 @@ Either way, restart `dsh web` afterwards so the new version loads.
|
|
|
106
106
|
## Use
|
|
107
107
|
|
|
108
108
|
1. `dsh web`, open the printed URL.
|
|
109
|
-
2. Settings → **Subscriptions**: click **Connect** on a provider. For Claude, credentials are imported instantly if you have run `claude` and logged in at least once; without them, Claude authorizes in the browser like the others. For Codex and Grok, authorize in the opened browser tab; if
|
|
110
|
-
3. In any session, open the model picker (`/model`) and choose a model under **ChatGPT (Codex)** / **Claude (Subscription)** / **Grok (Subscription)**.
|
|
109
|
+
2. Settings → **Subscriptions**: click **Connect** on a provider. For Claude, credentials are imported instantly if you have run `claude` and logged in at least once; without them, Claude authorizes in the browser like the others. For Codex and Grok, authorize in the opened browser tab; Copilot shows a GitHub device code to enter at `github.com/login/device`; if a browser flow can't complete (headless host), expand the manual fallback and paste the callback URL or code.
|
|
110
|
+
3. In any session, open the model picker (`/model`) and choose a model under **ChatGPT (Codex)** / **Claude (Subscription)** / **Grok (Subscription)** / **GitHub Copilot**.
|
|
111
111
|
|
|
112
112
|
Not logged in? The provider stays out of the picker, and requests fail with `MISSING_CREDENTIAL` pointing at the Settings page; nothing else breaks.
|
|
113
113
|
|
|
@@ -115,14 +115,23 @@ Not logged in? The provider stays out of the picker, and requests fail with `MIS
|
|
|
115
115
|
|
|
116
116
|
Every provider accepts several accounts: once one is connected, the card grows an **Add account** button (Claude offers **Browser authorization** and **Import Claude Code** separately). Accounts are keyed by their identity (email / login) — re-logging the same account updates it in place, a different account appends. Browser authorization signs in whichever account the browser currently uses, so switch accounts there first (or use an incognito window with the manual code) to add a different one. The ★ default account serves the direct provider routes; pool routes use every account. A Claude account imported from Claude Code stays synced with the CLI's credential store; OAuth-added Claude accounts refresh standalone so several accounts never fight over the Keychain entry.
|
|
117
117
|
|
|
118
|
+
### Default reasoning effort per model
|
|
119
|
+
|
|
120
|
+
Every logged-in provider card in Settings → Subscriptions carries a collapsible **Default reasoning effort** section. It starts collapsed — the header shows how many models advertise reasoning levels and how many you have overridden — and the model list (with its live catalog lookup) loads only once you expand it, so a provider with dozens of models does not stretch the page or make it pay for a lookup nobody asked for. Expanded, each model that advertises reasoning levels gets a row whose options are the levels that provider's live catalog advertises for that exact model; past 8 such models the section also offers a name filter, and models without reasoning levels collapse into a single count line instead of one dead row each. With several accounts connected, the model list is the union across that provider's accounts, so a model any account advertises gets a row; the levels offered for it come from the first account whose catalog lists it (the ★ default account first), matching what the session picker resolves.
|
|
121
|
+
|
|
122
|
+
Pick a level to make the session model picker preselect it whenever you switch to the model — no more settling for the provider's own default (e.g. Claude shows `Default`, Codex models follow `default_reasoning_level`). Choose **Follow provider** to clear the override. The choice is stored in `~/.dsh/plugins/subscriptions/model-defaults.json` (mode 0600) and survives restarts.
|
|
123
|
+
|
|
118
124
|
## Config
|
|
119
125
|
|
|
120
126
|
```yaml
|
|
121
127
|
- id: llm-subscriptions
|
|
122
128
|
name: dsh-plugin-subscriptions
|
|
123
129
|
config:
|
|
124
|
-
providers: [codex, claude] # subset; default all
|
|
130
|
+
providers: [codex, claude] # subset; default all four
|
|
125
131
|
streamIdleTimeoutMs: 300000
|
|
132
|
+
rateLimit:
|
|
133
|
+
wait: true # wait out a closed rate-limit window (default)
|
|
134
|
+
maxWaitMs: 21600000 # ceiling on one wait; 6 h, covers a 5-hour session window
|
|
126
135
|
models: # override the discovered/built-in catalogs
|
|
127
136
|
codex:
|
|
128
137
|
- { id: gpt-5.6-sol, name: GPT-5.6 Sol, contextWindow: 272000, inputModalities: [text, image] }
|
|
@@ -145,7 +154,7 @@ When a provider has **two or more logged-in accounts**, the picker shows the **u
|
|
|
145
154
|
- **Explicit account lists (`families`).** Replace the auto member list for one catalog model (same provider only; cross-provider members are ignored). Pin `account` or omit it for the default.
|
|
146
155
|
- **Tier extras (`tiers`, optional).** Extra picker rows with heterogeneous fallbacks, listed under the first member's provider. Not created automatically.
|
|
147
156
|
|
|
148
|
-
Selection is sticky per session (prompt caches survive) with two strategies: `priority` (first healthy member wins) and `quota_aware` (the default — each member is scored by its required burn rate, `remaining quota / time until window reset`, so a window about to reset with plenty left gets spent instead of wasted; the sticky member holds until a challenger out-scores it by `switchMargin`). Members past 95% on any usage window are gated out; failures fail over before the first stream chunk with cooldowns (`retry-after
|
|
157
|
+
Selection is sticky per session (prompt caches survive) with two strategies: `priority` (first healthy member wins) and `quota_aware` (the default — each member is scored by its required burn rate, `remaining quota / time until window reset`, so a window about to reset with plenty left gets spent instead of wasted; the sticky member holds until a challenger out-scores it by `switchMargin`). Members past 95% on any usage window are gated out; failures fail over before the first stream chunk with cooldowns (`retry-after`, or the window's own disclosed reset when the provider sends one) — quota and rate-limit failures cool the whole account down (its quota is account-level; Claude's model-scoped lanes cool per member), transient server failures cool only the failing member. Copilot exposes no usage telemetry, so it scores zero and naturally serves as the fallback of last resort.
|
|
149
158
|
|
|
150
159
|
```yaml
|
|
151
160
|
- id: llm-subscriptions
|
|
@@ -167,6 +176,36 @@ Selection is sticky per session (prompt caches survive) with two strategies: `pr
|
|
|
167
176
|
- { provider: grok, model: grok-4.6 }
|
|
168
177
|
```
|
|
169
178
|
|
|
179
|
+
### Waiting out a rate-limit window
|
|
180
|
+
|
|
181
|
+
A subscription plan is rate-limit shaped by design — a 5-hour session window, a weekly one, and on some plans a per-model weekly one — so a 429 is not a dead end: the window reopens at a time the provider discloses. Each route reads that reset off its own 429 and turns it into that account's pool cooldown (see Model pools above) instead of a fixed 5-minute guess.
|
|
182
|
+
|
|
183
|
+
Only a signal that names the window which actually rejected the request is read: Anthropic's `anthropic-ratelimit-unified-reset`, the seconds Codex puts on a `usage_limit_reached` rejection, the delay xAI names in the error body, or a plain `retry-after`. The per-bucket rollover snapshots (`anthropic-ratelimit-{requests,tokens,…}-reset`, `x-codex-*-reset-after-seconds`, `x-ratelimit-reset-*`) ride every response and cannot say which bucket refused — the earliest is usually one that still had room — so a 429 carrying nothing else is logged through the plugin's warning sink, naming the headers and the head of the body, rather than parking the turn (or the pool cooldown) on a guess.
|
|
184
|
+
|
|
185
|
+
Reading is confined to a 429. Every other failure keeps its short local backoff: those same headers ride a transient 500 too, and honouring them there would hold a turn for the rest of the window over an overload that clears in a second.
|
|
186
|
+
|
|
187
|
+
With a pool, this is what actually does the waiting: a 429'd account is parked until its own disclosed reset and the request fails over to another account of the same provider immediately — no wait, no lost turn. Only once **every** account (the whole pool) is cooling down does the adapter report a `RATE_LIMIT` carrying the pool's *earliest* reset as the wait to take. With a single account (no pool, or a provider with only one login), that same disclosed reset is reported directly.
|
|
188
|
+
|
|
189
|
+
Waiting on that reported delay is executed by [`@deepseek-ai/dsh-llm-retry`](https://www.npmjs.com/package/@deepseek-ai/dsh-llm-retry), which every route's retry policy is written for: add it to the composition, or nothing waits and a closed window fails the turn as before (falling back to whichever other pool accounts are healthy, if any).
|
|
190
|
+
|
|
191
|
+
```yaml
|
|
192
|
+
- name: '@deepseek-ai/dsh-llm-retry'
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
- name: dsh-plugin-subscriptions
|
|
197
|
+
config:
|
|
198
|
+
rateLimit:
|
|
199
|
+
wait: true # default; false keeps the previous seconds-scale behaviour
|
|
200
|
+
maxWaitMs: 21600000 # 6 h — covers a 5-hour session window with slack
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
A reset further out than `maxWaitMs` — a weekly window days away, or a whole pool cooling down past it — fails the turn immediately with the reset time attached, rather than parking the session for days. `wait: false` drops back to local backoff alone.
|
|
204
|
+
|
|
205
|
+
All four routes share Claude Code's own retry shape: ten retries after the first attempt, backing off from 1 s with 20% jitter under a 60 s cap. These are consumer subscription endpoints that shed load in bursts, and the dsh-llm defaults (five retries from 500 ms to 10 s) give up after about fifteen seconds, which is short for that. A 429 that discloses no reset is now retried locally for roughly 17 minutes before the turn fails — about 5 minutes with `wait: false`, where the 60 s cap actually binds. Copilot currently uses the generic `retry-after` signal; unrecognized GitHub rate-limit headers are surfaced through the plugin warning sink for a future provider-specific reader.
|
|
206
|
+
|
|
207
|
+
One trade-off worth knowing: the delay ceiling is shared with that local backoff, so raising `maxWaitMs` also raises how long an unrelated transient failure (`TRANSPORT`, `SERVER`, `TIMEOUT`) can back off for before the finite retry budget runs out — up to 512 s on the last of the ten retries instead of the 60 s cap.
|
|
208
|
+
|
|
170
209
|
## Proxy
|
|
171
210
|
|
|
172
211
|
Every subscription request — token exchanges, model-API streams, usage lookups, model discovery, and the `x_search` / `image_generate` / `video_generate` tools — can be routed through an HTTP(S) proxy. Configure it in **Settings → Subscriptions → Proxy → Configure…**: enable the flag, enter the proxy URL (`http://127.0.0.1:7890`), optional username/password, and an optional comma-separated bypass list of hostnames that stay direct (`127.0.0.1`, `localhost`, `*.example.com`). The password is stored in `~/.dsh/plugins/subscriptions/proxy.json` (mode 0600) and is never returned to the browser. A "Test" button probes one endpoint through the current configuration and shows the HTTP status/latency.
|
|
@@ -189,7 +228,7 @@ After `pnpm build`, restart `dsh web` to pick up changes.
|
|
|
189
228
|
|
|
190
229
|
- `src/index.ts` — plugin entry: config schema, adapter registration, auth-change re-announce, RPC wiring
|
|
191
230
|
- `src/auth/` — PKCE/JWT helpers, token store, OAuth flow engine (temp loopback callback server), Claude Code credential reader (Keychain/file), `/subscriptions-auth` RPC channel
|
|
192
|
-
- `src/providers/` — per-provider OAuth constants/exchange/refresh + `LlmAdapter`s, multi-account token plumbing (`accounts.ts`),
|
|
231
|
+
- `src/providers/` — per-provider OAuth constants/exchange/refresh + `LlmAdapter`s, multi-account token plumbing (`accounts.ts`), the pool (`pool.ts` + `pool-health.ts` / `pool-usage.ts` / `pool-family.ts`), and `rate-limit.ts` (reset-instant parsing + retry policy)
|
|
193
232
|
- `src/translate/` — dsh `Message[]` ⟷ OpenAI Responses / Anthropic Messages wire formats, SSE → `StreamChunk`
|
|
194
233
|
- `src/tools/` — `x_search`, `image_generate`, and `video_generate`
|
|
195
234
|
- `src/client/` — the Settings → Subscriptions page (browser half, zh/en, theme-token aware)
|
package/README.zh.md
CHANGED
|
@@ -106,8 +106,8 @@ GitHub 安装的:重新执行一遍 `add github:V1ki/dsh-plugin-subscriptions`
|
|
|
106
106
|
## 使用
|
|
107
107
|
|
|
108
108
|
1. `dsh web`,打开打印的 URL。
|
|
109
|
-
2. **设置 → 订阅**:点对应 provider 的「连接」。若先运行过 `claude` 并登录,Claude 会即时导入凭据;没有凭据时,Claude 也和其他 provider 一样在浏览器里授权。Codex 和 Grok
|
|
110
|
-
3. 在任意会话里打开模型选择器(`/model`),选择 **ChatGPT (Codex)** / **Claude (Subscription)** / **Grok (Subscription)** 下的模型。
|
|
109
|
+
2. **设置 → 订阅**:点对应 provider 的「连接」。若先运行过 `claude` 并登录,Claude 会即时导入凭据;没有凭据时,Claude 也和其他 provider 一样在浏览器里授权。Codex 和 Grok 在打开的标签页里授权;Copilot 会显示 GitHub 设备码,需在 `github.com/login/device` 输入;无浏览器环境下可展开手动兜底,粘贴回调 URL 或授权码。
|
|
110
|
+
3. 在任意会话里打开模型选择器(`/model`),选择 **ChatGPT (Codex)** / **Claude (Subscription)** / **Grok (Subscription)** / **GitHub Copilot** 下的模型。
|
|
111
111
|
|
|
112
112
|
未登录时:该 provider 不出现在选择器里;直接请求会报 `MISSING_CREDENTIAL` 并提示去设置页登录,不影响其他功能。
|
|
113
113
|
|
|
@@ -115,14 +115,23 @@ GitHub 安装的:重新执行一遍 `add github:V1ki/dsh-plugin-subscriptions`
|
|
|
115
115
|
|
|
116
116
|
每个 provider 可以登录多个账号:连上第一个之后,卡片会出现「添加账号」按钮(Claude 拆分为「浏览器授权」和「导入 Claude Code」两种)。账号按身份(邮箱/用户名)归档——重复登录同一账号是覆盖更新,不同账号才是新增。浏览器授权以浏览器当前登录的账号为准,要添加不同账号请先在浏览器切换账号,或用无痕窗口走手动授权码。★ 默认账号服务直连路由;池路由会使用所有账号。从 Claude Code 导入的 Claude 账号会与 CLI 的凭据存储保持同步;OAuth 添加的 Claude 账号独立刷新,多个账号不会互相覆盖 Keychain。
|
|
117
117
|
|
|
118
|
+
### 按模型的默认推理档
|
|
119
|
+
|
|
120
|
+
**设置 → 订阅**里每个已登录 provider 卡片都有一个可折叠的**默认推理档**区块。默认收起,标题栏直接给出「多少个模型声明了推理档 / 已覆盖多少个」;模型列表(以及它背后的 live 目录查询)只在展开时才加载 —— 这样模型数量很多的 provider(Copilot 动辄几十个)既不会把页面撑长,也不会白跑一次目录查询。展开后,凡声明了推理档的模型各占一行,可选档位就是该 provider live 目录为这个模型声明的档位;此类模型超过 8 个时区块还会给出一个名称筛选框;没有推理档的模型不再一行一条占位,而是合并成一行计数说明。登录了多个账号时,模型列表是该 provider 各账号目录的并集 —— 任一账号声明的模型都会出现;而某个模型可选的档位取自**第一个列出它的账号**(★ 默认账号优先),与会话选择器解析到的一致。
|
|
121
|
+
|
|
122
|
+
选中某档后,会话模型选择器在切换到该模型时会自动预选该档位,不必再接受 provider 自己的默认值(例如 Claude 只显示 `Default`,Codex 模型跟随 `default_reasoning_level`)。选择「跟随服务商」可清除覆盖。配置存于 `~/.dsh/plugins/subscriptions/model-defaults.json`(权限 0600),重启后依然生效。
|
|
123
|
+
|
|
118
124
|
## 配置
|
|
119
125
|
|
|
120
126
|
```yaml
|
|
121
127
|
- id: llm-subscriptions
|
|
122
128
|
name: dsh-plugin-subscriptions
|
|
123
129
|
config:
|
|
124
|
-
providers: [codex, claude] #
|
|
130
|
+
providers: [codex, claude] # 子集;默认四个全启用
|
|
125
131
|
streamIdleTimeoutMs: 300000
|
|
132
|
+
rateLimit:
|
|
133
|
+
wait: true # 等待限流窗口重开(默认开启)
|
|
134
|
+
maxWaitMs: 21600000 # 单次等待上限;6 小时,足够覆盖 5 小时会话窗口
|
|
126
135
|
models: # 覆盖实时发现/内置目录
|
|
127
136
|
codex:
|
|
128
137
|
- { id: gpt-5.6-sol, name: GPT-5.6 Sol, contextWindow: 272000, inputModalities: [text, image] }
|
|
@@ -166,6 +175,36 @@ tools+effort 的自动改道。
|
|
|
166
175
|
- { provider: grok, model: grok-4.6 }
|
|
167
176
|
```
|
|
168
177
|
|
|
178
|
+
### 等待限流窗口
|
|
179
|
+
|
|
180
|
+
订阅套餐天然是按限流窗口计费的 —— 5 小时会话窗口、周窗口,部分套餐还有按模型的周窗口 —— 所以 429 并不是终点:窗口会在 provider 自己告知的时刻重开。每条路由从自己的 429 里读出这个时刻,把它变成该账号在模型池里的冷却时长(见上文「模型池」),而不是固定猜测的 5 分钟。
|
|
181
|
+
|
|
182
|
+
只有能指明「是哪个窗口拒绝了这次请求」的信号才会被读取:Anthropic 的 `anthropic-ratelimit-unified-reset`、Codex 在 `usage_limit_reached` 上给出的秒数、xAI 在错误体里给出的延迟,或通用的 `retry-after`。各分桶的滚动快照(`anthropic-ratelimit-{requests,tokens,…}-reset`、`x-codex-*-reset-after-seconds`、`x-ratelimit-reset-*`)每个响应上都有,说不出是哪个桶拒绝的 —— 其中最早的那个往往正是还有余量的桶 —— 所以只带这些的 429 会通过插件的告警回调打印出相关 header 与响应体开头,而不是照着猜测把本轮(或池冷却)挂起。
|
|
183
|
+
|
|
184
|
+
读取只发生在 429 上。其他失败仍走各自的短本地退避:同样这些 header 也会出现在瞬时 500 上,在那里照办等于为一次一秒就恢复的过载把本轮挂满整个窗口。
|
|
185
|
+
|
|
186
|
+
有模型池时,真正在做等待这件事的其实是账号 failover:某个账号 429 了就按它自己披露的重开时刻冷却下来,请求立刻切到同 provider 的下一个账号 —— 不等待,也不丢本轮对话。只有**整个池**(所有账号)都在冷却时,adapter 才会上报一个 `RATE_LIMIT`,携带池里**最早**的重开时刻作为应等待的时长。单账号(没配池,或该 provider 只登了一个号)时,同样的披露时刻会被直接上报。
|
|
187
|
+
|
|
188
|
+
真正执行这段等待的是 [`@deepseek-ai/dsh-llm-retry`](https://www.npmjs.com/package/@deepseek-ai/dsh-llm-retry),四条路由的重试策略都是为它写的:把它加进编排,否则不会有任何等待,关闭的窗口仍旧直接让本轮失败(如果池里还有别的健康账号,会先 failover 过去)。Copilot 当前使用通用的 `retry-after` 信号;GitHub 未识别的限流 header 会通过插件告警回调暴露出来,后续再添加 provider 专用解析器。
|
|
189
|
+
|
|
190
|
+
```yaml
|
|
191
|
+
- name: '@deepseek-ai/dsh-llm-retry'
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
- name: dsh-plugin-subscriptions
|
|
196
|
+
config:
|
|
197
|
+
rateLimit:
|
|
198
|
+
wait: true # 默认;false 恢复此前秒级的行为
|
|
199
|
+
maxWaitMs: 21600000 # 6 小时 —— 留有余量地覆盖 5 小时会话窗口
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
重开时刻超过 `maxWaitMs`(比如几天后才重置的周窗口,或者整个池的冷却时间超过这个上限)会立即失败并带上重开时刻,而不是把会话挂上好几天。`wait: false` 则只保留本地退避。
|
|
203
|
+
|
|
204
|
+
四条路由共用 Claude Code 自己的重试形状:首次尝试之后重试 10 次,从 1 秒开始退避,带 20% 抖动,上限 60 秒。这些都是面向消费者的订阅端点,过载时按突发丢流量,而 dsh-llm 默认值(5 次重试,500 毫秒到 10 秒)约 15 秒就放弃,对这种场景偏短。没有给出重开时刻的 429 现在会本地重试约 17 分钟才让本轮失败 —— `wait: false` 下约 5 分钟,那时 60 秒上限才真正生效。
|
|
205
|
+
|
|
206
|
+
一个需要知道的取舍:延迟上限与这份本地退避共用,调高 `maxWaitMs` 同时也抬高了无关瞬时失败(`TRANSPORT`、`SERVER`、`TIMEOUT`)在有限重试预算耗尽前的退避时长 —— 第 10 次重试最长会从 60 秒上限变成 512 秒。
|
|
207
|
+
|
|
169
208
|
## 代理
|
|
170
209
|
|
|
171
210
|
所有订阅相关请求 —— token 交换、模型 API 流式调用、用量查询、模型目录发现,以及 `x_search` / `image_generate` / `video_generate` 工具 —— 都可以通过 HTTP(S) 代理发出。在 **设置 → 订阅 → 代理 → 配置…** 中设置:勾选启用,填写代理地址(`http://127.0.0.1:7890`)、可选用户名/密码,以及可选的逗号分隔绕过列表(保持直连的主机名,如 `127.0.0.1`、`localhost`、`*.example.com`)。密码保存在 `~/.dsh/plugins/subscriptions/proxy.json`(权限 0600),不会回传给浏览器;「测试」按钮会用当前配置探测一次端点,显示 HTTP 状态码与耗时。
|
|
@@ -188,7 +227,7 @@ pnpm test # 编译后跑 node --test 单测
|
|
|
188
227
|
|
|
189
228
|
- `src/index.ts` —— 插件入口:配置 schema、adapter 注册、登录态变更通告、RPC 接线
|
|
190
229
|
- `src/auth/` —— PKCE/JWT 工具、token 存储、OAuth 流程引擎(临时本地回调服务)、Claude Code 凭据读取器(Keychain/文件)、`/subscriptions-auth` RPC 通道
|
|
191
|
-
- `src/providers/` —— 各 provider 的 OAuth 常量/换发/刷新 + `LlmAdapter`
|
|
230
|
+
- `src/providers/` —— 各 provider 的 OAuth 常量/换发/刷新 + `LlmAdapter` 实现,多账号 token 管理(`accounts.ts`),模型池(`pool.ts` + `pool-health.ts` / `pool-usage.ts` / `pool-family.ts`),以及 `rate-limit.ts`(限流重开时刻解析 + 重试策略)
|
|
192
231
|
- `src/translate/` —— dsh `Message[]` 与 OpenAI Responses / Anthropic Messages 格式互转,SSE → `StreamChunk`
|
|
193
232
|
- `src/tools/` —— `x_search`、`image_generate` 与 `video_generate`
|
|
194
233
|
- `src/client/` —— 设置 → 订阅页面(浏览器面,中英文,跟随明暗主题)
|
package/lib/auth/rpc.d.ts
CHANGED
|
@@ -73,6 +73,34 @@ export interface ProxyConfigController {
|
|
|
73
73
|
proxy?: ProxyDraft;
|
|
74
74
|
}): Promise<ProxyTestResult>;
|
|
75
75
|
}
|
|
76
|
+
/** One model's default-effort picker state, as rendered by the Settings page. */
|
|
77
|
+
export interface ModelDefaultView {
|
|
78
|
+
/** Wire model id. */
|
|
79
|
+
id: string;
|
|
80
|
+
/** Human-readable display name. */
|
|
81
|
+
name: string;
|
|
82
|
+
/** Advertised effort levels, in catalog order (empty when the model has no reasoning). */
|
|
83
|
+
efforts: {
|
|
84
|
+
id: string;
|
|
85
|
+
name: string;
|
|
86
|
+
}[];
|
|
87
|
+
/** The user-configured default effort, when set. */
|
|
88
|
+
configured?: string;
|
|
89
|
+
}
|
|
90
|
+
/** One provider's default-effort picker state. */
|
|
91
|
+
export interface ModelDefaultsCatalog {
|
|
92
|
+
/** The subscription provider route. */
|
|
93
|
+
provider: ProviderId;
|
|
94
|
+
/** Models the picker can configure, in catalog order. */
|
|
95
|
+
models: ModelDefaultView[];
|
|
96
|
+
}
|
|
97
|
+
/** Default-effort picker operations behind the `modelDefaults/setModelDefault` endpoints. */
|
|
98
|
+
export interface ModelDefaultsController {
|
|
99
|
+
/** Per-provider picker state for the Settings page. */
|
|
100
|
+
catalog(): Promise<ModelDefaultsCatalog[]>;
|
|
101
|
+
/** Set one model's configured default effort; undefined clears the override. */
|
|
102
|
+
set(provider: ProviderId, model: string, effort: string | undefined): Promise<void>;
|
|
103
|
+
}
|
|
76
104
|
/** Provider-agnostic auth operations the RPC handler delegates to. */
|
|
77
105
|
export interface AuthController {
|
|
78
106
|
/** Current status of one provider. */
|
|
@@ -105,10 +133,12 @@ export interface AuthController {
|
|
|
105
133
|
/**
|
|
106
134
|
* Current subscription usage of one account.
|
|
107
135
|
* @param signal - caller cancellation from the RPC transport.
|
|
136
|
+
* @param force - bypass a fresh cached snapshot for an honest re-check
|
|
137
|
+
* (the manual Refresh button); a live failure cooldown still applies.
|
|
108
138
|
* @returns `{ supported: false }` when the provider has no usage endpoint.
|
|
109
139
|
* @throws when logged out or the usage lookup fails.
|
|
110
140
|
*/
|
|
111
|
-
usage(provider: ProviderId, account: string, signal: AbortSignal): Promise<ProviderUsage>;
|
|
141
|
+
usage(provider: ProviderId, account: string, signal: AbortSignal, force?: boolean): Promise<ProviderUsage>;
|
|
112
142
|
/**
|
|
113
143
|
* Read one image attachment's bytes for inline display.
|
|
114
144
|
* @param ref - the full durable reference (`readImage` verifies against it).
|
|
@@ -127,11 +157,15 @@ export interface AuthController {
|
|
|
127
157
|
*/
|
|
128
158
|
readVideo(name: string, signal: AbortSignal): Promise<VideoBytesResult>;
|
|
129
159
|
}
|
|
160
|
+
/** Payload carried no usable provider id — an RPC client bug, not a server failure. */
|
|
161
|
+
export declare class BadRequest extends Error {
|
|
162
|
+
}
|
|
130
163
|
/**
|
|
131
164
|
* Register the `/subscriptions-auth` RPC channel when a host connection exists.
|
|
132
165
|
* @param ctx - the plugin context (headless profiles have no `connection`).
|
|
133
166
|
* @param controller - the auth operations backing the endpoints.
|
|
134
167
|
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
135
168
|
* @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
|
|
169
|
+
* @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
|
|
136
170
|
*/
|
|
137
|
-
export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, proxy?: ProxyConfigController | undefined): void;
|
|
171
|
+
export declare function registerAuthRpc(ctx: Context, controller: AuthController, speed: SpeedController, proxy?: ProxyConfigController | undefined, modelDefaults?: ModelDefaultsController | undefined): void;
|
package/lib/auth/rpc.js
CHANGED
|
@@ -13,7 +13,7 @@ const IMAGE_MEDIA_TYPES = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']
|
|
|
13
13
|
/** Bare MP4 file names the `video` endpoint accepts (no path separators). */
|
|
14
14
|
const VIDEO_NAME_PATTERN = /^[\w.-]+\.mp4$/;
|
|
15
15
|
/** Payload carried no usable provider id — an RPC client bug, not a server failure. */
|
|
16
|
-
class BadRequest extends Error {
|
|
16
|
+
export class BadRequest extends Error {
|
|
17
17
|
}
|
|
18
18
|
function ok(value) {
|
|
19
19
|
return { ok: true, value };
|
|
@@ -42,6 +42,24 @@ function readString(payload, field) {
|
|
|
42
42
|
}
|
|
43
43
|
return value;
|
|
44
44
|
}
|
|
45
|
+
/** Validate the `setModelDefault` endpoint's payload. */
|
|
46
|
+
function readModelDefaultInput(payload) {
|
|
47
|
+
const provider = readProvider(payload);
|
|
48
|
+
const model = readString(payload, 'model');
|
|
49
|
+
const record = payload;
|
|
50
|
+
let effort;
|
|
51
|
+
if (record.effort !== undefined) {
|
|
52
|
+
if (typeof record.effort !== 'string' || record.effort.length === 0) {
|
|
53
|
+
throw new BadRequest('payload.effort must be a non-empty string when present');
|
|
54
|
+
}
|
|
55
|
+
effort = record.effort;
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
provider,
|
|
59
|
+
model,
|
|
60
|
+
...(effort === undefined ? {} : { effort }),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
45
63
|
/** Validate the optional Claude login method. */
|
|
46
64
|
function readLoginMethod(payload, provider) {
|
|
47
65
|
const method = payload.method;
|
|
@@ -108,6 +126,17 @@ function readVideoName(payload) {
|
|
|
108
126
|
}
|
|
109
127
|
return name;
|
|
110
128
|
}
|
|
129
|
+
/** Validate the `usage` endpoint's optional force flag. */
|
|
130
|
+
function readForce(payload) {
|
|
131
|
+
if (typeof payload !== 'object' || payload === null)
|
|
132
|
+
return false;
|
|
133
|
+
const force = payload.force;
|
|
134
|
+
if (force === undefined)
|
|
135
|
+
return false;
|
|
136
|
+
if (typeof force !== 'boolean')
|
|
137
|
+
throw new BadRequest('payload.force must be a boolean when present');
|
|
138
|
+
return force;
|
|
139
|
+
}
|
|
111
140
|
/** Validate the session id both speed endpoints carry. */
|
|
112
141
|
function readSessionId(payload) {
|
|
113
142
|
if (typeof payload !== 'object' || payload === null)
|
|
@@ -196,7 +225,7 @@ function readProxyTestPayload(payload) {
|
|
|
196
225
|
...proxy === undefined ? {} : { proxy },
|
|
197
226
|
};
|
|
198
227
|
}
|
|
199
|
-
async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
228
|
+
async function dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal) {
|
|
200
229
|
switch (endpoint) {
|
|
201
230
|
case 'status': {
|
|
202
231
|
const entries = await Promise.all(PROVIDER_IDS.map(async (provider) => [provider, await controller.status(provider)]));
|
|
@@ -226,7 +255,7 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
226
255
|
}
|
|
227
256
|
case 'usage': {
|
|
228
257
|
const provider = readProvider(payload);
|
|
229
|
-
return ok(await controller.usage(provider, readString(payload, 'account'), signal));
|
|
258
|
+
return ok(await controller.usage(provider, readString(payload, 'account'), signal, readForce(payload)));
|
|
230
259
|
}
|
|
231
260
|
case 'image':
|
|
232
261
|
return ok(await controller.readImage(readImageRef(payload), signal));
|
|
@@ -249,6 +278,18 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
249
278
|
if (proxy === undefined)
|
|
250
279
|
throw new BadRequest('proxy configuration is unavailable');
|
|
251
280
|
return ok(await proxy.test(readProxyTestPayload(payload)));
|
|
281
|
+
case 'modelDefaults':
|
|
282
|
+
if (modelDefaults === undefined)
|
|
283
|
+
throw new BadRequest('model defaults are unavailable');
|
|
284
|
+
return ok(await modelDefaults.catalog());
|
|
285
|
+
case 'setModelDefault':
|
|
286
|
+
if (modelDefaults === undefined)
|
|
287
|
+
throw new BadRequest('model defaults are unavailable');
|
|
288
|
+
{
|
|
289
|
+
const input = readModelDefaultInput(payload);
|
|
290
|
+
await modelDefaults.set(input.provider, input.model, input.effort);
|
|
291
|
+
}
|
|
292
|
+
return ok({ ok: true });
|
|
252
293
|
default:
|
|
253
294
|
throw new BadRequest(`unknown /subscriptions-auth endpoint "${endpoint}"`);
|
|
254
295
|
}
|
|
@@ -259,8 +300,9 @@ async function dispatch(controller, speed, proxy, endpoint, payload, signal) {
|
|
|
259
300
|
* @param controller - the auth operations backing the endpoints.
|
|
260
301
|
* @param speed - the per-session speed-tier state backing the Speed toggle.
|
|
261
302
|
* @param proxy - optional proxy-config controller backing `proxyGet`/`proxySet`/`proxyTest`.
|
|
303
|
+
* @param modelDefaults - optional per-model default-effort state backing `modelDefaults`/`setModelDefault`.
|
|
262
304
|
*/
|
|
263
|
-
export function registerAuthRpc(ctx, controller, speed, proxy = undefined) {
|
|
305
|
+
export function registerAuthRpc(ctx, controller, speed, proxy = undefined, modelDefaults = undefined) {
|
|
264
306
|
// `connection` is not in this plugin's inject list (headless compositions
|
|
265
307
|
// lack it), so its startup order is unconstrained: defer registration until
|
|
266
308
|
// the service exists instead of probing once at apply time.
|
|
@@ -268,7 +310,7 @@ export function registerAuthRpc(ctx, controller, speed, proxy = undefined) {
|
|
|
268
310
|
const connection = ctx.get('connection');
|
|
269
311
|
ctx.effect(() => connection.rpc.handle(SUBSCRIPTIONS_AUTH_CHANNEL, async (endpoint, payload, signal) => {
|
|
270
312
|
try {
|
|
271
|
-
return await dispatch(controller, speed, proxy, endpoint, payload, signal);
|
|
313
|
+
return await dispatch(controller, speed, proxy, modelDefaults, endpoint, payload, signal);
|
|
272
314
|
}
|
|
273
315
|
catch (error) {
|
|
274
316
|
return failure(error);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConnectionHandle } from '@deepseek-ai/dsh-api-remotes/client';
|
|
2
|
-
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-
|
|
2
|
+
import type { ToolCallBlock } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
3
3
|
import type { ImageLoader } from './ImageGallery.js';
|
|
4
4
|
import type { SubscriptionsKey } from './locales.js';
|
|
5
5
|
/** Mirror of ui-tool's ToolCallOwnerProps (see the module header). */
|
|
@@ -12,6 +12,25 @@ export interface SpeedSelectState {
|
|
|
12
12
|
visible: boolean;
|
|
13
13
|
tier: SpeedTier;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Minimal structural face of ui-model-selection's `ctx.modelDirectories`
|
|
17
|
+
* service (the sanctioned cross-plugin channel: cordis services, not value
|
|
18
|
+
* imports — same mirroring discipline as ToolCallOwnerProps). Both dsh lines
|
|
19
|
+
* ship it with this shape; it replaces rc.2's `connection.api.sessions.models`
|
|
20
|
+
* read, which the 0.1.2-alpha removed along with the whole `.api` face.
|
|
21
|
+
*/
|
|
22
|
+
export interface ModelDirectoriesLike {
|
|
23
|
+
/** Resolve one session's shared model directory (throws for unknown sessions). */
|
|
24
|
+
directoryFor(sessionId: string): {
|
|
25
|
+
/** Load the directory; `current` is the session's effective model selection. */
|
|
26
|
+
load(): Promise<{
|
|
27
|
+
current: {
|
|
28
|
+
provider: string;
|
|
29
|
+
model: string;
|
|
30
|
+
} | null;
|
|
31
|
+
}>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
15
34
|
/** Injected dependencies of {@link SpeedSelect} (slot `inject`, session-bound). */
|
|
16
35
|
export interface SpeedSelectInjected {
|
|
17
36
|
/** Load the session's speed state; `visible` false keeps the control hidden. */
|
|
@@ -32,9 +51,13 @@ export type SpeedSelectProps = PropsRuntime<'conversation.input.right'> & Partia
|
|
|
32
51
|
* known state, so a transient failure never locks the toggle away.
|
|
33
52
|
*
|
|
34
53
|
* `sessionId` is a plain string: slot and command contexts brand it through
|
|
35
|
-
* different dsh-session copies, and only the
|
|
54
|
+
* different dsh-session copies, and only the service boundary needs one.
|
|
55
|
+
*
|
|
56
|
+
* `models` resolves lazily per call: the ui-model-selection service may
|
|
57
|
+
* register after this plugin applies, and a shell without it (no model seat
|
|
58
|
+
* at all) simply keeps the toggle hidden.
|
|
36
59
|
*/
|
|
37
|
-
export declare function createSpeedLoader(connection: ConnectionHandle, sessionId: string): SpeedSelectInjected['loadSpeed'];
|
|
60
|
+
export declare function createSpeedLoader(connection: ConnectionHandle, models: () => ModelDirectoriesLike | undefined, sessionId: string): SpeedSelectInjected['loadSpeed'];
|
|
38
61
|
/** The `setSpeed` half of the inject face: boolean outcome for the component's busy state. */
|
|
39
62
|
export declare function createSpeedSetter(connection: ConnectionHandle, sessionId: string): SpeedSelectInjected['setSpeed'];
|
|
40
63
|
/**
|
|
@@ -22,15 +22,19 @@ import { en } from './locales.js';
|
|
|
22
22
|
* known state, so a transient failure never locks the toggle away.
|
|
23
23
|
*
|
|
24
24
|
* `sessionId` is a plain string: slot and command contexts brand it through
|
|
25
|
-
* different dsh-session copies, and only the
|
|
25
|
+
* different dsh-session copies, and only the service boundary needs one.
|
|
26
|
+
*
|
|
27
|
+
* `models` resolves lazily per call: the ui-model-selection service may
|
|
28
|
+
* register after this plugin applies, and a shell without it (no model seat
|
|
29
|
+
* at all) simply keeps the toggle hidden.
|
|
26
30
|
*/
|
|
27
|
-
export function createSpeedLoader(connection, sessionId) {
|
|
31
|
+
export function createSpeedLoader(connection, models, sessionId) {
|
|
28
32
|
return async () => {
|
|
29
33
|
const state = await callSubscriptionsAuth(connection.rpc, 'speed', { sessionId });
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
const current =
|
|
34
|
+
const directories = models();
|
|
35
|
+
if (directories === undefined)
|
|
36
|
+
return { visible: false, tier: state.tier };
|
|
37
|
+
const { current } = await directories.directoryFor(sessionId).load();
|
|
34
38
|
const visible = current !== null && current.provider === 'codex'
|
|
35
39
|
&& state.fastModels.includes(current.model);
|
|
36
40
|
return { visible, tier: state.tier };
|
|
@@ -29,6 +29,23 @@ export interface ProviderUsage {
|
|
|
29
29
|
windows?: UsageWindow[];
|
|
30
30
|
plan?: string;
|
|
31
31
|
}
|
|
32
|
+
/** One model's default-effort picker state as answered by `modelDefaults`. */
|
|
33
|
+
export interface ModelDefaultView {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
/** Advertised effort levels, in catalog order (empty when the model has no reasoning). */
|
|
37
|
+
efforts: {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
}[];
|
|
41
|
+
/** The user-configured default effort, when set. */
|
|
42
|
+
configured?: string;
|
|
43
|
+
}
|
|
44
|
+
/** `modelDefaults` endpoint value: one provider's picker state. */
|
|
45
|
+
export interface ModelDefaultsCatalog {
|
|
46
|
+
provider: SubscriptionProvider;
|
|
47
|
+
models: ModelDefaultView[];
|
|
48
|
+
}
|
|
32
49
|
/** `proxyGet` endpoint value: the node half owns this shape (no secrets). */
|
|
33
50
|
export interface ProxyConfigView {
|
|
34
51
|
enabled: boolean;
|
|
@@ -67,6 +84,63 @@ export type SubscriptionsSectionProps = Partial<SubscriptionsSectionInjected>;
|
|
|
67
84
|
* @returns the success value, cast by the caller to the endpoint's shape.
|
|
68
85
|
*/
|
|
69
86
|
export declare function callSubscriptionsAuth<T>(rpc: ConnectionHandle['rpc'], endpoint: string, payload: unknown): Promise<T>;
|
|
87
|
+
/** What one provider's collapsible default-effort section renders. */
|
|
88
|
+
export interface ModelDefaultsView {
|
|
89
|
+
/** Models with reasoning levels, after the name filter — one row each. */
|
|
90
|
+
shown: ModelDefaultView[];
|
|
91
|
+
/** Models with reasoning levels before filtering (the header total). */
|
|
92
|
+
total: number;
|
|
93
|
+
/** How many of those carry a user override (the header count). */
|
|
94
|
+
overridden: number;
|
|
95
|
+
/** Models without reasoning levels: one count line, never a row each. */
|
|
96
|
+
withoutEfforts: number;
|
|
97
|
+
/** Whether the list is long enough to deserve a filter box. */
|
|
98
|
+
showFilter: boolean;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Derive one provider's default-effort section from its catalog and filter.
|
|
102
|
+
* Pure so the collapsed-header counts and the filter stay testable without a
|
|
103
|
+
* DOM: rows come only from models that advertise levels, the count of the rest
|
|
104
|
+
* rides as one line, and the filter matches display name or model id.
|
|
105
|
+
* @param models - the provider's catalog models, or undefined while loading.
|
|
106
|
+
* @param filter - the raw filter input (trimmed and lowercased here).
|
|
107
|
+
* @returns the section's rows and header counts.
|
|
108
|
+
*/
|
|
109
|
+
export declare function deriveModelDefaultsView(models: readonly ModelDefaultView[] | undefined, filter: string): ModelDefaultsView;
|
|
110
|
+
/** Inputs of the default-effort fetch decision (see {@link shouldFetchModelDefaults}). */
|
|
111
|
+
export interface ModelDefaultsFetchInput {
|
|
112
|
+
/** Providers that currently have at least one account. */
|
|
113
|
+
loggedIn: readonly SubscriptionProvider[];
|
|
114
|
+
/** Providers whose disclosure is open. */
|
|
115
|
+
open: readonly SubscriptionProvider[];
|
|
116
|
+
/** The account signature the last completed fetch was answered for. */
|
|
117
|
+
loadedFor: string | undefined;
|
|
118
|
+
/** The account signature of the current status snapshot. */
|
|
119
|
+
signature: string;
|
|
120
|
+
/** Whether the last attempt failed (a failure latches until Retry). */
|
|
121
|
+
failed: boolean;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether the default-effort catalog needs (re)fetching.
|
|
125
|
+
*
|
|
126
|
+
* Fetching is gated on an *attempt* signature rather than on the payload
|
|
127
|
+
* being empty: an empty answer is a legitimate result (a narrowed
|
|
128
|
+
* `config.providers`, or a catalog that is momentarily unavailable), and
|
|
129
|
+
* treating it as "not loaded yet" re-ran this effect forever. The signature
|
|
130
|
+
* also covers the accounts, so logging a second provider in refetches
|
|
131
|
+
* instead of leaving that card on the previous answer.
|
|
132
|
+
* @param input - the decision inputs.
|
|
133
|
+
* @returns true when the caller should start a fetch.
|
|
134
|
+
*/
|
|
135
|
+
export declare function shouldFetchModelDefaults(input: ModelDefaultsFetchInput): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Stable signature of the accounts a catalog answer depends on. A change
|
|
138
|
+
* means a previous answer is stale (an account arrived or left), so the next
|
|
139
|
+
* open disclosure refetches.
|
|
140
|
+
* @param statuses - the per-provider status snapshot.
|
|
141
|
+
* @returns a signature string, stable across renders with equal accounts.
|
|
142
|
+
*/
|
|
143
|
+
export declare function modelDefaultsSignature(statuses: Partial<Record<SubscriptionProvider, ProviderStatus>>): string;
|
|
70
144
|
/**
|
|
71
145
|
* The Subscriptions settings page component.
|
|
72
146
|
* @param props - the slot inject face ({@link SubscriptionsSectionInjected}).
|