@linxin666/dsh-usage 0.3.7 → 0.3.9

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