@ychris12138/dsh-usage-stats 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -23
- package/SECURITY.md +1 -1
- package/docs/release-checklist.md +23 -11
- package/docs/release-notes-v0.3.1.md +40 -0
- package/docs/release-notes-v0.3.2.md +36 -0
- package/lib/accounts.js +3 -1
- package/lib/balance.js +115 -12
- package/lib/client.js +159 -317
- package/lib/index.js +267 -47
- package/lib/orcarouter.js +79 -0
- package/lib/provider-identity.js +3 -0
- package/package.json +6 -3
package/lib/client.js
CHANGED
|
@@ -31,15 +31,6 @@ window.__ModuleLoader__.load({
|
|
|
31
31
|
".usg_badgeOk{color:var(--dsw-alias-state-success-primary)}",
|
|
32
32
|
".usg_badgeBad{color:var(--dsw-alias-state-error-primary)}",
|
|
33
33
|
".usg_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}",
|
|
34
|
-
".usg_sessionPill{box-sizing:border-box;max-width:220px;height:28px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-fill-l1,transparent);border:1px solid var(--dsw-alias-border-l2);border-radius:999px;align-items:center;gap:5px;padding:0 8px;font:inherit;font-size:11px;line-height:16px;cursor:pointer;display:inline-flex;white-space:nowrap;overflow:hidden}",
|
|
35
|
-
".usg_sessionPill:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
36
|
-
".usg_sessionPill:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}",
|
|
37
|
-
".usg_sessionPill[data-tone=normal]{color:var(--dsw-alias-label-secondary)}",
|
|
38
|
-
".usg_sessionPill[data-tone=warning]{color:var(--dsw-alias-state-warn-label,var(--dsw-alias-state-warn-primary));border-color:var(--dsw-alias-state-warn-secondary,var(--dsw-alias-border-l2))}",
|
|
39
|
-
".usg_sessionPill[data-tone=critical]{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-secondary,var(--dsw-alias-border-l2))}",
|
|
40
|
-
".usg_sessionPillProvider{max-width:88px;color:var(--dsw-alias-label-primary);font-weight:500;text-overflow:ellipsis;overflow:hidden}",
|
|
41
|
-
".usg_sessionPillSeparator{color:var(--dsw-alias-label-caption);flex:none}",
|
|
42
|
-
".usg_sessionPillValue{max-width:112px;font-variant-numeric:tabular-nums;text-overflow:ellipsis;overflow:hidden}",
|
|
43
34
|
".usg_layer.usg_rail{width:36px;height:36px;margin:0}",
|
|
44
35
|
".usg_layer.usg_rail .usg_badge{border-radius:50%;justify-content:center;gap:0;width:36px;height:36px;padding:0}",
|
|
45
36
|
".usg_layer.usg_rail .usg_footerButtons{flex-direction:column;gap:2px}",
|
|
@@ -77,6 +68,7 @@ window.__ModuleLoader__.load({
|
|
|
77
68
|
".usg_accountCard[data-provider=ollama],.usg_accountCard[data-adapter=ollama]{--usg-providerAccent:#d97706}",
|
|
78
69
|
".usg_accountCard[data-provider=zai],.usg_accountCard[data-provider=zai-coding-cn]{--usg-providerAccent:#7656e8}",
|
|
79
70
|
".usg_accountCard[data-provider=openrouter]{--usg-providerAccent:#6366f1}",
|
|
71
|
+
".usg_accountCard[data-provider=orcarouter]{--usg-providerAccent:#0891b2}",
|
|
80
72
|
".usg_accountCard[data-provider=moonshotai],.usg_accountCard[data-provider=moonshotai-cn],.usg_accountCard[data-provider=kimi],.usg_accountCard[data-provider=kimi-coding]{--usg-providerAccent:#e07a1f}",
|
|
81
73
|
".usg_accountHead{align-items:center;gap:8px;display:flex}",
|
|
82
74
|
".usg_accountMark{width:24px;height:24px;color:#fff;background:var(--usg-providerAccent);border-radius:7px;justify-content:center;align-items:center;font-size:10px;font-weight:700;display:flex;box-shadow:0 4px 12px color-mix(in srgb,var(--usg-providerAccent) 25%,transparent)}",
|
|
@@ -131,8 +123,8 @@ window.__ModuleLoader__.load({
|
|
|
131
123
|
".usg_day{width:100%;min-height:30px;align-items:center;gap:8px;border:0;background:0 0;border-bottom:1px solid var(--dsw-alias-border-l1);padding:5px 0;font:inherit;text-align:left;cursor:pointer;display:flex}",
|
|
132
124
|
".usg_day:last-child{border-bottom:0}",
|
|
133
125
|
".usg_day:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
134
|
-
".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:
|
|
135
|
-
".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
|
|
126
|
+
".usg_dayDate{color:var(--dsw-alias-label-secondary);flex:0 1 104px;min-width:0;text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums;overflow:hidden}",
|
|
127
|
+
".usg_dayTokens{color:var(--dsw-alias-label-primary);flex:none;min-width:84px;text-align:right;font-size:12px;line-height:18px;font-variant-numeric:tabular-nums}",
|
|
136
128
|
".usg_dayHit{color:var(--dsw-alias-label-tertiary);flex:none;width:52px;font-size:11px;line-height:18px;font-variant-numeric:tabular-nums;text-align:right}",
|
|
137
129
|
".usg_dayBar{background:var(--usg-blue);border-radius:2px;height:6px;flex:1;min-width:4px;opacity:.65}",
|
|
138
130
|
".usg_detailHeader{align-items:center;gap:8px;display:flex}",
|
|
@@ -170,10 +162,6 @@ window.__ModuleLoader__.load({
|
|
|
170
162
|
badgeOk: "usg_badgeOk",
|
|
171
163
|
badgeBad: "usg_badgeBad",
|
|
172
164
|
badgeCount: "usg_badgeCount",
|
|
173
|
-
sessionPill: "usg_sessionPill",
|
|
174
|
-
sessionPillProvider: "usg_sessionPillProvider",
|
|
175
|
-
sessionPillSeparator: "usg_sessionPillSeparator",
|
|
176
|
-
sessionPillValue: "usg_sessionPillValue",
|
|
177
165
|
panel: "usg_panel",
|
|
178
166
|
header: "usg_header",
|
|
179
167
|
headerLeft: "usg_headerLeft",
|
|
@@ -454,201 +442,66 @@ window.__ModuleLoader__.load({
|
|
|
454
442
|
})) : [];
|
|
455
443
|
}
|
|
456
444
|
|
|
445
|
+
const ORCAROUTER_ADD_SENTINEL = "__add_orcarouter__";
|
|
446
|
+
const ORCAROUTER_INTEGRATION_PATH = "/api/usage-stats/integrations/orcarouter";
|
|
447
|
+
|
|
448
|
+
/** Append the explicit setup action without changing the real provider list. */
|
|
449
|
+
function buildProviderPickerChoices(providers, integration) {
|
|
450
|
+
if (!Array.isArray(providers)) return [];
|
|
451
|
+
if (providers.some((provider) => provider?.id === "orcarouter")) return providers;
|
|
452
|
+
if (integration?.available !== true || integration?.installed !== false) return providers;
|
|
453
|
+
return [...providers, {
|
|
454
|
+
id: ORCAROUTER_ADD_SENTINEL,
|
|
455
|
+
displayName: "OrcaRouter",
|
|
456
|
+
accountMode: "balance",
|
|
457
|
+
synthetic: true
|
|
458
|
+
}];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** Localized selector label; sponsorship is a small disclosure, not a card. */
|
|
462
|
+
function providerChoiceLabel(provider, translate) {
|
|
463
|
+
const label = typeof provider?.displayName === "string" && provider.displayName.trim() !== ""
|
|
464
|
+
? provider.displayName
|
|
465
|
+
: String(provider?.id ?? "");
|
|
466
|
+
if (provider?.id !== "orcarouter" && provider?.id !== ORCAROUTER_ADD_SENTINEL) return label;
|
|
467
|
+
const suffix = typeof translate === "function" ? translate("orcarouter.choiceSuffix") : "";
|
|
468
|
+
return typeof suffix === "string" && suffix !== "" ? `${label}${suffix}` : label;
|
|
469
|
+
}
|
|
470
|
+
|
|
457
471
|
/** Locale-safe template interpolation: `t("key", {a})` replaces `{a}`. */
|
|
458
472
|
function interpolate(template, params) {
|
|
459
473
|
if (params === void 0) return template;
|
|
460
474
|
return template.replace(/\{(\w+)\}/g, (match, key) => (Object.hasOwn(params, key) ? String(params[key]) : match));
|
|
461
475
|
}
|
|
462
476
|
|
|
463
|
-
async function fetchJson(path) {
|
|
464
|
-
const response = await fetch(path, {
|
|
477
|
+
async function fetchJson(path, options = {}) {
|
|
478
|
+
const response = await fetch(path, {
|
|
479
|
+
...options,
|
|
480
|
+
headers: { accept: "application/json", ...options.headers }
|
|
481
|
+
});
|
|
465
482
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
466
483
|
const payload = await response.json();
|
|
467
484
|
if (payload === null || typeof payload !== "object") throw new Error("unexpected response");
|
|
468
485
|
return payload;
|
|
469
486
|
}
|
|
470
487
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
* the server-side currentRoute can change, without reacting to every input
|
|
476
|
-
* keystroke or adding a timer/DOM observer.
|
|
477
|
-
*/
|
|
478
|
-
function sessionContextSignalOf(session) {
|
|
479
|
-
const nodes = Array.isArray(session?.nodes) ? session.nodes.length : 0;
|
|
480
|
-
const order = Array.isArray(session?.chat?.order) ? session.chat.order.length : 0;
|
|
481
|
-
return `${session?.running === true ? 1 : 0}:${nodes}:${order}:${session?.partial === null || session?.partial === void 0 ? 0 : 1}:${session?.removed === true ? 1 : 0}`;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
/** Event signature from DSH's existing per-session model selector store. */
|
|
485
|
-
function modelSelectionSignalOf(snapshot) {
|
|
486
|
-
const provider = typeof snapshot?.current?.provider === "string" ? snapshot.current.provider : "";
|
|
487
|
-
const model = typeof snapshot?.current?.model === "string" ? snapshot.current.model : "";
|
|
488
|
-
return JSON.stringify([provider, model]);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/** Stable signature for DSH's native cumulative tokenUsage projection. */
|
|
492
|
-
function tokenUsageSignalOf(projection) {
|
|
493
|
-
return JSON.stringify([
|
|
494
|
-
projection?.uncachedInputTokens,
|
|
495
|
-
projection?.outputTokens,
|
|
496
|
-
projection?.cacheReadTokens,
|
|
497
|
-
projection?.cacheWriteTokens
|
|
498
|
-
]);
|
|
488
|
+
async function loadOrcaRouterIntegration(request = fetchJson) {
|
|
489
|
+
const payload = await request(ORCAROUTER_INTEGRATION_PATH);
|
|
490
|
+
if (payload?.ok !== true || payload.integration === null || typeof payload.integration !== "object") throw new Error("integration status unavailable");
|
|
491
|
+
return payload.integration;
|
|
499
492
|
}
|
|
500
493
|
|
|
501
|
-
function
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
}
|
|
513
|
-
return buckets;
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* The browser never prices cumulative tokens. It displays the server's
|
|
518
|
-
* event-time estimate only when its folded buckets exactly match DSH's
|
|
519
|
-
* native current-session projection.
|
|
520
|
-
*/
|
|
521
|
-
function currentSessionCostLabel(session, projection) {
|
|
522
|
-
const buckets = projectionBucketsOf(projection);
|
|
523
|
-
if (session === null || typeof session !== "object" || buckets === null) return "—";
|
|
524
|
-
for (const key of ["inputTokens", "outputTokens", "cacheReadTokens", "cacheWriteTokens"]) {
|
|
525
|
-
if (session[key] !== buckets[key]) return "—";
|
|
526
|
-
}
|
|
527
|
-
if (session.costComplete !== true || typeof session.estimatedCost !== "number" || !Number.isFinite(session.estimatedCost)
|
|
528
|
-
|| typeof session.currency !== "string" || session.currency === "") return "—";
|
|
529
|
-
return `≈${fmtCurrency(session.estimatedCost, session.currency)}`;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/**
|
|
533
|
-
* Resolve one explicit DSH session through the server-owned identity and
|
|
534
|
-
* account protocols. The account endpoint reads AccountService's existing
|
|
535
|
-
* cache; this function owns no provider mapping, credential, or cache.
|
|
536
|
-
*/
|
|
537
|
-
async function loadSessionPillSnapshot(sessionId, request = fetchJson, selectedRoute = null) {
|
|
538
|
-
if (typeof sessionId !== "string" || sessionId === "") return null;
|
|
539
|
-
const selectionQuery = typeof selectedRoute?.provider === "string" && selectedRoute.provider !== ""
|
|
540
|
-
&& typeof selectedRoute.model === "string" && selectedRoute.model !== ""
|
|
541
|
-
? `&provider=${encodeURIComponent(selectedRoute.provider)}&model=${encodeURIComponent(selectedRoute.model)}`
|
|
542
|
-
: "";
|
|
543
|
-
const contextPayload = await request(`/api/usage-stats/session-context?session=${encodeURIComponent(sessionId)}${selectionQuery}`);
|
|
544
|
-
if (contextPayload?.ok !== true || contextPayload.context === null || typeof contextPayload.context !== "object") return null;
|
|
545
|
-
const context = contextPayload.context;
|
|
546
|
-
const accountId = typeof context.accountId === "string" && context.accountId !== ""
|
|
547
|
-
? context.accountId
|
|
548
|
-
: typeof context.providerId === "string" && context.providerId !== "" ? context.providerId : null;
|
|
549
|
-
if (accountId === null) return null;
|
|
550
|
-
try {
|
|
551
|
-
const accountPayload = await request(`/api/usage-stats/account?provider=${encodeURIComponent(accountId)}&activity=active`);
|
|
552
|
-
if (accountPayload?.ok === true && accountPayload.account !== null && typeof accountPayload.account === "object") {
|
|
553
|
-
return { context, account: accountPayload.account };
|
|
554
|
-
}
|
|
555
|
-
return {
|
|
556
|
-
context,
|
|
557
|
-
account: null,
|
|
558
|
-
status: accountPayload?.error === "unknown-provider" ? "unsupported" : "unavailable"
|
|
559
|
-
};
|
|
560
|
-
} catch {
|
|
561
|
-
return { context, account: null, status: "unavailable" };
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
function percentLabel(value) {
|
|
566
|
-
return `${value.toFixed(value % 1 === 0 ? 0 : 1)}%`;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/** Reduce a server snapshot to display-only fields; never retain secrets. */
|
|
570
|
-
function sessionPillViewOf(snapshot, translate, now = Date.now()) {
|
|
571
|
-
if (snapshot === null || snapshot === void 0 || snapshot.context === null || typeof snapshot.context !== "object") return null;
|
|
572
|
-
const account = snapshot.account !== null && typeof snapshot.account === "object" ? snapshot.account : null;
|
|
573
|
-
const providerId = typeof snapshot.context.accountId === "string" && snapshot.context.accountId !== ""
|
|
574
|
-
? snapshot.context.accountId
|
|
575
|
-
: typeof snapshot.context.providerId === "string" && snapshot.context.providerId !== "" ? snapshot.context.providerId : null;
|
|
576
|
-
if (providerId === null) return null;
|
|
577
|
-
const providerLabel = typeof account?.displayName === "string" && account.displayName !== ""
|
|
578
|
-
? account.displayName
|
|
579
|
-
: typeof snapshot.context.providerId === "string" && snapshot.context.providerId !== "" ? snapshot.context.providerId : providerId;
|
|
580
|
-
let status = typeof account?.status === "string" ? account.status : typeof snapshot.status === "string" ? snapshot.status : "unavailable";
|
|
581
|
-
const costLabel = currentSessionCostLabel(snapshot.context.session, snapshot.tokenUsage);
|
|
582
|
-
if (account?.stale === true && status === "ok") status = "unavailable";
|
|
583
|
-
if (status !== "ok") {
|
|
584
|
-
const accountValue = status === "unsupported"
|
|
585
|
-
? translate("sessionPill.status.unsupported")
|
|
586
|
-
: subscriptionStatusLabel(status, translate);
|
|
587
|
-
const value = `${accountValue} · ${costLabel}`;
|
|
588
|
-
return {
|
|
589
|
-
providerId,
|
|
590
|
-
providerLabel,
|
|
591
|
-
status,
|
|
592
|
-
value,
|
|
593
|
-
tone: "neutral",
|
|
594
|
-
ariaLabel: translate("sessionPill.aria", { provider: providerLabel, value })
|
|
595
|
-
};
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
let value = null;
|
|
599
|
-
let auxiliaryLabel = "";
|
|
600
|
-
if (account?.mode === "subscription") {
|
|
601
|
-
const windows = Array.isArray(account.windows)
|
|
602
|
-
? account.windows.filter((entry) => entry !== null && typeof entry === "object" && typeof entry.remainingPercent === "number" && Number.isFinite(entry.remainingPercent))
|
|
603
|
-
: [];
|
|
604
|
-
const tightest = windows.reduce((current, entry) => current === null || entry.remainingPercent < current.remainingPercent ? entry : current, null);
|
|
605
|
-
if (tightest !== null) {
|
|
606
|
-
value = `${quotaLabel(tightest.kind, translate)} · ${percentLabel(tightest.remainingPercent)}`;
|
|
607
|
-
auxiliaryLabel = resetLabel(tightest.resetsAt, translate, now);
|
|
608
|
-
}
|
|
609
|
-
} else if (account?.balance?.unlimited === true) {
|
|
610
|
-
value = "∞";
|
|
611
|
-
} else if (account?.balance !== null && account?.balance !== void 0 && account.balance.remaining !== null && account.balance.remaining !== void 0 && Number.isFinite(Number(account.balance.remaining))) {
|
|
612
|
-
value = fmtCurrency(account.balance.remaining, account.balance.currency);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (value === null) {
|
|
616
|
-
const invalid = translate("account.status.invalidResponse");
|
|
617
|
-
const invalidValue = `${invalid} · ${costLabel}`;
|
|
618
|
-
return {
|
|
619
|
-
providerId,
|
|
620
|
-
providerLabel,
|
|
621
|
-
status: "invalid-response",
|
|
622
|
-
value: invalidValue,
|
|
623
|
-
tone: "neutral",
|
|
624
|
-
ariaLabel: translate("sessionPill.aria", { provider: providerLabel, value: invalidValue })
|
|
625
|
-
};
|
|
626
|
-
}
|
|
627
|
-
const alertLevel = account?.alert?.level;
|
|
628
|
-
const tone = alertLevel === "normal" || alertLevel === "warning" || alertLevel === "critical" ? alertLevel : "neutral";
|
|
629
|
-
value = `${value} · ${costLabel}`;
|
|
630
|
-
const accessibleValue = auxiliaryLabel === "" ? value : `${value} · ${auxiliaryLabel}`;
|
|
631
|
-
return {
|
|
632
|
-
providerId,
|
|
633
|
-
providerLabel,
|
|
634
|
-
status,
|
|
635
|
-
value,
|
|
636
|
-
tone,
|
|
637
|
-
ariaLabel: translate("sessionPill.aria", { provider: providerLabel, value: accessibleValue })
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const usageStatsPanelOpeners = /* @__PURE__ */ new Set();
|
|
642
|
-
|
|
643
|
-
function subscribeUsageStatsPanel(opener) {
|
|
644
|
-
if (typeof opener !== "function") return () => {};
|
|
645
|
-
usageStatsPanelOpeners.add(opener);
|
|
646
|
-
return () => usageStatsPanelOpeners.delete(opener);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function requestUsageStatsPanel(providerId) {
|
|
650
|
-
const openers = [...usageStatsPanelOpeners];
|
|
651
|
-
openers[openers.length - 1]?.(providerId);
|
|
494
|
+
async function addOrcaRouterIntegration(request = fetchJson) {
|
|
495
|
+
const payload = await request(ORCAROUTER_INTEGRATION_PATH, {
|
|
496
|
+
method: "POST",
|
|
497
|
+
headers: {
|
|
498
|
+
"content-type": "application/json",
|
|
499
|
+
"x-dsh-usage-stats-action": "add-orcarouter"
|
|
500
|
+
},
|
|
501
|
+
body: "{}"
|
|
502
|
+
});
|
|
503
|
+
if (payload?.ok !== true || payload.integration?.installed !== true) throw new Error("provider preset was not added");
|
|
504
|
+
return payload.integration;
|
|
652
505
|
}
|
|
653
506
|
|
|
654
507
|
function enableFooterActionWrapping(host) {
|
|
@@ -663,69 +516,6 @@ window.__ModuleLoader__.load({
|
|
|
663
516
|
};
|
|
664
517
|
}
|
|
665
518
|
|
|
666
|
-
/** Pure compact pill view, separated for render/click regression tests. */
|
|
667
|
-
function CurrentSessionPillView({ snapshot, translate, onOpen = requestUsageStatsPanel, now = Date.now() }) {
|
|
668
|
-
const view = sessionPillViewOf(snapshot, translate, now);
|
|
669
|
-
if (view === null) return null;
|
|
670
|
-
return react_jsx_runtime.jsxs("button", {
|
|
671
|
-
type: "button",
|
|
672
|
-
className: S.sessionPill,
|
|
673
|
-
"data-current-session-pill": true,
|
|
674
|
-
"data-provider": view.providerId,
|
|
675
|
-
"data-status": view.status,
|
|
676
|
-
"data-tone": view.tone,
|
|
677
|
-
"aria-label": view.ariaLabel,
|
|
678
|
-
title: view.ariaLabel,
|
|
679
|
-
onClick: () => onOpen(view.providerId),
|
|
680
|
-
children: [
|
|
681
|
-
react_jsx_runtime.jsx("span", { className: S.sessionPillProvider, children: view.providerLabel }),
|
|
682
|
-
react_jsx_runtime.jsx("span", { className: S.sessionPillSeparator, "aria-hidden": true, children: "·" }),
|
|
683
|
-
react_jsx_runtime.jsx("span", { className: S.sessionPillValue, children: view.value })
|
|
684
|
-
]
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
/** Formal `conversation.input.right` slot occupant. */
|
|
689
|
-
function CurrentSessionPill({ sessionId, session, modelDirectory, useProjection = () => void 0, request = fetchJson, t }) {
|
|
690
|
-
const translate = (key, params) => interpolate(t !== void 0 ? t(key) : key, params);
|
|
691
|
-
const [loaded, setLoaded] = react.useState({ key: null, snapshot: null });
|
|
692
|
-
const loaderRef = react.useRef(null);
|
|
693
|
-
if (loaderRef.current === null) loaderRef.current = createLoader();
|
|
694
|
-
const signal = sessionContextSignalOf(session);
|
|
695
|
-
const tokenUsage = useProjection("tokenUsage");
|
|
696
|
-
const tokenSignal = tokenUsageSignalOf(tokenUsage);
|
|
697
|
-
const subscribeModelSelection = react.useCallback((notify) => typeof modelDirectory?.subscribe === "function"
|
|
698
|
-
? modelDirectory.subscribe(notify)
|
|
699
|
-
: () => {}, [modelDirectory]);
|
|
700
|
-
const getModelSelectionSignal = react.useCallback(() => modelSelectionSignalOf(
|
|
701
|
-
typeof modelDirectory?.getSnapshot === "function" ? modelDirectory.getSnapshot() : null
|
|
702
|
-
), [modelDirectory]);
|
|
703
|
-
const modelSignal = react.useSyncExternalStore(subscribeModelSelection, getModelSelectionSignal, getModelSelectionSignal);
|
|
704
|
-
const requestKey = JSON.stringify([sessionId, signal, modelSignal, tokenSignal]);
|
|
705
|
-
const [selectedProvider, selectedModel] = JSON.parse(modelSignal);
|
|
706
|
-
const selectedRoute = selectedProvider !== "" && selectedModel !== ""
|
|
707
|
-
? { provider: selectedProvider, model: selectedModel }
|
|
708
|
-
: null;
|
|
709
|
-
react.useEffect(() => {
|
|
710
|
-
const seq = loaderRef.current.start();
|
|
711
|
-
let mounted = true;
|
|
712
|
-
setLoaded({ key: requestKey, snapshot: null });
|
|
713
|
-
loadSessionPillSnapshot(sessionId, request, selectedRoute).then((next) => {
|
|
714
|
-
if (mounted && loaderRef.current.isCurrent(seq)) setLoaded({ key: requestKey, snapshot: next });
|
|
715
|
-
}).catch(() => {
|
|
716
|
-
if (mounted && loaderRef.current.isCurrent(seq)) setLoaded({ key: requestKey, snapshot: null });
|
|
717
|
-
});
|
|
718
|
-
return () => {
|
|
719
|
-
mounted = false;
|
|
720
|
-
};
|
|
721
|
-
}, [sessionId, signal, modelSignal, tokenSignal, request, requestKey, selectedRoute?.provider, selectedRoute?.model]);
|
|
722
|
-
return react_jsx_runtime.jsx(CurrentSessionPillView, {
|
|
723
|
-
snapshot: loaded.key === requestKey && loaded.snapshot !== null ? { ...loaded.snapshot, tokenUsage } : null,
|
|
724
|
-
translate
|
|
725
|
-
});
|
|
726
|
-
}
|
|
727
|
-
//#endregion
|
|
728
|
-
|
|
729
519
|
/**
|
|
730
520
|
* Build one month's calendar heatmap: weeks as rows (Mon-first), only
|
|
731
521
|
* the month's own days, padded with null placeholders. Cell tokens come
|
|
@@ -798,7 +588,11 @@ window.__ModuleLoader__.load({
|
|
|
798
588
|
}
|
|
799
589
|
|
|
800
590
|
function validStoredProvider(value) {
|
|
801
|
-
return typeof value === "string"
|
|
591
|
+
return typeof value === "string"
|
|
592
|
+
&& value !== ORCAROUTER_ADD_SENTINEL
|
|
593
|
+
&& value.length > 0
|
|
594
|
+
&& value.length <= 256
|
|
595
|
+
&& !value.includes("\0") ? value : null;
|
|
802
596
|
}
|
|
803
597
|
|
|
804
598
|
function readSelectedProvider(storage = browserStorage()) {
|
|
@@ -814,6 +608,10 @@ window.__ModuleLoader__.load({
|
|
|
814
608
|
|
|
815
609
|
function writeSelectedProvider(providerId, storage = browserStorage()) {
|
|
816
610
|
try {
|
|
611
|
+
// The synthetic dropdown entry is an action, never provider state. A
|
|
612
|
+
// defensive no-op also preserves the previous real selection if this
|
|
613
|
+
// helper is called directly by a future UI refactor.
|
|
614
|
+
if (providerId === ORCAROUTER_ADD_SENTINEL) return;
|
|
817
615
|
const normalized = validStoredProvider(providerId);
|
|
818
616
|
if (normalized === null) storage?.removeItem?.(SELECTED_PROVIDER_STORAGE_KEY);
|
|
819
617
|
else storage?.setItem?.(SELECTED_PROVIDER_STORAGE_KEY, normalized);
|
|
@@ -824,6 +622,9 @@ window.__ModuleLoader__.load({
|
|
|
824
622
|
}
|
|
825
623
|
|
|
826
624
|
function reconcileSelectedProvider(current, providers, storage = browserStorage()) {
|
|
625
|
+
// An empty response can be a transient provider-service refresh. Keep the
|
|
626
|
+
// current choice until a non-empty authoritative list can prove removal.
|
|
627
|
+
if (!Array.isArray(providers) || providers.length === 0) return current;
|
|
827
628
|
if (Array.isArray(providers) && providers.some((provider) => provider?.id === current)) return current;
|
|
828
629
|
if (current !== null) writeSelectedProvider(null, storage);
|
|
829
630
|
if (!Array.isArray(providers) || providers.length === 0) return null;
|
|
@@ -856,8 +657,12 @@ window.__ModuleLoader__.load({
|
|
|
856
657
|
const [account, setAccount] = react.useState(null);
|
|
857
658
|
const [accountLoading, setAccountLoading] = react.useState(false);
|
|
858
659
|
const [accountError, setAccountError] = react.useState(null);
|
|
660
|
+
const [orcaRouterIntegration, setOrcaRouterIntegration] = react.useState(null);
|
|
661
|
+
const [orcaRouterPending, setOrcaRouterPending] = react.useState(false);
|
|
662
|
+
const [orcaRouterError, setOrcaRouterError] = react.useState(false);
|
|
859
663
|
const [refreshedAt, setRefreshedAt] = react.useState(null);
|
|
860
664
|
const mountedRef = react.useRef(true);
|
|
665
|
+
const orcaRouterMutationRef = react.useRef(false);
|
|
861
666
|
const usageLoaderRef = react.useRef(null);
|
|
862
667
|
const accountLoaderRef = react.useRef(null);
|
|
863
668
|
const layerRef = react.useRef(null);
|
|
@@ -865,22 +670,24 @@ window.__ModuleLoader__.load({
|
|
|
865
670
|
if (usageLoaderRef.current === null) usageLoaderRef.current = createLoader();
|
|
866
671
|
if (accountLoaderRef.current === null) accountLoaderRef.current = createLoader();
|
|
867
672
|
const providerChoices = react.useMemo(() => buildProviderChoices(providers), [providers]);
|
|
673
|
+
const providerPickerChoices = react.useMemo(
|
|
674
|
+
() => buildProviderPickerChoices(providerChoices, orcaRouterIntegration),
|
|
675
|
+
[providerChoices, orcaRouterIntegration]
|
|
676
|
+
);
|
|
868
677
|
const selectedProviderInfo = providerChoices.find((provider) => provider.id === selectedProvider) ?? null;
|
|
869
678
|
const selectProvider = react.useCallback((providerId) => {
|
|
679
|
+
if (providerId === ORCAROUTER_ADD_SENTINEL) return;
|
|
870
680
|
const normalized = validStoredProvider(providerId);
|
|
871
681
|
setSelectedProvider(normalized);
|
|
872
682
|
writeSelectedProvider(normalized, storageRef.current);
|
|
873
683
|
}, []);
|
|
874
|
-
const
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
// clicks, a new panel implementation, or another account state/cache.
|
|
882
|
-
react.useEffect(() => subscribeUsageStatsPanel(openForProvider), [openForProvider]);
|
|
883
|
-
|
|
684
|
+
const closePanel = react.useCallback(() => {
|
|
685
|
+
// Re-assert the last visible choice at the explicit close boundary. This
|
|
686
|
+
// keeps the X action stable even when storage was temporarily unavailable
|
|
687
|
+
// during an earlier provider-selection event.
|
|
688
|
+
if (selectedProvider !== null) writeSelectedProvider(selectedProvider, storageRef.current);
|
|
689
|
+
setOpen(false);
|
|
690
|
+
}, [selectedProvider]);
|
|
884
691
|
const loadUsage = react.useCallback(() => {
|
|
885
692
|
const seq = usageLoaderRef.current.start();
|
|
886
693
|
setUsageError(null);
|
|
@@ -898,16 +705,59 @@ window.__ModuleLoader__.load({
|
|
|
898
705
|
});
|
|
899
706
|
}, []);
|
|
900
707
|
|
|
901
|
-
const loadProviders = react.useCallback(() => {
|
|
902
|
-
|
|
903
|
-
|
|
708
|
+
const loadProviders = react.useCallback(async () => {
|
|
709
|
+
try {
|
|
710
|
+
const payload = await fetchJson("/api/usage-stats/providers");
|
|
711
|
+
if (!mountedRef.current) return null;
|
|
904
712
|
const list = authoritativeProviderList(payload);
|
|
905
|
-
if (list === null) return;
|
|
713
|
+
if (list === null) return null;
|
|
906
714
|
setProviders(list);
|
|
907
715
|
setProvidersAuthoritative(true);
|
|
908
|
-
|
|
716
|
+
return list;
|
|
717
|
+
} catch {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
}, []);
|
|
721
|
+
|
|
722
|
+
const loadOrcaRouterStatus = react.useCallback(async () => {
|
|
723
|
+
if (mountedRef.current) setOrcaRouterError(false);
|
|
724
|
+
try {
|
|
725
|
+
const integration = await loadOrcaRouterIntegration();
|
|
726
|
+
if (mountedRef.current) setOrcaRouterIntegration(integration);
|
|
727
|
+
return integration;
|
|
728
|
+
} catch {
|
|
729
|
+
if (mountedRef.current) setOrcaRouterIntegration(null);
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
909
732
|
}, []);
|
|
910
733
|
|
|
734
|
+
const selectProviderChoice = react.useCallback(async (providerId) => {
|
|
735
|
+
if (providerId !== ORCAROUTER_ADD_SENTINEL) {
|
|
736
|
+
setOrcaRouterError(false);
|
|
737
|
+
selectProvider(providerId);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (orcaRouterMutationRef.current) return;
|
|
741
|
+
orcaRouterMutationRef.current = true;
|
|
742
|
+
setOrcaRouterPending(true);
|
|
743
|
+
setOrcaRouterError(false);
|
|
744
|
+
try {
|
|
745
|
+
const integration = await addOrcaRouterIntegration();
|
|
746
|
+
const refreshedProviders = await loadProviders();
|
|
747
|
+
if (!mountedRef.current) return;
|
|
748
|
+
if (!Array.isArray(refreshedProviders) || !refreshedProviders.some((provider) => provider?.id === "orcarouter")) {
|
|
749
|
+
throw new Error("installed provider was not returned");
|
|
750
|
+
}
|
|
751
|
+
setOrcaRouterIntegration(integration);
|
|
752
|
+
selectProvider("orcarouter");
|
|
753
|
+
} catch {
|
|
754
|
+
if (mountedRef.current) setOrcaRouterError(true);
|
|
755
|
+
} finally {
|
|
756
|
+
orcaRouterMutationRef.current = false;
|
|
757
|
+
if (mountedRef.current) setOrcaRouterPending(false);
|
|
758
|
+
}
|
|
759
|
+
}, [loadProviders, selectProvider]);
|
|
760
|
+
|
|
911
761
|
const loadAccount = react.useCallback((providerId, force = false, activity = null) => {
|
|
912
762
|
const seq = accountLoaderRef.current.start();
|
|
913
763
|
setAccountLoading(true);
|
|
@@ -990,13 +840,14 @@ window.__ModuleLoader__.load({
|
|
|
990
840
|
if (!open) return;
|
|
991
841
|
loadUsage();
|
|
992
842
|
loadProviders();
|
|
843
|
+
loadOrcaRouterStatus();
|
|
993
844
|
const usageTimer = window.setInterval(loadUsage, 60000);
|
|
994
845
|
const providerTimer = window.setInterval(loadProviders, 300000);
|
|
995
846
|
return () => {
|
|
996
847
|
window.clearInterval(usageTimer);
|
|
997
848
|
window.clearInterval(providerTimer);
|
|
998
849
|
};
|
|
999
|
-
}, [open, loadUsage, loadProviders]);
|
|
850
|
+
}, [open, loadUsage, loadProviders, loadOrcaRouterStatus]);
|
|
1000
851
|
|
|
1001
852
|
// Fetch the selected account for BOTH the panel and the collapsed
|
|
1002
853
|
// sidebar badge. The server owns the adaptive upstream schedule; this
|
|
@@ -1146,7 +997,7 @@ window.__ModuleLoader__.load({
|
|
|
1146
997
|
type: "button",
|
|
1147
998
|
className: S.iconButton,
|
|
1148
999
|
"aria-label": translate("action.close"),
|
|
1149
|
-
onClick:
|
|
1000
|
+
onClick: closePanel,
|
|
1150
1001
|
children: react_jsx_runtime.jsx(primitives.IconCloseOutline16, { size: 14 })
|
|
1151
1002
|
})
|
|
1152
1003
|
})
|
|
@@ -1168,20 +1019,27 @@ window.__ModuleLoader__.load({
|
|
|
1168
1019
|
children: react_jsx_runtime.jsx("h3", { className: S.sectionTitle, children: translate("account.title") })
|
|
1169
1020
|
}),
|
|
1170
1021
|
react_jsx_runtime.jsx(ProviderPicker, {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1022
|
+
providers: providerPickerChoices,
|
|
1023
|
+
selectedProvider,
|
|
1024
|
+
onSelect: selectProviderChoice,
|
|
1025
|
+
disabled: orcaRouterPending,
|
|
1174
1026
|
translate
|
|
1175
1027
|
}),
|
|
1028
|
+
orcaRouterError ? react_jsx_runtime.jsx("p", {
|
|
1029
|
+
className: S.note,
|
|
1030
|
+
role: "status",
|
|
1031
|
+
"data-orcarouter-action-error": true,
|
|
1032
|
+
children: translate("orcarouter.addError")
|
|
1033
|
+
}) : null,
|
|
1176
1034
|
selectedProviderInfo === null ? react_jsx_runtime.jsx("p", { className: S.note, children: translate("account.loading") }) : react_jsx_runtime.jsx("div", {
|
|
1177
1035
|
className: S.accountGrid,
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1036
|
+
children: react_jsx_runtime.jsx(ProviderAccountCard, {
|
|
1037
|
+
provider: selectedProviderInfo,
|
|
1038
|
+
account: account?.id === selectedProvider ? account : null,
|
|
1039
|
+
accountLoading,
|
|
1040
|
+
accountError,
|
|
1041
|
+
translate,
|
|
1042
|
+
onRetry: () => loadAccount(selectedProvider, true, "detail")
|
|
1185
1043
|
}, selectedProviderInfo.id)
|
|
1186
1044
|
}),
|
|
1187
1045
|
react_jsx_runtime.jsx("section", {
|
|
@@ -1352,6 +1210,7 @@ window.__ModuleLoader__.load({
|
|
|
1352
1210
|
"opencode-go": "GO",
|
|
1353
1211
|
ollama: "OL",
|
|
1354
1212
|
openrouter: "OR",
|
|
1213
|
+
orcarouter: "OC",
|
|
1355
1214
|
moonshotai: "K",
|
|
1356
1215
|
"moonshotai-cn": "K",
|
|
1357
1216
|
kimi: "K",
|
|
@@ -1404,7 +1263,7 @@ window.__ModuleLoader__.load({
|
|
|
1404
1263
|
}
|
|
1405
1264
|
|
|
1406
1265
|
/** Provider selector shared by monetary and subscription account modes. */
|
|
1407
|
-
function ProviderPicker({ providers, selectedProvider, onSelect, translate }) {
|
|
1266
|
+
function ProviderPicker({ providers, selectedProvider, onSelect, disabled = false, translate }) {
|
|
1408
1267
|
if (providers.length === 0) return null;
|
|
1409
1268
|
return react_jsx_runtime.jsxs("label", {
|
|
1410
1269
|
className: S.providerPicker,
|
|
@@ -1413,11 +1272,12 @@ window.__ModuleLoader__.load({
|
|
|
1413
1272
|
react_jsx_runtime.jsx("select", {
|
|
1414
1273
|
className: S.providerSelect,
|
|
1415
1274
|
value: selectedProvider ?? "",
|
|
1275
|
+
disabled,
|
|
1416
1276
|
"aria-label": translate("account.provider"),
|
|
1417
1277
|
onChange: (event) => onSelect(event.target.value),
|
|
1418
1278
|
children: providers.map((provider) => react_jsx_runtime.jsx("option", {
|
|
1419
1279
|
value: provider.id,
|
|
1420
|
-
children: provider
|
|
1280
|
+
children: providerChoiceLabel(provider, translate)
|
|
1421
1281
|
}, provider.id))
|
|
1422
1282
|
})
|
|
1423
1283
|
]
|
|
@@ -1768,8 +1628,8 @@ window.__ModuleLoader__.load({
|
|
|
1768
1628
|
const zh = {
|
|
1769
1629
|
"panel.title": "用量与余额",
|
|
1770
1630
|
"panel.badge": "用量/余额",
|
|
1771
|
-
"
|
|
1772
|
-
"
|
|
1631
|
+
"orcarouter.choiceSuffix": "(赞助集成)",
|
|
1632
|
+
"orcarouter.addError": "未能添加 OrcaRouter,请稍后重试。",
|
|
1773
1633
|
"account.title": "供应商账户",
|
|
1774
1634
|
"account.provider": "当前供应商",
|
|
1775
1635
|
"account.balanceMode": "API 余额",
|
|
@@ -1892,8 +1752,8 @@ window.__ModuleLoader__.load({
|
|
|
1892
1752
|
const en = {
|
|
1893
1753
|
"panel.title": "Usage & Balance",
|
|
1894
1754
|
"panel.badge": "Usage/Balance",
|
|
1895
|
-
"
|
|
1896
|
-
"
|
|
1755
|
+
"orcarouter.choiceSuffix": " (Sponsored integration)",
|
|
1756
|
+
"orcarouter.addError": "Could not add OrcaRouter. Please try again.",
|
|
1897
1757
|
"account.title": "Provider account",
|
|
1898
1758
|
"account.provider": "Current provider",
|
|
1899
1759
|
"account.balanceMode": "API balance",
|
|
@@ -2020,8 +1880,7 @@ window.__ModuleLoader__.load({
|
|
|
2020
1880
|
const inject = ["slots", "locale"];
|
|
2021
1881
|
|
|
2022
1882
|
/**
|
|
2023
|
-
* Client plugin body: register dictionaries
|
|
2024
|
-
* the current-session pill beside the host's model selector.
|
|
1883
|
+
* Client plugin body: register dictionaries and the sidebar account panel.
|
|
2025
1884
|
* @param ctx - client root context.
|
|
2026
1885
|
*/
|
|
2027
1886
|
function apply(ctx) {
|
|
@@ -2032,26 +1891,12 @@ window.__ModuleLoader__.load({
|
|
|
2032
1891
|
locale: NS,
|
|
2033
1892
|
order: 10
|
|
2034
1893
|
}, UsageStatsPanel));
|
|
2035
|
-
// The model-directory service is optional so older/mutated DSH clients
|
|
2036
|
-
// keep the sidebar panel even when the inline slot cannot be supported.
|
|
2037
|
-
ctx.inject(["slots", "modelDirectories"], (scope) => {
|
|
2038
|
-
const models = scope.modelDirectories;
|
|
2039
|
-
scope.slots.inject("conversation.input.right", () => scope.slots.register({
|
|
2040
|
-
name: "conversation.input.right",
|
|
2041
|
-
id: "usage-stats-current-session-pill",
|
|
2042
|
-
locale: NS,
|
|
2043
|
-
order: 10,
|
|
2044
|
-
inject: (sessionId) => ({ modelDirectory: models.directoryFor(sessionId).store })
|
|
2045
|
-
}, CurrentSessionPill));
|
|
2046
|
-
});
|
|
2047
1894
|
}
|
|
2048
1895
|
//#endregion
|
|
2049
1896
|
|
|
2050
1897
|
exports.apply = apply;
|
|
2051
1898
|
exports.inject = inject;
|
|
2052
1899
|
exports.UsageStatsPanel = UsageStatsPanel;
|
|
2053
|
-
exports.CurrentSessionPill = CurrentSessionPill;
|
|
2054
|
-
exports.CurrentSessionPillView = CurrentSessionPillView;
|
|
2055
1900
|
exports.DayDetail = DayDetail;
|
|
2056
1901
|
exports.ProviderAccountCard = ProviderAccountCard;
|
|
2057
1902
|
exports.MonthHeatmap = MonthHeatmap;
|
|
@@ -2059,6 +1904,9 @@ window.__ModuleLoader__.load({
|
|
|
2059
1904
|
exports.cellColor = cellColor;
|
|
2060
1905
|
exports.createLoader = createLoader;
|
|
2061
1906
|
exports.buildProviderChoices = buildProviderChoices;
|
|
1907
|
+
exports.buildProviderPickerChoices = buildProviderPickerChoices;
|
|
1908
|
+
exports.loadOrcaRouterIntegration = loadOrcaRouterIntegration;
|
|
1909
|
+
exports.addOrcaRouterIntegration = addOrcaRouterIntegration;
|
|
2062
1910
|
exports.modelLabelOf = modelLabelOf;
|
|
2063
1911
|
exports.fmt = fmt;
|
|
2064
1912
|
exports.fmtCurrency = fmtCurrency;
|
|
@@ -2068,15 +1916,9 @@ window.__ModuleLoader__.load({
|
|
|
2068
1916
|
exports.shouldDismissPanel = shouldDismissPanel;
|
|
2069
1917
|
exports.safeDiagnosticReason = safeDiagnosticReason;
|
|
2070
1918
|
exports.formatResetCountdown = formatResetCountdown;
|
|
2071
|
-
exports.loadSessionPillSnapshot = loadSessionPillSnapshot;
|
|
2072
|
-
exports.modelSelectionSignalOf = modelSelectionSignalOf;
|
|
2073
|
-
exports.tokenUsageSignalOf = tokenUsageSignalOf;
|
|
2074
|
-
exports.currentSessionCostLabel = currentSessionCostLabel;
|
|
2075
|
-
exports.requestUsageStatsPanel = requestUsageStatsPanel;
|
|
2076
|
-
exports.sessionContextSignalOf = sessionContextSignalOf;
|
|
2077
|
-
exports.sessionPillViewOf = sessionPillViewOf;
|
|
2078
|
-
exports.subscribeUsageStatsPanel = subscribeUsageStatsPanel;
|
|
2079
1919
|
exports.enableFooterActionWrapping = enableFooterActionWrapping;
|
|
1920
|
+
exports.providerChoiceLabel = providerChoiceLabel;
|
|
1921
|
+
exports.ORCAROUTER_ADD_SENTINEL = ORCAROUTER_ADD_SENTINEL;
|
|
2080
1922
|
exports.SELECTED_PROVIDER_STORAGE_KEY = SELECTED_PROVIDER_STORAGE_KEY;
|
|
2081
1923
|
exports.authoritativeProviderList = authoritativeProviderList;
|
|
2082
1924
|
exports.readSelectedProvider = readSelectedProvider;
|