@linxin666/dsh-usage 0.3.7 → 0.3.10

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/lib/client.js CHANGED
@@ -68,6 +68,9 @@ window.__ModuleLoader__.load({
68
68
  "usage.error": "加载失败:{error}",
69
69
  "usage.current": "当前",
70
70
  "usage.today": "今日用量",
71
+ "usage.today.cost": "今日消费(估算)",
72
+ "usage.peak.on": "DeepSeek 高峰时段:计价 ×2,{time} 结束",
73
+ "usage.peak.off": "DeepSeek 空闲时段:计价为高峰一半,{time} 进入高峰",
71
74
  "usage.calls": "{n} 次调用",
72
75
  "usage.tokens.total": "总 tokens",
73
76
  "usage.tokens.input": "输入",
@@ -82,10 +85,12 @@ window.__ModuleLoader__.load({
82
85
  "usage.oauth": "OAuth 凭据,不做余额查询",
83
86
  "usage.plan.reset": "{date} 重置",
84
87
  "usage.plan.noPlan": "未检测到套餐数据",
88
+ "usage.plan.noneConfigured": "没有已配置的套餐类 provider(如 Kimi、GLM、OpenCode Go、MiniMax、Codex 订阅)",
85
89
  "usage.plan.windows.5h": "5 小时",
86
90
  "usage.plan.windows.week": "每周",
87
91
  "usage.plan.windows.month": "每月",
88
92
  "usage.provider.error": "查询失败:{error}",
93
+ "usage.errorListSeparator": ";",
89
94
  "usage.config.title": "设置",
90
95
  "usage.config.enabled": "启用插件",
91
96
  "usage.config.pollIntervalSec": "轮询间隔(秒)",
@@ -106,6 +111,9 @@ window.__ModuleLoader__.load({
106
111
  "usage.error": "Failed to load: {error}",
107
112
  "usage.current": "Current",
108
113
  "usage.today": "Today",
114
+ "usage.today.cost": "Today spend (estimated)",
115
+ "usage.peak.on": "DeepSeek peak hours: 2x pricing, ends {time}",
116
+ "usage.peak.off": "DeepSeek off-peak: half of peak pricing, peak returns {time}",
109
117
  "usage.calls": "{n} calls",
110
118
  "usage.tokens.total": "Total tokens",
111
119
  "usage.tokens.input": "Input",
@@ -120,10 +128,12 @@ window.__ModuleLoader__.load({
120
128
  "usage.oauth": "OAuth credential, no balance query",
121
129
  "usage.plan.reset": "resets {date}",
122
130
  "usage.plan.noPlan": "No plan data detected",
131
+ "usage.plan.noneConfigured": "No plan-capable provider configured (such as Kimi, GLM, OpenCode Go, MiniMax, Codex subscription)",
123
132
  "usage.plan.windows.5h": "5 hours",
124
133
  "usage.plan.windows.week": "Weekly",
125
134
  "usage.plan.windows.month": "Monthly",
126
135
  "usage.provider.error": "Query failed: {error}",
136
+ "usage.errorListSeparator": "; ",
127
137
  "usage.config.title": "Settings",
128
138
  "usage.config.enabled": "Enable plugin",
129
139
  "usage.config.pollIntervalSec": "Poll interval (seconds)",
@@ -148,7 +158,7 @@ window.__ModuleLoader__.load({
148
158
  }
149
159
  //#endregion
150
160
  //#region \0dsh-css:packages/dsh-usage/src/client/usage.module.css.mjs
151
- const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_trend{align-items:flex-end;gap:3px;height:56px;display:flex}.cvtkAW_trendBar{background:color-mix(in srgb, currentColor 30%, transparent);border-radius:3px 3px 0 0;flex:1 1 0;min-width:4px;min-height:2px}.cvtkAW_trendBarToday{background:color-mix(in srgb, currentColor 65%, transparent)}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}";
161
+ const css = ".cvtkAW_section{color:inherit;flex-direction:column;gap:16px;display:flex}.cvtkAW_header{justify-content:space-between;align-items:center;gap:12px;display:flex}.cvtkAW_currentProvider{opacity:.75;font-size:13px}.cvtkAW_refreshBtn{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.85;background:0 0;border:1px solid;border-radius:8px;padding:4px 12px;font-size:12px;transition:opacity .12s,background-color .12s}.cvtkAW_refreshBtn:hover:not(:disabled){opacity:1;background:color-mix(in srgb, currentColor 8%, transparent)}.cvtkAW_refreshBtn:disabled{cursor:default;opacity:.5}.cvtkAW_refreshBtn:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabs{border-bottom:1px solid color-mix(in srgb, currentColor 14%, transparent);gap:4px;display:flex}.cvtkAW_tab{appearance:none;color:inherit;font:inherit;cursor:pointer;opacity:.65;background:0 0;border:none;border-bottom:2px solid #0000;margin-bottom:-1px;padding:6px 14px;font-size:13px}.cvtkAW_tab:hover{opacity:.9}.cvtkAW_tab:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}.cvtkAW_tabActive{opacity:1;border-bottom-color:currentColor;font-weight:600}.cvtkAW_card{border:1px solid color-mix(in srgb, currentColor 14%, transparent);border-radius:12px;flex-direction:column;gap:10px;padding:14px 16px;display:flex}.cvtkAW_cardTitle{letter-spacing:.04em;text-transform:uppercase;opacity:.6;font-size:12px;font-weight:600}.cvtkAW_statRow{flex-wrap:wrap;gap:18px;display:flex}.cvtkAW_stat{flex-direction:column;gap:2px;display:flex}.cvtkAW_statValue{font-variant-numeric:tabular-nums;font-size:18px;font-weight:600}.cvtkAW_statLabel{opacity:.6;font-size:11px}.cvtkAW_providerRow{border-top:1px solid color-mix(in srgb, currentColor 8%, transparent);justify-content:space-between;align-items:center;gap:12px;padding:6px 0;font-size:13px;display:flex}.cvtkAW_providerRow:first-of-type{border-top:none}.cvtkAW_providerName{align-items:center;gap:8px;min-width:0;display:flex}.cvtkAW_providerTokens{font-variant-numeric:tabular-nums;opacity:.75;white-space:nowrap}.cvtkAW_providerBalance{font-variant-numeric:tabular-nums;white-space:nowrap;font-weight:600}.cvtkAW_currentBadge{border:1px solid color-mix(in srgb, currentColor 35%, transparent);opacity:.8;border-radius:999px;flex:none;padding:1px 7px;font-size:10px;font-weight:600}.cvtkAW_chart{flex-direction:column;gap:12px;display:flex}.cvtkAW_chartProvider{flex-direction:column;gap:4px;display:flex}.cvtkAW_chartHead{justify-content:space-between;align-items:baseline;gap:10px;font-size:13px;display:flex}.cvtkAW_chartTokens{font-variant-numeric:tabular-nums;opacity:.65;white-space:nowrap;font-size:11px}.cvtkAW_chartBar{background:color-mix(in srgb, currentColor 8%, transparent);border-radius:999px;height:8px;display:block;overflow:hidden}.cvtkAW_chartFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_chartModel{opacity:.8;grid-template-columns:minmax(80px,180px) 1fr auto;align-items:center;gap:8px;padding-left:14px;font-size:11px;display:grid}.cvtkAW_chartModelName{text-overflow:ellipsis;white-space:nowrap;opacity:.8;overflow:hidden}.cvtkAW_chartModelBar{background:color-mix(in srgb, currentColor 6%, transparent);border-radius:999px;height:4px;display:block;overflow:hidden}.cvtkAW_chartModelFill{background:color-mix(in srgb, currentColor 35%, transparent);border-radius:999px;height:100%;transition:width .3s;display:block}.cvtkAW_trendAxis{opacity:.5;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:10px;display:flex}.cvtkAW_muted{opacity:.6;font-size:12px}.cvtkAW_errorLine{opacity:.75;font-size:12px}.cvtkAW_planCard{flex-direction:column;gap:8px;display:flex}.cvtkAW_planHead{justify-content:space-between;align-items:baseline;gap:10px;display:flex}.cvtkAW_planName{font-size:14px;font-weight:600}.cvtkAW_windowRow{flex-direction:column;gap:4px;display:flex}.cvtkAW_windowLabel{opacity:.8;font-variant-numeric:tabular-nums;justify-content:space-between;font-size:12px;display:flex}.cvtkAW_bar{background:color-mix(in srgb, currentColor 10%, transparent);border-radius:999px;height:6px;overflow:hidden}.cvtkAW_barFill{background:color-mix(in srgb, currentColor 55%, transparent);border-radius:999px;height:100%;transition:width .3s}.cvtkAW_barWarn{background:#d97706}.cvtkAW_barLow{background:#dc2626}.cvtkAW_resetLine{opacity:.55;font-variant-numeric:tabular-nums;font-size:11px}.cvtkAW_settingsGrid{flex-wrap:wrap;align-items:center;gap:16px;display:flex}.cvtkAW_settingItem{align-items:center;gap:8px;font-size:13px;display:flex}.cvtkAW_settingItem input[type=checkbox]{accent-color:currentColor}.cvtkAW_settingItem input[type=number]{border:1px solid color-mix(in srgb, currentColor 25%, transparent);width:90px;color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem select{border:1px solid color-mix(in srgb, currentColor 25%, transparent);color:inherit;font:inherit;background:0 0;border-radius:6px;padding:3px 8px;font-size:13px}.cvtkAW_settingItem input:focus-visible,.cvtkAW_settingItem select:focus-visible{box-shadow:0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);outline:none}";
152
162
  const tagId = "@linxin666/dsh-usage/usage.module.css";
153
163
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
154
164
  const tag = document.createElement("style");
@@ -164,6 +174,16 @@ window.__ModuleLoader__.load({
164
174
  "barWarn": "cvtkAW_barWarn",
165
175
  "card": "cvtkAW_card",
166
176
  "cardTitle": "cvtkAW_cardTitle",
177
+ "chart": "cvtkAW_chart",
178
+ "chartBar": "cvtkAW_chartBar",
179
+ "chartFill": "cvtkAW_chartFill",
180
+ "chartHead": "cvtkAW_chartHead",
181
+ "chartModel": "cvtkAW_chartModel",
182
+ "chartModelBar": "cvtkAW_chartModelBar",
183
+ "chartModelFill": "cvtkAW_chartModelFill",
184
+ "chartModelName": "cvtkAW_chartModelName",
185
+ "chartProvider": "cvtkAW_chartProvider",
186
+ "chartTokens": "cvtkAW_chartTokens",
167
187
  "currentBadge": "cvtkAW_currentBadge",
168
188
  "currentProvider": "cvtkAW_currentProvider",
169
189
  "errorLine": "cvtkAW_errorLine",
@@ -188,14 +208,470 @@ window.__ModuleLoader__.load({
188
208
  "tab": "cvtkAW_tab",
189
209
  "tabActive": "cvtkAW_tabActive",
190
210
  "tabs": "cvtkAW_tabs",
191
- "trend": "cvtkAW_trend",
192
211
  "trendAxis": "cvtkAW_trendAxis",
193
- "trendBar": "cvtkAW_trendBar",
194
- "trendBarToday": "cvtkAW_trendBarToday",
195
212
  "windowLabel": "cvtkAW_windowLabel",
196
213
  "windowRow": "cvtkAW_windowRow"
197
214
  };
198
215
  //#endregion
216
+ //#region src/core/adapters.ts
217
+ /** Parse a string/number into a finite number, else undefined. */
218
+ function toNum(value) {
219
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
220
+ if (typeof value === "string" && value.trim() !== "") {
221
+ const parsed = Number(value);
222
+ return Number.isFinite(parsed) ? parsed : void 0;
223
+ }
224
+ }
225
+ /** Read a string field that must be a non-empty string. */
226
+ function str(value) {
227
+ return typeof value === "string" && value.trim() !== "" ? value.trim() : void 0;
228
+ }
229
+ /** Format a number to a fixed 2-decimal display string. */
230
+ function money(value) {
231
+ return value.toFixed(2);
232
+ }
233
+ /** Millisecond epoch or ISO string → normalized ISO 8601, else undefined. */
234
+ function toIso(value) {
235
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
236
+ const ms = value < 0xe8d4a51000 ? value * 1e3 : value;
237
+ const date = new Date(ms);
238
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
239
+ }
240
+ const text = str(value);
241
+ if (text === void 0) return void 0;
242
+ if (/^\d+$/.test(text)) return toIso(Number(text));
243
+ const date = new Date(text);
244
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
245
+ }
246
+ /** Used-percent helper guarding zero/absent limits. */
247
+ function usedPercent(used, limit) {
248
+ const usedNum = toNum(used);
249
+ const limitNum = toNum(limit);
250
+ if (usedNum === void 0 || limitNum === void 0 || limitNum <= 0) return void 0;
251
+ return Math.max(0, Math.min(100, usedNum / limitNum * 100));
252
+ }
253
+ function bearer(apiKey) {
254
+ return { authorization: `Bearer ${apiKey}` };
255
+ }
256
+ /**
257
+ * The official pay-as-you-go balance. `deepseek` is the configurable-catalog
258
+ * route key; `deepseek-official` is the live provider route the llm-deepseek
259
+ * adapter registers (sessions and agent-default-model carry it), so both ids
260
+ * must resolve here or the current provider would never be probed.
261
+ */
262
+ const DEEPSEEK = {
263
+ ids: ["deepseek", "deepseek-official"],
264
+ displayName: "DeepSeek",
265
+ balance: {
266
+ build: ({ apiKey }) => ({
267
+ url: "https://api.deepseek.com/user/balance",
268
+ headers: bearer(apiKey)
269
+ }),
270
+ parse: (status, body) => {
271
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
272
+ const infos = body.balance_infos;
273
+ if (!Array.isArray(infos) || infos.length === 0) return void 0;
274
+ const first = infos[0];
275
+ if (typeof first !== "object" || first === null) return void 0;
276
+ const currency = str(first.currency);
277
+ const total = str(first.total_balance);
278
+ if (currency === void 0 || total === void 0) return void 0;
279
+ return {
280
+ currency,
281
+ totalBalance: total
282
+ };
283
+ }
284
+ }
285
+ };
286
+ /** Moonshot pay-as-you-go balance; CN bills in CNY, international in USD. */
287
+ function moonshotBalance(host, currency, ids) {
288
+ return {
289
+ ids,
290
+ displayName: "Moonshot AI",
291
+ balance: {
292
+ build: ({ apiKey }) => ({
293
+ url: `https://${host}/v1/users/me/balance`,
294
+ headers: bearer(apiKey)
295
+ }),
296
+ parse: (status, body) => {
297
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
298
+ const data = body.data;
299
+ if (typeof data !== "object" || data === null) return void 0;
300
+ const available = toNum(data.available_balance);
301
+ if (available === void 0) return void 0;
302
+ return {
303
+ currency,
304
+ totalBalance: money(available)
305
+ };
306
+ }
307
+ }
308
+ };
309
+ }
310
+ /** Kimi For Coding quota: top-level `usage` is the weekly summary, `limits[]` the per-window rows. */
311
+ const KIMI_CODING = {
312
+ ids: ["kimi-coding"],
313
+ displayName: "Kimi For Coding",
314
+ plan: {
315
+ build: ({ apiKey }) => ({
316
+ url: "https://api.kimi.com/coding/v1/usages",
317
+ headers: bearer(apiKey)
318
+ }),
319
+ parse: (status, body) => {
320
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
321
+ const root = body;
322
+ const windows = [];
323
+ const limits = root.limits;
324
+ if (Array.isArray(limits)) for (const entry of limits) {
325
+ if (typeof entry !== "object" || entry === null) continue;
326
+ const detail = entry.detail;
327
+ const window = entry.window;
328
+ if (typeof detail !== "object" || detail === null) continue;
329
+ const row = detail;
330
+ const duration = typeof window === "object" && window !== null ? toNum(window.duration) : void 0;
331
+ const unit = typeof window === "object" && window !== null ? str(window.timeUnit) : void 0;
332
+ const key = duration === 300 && unit === "TIME_UNIT_MINUTE" ? "5h" : duration !== void 0 ? `w-${duration}` : "window";
333
+ const percent = usedPercent(row.used, row.limit);
334
+ windows.push({
335
+ key,
336
+ name: str(row.name),
337
+ percent,
338
+ resetsAt: toIso(row.resetTime)
339
+ });
340
+ }
341
+ const usage = root.usage;
342
+ if (typeof usage === "object" && usage !== null) {
343
+ const weekly = usage;
344
+ windows.push({
345
+ key: "week",
346
+ name: "Weekly",
347
+ percent: usedPercent(weekly.used, weekly.limit),
348
+ resetsAt: toIso(weekly.resetTime)
349
+ });
350
+ }
351
+ if (windows.length === 0) return void 0;
352
+ const user = root.user;
353
+ const membership = typeof user === "object" && user !== null ? user.membership : void 0;
354
+ return {
355
+ planName: typeof membership === "object" && membership !== null ? str(membership.level) : void 0,
356
+ windows
357
+ };
358
+ }
359
+ }
360
+ };
361
+ /** GLM Coding Plan quota; auth is the RAW key without a Bearer prefix. */
362
+ function glmPlan(host, ids) {
363
+ return {
364
+ ids,
365
+ displayName: "GLM Coding Plan",
366
+ plan: {
367
+ build: ({ apiKey }) => ({
368
+ url: `https://${host}/api/monitor/usage/quota/limit`,
369
+ headers: {
370
+ authorization: apiKey,
371
+ "accept-language": "en-US,en"
372
+ }
373
+ }),
374
+ parse: (status, body) => {
375
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
376
+ const root = body;
377
+ if (root.success !== true) return void 0;
378
+ const data = root.data;
379
+ if (typeof data !== "object" || data === null) return void 0;
380
+ const limits = data.limits;
381
+ if (!Array.isArray(limits)) return void 0;
382
+ const windows = [];
383
+ for (const entry of limits) {
384
+ if (typeof entry !== "object" || entry === null) continue;
385
+ const row = entry;
386
+ const unit = toNum(row.unit);
387
+ const percent = toNum(row.percentage);
388
+ windows.push({
389
+ key: unit === 3 ? "5h" : unit === 6 ? "week" : unit !== void 0 ? `unit-${unit}` : "window",
390
+ percent: percent === void 0 ? void 0 : Math.max(0, Math.min(100, percent)),
391
+ resetsAt: toIso(row.nextResetTime)
392
+ });
393
+ }
394
+ if (windows.length === 0) return void 0;
395
+ return {
396
+ planName: str(data.level),
397
+ windows
398
+ };
399
+ }
400
+ }
401
+ };
402
+ }
403
+ /** OpenCode Go quota: percent-only rolling/weekly/monthly windows. */
404
+ const OPENCODE_GO = {
405
+ ids: ["opencode-go"],
406
+ displayName: "OpenCode Go",
407
+ plan: {
408
+ build: ({ apiKey }) => ({
409
+ url: "https://opencode.ai/zen/go/v1/usage",
410
+ headers: bearer(apiKey)
411
+ }),
412
+ parse: (status, body) => {
413
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
414
+ const usage = body.usage;
415
+ if (typeof usage !== "object" || usage === null) return void 0;
416
+ const windows = [];
417
+ for (const [field, key] of [
418
+ ["rolling", "5h"],
419
+ ["weekly", "week"],
420
+ ["monthly", "month"]
421
+ ]) {
422
+ const entry = usage[field];
423
+ if (typeof entry !== "object" || entry === null) continue;
424
+ const row = entry;
425
+ const percent = toNum(row.percent);
426
+ windows.push({
427
+ key,
428
+ percent: percent === void 0 ? void 0 : Math.max(0, Math.min(100, percent)),
429
+ resetsAt: percent === 0 ? void 0 : toIso(row.resetsAt)
430
+ });
431
+ }
432
+ if (windows.length === 0) return void 0;
433
+ return { windows };
434
+ }
435
+ }
436
+ };
437
+ /** MiniMax coding-plan remains: remaining-percent semantics, `general` model entry. */
438
+ function minimaxPlan(host, ids) {
439
+ return {
440
+ ids,
441
+ displayName: "MiniMax Coding Plan",
442
+ plan: {
443
+ build: ({ apiKey }) => ({
444
+ url: `https://${host}/v1/api/openplatform/coding_plan/remains`,
445
+ headers: bearer(apiKey)
446
+ }),
447
+ parse: (status, body) => {
448
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
449
+ const remains = body.model_remains;
450
+ if (!Array.isArray(remains)) return void 0;
451
+ const general = remains.find((entry) => typeof entry === "object" && entry !== null && entry.model_name === "general");
452
+ if (typeof general !== "object" || general === null) return void 0;
453
+ const row = general;
454
+ const windows = [];
455
+ const intervalRemaining = toNum(row.current_interval_remaining_percent);
456
+ if (intervalRemaining !== void 0) windows.push({
457
+ key: "5h",
458
+ percent: Math.max(0, Math.min(100, 100 - intervalRemaining)),
459
+ resetsAt: toIso(row.end_time)
460
+ });
461
+ if (row.current_weekly_status === 1) {
462
+ const weeklyRemaining = toNum(row.current_weekly_remaining_percent);
463
+ if (weeklyRemaining !== void 0) windows.push({
464
+ key: "week",
465
+ percent: Math.max(0, Math.min(100, 100 - weeklyRemaining)),
466
+ resetsAt: toIso(row.weekly_end_time)
467
+ });
468
+ }
469
+ if (windows.length === 0) return void 0;
470
+ return { windows };
471
+ }
472
+ }
473
+ };
474
+ }
475
+ const OPENROUTER = {
476
+ ids: ["openrouter"],
477
+ displayName: "OpenRouter",
478
+ balance: {
479
+ build: ({ apiKey }) => ({
480
+ url: "https://openrouter.ai/api/v1/credits",
481
+ headers: bearer(apiKey)
482
+ }),
483
+ parse: (status, body) => {
484
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
485
+ const data = body.data;
486
+ if (typeof data !== "object" || data === null) return void 0;
487
+ const credits = toNum(data.total_credits);
488
+ const used = toNum(data.total_usage) ?? 0;
489
+ if (credits === void 0) return void 0;
490
+ return {
491
+ currency: "USD",
492
+ totalBalance: money(credits - used)
493
+ };
494
+ }
495
+ }
496
+ };
497
+ function siliconFlow(host, ids, currency) {
498
+ return {
499
+ ids,
500
+ displayName: "SiliconFlow",
501
+ balance: {
502
+ build: ({ apiKey }) => ({
503
+ url: `https://${host}/v1/user/info`,
504
+ headers: bearer(apiKey)
505
+ }),
506
+ parse: (status, body) => {
507
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
508
+ const data = body.data;
509
+ if (typeof data !== "object" || data === null) return void 0;
510
+ const total = str(data.totalBalance);
511
+ if (total === void 0) return void 0;
512
+ return {
513
+ currency,
514
+ totalBalance: total
515
+ };
516
+ }
517
+ }
518
+ };
519
+ }
520
+ const ZENMUX = {
521
+ ids: ["zenmux"],
522
+ displayName: "ZenMux",
523
+ balance: {
524
+ build: ({ apiKey }) => ({
525
+ url: "https://zenmux.ai/api/v1/management/payg/balance",
526
+ headers: bearer(apiKey)
527
+ }),
528
+ parse: (status, body) => {
529
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
530
+ const data = body.data;
531
+ if (typeof data !== "object" || data === null) return void 0;
532
+ const credits = toNum(data.total_credits);
533
+ if (credits === void 0) return void 0;
534
+ return {
535
+ currency: "USD",
536
+ totalBalance: money(credits)
537
+ };
538
+ }
539
+ }
540
+ };
541
+ /**
542
+ * OpenAI Codex (ChatGPT subscription) quota over the OAuth access token the
543
+ * pi-ai grant stores — the one plan adapter whose credential is an OAuth
544
+ * token rather than an API key. A 401 here means the stored access token has
545
+ * expired; it refreshes when the harness next runs a Codex request, so the
546
+ * error line tells the user to use Codex once and refresh.
547
+ */
548
+ const OPENAI_CODEX = {
549
+ ids: ["openai-codex"],
550
+ displayName: "Codex (ChatGPT 订阅)",
551
+ plan: {
552
+ build: ({ apiKey, accountId }) => ({
553
+ url: "https://chatgpt.com/backend-api/wham/usage",
554
+ headers: {
555
+ authorization: `Bearer ${apiKey}`,
556
+ "user-agent": "codex-cli",
557
+ accept: "application/json",
558
+ ...accountId !== void 0 ? { "chatgpt-account-id": accountId } : {}
559
+ }
560
+ }),
561
+ parse: (status, body) => {
562
+ if (status !== 200 || typeof body !== "object" || body === null) return void 0;
563
+ const rateLimit = body.rate_limit;
564
+ if (typeof rateLimit !== "object" || rateLimit === null) return void 0;
565
+ const windows = [];
566
+ const rows = rateLimit.primary_window !== void 0 || rateLimit.secondary_window !== void 0 ? [rateLimit.primary_window, rateLimit.secondary_window] : [];
567
+ for (const entry of rows) {
568
+ if (typeof entry !== "object" || entry === null) continue;
569
+ const row = entry;
570
+ const percent = toNum(row.used_percent);
571
+ if (percent === void 0) continue;
572
+ const seconds = toNum(row.limit_window_seconds);
573
+ windows.push({
574
+ key: codexWindowKey(seconds),
575
+ percent: Math.max(0, Math.min(100, percent)),
576
+ resetsAt: toIso(row.reset_at)
577
+ });
578
+ }
579
+ if (windows.length === 0) return void 0;
580
+ return { windows };
581
+ }
582
+ }
583
+ };
584
+ /** Map a Codex window length in seconds onto the shared window key vocabulary. */
585
+ function codexWindowKey(seconds) {
586
+ if (seconds === void 0) return "window";
587
+ if (seconds === 18e3) return "5h";
588
+ if (seconds === 604800) return "week";
589
+ if (seconds === 2592e3) return "month";
590
+ const hours = seconds / 3600;
591
+ return hours >= 24 ? `${Math.round(hours / 24)}_day` : `${Math.round(hours)}_hour`;
592
+ }
593
+ /**
594
+ * The adapter registry, in no particular order. Route keys come from the
595
+ * pi-ai provider catalog plus the routes this deployment observed in user
596
+ * configuration (`zenmux`).
597
+ */
598
+ const PROVIDER_ADAPTERS = [
599
+ DEEPSEEK,
600
+ moonshotBalance("api.moonshot.cn", "CNY", ["moonshotai-cn"]),
601
+ moonshotBalance("api.moonshot.ai", "USD", ["moonshotai"]),
602
+ KIMI_CODING,
603
+ glmPlan("open.bigmodel.cn", ["zai-coding-cn"]),
604
+ glmPlan("api.z.ai", ["zai-coding"]),
605
+ OPENCODE_GO,
606
+ minimaxPlan("api.minimaxi.com", ["minimax-cn"]),
607
+ minimaxPlan("api.minimax.io", ["minimax"]),
608
+ OPENAI_CODEX,
609
+ OPENROUTER,
610
+ siliconFlow("api.siliconflow.cn", ["siliconflow", "siliconflow-cn"], "CNY"),
611
+ siliconFlow("api.siliconflow.com", ["siliconflow-intl"], "USD"),
612
+ ZENMUX
613
+ ];
614
+ /** Find the adapter serving a provider route key, if any. */
615
+ function adapterFor(provider) {
616
+ return PROVIDER_ADAPTERS.find((adapter) => adapter.ids.includes(provider));
617
+ }
618
+ /**
619
+ * Whether a provider route belongs to the official DeepSeek family: the only
620
+ * family with a spend price book and a settings-section-owned env credential
621
+ * (llm-deepseek) rather than a pi-ai profile. Drives the env fallback in
622
+ * credential resolution and the fold-time cost stamping.
623
+ */
624
+ function isDeepSeekProviderRoute(provider) {
625
+ return adapterFor(provider) === DEEPSEEK;
626
+ }
627
+ //#endregion
628
+ //#region src/core/pricing.ts
629
+ /** The published peak windows: weekday 09:00-12:00 and 14:00-18:00. */
630
+ const PEAK_WINDOWS = [{
631
+ from: 540,
632
+ to: 720
633
+ }, {
634
+ from: 840,
635
+ to: 1080
636
+ }];
637
+ /** Beijing is UTC+8 year-round (no DST), so a fixed shift is exact. */
638
+ const BEIJING_UTC_OFFSET_MS = 8 * 36e5;
639
+ function withinWindow(minuteOfDay) {
640
+ return PEAK_WINDOWS.find((window) => minuteOfDay >= window.from && minuteOfDay < window.to);
641
+ }
642
+ /**
643
+ * The DeepSeek billing period at `ms`, plus when it next flips. The clock is
644
+ * Beijing time regardless of the host timezone (UTC+8 has no DST, so a fixed
645
+ * shift is exact). `boundaryMs` is the instant the current period ends — the
646
+ * window's close while peaking, the next window's open otherwise.
647
+ */
648
+ function deepseekPeriodAt(ms) {
649
+ const shifted = new Date(ms + BEIJING_UTC_OFFSET_MS);
650
+ const weekday = shifted.getUTCDay();
651
+ const minuteOfDay = shifted.getUTCHours() * 60 + shifted.getUTCMinutes();
652
+ const current = weekday >= 1 && weekday <= 5 ? withinWindow(minuteOfDay) : void 0;
653
+ if (current !== void 0) return {
654
+ peak: true,
655
+ boundaryMs: ms + (current.to - minuteOfDay) * 6e4 - shifted.getUTCSeconds() * 1e3 - shifted.getUTCMilliseconds()
656
+ };
657
+ for (let dayOffset = 0; dayOffset < 8; dayOffset += 1) {
658
+ const day = new Date(ms + BEIJING_UTC_OFFSET_MS + dayOffset * 864e5);
659
+ if (day.getUTCDay() < 1 || day.getUTCDay() > 5) continue;
660
+ const realDayStart = Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate()) - BEIJING_UTC_OFFSET_MS;
661
+ for (const window of PEAK_WINDOWS) {
662
+ if (dayOffset === 0 && window.from <= minuteOfDay) continue;
663
+ return {
664
+ peak: false,
665
+ boundaryMs: realDayStart + window.from * 6e4
666
+ };
667
+ }
668
+ }
669
+ return {
670
+ peak: false,
671
+ boundaryMs: ms + 864e5
672
+ };
673
+ }
674
+ //#endregion
199
675
  //#region src/client/UsageSectionCard.tsx
200
676
  /**
201
677
  * The usage statistics settings section: two tabs (用量: today's usage,
@@ -225,6 +701,20 @@ window.__ModuleLoader__.load({
225
701
  return "";
226
702
  }
227
703
  }
704
+ function formatClock(ms) {
705
+ try {
706
+ return new Date(ms).toLocaleTimeString([], {
707
+ hour: "2-digit",
708
+ minute: "2-digit"
709
+ });
710
+ } catch {
711
+ return "";
712
+ }
713
+ }
714
+ /** Formatted CNY spend estimate (the only priced currency today). */
715
+ function formatCost(cost) {
716
+ return "¥" + cost.toFixed(2);
717
+ }
228
718
  function toneClass(percent) {
229
719
  if (percent >= 90) return usage_module_css_default.barLow;
230
720
  if (percent >= 70) return usage_module_css_default.barWarn;
@@ -369,10 +859,6 @@ window.__ModuleLoader__.load({
369
859
  };
370
860
  }, [poll]);
371
861
  const snapshot = ui.snapshot;
372
- const trendMax = (0, react.useMemo)(() => {
373
- if (snapshot === null) return 0;
374
- return Math.max(1, ...snapshot.usage.days.map((day) => day.totals.inputTokens + day.totals.outputTokens + day.totals.cacheReadTokens + day.totals.cacheWriteTokens));
375
- }, [snapshot]);
376
862
  const onRefresh = () => {
377
863
  setRefreshing(true);
378
864
  try {
@@ -393,7 +879,9 @@ window.__ModuleLoader__.load({
393
879
  });
394
880
  const current = snapshot.current;
395
881
  const currentProvider = snapshot.providers.find((provider) => provider.provider === current.provider);
396
- const planProviders = snapshot.providers.filter((provider) => provider.plan !== void 0 || provider.supported && provider.credential !== "none" && provider.credential !== "oauth");
882
+ const deepseekPeriod = deepseekPeriodAt(Date.now());
883
+ const deepseekVisible = current.provider !== void 0 && isDeepSeekProviderRoute(current.provider) || snapshot.usage.today.providers.some((row) => isDeepSeekProviderRoute(row.provider));
884
+ const planProviders = snapshot.providers.filter((provider) => provider.planSupported === true || provider.planSupported === void 0 && provider.plan !== void 0);
397
885
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
398
886
  className: usage_module_css_default.section,
399
887
  "data-dsh-plugin": "usage",
@@ -451,10 +939,26 @@ window.__ModuleLoader__.load({
451
939
  className: usage_module_css_default.cardTitle,
452
940
  children: t("usage.today")
453
941
  }),
942
+ deepseekVisible && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
943
+ className: usage_module_css_default.muted,
944
+ "data-dsh-part": "peak-status",
945
+ children: t(deepseekPeriod.peak ? "usage.peak.on" : "usage.peak.off", { time: formatClock(deepseekPeriod.boundaryMs) })
946
+ }),
454
947
  snapshot.usage.today.totals.calls === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
455
948
  className: usage_module_css_default.muted,
456
949
  children: t("usage.noData")
457
950
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TotalsRow, { totals: snapshot.usage.today.totals }),
951
+ snapshot.usage.today.totals.cost > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
952
+ className: usage_module_css_default.providerRow,
953
+ "data-dsh-part": "today-cost",
954
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
955
+ className: usage_module_css_default.providerName,
956
+ children: t("usage.today.cost")
957
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
958
+ className: usage_module_css_default.providerTokens,
959
+ children: formatCost(snapshot.usage.today.totals.cost)
960
+ })]
961
+ }),
458
962
  snapshot.usage.today.providers.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
459
963
  "data-dsh-part": "provider-list",
460
964
  children: snapshot.usage.today.providers.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -470,7 +974,8 @@ window.__ModuleLoader__.load({
470
974
  children: [
471
975
  formatTokens(row.totals.inputTokens + row.totals.cacheReadTokens + row.totals.cacheWriteTokens + row.totals.outputTokens),
472
976
  " · ",
473
- t("usage.calls", { n: row.totals.calls })
977
+ t("usage.calls", { n: row.totals.calls }),
978
+ row.totals.cost > 0 ? ` · ${formatCost(row.totals.cost)}` : ""
474
979
  ]
475
980
  })]
476
981
  }, row.provider))
@@ -485,43 +990,27 @@ window.__ModuleLoader__.load({
485
990
  className: usage_module_css_default.cardTitle,
486
991
  children: t("usage.balance")
487
992
  }),
488
- snapshot.providers.filter((provider) => provider.supported).length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
489
- className: usage_module_css_default.muted,
490
- children: t("usage.balance.unsupported")
491
- }) : snapshot.providers.filter((provider) => provider.supported).map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderRow, {
492
- provider,
493
- current: current.provider
494
- }, provider.provider)),
993
+ (() => {
994
+ const rows = snapshot.providers.filter((provider) => provider.balanceSupported === true || provider.balanceSupported === void 0 && provider.supported);
995
+ if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
996
+ className: usage_module_css_default.muted,
997
+ children: t("usage.balance.unsupported")
998
+ });
999
+ return rows.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ProviderRow, {
1000
+ provider,
1001
+ current: current.provider
1002
+ }, provider.provider));
1003
+ })(),
495
1004
  snapshot.providers.some((provider) => provider.error !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
496
1005
  className: usage_module_css_default.errorLine,
497
- children: snapshot.providers.filter((provider) => provider.error !== void 0).map((provider) => `${provider.displayName}: ${t("usage.provider.error", { error: provider.error ?? "" })}`).join(";")
1006
+ children: snapshot.providers.filter((provider) => provider.error !== void 0).map((provider) => `${provider.displayName}: ${t("usage.provider.error", { error: provider.error ?? "" })}`).join(t("usage.errorListSeparator"))
498
1007
  })
499
1008
  ]
500
1009
  }),
501
- snapshot.usage.days.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
502
- className: usage_module_css_default.card,
503
- "data-dsh-part": "trend-card",
504
- children: [
505
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
506
- className: usage_module_css_default.cardTitle,
507
- children: t("usage.trend")
508
- }),
509
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
510
- className: usage_module_css_default.trend,
511
- children: snapshot.usage.days.map((day, index) => {
512
- const total = day.totals.inputTokens + day.totals.outputTokens + day.totals.cacheReadTokens + day.totals.cacheWriteTokens;
513
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
514
- className: index === snapshot.usage.days.length - 1 ? `${usage_module_css_default.trendBar} ${usage_module_css_default.trendBarToday}` : usage_module_css_default.trendBar,
515
- style: { height: `${Math.max(3, Math.round(total / trendMax * 100))}%` },
516
- title: `${day.date}: ${formatTokens(total)}`
517
- }, day.date);
518
- })
519
- }),
520
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
521
- className: usage_module_css_default.trendAxis,
522
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: snapshot.usage.days[0]?.date.slice(5) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: snapshot.usage.days[snapshot.usage.days.length - 1]?.date.slice(5) })]
523
- })
524
- ]
1010
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RangeCard, {
1011
+ range: snapshot.usage.range,
1012
+ providers: snapshot.providers,
1013
+ currentProvider: current.provider
525
1014
  }),
526
1015
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsRow, {
527
1016
  settings,
@@ -533,14 +1022,116 @@ window.__ModuleLoader__.load({
533
1022
  className: usage_module_css_default.card,
534
1023
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
535
1024
  className: usage_module_css_default.muted,
536
- children: t("usage.plan.noPlan")
1025
+ children: t("usage.plan.noneConfigured")
537
1026
  })
538
- }) : planProviders.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanCard, { provider }, provider.provider)))
1027
+ }) : planProviders.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanCard, {
1028
+ provider,
1029
+ current: current.provider
1030
+ }, provider.provider)))
539
1031
  ]
540
1032
  });
541
1033
  }
1034
+ /** How many model sub-bars render under one provider bar. */
1035
+ const CHART_MODEL_CAP = 3;
1036
+ /**
1037
+ * The 近 30 天 card: horizontal bars per provider over the trend window,
1038
+ * each with its heaviest models as nested sub-bars. Window totals come from
1039
+ * the host's aggregated `usage.range` (an older host without it renders no
1040
+ * card instead of a wrong one).
1041
+ */
1042
+ function RangeCard(props) {
1043
+ const { range, providers, currentProvider } = props;
1044
+ if (range === void 0) return null;
1045
+ const grandTotal = range.totals.inputTokens + range.totals.outputTokens + range.totals.cacheReadTokens + range.totals.cacheWriteTokens;
1046
+ const maxProvider = Math.max(1, ...range.providers.map((row) => totalOf(row.totals)));
1047
+ const nameOf = (id) => providers.find((provider) => provider.provider === id)?.displayName ?? id;
1048
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1049
+ className: usage_module_css_default.card,
1050
+ "data-dsh-part": "trend-card",
1051
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1052
+ className: usage_module_css_default.cardTitle,
1053
+ children: t("usage.trend")
1054
+ }), range.providers.length === 0 || grandTotal === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1055
+ className: usage_module_css_default.muted,
1056
+ children: t("usage.noData")
1057
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1058
+ className: usage_module_css_default.chart,
1059
+ "data-dsh-part": "usage-chart",
1060
+ children: [range.providers.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChartProviderRow, {
1061
+ row,
1062
+ name: nameOf(row.provider),
1063
+ max: maxProvider,
1064
+ current: currentProvider === row.provider
1065
+ }, row.provider)), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1066
+ className: usage_module_css_default.trendAxis,
1067
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: range.from.slice(5) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: range.to.slice(5) })]
1068
+ })]
1069
+ })]
1070
+ });
1071
+ }
1072
+ function ChartProviderRow(props) {
1073
+ const { row, name, max, current } = props;
1074
+ const total = totalOf(row.totals);
1075
+ const maxModel = Math.max(1, ...row.models.slice(0, CHART_MODEL_CAP).map((model) => totalOf(model.totals)));
1076
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1077
+ className: usage_module_css_default.chartProvider,
1078
+ children: [
1079
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1080
+ className: usage_module_css_default.chartHead,
1081
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1082
+ className: usage_module_css_default.providerName,
1083
+ children: [name, current && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1084
+ className: usage_module_css_default.currentBadge,
1085
+ children: t("usage.current")
1086
+ })]
1087
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1088
+ className: usage_module_css_default.chartTokens,
1089
+ children: [
1090
+ formatTokens(total),
1091
+ " · ",
1092
+ t("usage.calls", { n: row.totals.calls })
1093
+ ]
1094
+ })]
1095
+ }),
1096
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1097
+ className: usage_module_css_default.chartBar,
1098
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1099
+ className: usage_module_css_default.chartFill,
1100
+ style: { width: `${Math.max(2, Math.round(total / max * 100))}%` }
1101
+ })
1102
+ }),
1103
+ row.models.slice(0, CHART_MODEL_CAP).map((model) => {
1104
+ const modelTotal = totalOf(model.totals);
1105
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1106
+ className: usage_module_css_default.chartModel,
1107
+ title: `${row.provider} · ${model.model}: ${formatTokens(modelTotal)}`,
1108
+ children: [
1109
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1110
+ className: usage_module_css_default.chartModelName,
1111
+ children: model.model
1112
+ }),
1113
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1114
+ className: usage_module_css_default.chartModelBar,
1115
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1116
+ className: usage_module_css_default.chartModelFill,
1117
+ style: { width: `${Math.max(3, Math.round(modelTotal / maxModel * 100))}%` }
1118
+ })
1119
+ }),
1120
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1121
+ className: usage_module_css_default.chartTokens,
1122
+ children: formatTokens(modelTotal)
1123
+ })
1124
+ ]
1125
+ }, model.model);
1126
+ })
1127
+ ]
1128
+ });
1129
+ }
1130
+ function totalOf(totals) {
1131
+ return totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens + totals.outputTokens;
1132
+ }
542
1133
  function PlanCard(props) {
543
- const { provider } = props;
1134
+ const { provider, current } = props;
544
1135
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
545
1136
  className: `${usage_module_css_default.card} ${usage_module_css_default.planCard}`,
546
1137
  "data-dsh-part": "plan-card",
@@ -549,14 +1140,24 @@ window.__ModuleLoader__.load({
549
1140
  className: usage_module_css_default.planHead,
550
1141
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
551
1142
  className: usage_module_css_default.planName,
552
- children: [provider.displayName, provider.plan?.planName !== void 0 ? ` · ${provider.plan.planName}` : ""]
1143
+ children: [
1144
+ provider.displayName,
1145
+ current === provider.provider && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1146
+ className: usage_module_css_default.currentBadge,
1147
+ children: t("usage.current")
1148
+ }),
1149
+ provider.plan?.planName !== void 0 ? ` · ${provider.plan.planName}` : ""
1150
+ ]
553
1151
  })
554
1152
  }),
555
1153
  provider.error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
556
1154
  className: usage_module_css_default.errorLine,
557
1155
  children: t("usage.provider.error", { error: provider.error })
558
1156
  }),
559
- provider.plan === void 0 || provider.plan.windows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1157
+ provider.credential === "none" && provider.plan === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1158
+ className: usage_module_css_default.muted,
1159
+ children: t("usage.balance.noCredential")
1160
+ }) : provider.plan === void 0 || provider.plan.windows.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
560
1161
  className: usage_module_css_default.muted,
561
1162
  children: t("usage.plan.noPlan")
562
1163
  }) : provider.plan.windows.map((window) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -658,8 +1259,13 @@ window.__ModuleLoader__.load({
658
1259
  }
659
1260
  //#endregion
660
1261
  //#region src/client/index.ts
1262
+ /** Hard ceiling for one usage API call; a stalled host must not pile up requests. */
1263
+ const USAGE_FETCH_TIMEOUT_MS = 2e4;
661
1264
  async function usageFetch(path, method) {
662
- const response = await fetch(path, method === "POST" ? { method: "POST" } : {});
1265
+ const response = await fetch(path, {
1266
+ ...method === "POST" ? { method: "POST" } : {},
1267
+ signal: AbortSignal.timeout(USAGE_FETCH_TIMEOUT_MS)
1268
+ });
663
1269
  if (!response.ok) throw new Error("usage " + path + " failed: " + response.status);
664
1270
  return await response.json();
665
1271
  }
@@ -714,7 +1320,7 @@ window.__ModuleLoader__.load({
714
1320
  const seq = pollSeq + 1;
715
1321
  pollSeq = seq;
716
1322
  usageApi.refresh().then((snapshot) => {
717
- if (seq !== pollSeq) return;
1323
+ pollSeq = seq;
718
1324
  store.actions.setSnapshot(snapshot);
719
1325
  }, () => {
720
1326
  if (seq !== pollSeq) return;