dsh-provider-usage 0.3.10 → 0.3.12

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 CHANGED
@@ -15,15 +15,15 @@
15
15
  ## Features
16
16
 
17
17
  - **Auto-detection** — enumerates the provider routes registered in the current profile (`ctx.llm`); zero configuration for well-known routes.
18
- - **Per-kind wire adapters** — routes with no public balance/quota API (Google, Mistral, Groq, Bedrock, Azure, Qwen Token Plan, …) are listed as `unsupported` instead of being silently dropped:
18
+ - **Per-kind wire adapters** — routes with no public balance/usage API (Google, Mistral, Groq, Bedrock, Azure, Qwen Token Plan, …) are listed as `unsupported` instead of being silently dropped:
19
19
 
20
20
  | kind | routes | query | shows |
21
21
  |---|---|---|---|
22
22
  | `deepseek` | `deepseek-official`, `deepseek` | `GET {baseURL}/user/balance` | total / granted / topped-up balance |
23
23
  | `moonshot` | `moonshotai-cn`, `moonshotai` | `GET {baseURL}/users/me/balance` | available / voucher / cash balance |
24
- | `kimi-coding` | `kimi-coding` | `GET {baseURL}/v1/usages` | weekly quota + rate-limit windows, reset countdown |
24
+ | `kimi-coding` | `kimi-coding` | `GET {baseURL}/v1/usages` | weekly usage + rate-limit windows, reset countdown |
25
25
  | `openrouter` | `openrouter` | `GET {origin}/api/v1/credits` | credits used / total |
26
- | `github-copilot` | `github-copilot` | `GET api.github.com/copilot_internal/user` | plan quota snapshots (paid) or monthly quotas (free) |
26
+ | `github-copilot` | `github-copilot` | `GET api.github.com/copilot_internal/user` | plan usage snapshots (paid) or monthly usage (free) |
27
27
  | `openai-codex` | `openai-codex` | `GET {baseURL}/wham/usage` | ChatGPT subscription 5h / weekly windows + credits + spend control (**OAuth login**, not an API key — see [OpenAI Codex via OAuth](#openai-codex-via-oauth)) |
28
28
  | `openai` | `openai` | `GET {origin}/v1/organization/costs` | current-month spend (**admin key required**; a regular key fails with 403) |
29
29
  | `anthropic` | `anthropic` | `GET {baseURL}/v1/organizations/cost_report` | current-month spend (**admin key required**, `x-api-key` auth) |
@@ -33,7 +33,7 @@
33
33
  | `vercel-ai-gateway` | `vercel-ai-gateway` | `GET {baseURL}/v1/credits` | team credit balance |
34
34
  | `xai` | `xai` | `GET {baseURL}/billing/credits` | prepaid balance (USD) |
35
35
  - **Credentials stay safe** — API keys are resolved per request through the harness credentials service (environment variables / `~/.dsh/.credentials.yaml`); never cached, never written to disk. For OAuth providers (OpenAI Codex) the plugin reads the grant record the sign-in flow stored, and refreshes it transparently when it is about to expire.
36
- - **Floating ball widget** — a draggable floating ball opens the usage panel. Drop it anywhere in the viewport (position persisted); it docks by default at the bottom-left of the chat area with equal margins, and the panel-header home button sends it back. The panel scrolls when the provider list grows past its height, and its top edge is draggable to resize it taller or shorter (height persisted). The halo around the ball encodes the health of the provider **in use** — the focused session's own selection (its composer's model seat), tracked live on the client, so switching sessions re-highlights that session's provider immediately without re-selecting a model: green all good, amber some quota below 30% left or a balance below the yellow threshold, red on query failure / missing key / usage ≥90% / balance below the red threshold. Idle providers with low quota do not color the ball — switch to another provider with enough quota and the ball turns green again; the panel marks the in-use provider and still lists every provider's numbers.
36
+ - **Floating ball widget** — a draggable floating ball opens the usage panel. Drop it anywhere in the viewport (position persisted); it docks by default at the bottom-left of the chat area with equal margins, and the panel-header home button sends it back. The panel scrolls when the provider list grows past its height, and its top edge is draggable to resize it taller or shorter (height persisted). The halo around the ball encodes the health of the provider **in use** — the focused session's own selection (its composer's model seat), tracked live on the client, so switching sessions re-highlights that session's provider immediately without re-selecting a model: green all good, amber when a usage window has under 30% left or a balance below the yellow threshold, red on query failure / missing key / usage ≥90% / balance below the red threshold. Idle providers running low do not color the ball — switch to another provider with enough headroom and the ball turns green again; the panel marks the in-use provider and still lists every provider's numbers.
37
37
  - **Version badge** — the panel header shows the running plugin version next to the title, so it is obvious which release is loaded.
38
38
  - **Bilingual panel** — built-in Chinese/English UI; follows the harness language by default, with a one-click toggle in the panel header (persisted in localStorage).
39
39
  - **Configurable refresh** — adjustable in the panel (15s–30min, persisted in localStorage); the default comes from the plugin config.
@@ -50,7 +50,7 @@ The floating ball (bottom-left, with the green healthy halo) and the open usage
50
50
 
51
51
  ## OpenAI Codex via OAuth
52
52
 
53
- OpenAI Codex is a **ChatGPT-subscription** provider: it authenticates with an OAuth access token, not an API key, so there is nothing to paste into a key field. dsh itself has no OAuth *button* for this route — but this plugin can still query its quota, because it reads the OAuth grant out of the harness credential store. Set the provider up once, and the usage panel shows your real 5-hour / weekly Codex windows.
53
+ OpenAI Codex is a **ChatGPT-subscription** provider: it authenticates with an OAuth access token, not an API key, so there is nothing to paste into a key field. dsh itself has no OAuth *button* for this route — but this plugin can still query its usage, because it reads the OAuth grant out of the harness credential store. Set the provider up once, and the usage panel shows your real 5-hour / weekly Codex windows.
54
54
 
55
55
  ### 1. Make sure the route exists
56
56
 
@@ -80,7 +80,7 @@ records:
80
80
 
81
81
  > The grant must land in the harness credential store (the record above). Codex clients that keep their own credential file (e.g. `dsh-codex`'s `$DSH_HOME/.openai-codex-auth.json`, or the Codex CLI's `~/.codex/auth.json`) do not write this record, so this plugin cannot see them.
82
82
 
83
- ### 3. Watch the quota
83
+ ### 3. Watch the usage
84
84
 
85
85
  That's it. The route is auto-detected (`ctx.llm` lists `openai-codex`), and the plugin:
86
86
 
@@ -90,6 +90,37 @@ That's it. The route is auto-detected (`ctx.llm` lists `openai-codex`), and the
90
90
 
91
91
  The panel then shows your subscription's **5h limit**, **weekly** windows (used %, reset countdown) plus **credits** and **spend control** balances when the plan reports them. If the grant is missing the card reads "OAuth authorization missing (llm-pi-ai/openai-codex)" — sign in again with step 2.
92
92
 
93
+ ### 4. Troubleshooting: intermittent "Our servers are currently overloaded"
94
+
95
+ The Codex backend occasionally answers with `Codex error: Our servers are currently overloaded. Please try again later.`, and the harness fails the round with `PI_AI_ERROR`. This is **transient overload on OpenAI's side** (account, quota, and network are usually fine — `GET /wham/usage` confirms window headroom), yet the default configuration never retries, for two reasons:
96
+
97
+ 1. pi-ai's Codex client recognizes `overloaded` as retryable, but its default retry count is 0 (`dsh-llm-pi-ai` explicitly passes `maxRetries: 0`);
98
+ 2. once the error bubbles up, its text contains no `5xx` / `rate limit` / `timeout` keyword, so it is classified as the catch-all `PI_AI_ERROR` — which is **not** among `dsh-llm-retry`'s default retryable codes (`EMPTY_RESPONSE` / `RATE_LIMIT` / `SERVER` / `TIMEOUT` / `TRANSPORT`), so the round fails immediately.
99
+
100
+ Add a `retryPolicy` for this provider in `~/.dsh/settings.yaml` that includes `PI_AI_ERROR` in the retryable codes (takes effect on a new session):
101
+
102
+ ```yaml
103
+ llm-pi-ai:
104
+ providers:
105
+ openai-codex:
106
+ retryPolicy:
107
+ mode: normal
108
+ maxRetries: 10
109
+ retryableCodes:
110
+ - EMPTY_RESPONSE
111
+ - RATE_LIMIT
112
+ - SERVER
113
+ - TIMEOUT
114
+ - TRANSPORT
115
+ - PI_AI_ERROR
116
+ backoff:
117
+ initialDelayMs: 2000 # first retry delay, doubled each attempt
118
+ maxDelayMs: 60000 # per-attempt delay cap
119
+ jitterRatio: 0.2 # ±20% jitter
120
+ ```
121
+
122
+ Do not confuse this with the other, **deterministic** failure: some models are unavailable to ChatGPT-subscription accounts, and the backend answers `400 The '<model>' model is not supported when using Codex with a ChatGPT account.` (observed with e.g. `gpt-5.3-codex-spark`, `gpt-5-codex`). That is a permanent error — retrying won't help; switch to a model your account supports (e.g. the `gpt-5.4` / `gpt-5.5` / `gpt-5.6` families).
123
+
93
124
  ## Install
94
125
 
95
126
  > [!NOTE]
@@ -112,7 +143,7 @@ pnpm pack # produces dsh-provider-usage-<version>.tgz
112
143
  dsh plugin --profile web add ./dsh-provider-usage-<version>.tgz
113
144
  ```
114
145
 
115
- Install the **tarball**, not the repo directory: `dsh plugin add .` links the repo, whose own `node_modules` then shadows the harness's shared `@deepseek-ai/cordis` instance and the host half never registers (RPC 404). The link form is still handy for client-only UI iteration — the browser bundle is self-contained, so a rebuild + page refresh picks it up — but switch to the tarball (or the npm release) whenever you need the host half. If pnpm fails with `EPERM ... symlink` while replacing a linked install, delete the stale `node_modules/dsh-provider-usage` junction in the profile directory and retry.
146
+ Install the **tarball**, not the repo directory: `dsh plugin --profile web add .` links the repo, whose own `node_modules` then shadows the harness's shared `@deepseek-ai/cordis` instance and the host half never registers (RPC 404). The link form is still handy for client-only UI iteration — the browser bundle is self-contained, so a rebuild + page refresh picks it up — but switch to the tarball (or the npm release) whenever you need the host half. If pnpm fails with `EPERM ... symlink` while replacing a linked install, delete the stale `node_modules/dsh-provider-usage` junction in the profile directory and retry.
116
147
 
117
148
  Restart `dsh web` after changing the plugin set (a plugin add/remove requires a restart; afterwards, code changes only need a rebuild + re-add + page refresh).
118
149
 
@@ -122,7 +153,7 @@ Restart `dsh web` after changing the plugin set (a plugin add/remove requires a
122
153
  dsh plugin --profile web add dsh-provider-usage@latest
123
154
  ```
124
155
 
125
- Then restart `dsh web` and refresh the page. If the release you want was published very recently, your profile's supply-chain cooldown (`minimumReleaseAge`) may silently keep the older version — pin the exact version instead (`dsh plugin --profile web add dsh-provider-usage@0.3.1`) and dsh will exempt it automatically. The version badge in the panel header confirms which release is actually loaded.
156
+ Then restart `dsh web` and refresh the page. If the release you want was published very recently, your profile's supply-chain cooldown (`minimumReleaseAge`) may silently keep the older version — pin the exact version instead (`dsh plugin --profile web add dsh-provider-usage@0.3.10`) and dsh will exempt it automatically. The version badge in the panel header confirms which release is actually loaded.
126
157
 
127
158
  ## Configuration
128
159
 
@@ -136,6 +167,9 @@ Defaults work out of the box: the plugin auto-detects every provider route of th
136
167
  balanceRedThreshold: 10 # balance below this turns red (per the balance's own currency)
137
168
  balanceYellowThreshold: 30 # balance below this turns yellow (per the balance's own currency)
138
169
  autoDetect: true # enumerate provider routes from the llm registry
170
+ queryTimeoutMs: 20000 # per-attempt query timeout; each retry gets a fresh timeout
171
+ queryRetries: 2 # retries for transient errors (timeout/network/HTTP 408/425/429/5xx)
172
+ queryRetryDelayMs: 2000 # base retry delay, doubled per attempt, capped at 10s
139
173
  providers: [] # manual specs; an id matching a detected route overrides it
140
174
  ```
141
175
 
@@ -145,6 +179,9 @@ Defaults work out of the box: the plugin auto-detects every provider route of th
145
179
  | `balanceRedThreshold` | number | `10` | Balance below this amount turns red, compared in the balance's own currency |
146
180
  | `balanceYellowThreshold` | number | `30` | Balance below this amount turns yellow, compared in the balance's own currency |
147
181
  | `autoDetect` | boolean | `true` | Enumerate live provider routes from the llm registry |
182
+ | `queryTimeoutMs` | number | `20000` | Per-attempt query timeout (1000–120000); each retry gets a fresh timeout |
183
+ | `queryRetries` | number | `2` | Retries for transient errors — timeout / network / HTTP 408, 425, 429, 5xx (0–10); permanent 4xx errors fail immediately |
184
+ | `queryRetryDelayMs` | number | `2000` | Base delay between retries (100–60000), doubled per attempt, capped at 10s |
148
185
  | `providers` | array | `[]` | Manual provider specs: `{id, kind, baseURL, apiKeyEnv, displayName?, enabled?}`; `kind` is one of the adapter table above |
149
186
 
150
187
  The same fields can be hot-updated under the `provider-usage:` namespace in `~/.dsh/settings.yaml`.
@@ -163,7 +200,7 @@ config:
163
200
 
164
201
  ## How it works
165
202
 
166
- - **Host half** (`src/index.ts`): `UsageService extends TypertRemoteService` exposes `usage/list` via `@Remote('list')` (SRC mode, no codegen). Config is declared with schemastery, and `installSettingsSection` enables hot updates from settings.
203
+ - **Host half** (`src/index.ts`): `UsageService extends TypertRemoteService` exposes `usage/list` via the `Remote('list')` marker, applied without decorator syntax (SRC mode, no codegen). Config is declared with schemastery, and the settings provider's `installSection` enables hot updates from the settings document.
167
204
  - **Client half** (`src/client/`): a `window.__ModuleLoader__.load({id, factory})` bundle (built by tsdown) mounts through the `sidebar.footer.action` slot (used purely as a mount point — the trigger itself is a floating ball portaled to `document.body`) and polls `usage/list` through `ctx.connection.rpc.call('/api', 'usage/list', {args:{}})` on its own interval. The service stays stateless — every poll fetches live values.
168
205
 
169
206
  ## License
package/README_ZH.md CHANGED
@@ -10,20 +10,20 @@
10
10
 
11
11
  # dsh-provider-usage
12
12
 
13
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件:在 Web GUI 上悬浮一个可任意拖动的用量球,实时查看所有已配置 LLM provider 的账户余额与配额用量——不用再逐个登录 provider 控制台确认。
13
+ [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件:在 Web GUI 上悬浮一个可任意拖动的用量球,实时查看所有已配置 LLM provider 的账户余额与用量——不用再逐个登录 provider 控制台确认。
14
14
 
15
15
  ## 功能
16
16
 
17
17
  - **自动探测** —— 自动枚举当前 profile 中已注册的 provider 路由(`ctx.llm`),常见路由零配置。
18
- - **按 provider 类型查询额度** —— 没有公开余额/配额接口的路由(Google、Mistral、Groq、Bedrock、Azure、Qwen Token Plan 等)会在面板中标注为「不支持」,而不是被静默忽略:
18
+ - **按 provider 类型查询额度** —— 没有公开余额/用量接口的路由(Google、Mistral、Groq、Bedrock、Azure、Qwen Token Plan 等)会在面板中标注为「不支持」,而不是被静默忽略:
19
19
 
20
20
  | kind | 路由 | 查询接口 | 展示内容 |
21
21
  |---|---|---|---|
22
22
  | `deepseek` | `deepseek-official`、`deepseek` | `GET {baseURL}/user/balance` | 余额(含赠送/充值明细) |
23
23
  | `moonshot` | `moonshotai-cn`、`moonshotai` | `GET {baseURL}/users/me/balance` | 可用/代金券/现金余额 |
24
- | `kimi-coding` | `kimi-coding` | `GET {baseURL}/v1/usages` | 每周配额及各限速窗口,含重置倒计时 |
24
+ | `kimi-coding` | `kimi-coding` | `GET {baseURL}/v1/usages` | 每周用量及各限速窗口,含重置倒计时 |
25
25
  | `openrouter` | `openrouter` | `GET {origin}/api/v1/credits` | credit 已用/总额 |
26
- | `github-copilot` | `github-copilot` | `GET api.github.com/copilot_internal/user` | 付费档配额快照 / 免费档月度配额 |
26
+ | `github-copilot` | `github-copilot` | `GET api.github.com/copilot_internal/user` | 付费档用量快照 / 免费档月度用量 |
27
27
  | `openai-codex` | `openai-codex` | `GET {baseURL}/wham/usage` | ChatGPT 订阅 5h/周窗口 + credits + spend control(**OAuth 登录**,非 API key,见 [通过 OAuth 添加 OpenAI Codex](#通过-oauth-添加-openai-codex)) |
28
28
  | `openai` | `openai` | `GET {origin}/v1/organization/costs` | 当月花费(**需 Admin key**,普通 key 会 403) |
29
29
  | `anthropic` | `anthropic` | `GET {baseURL}/v1/organizations/cost_report` | 当月花费(**需 Admin key**,`x-api-key` 头) |
@@ -33,7 +33,7 @@
33
33
  | `vercel-ai-gateway` | `vercel-ai-gateway` | `GET {baseURL}/v1/credits` | 团队 credit 余额 |
34
34
  | `xai` | `xai` | `GET {baseURL}/billing/credits` | 预付余额(USD) |
35
35
  - **密钥安全** —— 通过 harness 凭据服务按次解析(环境变量 / `~/.dsh/.credentials.yaml`),不缓存、不落地。对 OAuth 类 Provider(OpenAI Codex)则直接读取登录流程存入的授权记录,并在令牌临近过期时自动刷新。
36
- - **悬浮球入口** —— 可任意拖动的悬浮球点击弹出用量面板,位置持久化;默认停靠在主对话区域左下角(左边距 = 底边距),面板头部的归位按钮一键回到默认位置;Provider 列表超过面板高度时自动滚动,面板**顶部边缘可拖拽**调整面板高度(变长/变短,localStorage 持久化);球体光晕表达**当前正在使用**的 Provider——即**当前聚焦 session 自己的模型选择**(composer 模型座同源,客户端实时跟踪),因此切换 session 后无需重新选择模型,面板会立即把"使用中"标记切到该 session 的 Provider:绿色正常、黄色配额剩余不足 30% 或余额低于黄阈值、红色查询失败/缺密钥/用量 ≥90% 或余额低于红阈值。闲置的 Provider 余量不足不再影响悬浮球颜色——切换到余量充足的另一个 Provider 后球体会恢复绿色;面板会标注"使用中"的 Provider,并照常列出所有 Provider 的用量明细。
36
+ - **悬浮球入口** —— 可任意拖动的悬浮球点击弹出用量面板,位置持久化;默认停靠在主对话区域左下角(左边距 = 底边距),面板头部的归位按钮一键回到默认位置;Provider 列表超过面板高度时自动滚动,面板**顶部边缘可拖拽**调整面板高度(变长/变短,localStorage 持久化);球体光晕表达**当前正在使用**的 Provider——即**当前聚焦 session 自己的模型选择**(composer 模型座同源,客户端实时跟踪),因此切换 session 后无需重新选择模型,面板会立即把"使用中"标记切到该 session 的 Provider:绿色正常、黄色用量窗口剩余不足 30% 或余额低于黄阈值、红色查询失败/缺密钥/用量 ≥90% 或余额低于红阈值。闲置的 Provider 余量不足不再影响悬浮球颜色——切换到余量充足的另一个 Provider 后球体会恢复绿色;面板会标注"使用中"的 Provider,并照常列出所有 Provider 的用量明细。
37
37
  - **版本徽章** —— 面板标题旁显示当前运行的插件版本,一眼确认加载的是哪个发布版。
38
38
  - **中英双语** —— 面板内置中英文界面,默认跟随 harness 系统语言,标题栏按钮一键切换(localStorage 持久化)。
39
39
  - **刷新周期可调** —— 面板内调整(15s–30min,localStorage 持久化),默认值由插件配置提供。
@@ -90,6 +90,37 @@ records:
90
90
 
91
91
  面板随即展示订阅的 **5h 上限**、**每周**窗口(已用百分比 + 重置倒计时),以及计划上报的 **credits** 与 **spend control** 余额。若授权记录缺失,卡片会显示「未完成 OAuth 授权(llm-pi-ai/openai-codex)」,按第 2 步重新登录即可。
92
92
 
93
+ ### 4. 故障排除:间歇性 "Our servers are currently overloaded"
94
+
95
+ Codex 后端偶尔会返回 `Codex error: Our servers are currently overloaded. Please try again later.`,harness 随即以 `PI_AI_ERROR` 判本轮失败。这是 **OpenAI 端的间歇性过载**(账号、配额、网络通常都正常——可用 `GET /wham/usage` 确认窗口余量),但默认配置下不会自动重试,原因有两层:
96
+
97
+ 1. pi-ai 的 Codex 客户端内部认得 `overloaded` 是可重试错误,但默认重试次数为 0(`dsh-llm-pi-ai` 显式传 `maxRetries: 0`);
98
+ 2. 错误冒泡后,其文本不含 `5xx` / `rate limit` / `timeout` 等关键词,被归类为兜底的 `PI_AI_ERROR`——而它**不在** `dsh-llm-retry` 的默认可重试码(`EMPTY_RESPONSE` / `RATE_LIMIT` / `SERVER` / `TIMEOUT` / `TRANSPORT`)里,于是整轮直接失败。
99
+
100
+ 在 `~/.dsh/settings.yaml` 中给该 provider 加一段 `retryPolicy`,把 `PI_AI_ERROR` 纳入可重试码即可(重开 session 后生效):
101
+
102
+ ```yaml
103
+ llm-pi-ai:
104
+ providers:
105
+ openai-codex:
106
+ retryPolicy:
107
+ mode: normal
108
+ maxRetries: 10
109
+ retryableCodes:
110
+ - EMPTY_RESPONSE
111
+ - RATE_LIMIT
112
+ - SERVER
113
+ - TIMEOUT
114
+ - TRANSPORT
115
+ - PI_AI_ERROR
116
+ backoff:
117
+ initialDelayMs: 2000 # 首次重试延迟,指数翻倍
118
+ maxDelayMs: 60000 # 单次延迟上限
119
+ jitterRatio: 0.2 # ±20% 抖动
120
+ ```
121
+
122
+ 注意区分另一类**必然失败**:部分模型对 ChatGPT 订阅账号不可用,后端直接返回 `400 The '<model>' model is not supported when using Codex with a ChatGPT account.`(实测如 `gpt-5.3-codex-spark`、`gpt-5-codex`)。这类是永久错误,重试无效——请换用账号支持的模型(如 `gpt-5.4` / `gpt-5.5` / `gpt-5.6` 系列)。
123
+
93
124
  ## 安装
94
125
 
95
126
  > [!NOTE]
@@ -112,7 +143,7 @@ pnpm pack # 产出 dsh-provider-usage-<version>.tgz
112
143
  dsh plugin --profile web add ./dsh-provider-usage-<version>.tgz
113
144
  ```
114
145
 
115
- 注意要安装 **tarball** 而不是仓库目录:`dsh plugin add .` 会链接整个仓库,仓库自带 `node_modules` 里的 `@deepseek-ai/cordis` 会遮蔽 harness 的共享实例,导致 host 半注册不上(RPC 404)。link 方式仍适合纯 UI 迭代(浏览器 bundle 自包含,重新 build + 刷新页面即生效),但需要 host 半时请切换到 tarball 或 npm 正式版。若替换 link 安装时 pnpm 报 `EPERM ... symlink`,手动删除 profile 目录下残留的 `node_modules/dsh-provider-usage` 联结后重试即可。
146
+ 注意要安装 **tarball** 而不是仓库目录:`dsh plugin --profile web add .` 会链接整个仓库,仓库自带 `node_modules` 里的 `@deepseek-ai/cordis` 会遮蔽 harness 的共享实例,导致 host 半注册不上(RPC 404)。link 方式仍适合纯 UI 迭代(浏览器 bundle 自包含,重新 build + 刷新页面即生效),但需要 host 半时请切换到 tarball 或 npm 正式版。若替换 link 安装时 pnpm 报 `EPERM ... symlink`,手动删除 profile 目录下残留的 `node_modules/dsh-provider-usage` 联结后重试即可。
116
147
 
117
148
  插件集合变化后需重启 `dsh web`;之后仅改动代码时重新 build + 重新 add + 刷新页面即可。
118
149
 
@@ -122,7 +153,7 @@ dsh plugin --profile web add ./dsh-provider-usage-<version>.tgz
122
153
  dsh plugin --profile web add dsh-provider-usage@latest
123
154
  ```
124
155
 
125
- 然后重启 `dsh web` 并刷新页面。如果目标版本刚发布不久,profile 的供应链冷静期(`minimumReleaseAge`)可能会静默停留在旧版——这时指定精确版本号(如 `dsh plugin --profile web add dsh-provider-usage@0.3.1`),dsh 会自动豁免该版本。面板标题旁的版本徽章可以确认实际加载的版本。
156
+ 然后重启 `dsh web` 并刷新页面。如果目标版本刚发布不久,profile 的供应链冷静期(`minimumReleaseAge`)可能会静默停留在旧版——这时指定精确版本号(如 `dsh plugin --profile web add dsh-provider-usage@0.3.10`),dsh 会自动豁免该版本。面板标题旁的版本徽章可以确认实际加载的版本。
126
157
 
127
158
  ## 配置说明
128
159
 
@@ -136,6 +167,9 @@ dsh plugin --profile web add dsh-provider-usage@latest
136
167
  balanceRedThreshold: 10 # 余额低于该值变红(按余额自身币种比较)
137
168
  balanceYellowThreshold: 30 # 余额低于该值变黄(按余额自身币种比较)
138
169
  autoDetect: true # 自动枚举 llm 注册表中的 provider
170
+ queryTimeoutMs: 20000 # 单次查询超时(毫秒),每次重试独立计时
171
+ queryRetries: 2 # 瞬时错误(超时/网络/HTTP 408/425/429/5xx)重试次数
172
+ queryRetryDelayMs: 2000 # 重试基础延迟(毫秒),逐次翻倍,封顶 10 秒
139
173
  providers: [] # 手动补充/覆盖 provider(id 相同则覆盖自动探测结果)
140
174
  ```
141
175
 
@@ -145,6 +179,9 @@ dsh plugin --profile web add dsh-provider-usage@latest
145
179
  | `balanceRedThreshold` | number | `10` | 余额低于该值变红,按余额自身币种比较 |
146
180
  | `balanceYellowThreshold` | number | `30` | 余额低于该值变黄,按余额自身币种比较 |
147
181
  | `autoDetect` | boolean | `true` | 从 llm 注册表自动枚举 provider |
182
+ | `queryTimeoutMs` | number | `20000` | 单次查询超时(毫秒),1000–120000,每次重试独立计时 |
183
+ | `queryRetries` | number | `2` | 瞬时错误(超时/网络/HTTP 408/425/429/5xx)的重试次数,0–10;4xx 永久错误不重试 |
184
+ | `queryRetryDelayMs` | number | `2000` | 重试基础延迟(毫秒),100–60000,指数翻倍,封顶 10 秒 |
148
185
  | `providers` | array | `[]` | 手动 provider 规格:`{id, kind, baseURL, apiKeyEnv, displayName?, enabled?}`,`kind` 取上表中的任一适配器 |
149
186
 
150
187
  也可以在 `~/.dsh/settings.yaml` 中通过 `provider-usage:` 命名空间热更新同样字段。
@@ -163,7 +200,7 @@ config:
163
200
 
164
201
  ## 架构
165
202
 
166
- - **Host 半**(`src/index.ts`):`UsageService extends TypertRemoteService`,`@Remote('list')` 暴露 `usage/list`(SRC 模式,无需代码生成);`Config` 用 schemastery 声明,`installSettingsSection` 支持 settings 热更新。
203
+ - **Host 半**(`src/index.ts`):`UsageService extends TypertRemoteService`,通过 `Remote('list')` 标记(以非装饰器方式应用)暴露 `usage/list`(SRC 模式,无需代码生成);`Config` 用 schemastery 声明,通过 settings provider 的 `installSection` 支持 settings 热更新。
167
204
  - **Client 半**(`src/client/`):`window.__ModuleLoader__.load({id, factory})` 格式 bundle(tsdown 构建),通过 `sidebar.footer.action` slot 挂载(仅作为挂载点——触发器本体是 portal 到 `document.body` 的悬浮球),通过 `ctx.connection.rpc.call('/api', 'usage/list', {args:{}})` 轮询。服务本身无状态——每次轮询都取实时值。
168
205
 
169
206
  ## 许可证
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,184 @@
1
+ {
2
+ "$comment": "LLM provider 余额/配额查询接口调研结论(机器可读版)。详见 provider-quota-endpoints.md。均经官方文档/开源实现验证,未臆造端点。",
3
+ "providers": [
4
+ {
5
+ "id": "openai",
6
+ "support": "partial",
7
+ "credentialType": "admin_api_key",
8
+ "credentialHint": "OpenAI Admin key (sk-admin-...),platform.openai.com → Organization → Admin keys;普通 sk- key 与 project service-account key 均不可用",
9
+ "endpoints": [
10
+ {
11
+ "kind": "cost",
12
+ "method": "GET",
13
+ "url": "https://api.openai.com/v1/organization/costs",
14
+ "headers": { "Authorization": "Bearer {ADMIN_KEY}" },
15
+ "query": {
16
+ "start_time": "unix 秒,必填",
17
+ "end_time": "unix 秒",
18
+ "bucket_width": "1d|1h|1m",
19
+ "group_by": "project_id|line_item",
20
+ "project_ids": "proj_... 可重复",
21
+ "limit": "int", "page": "cursor"
22
+ },
23
+ "responseShape": "{ object:'page', data:[{ start_time, end_time, results:[{ amount:{value,currency}, line_item, project_id }] }], has_more, next_page }"
24
+ },
25
+ {
26
+ "kind": "usage",
27
+ "method": "GET",
28
+ "url": "https://api.openai.com/v1/organization/usage/completions",
29
+ "headers": { "Authorization": "Bearer {ADMIN_KEY}" },
30
+ "query": { "start_time": "unix 秒,必填", "bucket_width": "1d|1h|1m", "group_by": "model|project_id|api_key_id|..." },
31
+ "responseShape": "{ data:[{ results:[{ model, input_tokens, output_tokens, input_cached_tokens, num_model_requests }] }], has_more, next_page }",
32
+ "siblings": ["embeddings", "images", "moderations", "audio_speeches", "audio_transcriptions", "vector_stores", "code_interpreter_sessions"]
33
+ },
34
+ {
35
+ "kind": "balance_legacy_best_effort",
36
+ "method": "GET",
37
+ "url": "https://api.openai.com/v1/dashboard/billing/credit_grants",
38
+ "headers": { "Authorization": "Bearer {LEGACY_USER_KEY}" },
39
+ "responseShape": "{ object:'credit_summary', total_granted, total_used, total_available, expires_at }",
40
+ "warnings": ["不在现行公开 API 文档", "多数普通 key 已失效", "仅作静默兜底,失败视为不可用"]
41
+ }
42
+ ],
43
+ "consoleUrl": "https://platform.openai.com/usage",
44
+ "sources": [
45
+ "https://developers.openai.com/api/reference/resources/admin/subresources/organization/subresources/usage/methods/costs",
46
+ "https://github.com/steipete/CodexBar/blob/main/docs/openai.md"
47
+ ]
48
+ },
49
+ {
50
+ "id": "openai-codex",
51
+ "support": "yes_oauth_only",
52
+ "credentialType": "oauth_access_token",
53
+ "credentialHint": "~/.codex/auth.json 中的 ChatGPT/Codex OAuth access token;stale 时引导用户运行 codex CLI,不要自行回写 auth.json",
54
+ "endpoints": [
55
+ {
56
+ "kind": "rate_limit_windows",
57
+ "method": "GET",
58
+ "url": "https://chatgpt.com/backend-api/wham/usage",
59
+ "headers": { "Authorization": "Bearer {OAUTH_TOKEN}" },
60
+ "responseShape": "{ plan_type, rate_limit:{ allowed, limit_reached, primary_window:{used_percent,limit_window_seconds,reset_after_seconds,reset_at}, secondary_window:{...} }, additional_rate_limits:[], credits:{has_credits,unlimited,balance} }",
61
+ "mapping": { "primary_window": "5 小时会话窗口", "secondary_window": "周窗口" }
62
+ },
63
+ {
64
+ "kind": "reset_credits",
65
+ "method": "GET",
66
+ "url": "https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
67
+ "headers": { "Authorization": "Bearer {OAUTH_TOKEN}" }
68
+ }
69
+ ],
70
+ "alternatives": ["本地 codex app-server JSON-RPC: account/read, account/rateLimits/read"],
71
+ "warnings": ["未公开内部接口,可能随时变更"],
72
+ "consoleUrl": "https://chatgpt.com/codex/settings/usage",
73
+ "sources": ["https://github.com/steipete/CodexBar/blob/main/docs/codex.md"]
74
+ },
75
+ {
76
+ "id": "anthropic",
77
+ "support": "partial",
78
+ "credentialType": "admin_api_key",
79
+ "credentialHint": "Anthropic Admin key (sk-ant-admin-...),Console → Settings → Admin keys;普通 sk-ant-api key 不可用;认证头是 x-api-key 而非 Bearer",
80
+ "endpoints": [
81
+ {
82
+ "kind": "usage",
83
+ "method": "GET",
84
+ "url": "https://api.anthropic.com/v1/organizations/usage_report/messages",
85
+ "headers": { "x-api-key": "{ADMIN_KEY}", "anthropic-version": "2023-06-01" },
86
+ "query": {
87
+ "starting_at": "RFC3339,必填",
88
+ "ending_at": "RFC3339",
89
+ "bucket_width": "1d|1h",
90
+ "group_by": "workspace_id|api_key_id|model|service_tier|context_window(多值)",
91
+ "models": "多值", "workspace_ids": "多值", "limit": "≤1000", "page": "cursor"
92
+ },
93
+ "responseShape": "{ data:[{ starting_at, ending_at, results:[{ model, workspace_id, api_key_id, service_tier, uncached_input_tokens, output_tokens, cache_read_input_tokens, cache_creation_5m_input_tokens, cache_creation_1h_input_tokens, server_tool_use }] }], has_more, next_page }"
94
+ },
95
+ {
96
+ "kind": "cost",
97
+ "method": "GET",
98
+ "url": "https://api.anthropic.com/v1/organizations/cost_report",
99
+ "headers": { "x-api-key": "{ADMIN_KEY}", "anthropic-version": "2023-06-01" },
100
+ "query": { "starting_at": "RFC3339,必填", "group_by": "workspace_id|description", "limit": "int", "page": "cursor" },
101
+ "responseShape": "{ data:[{ starting_at, ending_at, results:[{ amount(字符串美元), currency, cost_type, description, model, workspace_id, service_tier }] }], has_more, next_page }",
102
+ "warnings": ["bucket_width 仅支持 1d"]
103
+ },
104
+ {
105
+ "kind": "subscription_usage_oauth",
106
+ "method": "GET",
107
+ "url": "https://api.anthropic.com/api/oauth/usage",
108
+ "headers": { "Authorization": "Bearer {CLAUDE_CODE_OAUTH_TOKEN}", "anthropic-beta": "oauth-2025-04-20" },
109
+ "responseShape": "{ five_hour:{utilization,resets_at}, seven_day:{...}, seven_day_sonnet:{...}, ... }",
110
+ "warnings": ["仅 Claude Pro/Max 订阅", "需 user:profile scope", "非 API key 方案"]
111
+ }
112
+ ],
113
+ "consoleUrl": "https://console.anthropic.com/",
114
+ "sources": [
115
+ "https://platform.claude.com/docs/en/manage-claude/usage-cost-api",
116
+ "https://platform.claude.com/docs/en/api/admin/usage_report/retrieve_messages",
117
+ "https://github.com/steipete/CodexBar/blob/main/docs/claude.md"
118
+ ]
119
+ },
120
+ {
121
+ "id": "gemini",
122
+ "support": "no",
123
+ "credentialType": null,
124
+ "reason": "AI Studio API key 无任何配额/用量/余额查询 HTTP 接口;官方文档无此 API,响应无 rate-limit 头,社区仅能靠 429 反推。",
125
+ "greyArea": {
126
+ "comment": "Gemini CLI 私有 OAuth 接口,非 API key 方案,且 Google 已对个人账号关闭",
127
+ "endpoints": [
128
+ { "method": "POST", "url": "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota", "body": "{ project }", "responseShape": "buckets[].{modelId, remainingFraction, resetTime}" },
129
+ { "method": "POST", "url": "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", "body": "{ metadata:{ideType,pluginType} }", "responseShape": "{ paidTier, currentTier }" }
130
+ ]
131
+ },
132
+ "consoleUrl": "https://aistudio.google.com/usage",
133
+ "sources": [
134
+ "https://github.com/steipete/CodexBar/blob/main/docs/gemini.md",
135
+ "https://discuss.ai.google.dev/t/gemini-api-429-resource-exhausted-error-on-tier-1/114413"
136
+ ]
137
+ },
138
+ {
139
+ "id": "xai",
140
+ "support": "yes",
141
+ "credentialType": "management_key",
142
+ "credentialHint": "xAI Management key(console.x.ai → Management Keys;xai- 前缀但与推理 key 不同),需相应 ACL;team_id 在 console Team settings 复制;baseURL 是 management-api.x.ai 不是 api.x.ai",
143
+ "endpoints": [
144
+ {
145
+ "kind": "balance",
146
+ "method": "GET",
147
+ "url": "https://management-api.x.ai/v1/billing/teams/{team_id}/prepaid/balance",
148
+ "headers": { "Authorization": "Bearer {MANAGEMENT_KEY}" },
149
+ "responseShape": "{ total:{val}, changes:[{ teamId, changeOrigin:PURCHASE|SPEND|REFUND|MANUAL|AUTO_PURCHASE, topupStatus, amount:{val}, createTime }] }",
150
+ "warnings": ["金额单位=美元美分", "符号反直觉:SPEND 为正、PURCHASE 为负;可用余额 = -total.val/100 美元"]
151
+ },
152
+ { "kind": "spending_limits", "method": "GET", "url": "https://management-api.x.ai/v1/billing/teams/{team_id}/postpaid/spending-limits", "headers": { "Authorization": "Bearer {MANAGEMENT_KEY}" } },
153
+ { "kind": "invoice_preview", "method": "GET", "url": "https://management-api.x.ai/v1/billing/teams/{team_id}/postpaid/invoice/preview", "headers": { "Authorization": "Bearer {MANAGEMENT_KEY}" } },
154
+ {
155
+ "kind": "usage_analytics",
156
+ "method": "POST",
157
+ "url": "https://management-api.x.ai/v1/billing/teams/{team_id}/usage",
158
+ "headers": { "Authorization": "Bearer {MANAGEMENT_KEY}" },
159
+ "body": "{ analyticsRequest:{ timeRange:{startTime,endTime,timezone}, timeUnit, values:[{name,aggregation}], groupBy:[], filters:[] } }",
160
+ "responseShape": "{ timeSeries:[{ group, groupLabels, dataPoints:[{timestamp, values[]}] }], limitReached }"
161
+ },
162
+ { "kind": "model_rate_limits", "method": "GET", "url": "https://management-api.x.ai/auth/teams/{teamId}/models", "headers": { "Authorization": "Bearer {MANAGEMENT_KEY}" } },
163
+ { "kind": "key_validation", "method": "GET", "url": "https://management-api.x.ai/auth/management-keys/validation", "headers": { "Authorization": "Bearer {MANAGEMENT_KEY}" } }
164
+ ],
165
+ "consoleUrl": "https://console.x.ai/team/default/billing",
166
+ "sources": [
167
+ "https://docs.x.ai/developers/rest-api-reference/management",
168
+ "https://docs.x.ai/developers/rest-api-reference/management/billing",
169
+ "https://docs.x.ai/console/billing"
170
+ ]
171
+ },
172
+ {
173
+ "id": "mistral",
174
+ "support": "no",
175
+ "credentialType": null,
176
+ "reason": "无任何公开余额/用量查询接口;官方文档无 billing API,开源余额工具均标注 Console only。",
177
+ "consoleUrl": "https://console.mistral.ai/usage",
178
+ "sources": [
179
+ "https://github.com/hanmumuHL/check_balance",
180
+ "https://theneuralbase.com/mistral-api/learn/advanced/la-plateforme-dashboard-overview/"
181
+ ]
182
+ }
183
+ ]
184
+ }