@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/README.i18n.yaml +4 -3
- package/README.md +10 -4
- package/README.zh.md +10 -4
- package/lib/client.js +652 -48
- package/lib/client.js.map +1 -1
- package/lib/index.js +560 -368
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/core/adapters.d.ts +10 -1
- package/lib/types/core/ledger.d.ts +15 -2
- package/lib/types/core/pricing.d.ts +30 -0
- package/lib/types/core/types.d.ts +34 -1
- package/lib/types/host/usage-service.d.ts +72 -11
- package/lib/types/index.d.ts +2 -1
- package/package.json +15 -15
- package/src/client/UsageSectionCard.tsx +138 -55
- package/src/client/index.ts +11 -2
- package/src/client/locales.ts +8 -0
- package/src/client/usage.module.css +71 -12
- package/src/core/adapters.ts +84 -3
- package/src/core/ledger.ts +65 -3
- package/src/core/pricing.ts +109 -0
- package/src/core/types.ts +37 -2
- package/src/host/usage-service.ts +300 -131
- package/src/index.ts +30 -17
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}.
|
|
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
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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.
|
|
1023
|
+
children: t("usage.plan.noneConfigured")
|
|
537
1024
|
})
|
|
538
|
-
}) : planProviders.map((provider) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PlanCard, {
|
|
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: [
|
|
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.
|
|
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,
|
|
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
|
-
|
|
1321
|
+
pollSeq = seq;
|
|
718
1322
|
store.actions.setSnapshot(snapshot);
|
|
719
1323
|
}, () => {
|
|
720
1324
|
if (seq !== pollSeq) return;
|