tokentracker-cli 1.5.1 → 1.5.3
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/dashboard/dist/assets/{AchievementBadge-mB7HZSGE.js → AchievementBadge-uuDGO9Mq.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-DLzeq_eo.js → AchievementsPage-BCTVT7Jo.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DrXQyDns.js → AchievementsSection-B6fJSJSr.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-WrgKRybi.js → BadgeDetailModal-FjUN__9G.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-DFRaixVK.js → BotAnimated-B-KqY_Wr.js} +1 -1
- package/dashboard/dist/assets/{Card-bFq2tlO8.js → Card-DS8T8piP.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-gwhGxavy.js → CloudCheckoutPage-_0UjsFPu.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-BD4iBYeZ.js → CloudPage-BstgVBaV.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-C_KlPl0U.js → CommandPalette-DtVsYZgc.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-Cl1Bgv8M.js → CommunityStatsModal-DTpEJqV3.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-NWoYevYC.js → ConfirmModal-Lfdh2vr7.js} +1 -1
- package/dashboard/dist/assets/{Controls-D2-D2cfA.js → Controls-DpwQ7pkq.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-8EMVXuH6.js → DateRangePopover-CrWgrzT2.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DbP8d6bM.js → DevicePage-DbK5KWJS.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CEstgLa1.js → DialogClose-Dd2B_o03.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DP5TLLbz.js → DialogTitle-3JQBVha3.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Uvi3yMum.js → ElectricLogo-LYAqG8pG.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Dpzihg6f.js → FadeIn-DhDvUor7.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DllqemKr.js → HeaderGithubStar-9f1XmgYC.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Dc-TAH6l.js → HoverTooltip-BXhHAkP7.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-jd1D5ZKX.js → IpCheckPage-ChPdDeMl.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CB1ZVw-a.js → LandingPage-DnJ7Y9zw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-Bt2cRRmE.js → LeaderboardPage-BF9isLzu.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-BVlyVOFf.js → LeaderboardProBadge-BM4JKgkC.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-CwQFRJg_.js → LeaderboardProfileModal-DfZABOpN.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CeqcTbUG.js → LeaderboardProfilePage-BaZ38iT5.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B2_HoCVD.js → LocalOnlyNotice-DvhlYoqQ.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-nw9JOc2d.js → LoginCard-g_Gbnzhs.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-BHNLTK1D.js → LoginModal-DkJrI6b7.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-LI2GZKhM.js → LoginPage-CqCKSo0n.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CK6sPeI0.js → PetPage-DbF7ROsj.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-BsXchEVa.js → PopoverPopup-4WuH41ZT.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-vgCEy0dh.js → ProviderIcon-6j0A2KD9.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B4CPj2vX.js → ResetPasswordPage-DBd-iwuM.js} +1 -1
- package/dashboard/dist/assets/{Select-BkAy6P2r.js → Select-BBEpLeiG.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-C0RNqDrD.js → SelectItemText-Cn_U_rrO.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-B-_w-Z-a.js → SelfHostPage-2WZc6Rml.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +1 -0
- package/dashboard/dist/assets/{SessionsPage-CmWXBqD2.js → SessionsPage-CWS0iQ1L.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-CA70zP-t.js → SettingsPage-rXNr96i-.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CNamhfzU.js → SkillsPage-CudTvbZL.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-CXrVBSk6.js → TokenGalaxy-BRKrbTsJ.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-CR4MqElX.js → TooltipPopup-CsLKXhWC.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DQfcrEve.js → TrendMonitor-8onUTbkk.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BBXxtZbP.js → WidgetsPage-CcwE0lz8.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-C8i2KlMF.js → WrappedPage-Cj9LLYqP.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +1 -0
- package/dashboard/dist/assets/{agent-logos-CxQb8vir.js → agent-logos-ChBA1nAH.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-D6zkcLVt.js → arrow-left-Bb5mfoPc.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-BGb-XVn_.js → arrow-right-D1Ti2XUI.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-XTStagDJ.js → arrow-up-right-B_CjA_y8.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-Cz3NGZiS.js → calendar-days-DdfrAqie.js} +1 -1
- package/dashboard/dist/assets/{check-1z_vmHPD.js → check-29zGw1MK.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-CkSBY3bE.js → chevron-down-DAzlFX7g.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-pD5C4r_S.js → cloud-checkout-C2SeCjFM.js} +1 -1
- package/dashboard/dist/assets/{composite-BtDzx3a2.js → composite-DIf735Hq.js} +1 -1
- package/dashboard/dist/assets/{copy-Ctv_wxuD.js → copy-BOuOM0HQ.js} +1 -1
- package/dashboard/dist/assets/{download-DKbKMGSR.js → download-50tQRpGe.js} +1 -1
- package/dashboard/dist/assets/{external-link-g9cU017t.js → external-link-BhlQClVe.js} +1 -1
- package/dashboard/dist/assets/{flame-DZq3kXUt.js → flame-CeGfA-0L.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CXQfx0PC.js → format-tokens-Cq7YiAkJ.js} +1 -1
- package/dashboard/dist/assets/{gift-CKlKjGP6.js → gift-Do-UFhiT.js} +1 -1
- package/dashboard/dist/assets/{icons-xPGMnBjl.js → icons-C6EZ_HmP.js} +1 -1
- package/dashboard/dist/assets/{index-fVbP5wOv.js → index-BFrbVqsm.js} +1 -1
- package/dashboard/dist/assets/{index-CDBOKcDh.js → index-BWhbBAd9.js} +1 -1
- package/dashboard/dist/assets/{index-Dl0oFLNr.js → index-D9kwYWLX.js} +1 -1
- package/dashboard/dist/assets/{index-BwOjMtB2.js → index-c0crpe9Z.js} +1 -1
- package/dashboard/dist/assets/{info-D9xNIuGh.js → info-B4MXmsNE.js} +1 -1
- package/dashboard/dist/assets/{link-2-Bk5wYt2n.js → link-2-DuWCslzl.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CcN3pHWb.js → loader-circle-C2I3NZAO.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-CYcy1ici.js → local-usage-transfer-BQasvsUF.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DbpEzkJe.js → modifiers.esm-DbeNS6FT.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-WkWv1qIS.js → monitor-smartphone-C6Y4ktsc.js} +1 -1
- package/dashboard/dist/assets/{play-2yZGWMes.js → play-CwgcDJC2.js} +1 -1
- package/dashboard/dist/assets/{plus-mOF8tG1F.js → plus-B796wsUO.js} +1 -1
- package/dashboard/dist/assets/{provider-display-7gaOvgMp.js → provider-display-Cw26RFDm.js} +1 -1
- package/dashboard/dist/assets/{react-CdPiPDhk.js → react-d7Bk1-_K.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C7FJLAlh.js → safePolygon-CDE0dNv_.js} +1 -1
- package/dashboard/dist/assets/{search-C63nCrjo.js → search-DtKvsdRs.js} +1 -1
- package/dashboard/dist/assets/{server-BaG1rnuv.js → server-gAURHUyz.js} +1 -1
- package/dashboard/dist/assets/{shield-check-D_4-Cm6L.js → shield-check-DFVdDTwh.js} +1 -1
- package/dashboard/dist/assets/{skills-api-DfbzwpDv.js → skills-api-2rdPUrJI.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-D-v8FRX5.js → sortable.esm-DFlfwnJr.js} +1 -1
- package/dashboard/dist/assets/{store-Dv_FYrXK.js → store-BKPCk_de.js} +1 -1
- package/dashboard/dist/assets/{terminal-FQi6MaYt.js → terminal-BtWFakvS.js} +1 -1
- package/dashboard/dist/assets/{trash-TbK96dEz.js → trash-bpr9zTAM.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DGoWVZuk.js → use-community-stats-DMnGgLne.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-Dg6uSaYt.js → use-latest-request-guard-BSBlJJHe.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-DIk0AF3b.js → use-native-settings-OthqiF6g.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CUqV3Zgl.js → use-ordered-list-DpAtr0R-.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +1 -0
- package/dashboard/dist/assets/{use-transform-D_-w22c8.js → use-transform-BRz9dz_2.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-CwmhudAX.js +1 -0
- package/dashboard/dist/assets/{useCurrency-B_AuLCsL.js → useCurrency-CAA-tQ27.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-DfxvIif8.js → usePositioner-Cj2DPmwM.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-bbO13V4s.js → useTokenFormat-Bf5-f7Hx.js} +1 -1
- package/dashboard/dist/assets/{zap-k07-EXTK.js → zap-B8Fg6PaC.js} +1 -1
- package/dashboard/dist/index.html +6 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/serve.js +22 -13
- package/src/lib/codex-rollout-parser.js +43 -6
- package/src/lib/codex-usage-record.js +21 -0
- package/src/lib/deepseek-status.js +157 -0
- package/src/lib/local-api.js +44 -4
- package/src/lib/pricing/curated-overrides.json +94 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-status.js +15 -2
- package/src/lib/qoder-limits.js +95 -23
- package/src/lib/rollout.js +68 -14
- package/src/lib/session-analytics.js +10 -3
- package/src/lib/usage-limits.js +187 -43
- package/src/lib/workbuddy-limits.js +292 -27
- package/src/lib/zcode-limits.js +457 -42
- package/dashboard/dist/assets/DashboardPage-vITMnPXx.js +0 -1
- package/dashboard/dist/assets/DialogDescription-0wcygxtH.js +0 -1
- package/dashboard/dist/assets/LimitsPage-Bp4ZORmR.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-5YevsGGD.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-BEEzY7mq.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-Br-KAXh6.css +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-Djbdp7fi.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DMCok-vZ.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-h7aGsIQr.js +0 -1
package/src/lib/zcode-limits.js
CHANGED
|
@@ -10,6 +10,10 @@ const DEFAULT_BIGMODEL_MONITOR_BASE_URL = "https://bigmodel.cn";
|
|
|
10
10
|
const ZCODE_MONITOR_QUOTA_PATH = "/api/monitor/usage/quota/limit";
|
|
11
11
|
const DEFAULT_ZCODE_APP_VERSION = "3.2.5";
|
|
12
12
|
const DEFAULT_ZCODE_LOG_FALLBACK_MAX_AGE_MS = 6 * 60 * 60 * 1000;
|
|
13
|
+
const ZCODE_LIMITS_CACHE_FILE = "zcode-usage-limits-cache.json";
|
|
14
|
+
// Windows without a usable reset_at cannot be checked against a quota rollover,
|
|
15
|
+
// so they are only served while the snapshot is young enough to trust.
|
|
16
|
+
const ZCODE_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
13
17
|
|
|
14
18
|
function resolveZcodeHome({ home, env = process.env } = {}) {
|
|
15
19
|
if (typeof env.TOKENTRACKER_ZCODE_HOME === "string" && env.TOKENTRACKER_ZCODE_HOME.trim()) {
|
|
@@ -150,6 +154,16 @@ function loadZcodeSelectedPlans({ home, env } = {}) {
|
|
|
150
154
|
}
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Plan class ("start" | "coding") a plan id like "start-plan" or
|
|
159
|
+
* "individual-coding-plan" serves; null for anything else. Per-account
|
|
160
|
+
* spellings (`individual-`/`team-coding-plan`) and the builtin
|
|
161
|
+
* `coding-plan` both serve the same coding plan.
|
|
162
|
+
*/
|
|
163
|
+
function zcodePlanKindFromPlanId(planId) {
|
|
164
|
+
return /coding-plan$/.test(planId) ? "coding" : planId === "start-plan" ? "start" : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
153
167
|
/**
|
|
154
168
|
* ZCode 3.14+ records the chosen plan per family as `providerFamilyConnectionSelections`
|
|
155
169
|
* (`{ zai: { kind: "individual-coding-plan" } }`) instead of `modelProviderFamilySelectedKeys`.
|
|
@@ -161,7 +175,7 @@ function loadZcodeConnectionSelectedPlans(selections, domain) {
|
|
|
161
175
|
const out = [];
|
|
162
176
|
for (const family of families) {
|
|
163
177
|
const kind = typeof selections[family]?.kind === "string" ? selections[family].kind : "";
|
|
164
|
-
const planKind =
|
|
178
|
+
const planKind = zcodePlanKindFromPlanId(kind);
|
|
165
179
|
if (!planKind) continue;
|
|
166
180
|
const providerKey = `builtin:${family}-${planKind}-plan`;
|
|
167
181
|
if (out.some((plan) => plan.providerKey === providerKey)) continue;
|
|
@@ -276,6 +290,31 @@ function zcodeProviderFamily(providerKey) {
|
|
|
276
290
|
return providerKey === "builtin:zai-coding-plan" || providerKey === "builtin:zai-start-plan" ? "zai" : "bigmodel";
|
|
277
291
|
}
|
|
278
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Region ("bigmodel" | "zai") for any ZCode provider id, or null when the id
|
|
295
|
+
* names neither region. Covers the built-in plan keys and the 3.14+ per-account
|
|
296
|
+
* ids ZCode writes into its own logs (`account:<family>-<plan>…`).
|
|
297
|
+
* @returns {"bigmodel" | "zai" | null}
|
|
298
|
+
*/
|
|
299
|
+
function zcodeRegionFromProviderId(providerKey) {
|
|
300
|
+
const match = typeof providerKey === "string" ? providerKey.match(/^(?:builtin|account):(bigmodel|zai)-/) : null;
|
|
301
|
+
return match ? match[1] : null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Plan class ("start" | "coding") for any ZCode provider id, or null when the
|
|
306
|
+
* id names no known plan. Mirrors {@link zcodeRegionFromProviderId}: covers the
|
|
307
|
+
* built-in plan keys and the 3.14+ per-account ids ZCode writes into its own
|
|
308
|
+
* logs (`account:<family>-<plan>…`).
|
|
309
|
+
* @returns {"start" | "coding" | null}
|
|
310
|
+
*/
|
|
311
|
+
function zcodePlanKindFromProviderId(providerKey) {
|
|
312
|
+
const match = typeof providerKey === "string"
|
|
313
|
+
? providerKey.match(/^(?:builtin|account):(bigmodel|zai)-(.+)$/)
|
|
314
|
+
: null;
|
|
315
|
+
return match ? zcodePlanKindFromPlanId(match[2]) : null;
|
|
316
|
+
}
|
|
317
|
+
|
|
279
318
|
/**
|
|
280
319
|
* Region whose plan should be served first. The model settings' domain is the
|
|
281
320
|
* region the user actually picked, so it wins; the OAuth login region only
|
|
@@ -692,7 +731,10 @@ async function fetchZcodeBilling(apiKey, { fetchImpl = fetch, baseUrl, env, home
|
|
|
692
731
|
signal,
|
|
693
732
|
});
|
|
694
733
|
if (res.status === 401 || res.status === 403) {
|
|
695
|
-
|
|
734
|
+
const error = new Error("Not authenticated with ZCode. Run `zcode` in Terminal to log in.");
|
|
735
|
+
// Tagged so the failure chain surfaces re-login instead of stale cache bars.
|
|
736
|
+
error.authError = true;
|
|
737
|
+
throw error;
|
|
696
738
|
}
|
|
697
739
|
if (!res.ok) {
|
|
698
740
|
let detail = "";
|
|
@@ -770,7 +812,10 @@ async function fetchZcodeCodingPlanQuota(apiKey, { fetchImpl = fetch, quotaUrl,
|
|
|
770
812
|
headers,
|
|
771
813
|
});
|
|
772
814
|
if (res.status === 401 || res.status === 403) {
|
|
773
|
-
|
|
815
|
+
const error = new Error("Not authenticated with ZCode coding plan. Run `zcode` in Terminal to log in.");
|
|
816
|
+
// Tagged so the failure chain surfaces re-login instead of stale cache bars.
|
|
817
|
+
error.authError = true;
|
|
818
|
+
throw error;
|
|
774
819
|
}
|
|
775
820
|
let body = null;
|
|
776
821
|
try {
|
|
@@ -798,10 +843,11 @@ function quotaTimestampToIso(value) {
|
|
|
798
843
|
|
|
799
844
|
function findZcodeQuotaLimit(limits, type, unit, number = null) {
|
|
800
845
|
if (!Array.isArray(limits)) return null;
|
|
846
|
+
const types = Array.isArray(type) ? type : [type];
|
|
801
847
|
return (
|
|
802
848
|
limits.find((limit) => {
|
|
803
849
|
if (!limit || typeof limit !== "object") return false;
|
|
804
|
-
if (limit.type
|
|
850
|
+
if (!types.includes(limit.type)) return false;
|
|
805
851
|
if (zcodeValNumber(limit.unit) !== unit) return false;
|
|
806
852
|
if (number == null) return true;
|
|
807
853
|
return zcodeValNumber(limit.number) === number;
|
|
@@ -861,8 +907,10 @@ function normalizeZcodeCodingPlanQuotaResponse(body) {
|
|
|
861
907
|
const limits = Array.isArray(data.limits) ? data.limits : [];
|
|
862
908
|
// Match ZCode's own sidebar: TOKENS_LIMIT(unit=3,number=5)=5h,
|
|
863
909
|
// TOKENS_LIMIT(unit=6)=weekly, TIME_LIMIT(unit=5,number=1)=tool calls.
|
|
864
|
-
|
|
865
|
-
|
|
910
|
+
// ZCode 3.17.1 renamed the token-window rows to CREDIT_LIMIT while keeping
|
|
911
|
+
// the unit/number convention, so both names anchor the named windows (#937).
|
|
912
|
+
const fiveHourLimit = findZcodeQuotaLimit(limits, ["TOKENS_LIMIT", "CREDIT_LIMIT"], 3, 5);
|
|
913
|
+
const weeklyLimit = findZcodeQuotaLimit(limits, ["TOKENS_LIMIT", "CREDIT_LIMIT"], 6);
|
|
866
914
|
const toolsLimit = findZcodeQuotaLimit(limits, "TIME_LIMIT", 5, 1);
|
|
867
915
|
const hasNamedWindows = Boolean(fiveHourLimit || weeklyLimit || toolsLimit);
|
|
868
916
|
|
|
@@ -944,18 +992,31 @@ function extractZcodeBalanceLogRecord(line) {
|
|
|
944
992
|
}
|
|
945
993
|
}
|
|
946
994
|
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
995
|
+
/**
|
|
996
|
+
* One backward pass over the recent ZCode logs (today + yesterday), collecting:
|
|
997
|
+
* preferred — freshest record matching the candidate providers. Built-in ids
|
|
998
|
+
* must byte-equal a candidate key; the 3.14+ per-account ids
|
|
999
|
+
* (`account:<family>-<plan>…`) never do, so they match the exact
|
|
1000
|
+
* (region, plan kind) pair of one candidate — never the cross product of
|
|
1001
|
+
* the two traits, so a bigmodel-coding + zai-start candidate list cannot
|
|
1002
|
+
* adopt a zai coding-plan record.
|
|
1003
|
+
* fallback — freshest record of any provider.
|
|
1004
|
+
* byRegion — freshest record per region ("bigmodel" | "zai"), for the
|
|
1005
|
+
* other-region snapshot.
|
|
1006
|
+
* Records outside the age window count nowhere. Every eligible line of both
|
|
1007
|
+
* files is visited and buckets keep the max-timestamp record, since log lines
|
|
1008
|
+
* can land out of timestamp order.
|
|
1009
|
+
*/
|
|
1010
|
+
function scanZcodeBalanceLogs({ home, env = process.env, providerKeys = [], nowMs = Date.now() } = {}) {
|
|
1011
|
+
const scan = { preferred: null, fallback: null, byRegion: new Map() };
|
|
1012
|
+
if (env.TOKENTRACKER_ZCODE_DISABLE_LOG_FALLBACK === "1") return scan;
|
|
955
1013
|
const maxAgeMs = parsePositiveInteger(env.TOKENTRACKER_ZCODE_LOG_MAX_AGE_MS, DEFAULT_ZCODE_LOG_FALLBACK_MAX_AGE_MS);
|
|
956
1014
|
const preferredProviders = new Set(providerKeys.filter(Boolean));
|
|
957
|
-
|
|
958
|
-
|
|
1015
|
+
const preferredPairs = new Set(providerKeys.map((key) => {
|
|
1016
|
+
const region = zcodeRegionFromProviderId(key);
|
|
1017
|
+
const planKind = zcodePlanKindFromProviderId(key);
|
|
1018
|
+
return region && planKind ? `${region}:${planKind}` : null;
|
|
1019
|
+
}).filter(Boolean));
|
|
959
1020
|
for (const logPath of recentZcodeLogPaths({ home, env, nowMs })) {
|
|
960
1021
|
if (!fs.existsSync(logPath)) continue;
|
|
961
1022
|
let text = "";
|
|
@@ -970,14 +1031,36 @@ function loadLatestZcodeBalanceFromLogs({
|
|
|
970
1031
|
if (!record) continue;
|
|
971
1032
|
const ageMs = nowMs - record.timestampMs;
|
|
972
1033
|
if (ageMs < 0 || ageMs > maxAgeMs) continue;
|
|
973
|
-
if (!fallback || record.timestampMs > fallback.timestampMs) fallback = record;
|
|
974
|
-
|
|
975
|
-
|
|
1034
|
+
if (!scan.fallback || record.timestampMs > scan.fallback.timestampMs) scan.fallback = record;
|
|
1035
|
+
const region = zcodeRegionFromProviderId(record.providerKey);
|
|
1036
|
+
const regionRecord = region ? scan.byRegion.get(region) : null;
|
|
1037
|
+
if (region && (!regionRecord || record.timestampMs > regionRecord.timestampMs)) {
|
|
1038
|
+
scan.byRegion.set(region, record);
|
|
1039
|
+
}
|
|
1040
|
+
const regionMatch = Boolean(region)
|
|
1041
|
+
&& record.providerKey.startsWith("account:")
|
|
1042
|
+
&& preferredPairs.has(`${region}:${zcodePlanKindFromProviderId(record.providerKey)}`);
|
|
1043
|
+
if (
|
|
1044
|
+
(preferredProviders.has(record.providerKey) || regionMatch)
|
|
1045
|
+
&& (!scan.preferred || record.timestampMs > scan.preferred.timestampMs)
|
|
1046
|
+
) {
|
|
1047
|
+
scan.preferred = record;
|
|
976
1048
|
}
|
|
977
1049
|
}
|
|
978
1050
|
}
|
|
979
|
-
|
|
980
|
-
|
|
1051
|
+
return scan;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function loadLatestZcodeBalanceFromLogs({
|
|
1055
|
+
home,
|
|
1056
|
+
env = process.env,
|
|
1057
|
+
providerKeys = [],
|
|
1058
|
+
nowMs = Date.now(),
|
|
1059
|
+
requireProviderMatch = false,
|
|
1060
|
+
} = {}) {
|
|
1061
|
+
const scan = scanZcodeBalanceLogs({ home, env, providerKeys, nowMs });
|
|
1062
|
+
if (scan.preferred) return scan.preferred;
|
|
1063
|
+
return requireProviderMatch && providerKeys.filter(Boolean).length > 0 ? null : scan.fallback;
|
|
981
1064
|
}
|
|
982
1065
|
|
|
983
1066
|
function zcodeLogFallbackResult(logRecord, errors = []) {
|
|
@@ -987,23 +1070,276 @@ function zcodeLogFallbackResult(logRecord, errors = []) {
|
|
|
987
1070
|
error: null,
|
|
988
1071
|
source: "zcode-log",
|
|
989
1072
|
provider_key: logRecord.providerKey,
|
|
1073
|
+
region: zcodeRegionFromProviderId(logRecord.providerKey),
|
|
990
1074
|
log_timestamp: logRecord.log_timestamp,
|
|
1075
|
+
// The log line's age IS the data age: ZCode wrote it when it last fetched.
|
|
1076
|
+
cached_at: logRecord.log_timestamp,
|
|
1077
|
+
stale: true,
|
|
991
1078
|
provider_errors: errors,
|
|
992
1079
|
...normalizeZcodeBalanceResponse(logRecord.body),
|
|
993
1080
|
};
|
|
994
1081
|
}
|
|
995
1082
|
|
|
1083
|
+
// Last-good disk cache for ZCode quota windows, following the house pattern
|
|
1084
|
+
// (codex / ark / qoder): write on every live success, serve stale bars when the
|
|
1085
|
+
// live chain fails. One file, partitioned per built-in provider key
|
|
1086
|
+
// (region × plan kind) so one region's billing read never falls back to
|
|
1087
|
+
// another region's data (#938's ownership principle).
|
|
1088
|
+
|
|
1089
|
+
// Cache partitions are also scoped to the login that produced the windows:
|
|
1090
|
+
// two logins on the same region × plan kind share one providerKey, so an
|
|
1091
|
+
// account switch followed by a live failure would otherwise serve the
|
|
1092
|
+
// previous login's bars. The login id comes from the app's own user_info —
|
|
1093
|
+
// the same ownership source as login-scoped tokens — plus the applicable
|
|
1094
|
+
// team scope for team-scoped coding-plan quota. No secret material is
|
|
1095
|
+
// persisted, and an unreadable login ("unknown") matches only itself.
|
|
1096
|
+
function zcodeCacheIdentity(loginAccountId, teamContext) {
|
|
1097
|
+
const login = typeof loginAccountId === "string" && loginAccountId.trim() ? loginAccountId.trim() : "unknown";
|
|
1098
|
+
return `${login}|${typeof teamContext === "string" ? teamContext : ""}`;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function zcodeLimitsCachePath({ home } = {}) {
|
|
1102
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker", ZCODE_LIMITS_CACHE_FILE);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// A window whose reset_at has already passed is stale data, not a usable
|
|
1106
|
+
// fallback — the quota has rolled over, so drop it. Undated windows cannot be
|
|
1107
|
+
// checked against a reset and are bounded by the snapshot-age TTL instead.
|
|
1108
|
+
function zcodeCachedWindow(window, { cachedAtMs, nowMs } = {}) {
|
|
1109
|
+
if (!window || typeof window !== "object") return null;
|
|
1110
|
+
const resetAtMs = Date.parse(window.reset_at || "");
|
|
1111
|
+
if (Number.isFinite(resetAtMs)) return resetAtMs > nowMs ? window : null;
|
|
1112
|
+
return nowMs - cachedAtMs <= ZCODE_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? window : null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
function zcodeCachedBucket(bucket, { cachedAtMs, nowMs } = {}) {
|
|
1116
|
+
if (!bucket || typeof bucket !== "object") return null;
|
|
1117
|
+
if (!bucket.window || typeof bucket.window !== "object") {
|
|
1118
|
+
return nowMs - cachedAtMs <= ZCODE_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS ? bucket : null;
|
|
1119
|
+
}
|
|
1120
|
+
const window = zcodeCachedWindow(bucket.window, { cachedAtMs, nowMs });
|
|
1121
|
+
return window ? { ...bucket, window } : null;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Read the last-good windows for the first provider key with a usable cache
|
|
1126
|
+
* entry. `providerKeys` arrives in live candidate priority order; keys whose
|
|
1127
|
+
* live attempts all ended in auth errors are withheld by the caller, and
|
|
1128
|
+
* `identityByKey` scopes each key to the credentials that could have
|
|
1129
|
+
* produced its windows.
|
|
1130
|
+
*/
|
|
1131
|
+
function readZcodeLimitsCache({ home, nowMs = Date.now(), providerKeys = [], identityByKey } = {}) {
|
|
1132
|
+
let parsed;
|
|
1133
|
+
try {
|
|
1134
|
+
parsed = JSON.parse(fs.readFileSync(zcodeLimitsCachePath({ home }), "utf8"));
|
|
1135
|
+
} catch (_error) {
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
1139
|
+
for (const providerKey of providerKeys) {
|
|
1140
|
+
const raw = parsed[providerKey];
|
|
1141
|
+
if (!raw || typeof raw !== "object") continue;
|
|
1142
|
+
const acceptableIdentities = identityByKey?.get(providerKey);
|
|
1143
|
+
if (!acceptableIdentities || !acceptableIdentities.size) continue;
|
|
1144
|
+
if (typeof raw.identity !== "string" || !acceptableIdentities.has(raw.identity)) continue;
|
|
1145
|
+
const cachedAtMs = Date.parse(raw.cached_at || "");
|
|
1146
|
+
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) continue;
|
|
1147
|
+
const context = { cachedAtMs, nowMs };
|
|
1148
|
+
const buckets = (Array.isArray(raw.buckets) ? raw.buckets : [])
|
|
1149
|
+
.map((bucket) => zcodeCachedBucket(bucket, context))
|
|
1150
|
+
.filter(Boolean);
|
|
1151
|
+
const primaryWindow = zcodeCachedWindow(raw.primary_window, context);
|
|
1152
|
+
const secondaryWindow = zcodeCachedWindow(raw.secondary_window, context);
|
|
1153
|
+
const tertiaryWindow = zcodeCachedWindow(raw.tertiary_window, context);
|
|
1154
|
+
if (!primaryWindow && !secondaryWindow && !tertiaryWindow && !buckets.some((bucket) => bucket.window)) continue;
|
|
1155
|
+
return {
|
|
1156
|
+
configured: true,
|
|
1157
|
+
error: null,
|
|
1158
|
+
provider_key: providerKey,
|
|
1159
|
+
// Live results carry their provider's region for the other_region
|
|
1160
|
+
// attach; the cached shape mirrors them so a cache-served card keeps
|
|
1161
|
+
// that section too (issue #959).
|
|
1162
|
+
region: zcodeRegionFromProviderId(providerKey),
|
|
1163
|
+
plan_kind: typeof raw.plan_kind === "string" ? raw.plan_kind : null,
|
|
1164
|
+
plan_id: typeof raw.plan_id === "string" ? raw.plan_id : null,
|
|
1165
|
+
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
1166
|
+
buckets,
|
|
1167
|
+
primary_window: primaryWindow,
|
|
1168
|
+
secondary_window: secondaryWindow,
|
|
1169
|
+
tertiary_window: tertiaryWindow,
|
|
1170
|
+
cached: true,
|
|
1171
|
+
stale: true,
|
|
1172
|
+
cached_at: raw.cached_at,
|
|
1173
|
+
source: "disk-cache",
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
return null;
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Merge partitions into the disk cache atomically; best-effort, never throws.
|
|
1181
|
+
* Cross-process semantics are last-writer-wins on purpose, matching the house
|
|
1182
|
+
* pattern (ark / qoder / codex caches): a concurrent writer can discard a
|
|
1183
|
+
* partition added between its read and rename, which costs one refresh cycle
|
|
1184
|
+
* — the next fetch rewrites it. Snapshots in particular keep getting
|
|
1185
|
+
* re-persisted on every fetch while their region's rows are inside the scan
|
|
1186
|
+
* window, so a lost write self-heals long before the window expires.
|
|
1187
|
+
*/
|
|
1188
|
+
function mergeZcodeLimitsCacheEntries(entries, { home } = {}) {
|
|
1189
|
+
const cachePath = zcodeLimitsCachePath({ home });
|
|
1190
|
+
let payload = {};
|
|
1191
|
+
try {
|
|
1192
|
+
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
1193
|
+
if (parsed && typeof parsed === "object") payload = parsed;
|
|
1194
|
+
} catch (_error) {}
|
|
1195
|
+
Object.assign(payload, entries);
|
|
1196
|
+
try {
|
|
1197
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
1198
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
1199
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
1200
|
+
fs.renameSync(tmpPath, cachePath);
|
|
1201
|
+
} catch (_error) {}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/** Merge one provider partition into the disk cache; best-effort, never throws. */
|
|
1205
|
+
function writeZcodeLimitsCache(limits, { home, nowMs = Date.now(), providerKey, identity } = {}) {
|
|
1206
|
+
if (!providerKey || !identity || !limits?.configured || limits.error) return;
|
|
1207
|
+
if (!limits.primary_window && !limits.secondary_window && !limits.tertiary_window) return;
|
|
1208
|
+
mergeZcodeLimitsCacheEntries({
|
|
1209
|
+
[providerKey]: {
|
|
1210
|
+
identity,
|
|
1211
|
+
plan_kind: typeof limits.plan_kind === "string" ? limits.plan_kind : null,
|
|
1212
|
+
plan_id: typeof limits.plan_id === "string" ? limits.plan_id : null,
|
|
1213
|
+
plan_label: typeof limits.plan_label === "string" ? limits.plan_label : null,
|
|
1214
|
+
buckets: Array.isArray(limits.buckets) ? limits.buckets : [],
|
|
1215
|
+
primary_window: limits.primary_window || null,
|
|
1216
|
+
secondary_window: limits.secondary_window || null,
|
|
1217
|
+
tertiary_window: limits.tertiary_window || null,
|
|
1218
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
1219
|
+
},
|
|
1220
|
+
}, { home });
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Issue #959: ZCode only queries the balance of the region currently in use,
|
|
1224
|
+
// so the other region's log rows stop appearing the moment it goes idle and
|
|
1225
|
+
// then age out of the 6h scan window — the other_region section would vanish.
|
|
1226
|
+
// While a region's rows are still fresh, snapshot its latest record into a
|
|
1227
|
+
// per-region cache partition; withZcodeOtherRegion falls back to it after the
|
|
1228
|
+
// window expires. The snapshot shape comes straight from
|
|
1229
|
+
// normalizeZcodeBalanceResponse, so the no-reset_credits / no-start_plan red
|
|
1230
|
+
// line holds structurally. Log records carry no login id, so unlike the
|
|
1231
|
+
// windows partitions there is no identity guard here: staleness is
|
|
1232
|
+
// self-attested by the snapshot's log_timestamp (rendered as the section's
|
|
1233
|
+
// time row), and the section stays collapsed and informational.
|
|
1234
|
+
function zcodeLogSnapshotCacheKey(region) {
|
|
1235
|
+
return `log-snapshot:${region}`;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function writeZcodeLogSnapshotCache(byRegion, { home, nowMs = Date.now() } = {}) {
|
|
1239
|
+
if (!(byRegion instanceof Map) || byRegion.size === 0) return;
|
|
1240
|
+
const entries = {};
|
|
1241
|
+
for (const [region, record] of byRegion) {
|
|
1242
|
+
if (region !== "bigmodel" && region !== "zai") continue;
|
|
1243
|
+
let normalized;
|
|
1244
|
+
try {
|
|
1245
|
+
normalized = normalizeZcodeBalanceResponse(record.body);
|
|
1246
|
+
} catch (_error) {
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
entries[zcodeLogSnapshotCacheKey(region)] = {
|
|
1250
|
+
source: "zcode-log",
|
|
1251
|
+
region,
|
|
1252
|
+
provider_key: record.providerKey,
|
|
1253
|
+
log_timestamp: record.log_timestamp,
|
|
1254
|
+
...normalized,
|
|
1255
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
if (Object.keys(entries).length > 0) mergeZcodeLimitsCacheEntries(entries, { home });
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
/**
|
|
1262
|
+
* Read one region's persisted log snapshot, shaped exactly like the fresh
|
|
1263
|
+
* withZcodeOtherRegion attach (issue #938). Deliberately no per-window
|
|
1264
|
+
* expiry: clipping aged windows would make the cached section render
|
|
1265
|
+
* differently from the same record served fresh inside the scan window.
|
|
1266
|
+
*/
|
|
1267
|
+
function readZcodeLogSnapshotCache(region, { home, nowMs = Date.now() } = {}) {
|
|
1268
|
+
if (region !== "bigmodel" && region !== "zai") return null;
|
|
1269
|
+
let parsed;
|
|
1270
|
+
try {
|
|
1271
|
+
parsed = JSON.parse(fs.readFileSync(zcodeLimitsCachePath({ home }), "utf8"));
|
|
1272
|
+
} catch (_error) {
|
|
1273
|
+
return null;
|
|
1274
|
+
}
|
|
1275
|
+
const raw = parsed?.[zcodeLogSnapshotCacheKey(region)];
|
|
1276
|
+
if (!raw || typeof raw !== "object" || raw.source !== "zcode-log" || raw.region !== region) return null;
|
|
1277
|
+
if (!Number.isFinite(Date.parse(typeof raw.log_timestamp === "string" ? raw.log_timestamp : ""))) return null;
|
|
1278
|
+
const cachedAtMs = Date.parse(typeof raw.cached_at === "string" ? raw.cached_at : "");
|
|
1279
|
+
if (!Number.isFinite(cachedAtMs) || cachedAtMs > nowMs + 60_000) return null;
|
|
1280
|
+
return {
|
|
1281
|
+
source: "zcode-log",
|
|
1282
|
+
region,
|
|
1283
|
+
provider_key: typeof raw.provider_key === "string" ? raw.provider_key : null,
|
|
1284
|
+
log_timestamp: raw.log_timestamp,
|
|
1285
|
+
plan_kind: typeof raw.plan_kind === "string" ? raw.plan_kind : null,
|
|
1286
|
+
plan_id: typeof raw.plan_id === "string" ? raw.plan_id : null,
|
|
1287
|
+
plan_label: typeof raw.plan_label === "string" ? raw.plan_label : null,
|
|
1288
|
+
buckets: Array.isArray(raw.buckets) ? raw.buckets : [],
|
|
1289
|
+
primary_window: raw.primary_window || null,
|
|
1290
|
+
secondary_window: raw.secondary_window || null,
|
|
1291
|
+
tertiary_window: raw.tertiary_window || null,
|
|
1292
|
+
};
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
/**
|
|
1296
|
+
* Attach the freshest logged balance snapshot from the user's other region
|
|
1297
|
+
* (issue #938): ZCode's own usage logs carry both regions' billing data even
|
|
1298
|
+
* when only the active region's credentials are on disk. Once those rows age
|
|
1299
|
+
* out of the 6h scan window (ZCode only queries the active region's balance,
|
|
1300
|
+
* issue #959), fall back to the snapshot persisted by
|
|
1301
|
+
* writeZcodeLogSnapshotCache while they were fresh. Either way the section is
|
|
1302
|
+
* read-only and deliberately carries no reset_credits / start_plan extras —
|
|
1303
|
+
* those are login-scoped and must never ride along across regions.
|
|
1304
|
+
*/
|
|
1305
|
+
function withZcodeOtherRegion(result, byRegion, { home, env = process.env, nowMs = Date.now() } = {}) {
|
|
1306
|
+
if (!result || result.error || !result.region) return result;
|
|
1307
|
+
const otherRegion = result.region === "bigmodel" ? "zai" : "bigmodel";
|
|
1308
|
+
const record = byRegion instanceof Map ? byRegion.get(otherRegion) : null;
|
|
1309
|
+
if (record) {
|
|
1310
|
+
return {
|
|
1311
|
+
...result,
|
|
1312
|
+
other_region: {
|
|
1313
|
+
source: "zcode-log",
|
|
1314
|
+
region: otherRegion,
|
|
1315
|
+
provider_key: record.providerKey,
|
|
1316
|
+
log_timestamp: record.log_timestamp,
|
|
1317
|
+
...normalizeZcodeBalanceResponse(record.body),
|
|
1318
|
+
},
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
// Saved snapshots are log-derived data, so the log-fallback kill switch
|
|
1322
|
+
// silences them along with the scan that feeds them.
|
|
1323
|
+
if (env?.TOKENTRACKER_ZCODE_DISABLE_LOG_FALLBACK === "1") return result;
|
|
1324
|
+
const snapshot = readZcodeLogSnapshotCache(otherRegion, { home, nowMs });
|
|
1325
|
+
if (!snapshot) return result;
|
|
1326
|
+
return { ...result, other_region: snapshot };
|
|
1327
|
+
}
|
|
1328
|
+
|
|
996
1329
|
/**
|
|
997
1330
|
* Login tokens for reset cards / Start Plan grants, or null when the quota key
|
|
998
1331
|
* cannot be proven to belong to the signed-in login. Both extras are personal
|
|
999
1332
|
* to the current login. Any key carrying an account id (per-account
|
|
1000
1333
|
* credentials, or config.json keys that byte-equal one) must match the login's
|
|
1001
|
-
* id.
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1005
|
-
*
|
|
1006
|
-
* and
|
|
1334
|
+
* id. In a per-account layout (credentials.json lists a coding-plan key for
|
|
1335
|
+
* this family) an unproven config.json key may be a leftover from a switched
|
|
1336
|
+
* account, so the extras stay fail-closed there. In a legacy layout without
|
|
1337
|
+
* any per-account credential for the family, no other account is known to
|
|
1338
|
+
* this ZCode home, so the config.json key cannot be attributed to a different
|
|
1339
|
+
* login and rides with the login's extras (#937). When the login
|
|
1340
|
+
* identity itself is unreadable (user_info missing or unparseable) nothing can
|
|
1341
|
+
* prove the key belongs to the signed-in account either, so the extras are
|
|
1342
|
+
* skipped and only the main plan quota stays.
|
|
1007
1343
|
*/
|
|
1008
1344
|
function loadZcodeLoginScopedTokens(family, auth, { home, env } = {}) {
|
|
1009
1345
|
if (loadZcodeActiveProvider({ home, env }) !== family) return null;
|
|
@@ -1011,7 +1347,11 @@ function loadZcodeLoginScopedTokens(family, auth, { home, env } = {}) {
|
|
|
1011
1347
|
if (!loginAccountId) return null;
|
|
1012
1348
|
if (auth.accountId) {
|
|
1013
1349
|
if (auth.accountId !== loginAccountId) return null;
|
|
1014
|
-
} else if (auth.auth_source === "credential:account-provider"
|
|
1350
|
+
} else if (auth.auth_source === "credential:account-provider") {
|
|
1351
|
+
return null;
|
|
1352
|
+
} else if (auth.auth_source === "provider:config"
|
|
1353
|
+
&& Object.keys(loadZcodeAccountProviderAuths({ home, env }))
|
|
1354
|
+
.some((providerKey) => zcodeProviderFamily(providerKey) === family)) {
|
|
1015
1355
|
return null;
|
|
1016
1356
|
}
|
|
1017
1357
|
const zcodeToken = loadZcodeCredential("zcodejwttoken", { home, env });
|
|
@@ -1021,7 +1361,12 @@ function loadZcodeLoginScopedTokens(family, auth, { home, env } = {}) {
|
|
|
1021
1361
|
}
|
|
1022
1362
|
|
|
1023
1363
|
/**
|
|
1024
|
-
* Return normalized quota windows, trying eligible providers
|
|
1364
|
+
* Return normalized quota windows, trying eligible providers, recent billing
|
|
1365
|
+
* logs, and finally the last-good disk cache. The result carries the winning
|
|
1366
|
+
* provider's `region` ("bigmodel" | "zai") and, when ZCode's own logs hold one,
|
|
1367
|
+
* an `other_region` snapshot of the second region's plan (issue #938) — read-only
|
|
1368
|
+
* log data, never extended with login-scoped extras, served from the logs while
|
|
1369
|
+
* fresh and from the disk cache once the scan window expires (issue #959).
|
|
1025
1370
|
* Credentials and internal team routing identifiers are omitted from the result.
|
|
1026
1371
|
*/
|
|
1027
1372
|
async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now(), providerTimeoutMs } = {}) {
|
|
@@ -1030,13 +1375,41 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
|
|
|
1030
1375
|
return { configured: false };
|
|
1031
1376
|
}
|
|
1032
1377
|
const authCandidates = loadZcodeAuthCandidates({ home, env });
|
|
1378
|
+
// One log pass serves both the failed-candidate fallback and the other
|
|
1379
|
+
// region's snapshot. selectLogFallback keeps the former gate semantics:
|
|
1380
|
+
// builtin: candidates only adopt per-account records carrying the same
|
|
1381
|
+
// (region, plan kind) pair as one of them.
|
|
1382
|
+
const logScan = scanZcodeBalanceLogs({
|
|
1383
|
+
home,
|
|
1384
|
+
env,
|
|
1385
|
+
providerKeys: authCandidates.map((auth) => auth.providerKey),
|
|
1386
|
+
nowMs,
|
|
1387
|
+
});
|
|
1388
|
+
// Issue #959: capture each region's freshest log record while it is still
|
|
1389
|
+
// inside the scan window, so the other_region section survives the window
|
|
1390
|
+
// expiring (ZCode stops writing rows for a region the moment it goes idle).
|
|
1391
|
+
writeZcodeLogSnapshotCache(logScan.byRegion, { home, nowMs });
|
|
1392
|
+
const requireProviderMatch = authCandidates.some((auth) => auth.planKind === "coding-plan");
|
|
1393
|
+
const selectLogFallback = (errors) => {
|
|
1394
|
+
const record = logScan.preferred
|
|
1395
|
+
|| (requireProviderMatch && authCandidates.length > 0 ? null : logScan.fallback);
|
|
1396
|
+
return withZcodeOtherRegion(zcodeLogFallbackResult(record, errors), logScan.byRegion, { home, env, nowMs });
|
|
1397
|
+
};
|
|
1033
1398
|
if (!authCandidates.length) {
|
|
1034
|
-
|
|
1035
|
-
if (logFallback) return zcodeLogFallbackResult(logFallback, []);
|
|
1036
|
-
return { configured: false };
|
|
1399
|
+
return selectLogFallback([]) || { configured: false };
|
|
1037
1400
|
}
|
|
1038
1401
|
const errors = [];
|
|
1402
|
+
const partitionFailures = [];
|
|
1039
1403
|
let emptySuccess = null;
|
|
1404
|
+
// The current login per region, resolved once: cache partitions are keyed
|
|
1405
|
+
// to it so an account switch cannot inherit the previous login's windows.
|
|
1406
|
+
const loginIdByFamily = new Map();
|
|
1407
|
+
const loginIdFor = (family) => {
|
|
1408
|
+
if (!loginIdByFamily.has(family)) {
|
|
1409
|
+
loginIdByFamily.set(family, loadZcodeLoginAccountId(family, { home, env }));
|
|
1410
|
+
}
|
|
1411
|
+
return loginIdByFamily.get(family);
|
|
1412
|
+
};
|
|
1040
1413
|
for (const auth of authCandidates) {
|
|
1041
1414
|
try {
|
|
1042
1415
|
const body = auth.planKind === "coding-plan"
|
|
@@ -1064,8 +1437,18 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
|
|
|
1064
1437
|
configured: true,
|
|
1065
1438
|
error: null,
|
|
1066
1439
|
provider_key: auth.providerKey,
|
|
1440
|
+
region: zcodeProviderFamily(auth.providerKey),
|
|
1067
1441
|
...normalized,
|
|
1068
1442
|
};
|
|
1443
|
+
// Last-good disk cache (house pattern: codex / ark / qoder): persist the
|
|
1444
|
+
// windows so a later network failure serves stale bars instead of a
|
|
1445
|
+
// vanished card. Login-scoped extras stay live-only.
|
|
1446
|
+
writeZcodeLimitsCache(result, {
|
|
1447
|
+
home,
|
|
1448
|
+
nowMs,
|
|
1449
|
+
providerKey: auth.providerKey,
|
|
1450
|
+
identity: zcodeCacheIdentity(loginIdFor(zcodeProviderFamily(auth.providerKey)), auth.teamContext),
|
|
1451
|
+
});
|
|
1069
1452
|
if (Array.isArray(normalized.buckets) && normalized.buckets.length === 0 && authCandidates.length > 1) {
|
|
1070
1453
|
emptySuccess = emptySuccess || result;
|
|
1071
1454
|
continue;
|
|
@@ -1126,20 +1509,44 @@ async function fetchZcodeLimits({ home, env, fetchImpl = fetch, nowMs = Date.now
|
|
|
1126
1509
|
}
|
|
1127
1510
|
}
|
|
1128
1511
|
}
|
|
1129
|
-
return result;
|
|
1512
|
+
return withZcodeOtherRegion(result, logScan.byRegion, { home, env, nowMs });
|
|
1130
1513
|
} catch (error) {
|
|
1131
1514
|
errors.push(`${auth.providerKey}: ${error?.message || "Unknown error"}`);
|
|
1515
|
+
partitionFailures.push({ providerKey: auth.providerKey, authError: error?.authError === true });
|
|
1132
1516
|
}
|
|
1133
1517
|
}
|
|
1134
|
-
if (emptySuccess) return emptySuccess;
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1518
|
+
if (emptySuccess) return withZcodeOtherRegion(emptySuccess, logScan.byRegion, { home, env, nowMs });
|
|
1519
|
+
// Rank fallback partitions in live candidate priority order, and withhold
|
|
1520
|
+
// any partition whose attempts all ended in auth errors: re-login is
|
|
1521
|
+
// actionable, so neither a stale log record nor stale cache bars may mask
|
|
1522
|
+
// it (opencode-go auth_error / codex reauth precedent).
|
|
1523
|
+
const cacheOrder = [];
|
|
1524
|
+
for (const auth of authCandidates) {
|
|
1525
|
+
if (!cacheOrder.includes(auth.providerKey)) cacheOrder.push(auth.providerKey);
|
|
1526
|
+
}
|
|
1527
|
+
const cacheEligible = cacheOrder.filter((providerKey) =>
|
|
1528
|
+
partitionFailures.some((failure) => failure.providerKey === providerKey && !failure.authError));
|
|
1529
|
+
const authErrorOnlyProviders = new Set(cacheOrder.filter((providerKey) => !cacheEligible.includes(providerKey)));
|
|
1530
|
+
// Prefer the #942 scan (region+plan preferred match) over a second log pass,
|
|
1531
|
+
// then apply the #947 auth-error gate so re-login stays actionable.
|
|
1532
|
+
const logRecord = logScan.preferred
|
|
1533
|
+
|| (requireProviderMatch && authCandidates.length > 0 ? null : logScan.fallback);
|
|
1534
|
+
// A log record for a provider with no live candidate is untouched by this
|
|
1535
|
+
// gate: nothing was attempted for it, so no error is being masked.
|
|
1536
|
+
if (logRecord && !authErrorOnlyProviders.has(logRecord.providerKey)) {
|
|
1537
|
+
return withZcodeOtherRegion(zcodeLogFallbackResult(logRecord, errors), logScan.byRegion, { home, env, nowMs });
|
|
1538
|
+
}
|
|
1539
|
+
// Last stop before the error result: serve the disk cache in live candidate
|
|
1540
|
+
// priority order, scoped to the login that produced the windows so an
|
|
1541
|
+
// account switch cannot inherit the previous login's bars.
|
|
1542
|
+
const identityByKey = new Map();
|
|
1543
|
+
for (const auth of authCandidates) {
|
|
1544
|
+
if (!cacheEligible.includes(auth.providerKey)) continue;
|
|
1545
|
+
if (!identityByKey.has(auth.providerKey)) identityByKey.set(auth.providerKey, new Set());
|
|
1546
|
+
identityByKey.get(auth.providerKey).add(zcodeCacheIdentity(loginIdFor(zcodeProviderFamily(auth.providerKey)), auth.teamContext));
|
|
1547
|
+
}
|
|
1548
|
+
const cached = readZcodeLimitsCache({ home, nowMs, providerKeys: cacheEligible, identityByKey });
|
|
1549
|
+
if (cached) return withZcodeOtherRegion(cached, logScan.byRegion, { home, env, nowMs });
|
|
1143
1550
|
return {
|
|
1144
1551
|
configured: true,
|
|
1145
1552
|
error: errors[0] || "Unknown error",
|
|
@@ -1157,7 +1564,10 @@ module.exports = {
|
|
|
1157
1564
|
loadZcodeCredential,
|
|
1158
1565
|
loadZcodeLoginAccountId,
|
|
1159
1566
|
zcodeProviderFamily,
|
|
1567
|
+
zcodeRegionFromProviderId,
|
|
1568
|
+
zcodePlanKindFromProviderId,
|
|
1160
1569
|
loadLatestZcodeBalanceFromLogs,
|
|
1570
|
+
scanZcodeBalanceLogs,
|
|
1161
1571
|
resolveZcodeProviderBillingBaseUrl,
|
|
1162
1572
|
resolveZcodeProviderQuotaUrl,
|
|
1163
1573
|
loadZcodeAuthCandidates,
|
|
@@ -1168,5 +1578,10 @@ module.exports = {
|
|
|
1168
1578
|
fetchZcodeBilling,
|
|
1169
1579
|
fetchZcodeCodingPlanQuota,
|
|
1170
1580
|
fetchZcodeStartPlanGrants,
|
|
1581
|
+
readZcodeLimitsCache,
|
|
1582
|
+
writeZcodeLimitsCache,
|
|
1583
|
+
writeZcodeLogSnapshotCache,
|
|
1584
|
+
readZcodeLogSnapshotCache,
|
|
1585
|
+
zcodeCacheIdentity,
|
|
1171
1586
|
fetchZcodeLimits,
|
|
1172
1587
|
};
|