tokentracker-cli 1.5.13 → 1.5.15
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-Ct0vfdYb.js → AchievementBadge-CwjI28oT.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-dfoRxqOv.js → AchievementsPage-Dq544fjX.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DHFyssU0.js → AchievementsSection-DufV6nod.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-ChJYZMSq.js → BadgeDetailModal-vw8xydKu.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-C_qaJy1-.js → BotAnimated-De0anwsh.js} +1 -1
- package/dashboard/dist/assets/{Card-BhK2nmOu.js → Card-CuSeWBDM.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-Ele1kkut.js → CloudCheckoutPage-CxFcOwAj.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-C5DMqcoV.js → CloudPage-Beb7_JLW.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-C_hxoIwL.js → CommandPalette-PsUq2iRS.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BSTqBIEU.js → CommunityStatsModal-b54WaPbK.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DI4QK5EV.js → ConfirmModal-_NdunIh5.js} +1 -1
- package/dashboard/dist/assets/{Controls-zK1rzS_8.js → Controls-5Is6MWln.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BdDnmc60.js → DashboardPage-C0n0qC7h.js} +1 -1
- package/dashboard/dist/assets/{DateRangePopover-DqwgxpNF.js → DateRangePopover-C3WfGy-0.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-C0fQKKuI.js → DevicePage-Vn6k-mGd.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-C_cbyHpb.js → DialogClose-CXttnna-.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-C3KGos_G.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-BcBpd--_.js → DialogTitle-B0cNIo0E.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-Dw9gkExE.js → ElectricLogo-DC3XSfIu.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Ci69gBGl.js → FadeIn-H7S3TMHi.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-pb03c0kb.js → HeaderGithubStar-xgbIo5OP.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-Cqd0D789.js → HoverTooltip-CpzM5Dju.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-qILbvOJn.js → IpCheckPage-BdV7S4dY.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CCIwaAeu.js → LandingPage-D9f28ryO.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BwzABmKU.js → LeaderboardPage--WUFpkBI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-7kdzfw8r.js → LeaderboardProBadge-CEvrQ75Y.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-uMnN-IGe.js → LeaderboardProfileModal-Bn0HFr5h.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CQNc4MES.js → LeaderboardProfilePage-DJ7fQI68.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CJ1LImRm.js → LimitsPage-7tPDheuX.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-Dm0l_F0I.js → LocalOnlyNotice-DlGbVQS8.js} +1 -1
- package/dashboard/dist/assets/{LoginCard--tUiBVtr.js → LoginCard-BZZmzUGh.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B1-5fFMY.js → LoginModal-BT5_bIxU.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BYl6NnZ8.js → LoginPage-DMjDUPPi.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DJNGJZrR.js → PetPage-Dk3j6ITn.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-aRAdIkTs.js → PopoverPopup-pugRu3RC.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-uiA-KBe2.js → ProviderIcon-CtmL1CmY.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BZT353_T.js → ResetPasswordPage-BB6U7t8P.js} +1 -1
- package/dashboard/dist/assets/{Select-CPC04j0g.js → Select-7w1su8lw.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-D1vJVrFO.js → SelectItemText-CaEUmWkj.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-FL3EDqmb.js → SelfHostPage-DctWoF-A.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-BohSSNai.js → ServiceStatusPage-Dg3ljEXC.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Cf_HASvt.js → SessionsPage-B1dvW-be.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-B4ztKHnj.js → SettingsPage-DuXSX1R2.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-BFiQkY7O.js → SkillsPage-CvXQFgc9.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BHVdTJAP.js → TokenGalaxy-DKC8Rk_Y.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-BMtQyFn6.js → TooltipPopup-Dp9EpHLG.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-DN-g3CNm.js → TrendMonitor-DWnlBn3H.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-Bv32yu8M.js → WidgetsPage-GECNmb1Q.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-HVTOehNb.js → WrappedPage-Dq_1lOnb.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DnYJww7c.js → __vite-browser-external-CkF0pdTJ.js} +2 -2
- package/dashboard/dist/assets/{agent-logos-BOwAJQqL.js → agent-logos-Dqe3Vrah.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CeXuUuES.js → arrow-left-Cr_2QnDu.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-DOdBG1FZ.js → arrow-right-BsK2sJgt.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-Dponssa4.js → arrow-up-right-DkWMGqXo.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-CsxE6ot0.js → calendar-days-BB9VV438.js} +1 -1
- package/dashboard/dist/assets/{check-BroNG_vu.js → check-bpSCJZWh.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-C5ZpGeO1.js → chevron-down-CtECtDmx.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BtS-hZTc.js → cloud-checkout-C4PrzKhN.js} +1 -1
- package/dashboard/dist/assets/{composite-fgJbx88k.js → composite-BHisG82k.js} +1 -1
- package/dashboard/dist/assets/{copy-CHBuwNUs.js → copy-Dmxzd-Sw.js} +1 -1
- package/dashboard/dist/assets/{download-EKd2MHkv.js → download-CkdQzw2w.js} +1 -1
- package/dashboard/dist/assets/{external-link-GkSo9C78.js → external-link-C971FjvC.js} +1 -1
- package/dashboard/dist/assets/{flame-Cg96qJrE.js → flame-mKbp7kxw.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-ChG2_n4h.js → format-tokens-C35FrJnH.js} +1 -1
- package/dashboard/dist/assets/{gift-B3QDbWPE.js → gift-BX2lSOmc.js} +1 -1
- package/dashboard/dist/assets/{icons-BEAyMjau.js → icons-ov92u52T.js} +1 -1
- package/dashboard/dist/assets/{index-B0kZThUB.js → index-9OCL-BL8.js} +1 -1
- package/dashboard/dist/assets/{index-DFcQyAnr.js → index-Bgm38oHe.js} +1 -1
- package/dashboard/dist/assets/{index-Cuc8ZXgl.js → index-Ca5xkNrK.js} +1 -1
- package/dashboard/dist/assets/{index-B9a1fK8I.js → index-DF03nIHb.js} +1 -1
- package/dashboard/dist/assets/{info-796bNttV.js → info-B-f9hWcw.js} +1 -1
- package/dashboard/dist/assets/{link-2-LYRIPLwz.js → link-2-l1wuezTo.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DpKC9Uel.js → loader-circle-Lmjrs8XX.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-Ctbuc6U4.js → local-usage-transfer-Ds6cF41d.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-C5YTeNW1.js → modifiers.esm-tVlltJXO.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-DJUCfEvG.js → monitor-smartphone-XRiYiY8u.js} +1 -1
- package/dashboard/dist/assets/{play-LC-lnw6w.js → play-DaD9EMjk.js} +1 -1
- package/dashboard/dist/assets/{plus-CDF0S5xd.js → plus-D30fbsKQ.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CkITUZgR.js → provider-display-CKdhBOwz.js} +1 -1
- package/dashboard/dist/assets/{react-BIpM-G7K.js → react-BY0y0K4x.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-C3Ea423q.js → safePolygon-CSG_P7rG.js} +1 -1
- package/dashboard/dist/assets/{search-C-tN1mTM.js → search-Kiq8wVHA.js} +1 -1
- package/dashboard/dist/assets/{server-DJ4gq_fT.js → server-CjovGBGz.js} +1 -1
- package/dashboard/dist/assets/{shield-check-f75hry9N.js → shield-check-DF9MmMCq.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CZ_vfnRL.js → skills-api-CEzr6p92.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-mLJSvq3M.js → sortable.esm-D7jd9mE_.js} +1 -1
- package/dashboard/dist/assets/{store-CZV1KXw2.js → store-D9_jo_do.js} +1 -1
- package/dashboard/dist/assets/{terminal-B9vOwCl9.js → terminal-CLOlvzR-.js} +1 -1
- package/dashboard/dist/assets/{trash-BN9GbynA.js → trash-DIe0fsDD.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bc2d8tqa.js → use-community-stats-gDkdiRBz.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-_0H4Rk58.js → use-latest-request-guard-tXGuKvua.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-Dln-Rxps.js → use-limits-display-prefs-CCuLFy00.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CpkcGzXp.js → use-native-settings-hgYqaiR7.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DpGA5U4S.js → use-ordered-list-D6-YzYJI.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DfbDxs5a.js +1 -0
- package/dashboard/dist/assets/{use-transform-JXJt1W74.js → use-transform-BEiXvAuE.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BK_H1-b7.js → use-usage-limits-BlwtyPtT.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-ilbtKs16.js → useCurrency-CwCGJLHb.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-M35LtCGR.js → usePositioner-BqHtRIrE.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-BtTQjv7z.js → useTokenFormat-BUNBUX-x.js} +1 -1
- package/dashboard/dist/assets/{zap-5xemt9EH.js → zap-C0RFs2qQ.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/sync.js +2 -2
- package/src/lib/limits-cache.js +475 -0
- package/src/lib/pricing/curated-overrides.json +24 -0
- package/src/lib/pricing/matcher.js +4 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +261 -102
- package/src/lib/usage-limits.js +370 -1063
- package/dashboard/dist/assets/DialogDescription--_wg9JdV.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-Cy21hg3t.js +0 -1
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Shared last-good disk-cache pipeline for usage-limits providers (issue #941).
|
|
4
|
+
//
|
|
5
|
+
// Fifteen providers shipped their own copy of the same pipeline: a cache file
|
|
6
|
+
// under ~/.tokentracker/tracker/, written only on a successful live read
|
|
7
|
+
// (tmp + rename, cached_at stamped), read back only when the live read failed
|
|
8
|
+
// with a transient error and every cached window still lies in the future.
|
|
9
|
+
// The copies differed almost exclusively in three parameters — the identity
|
|
10
|
+
// partition key, the field/window list, and the TTL gates — so this module
|
|
11
|
+
// folds the pipeline into one factory plus the storage primitives. Per the
|
|
12
|
+
// umbrella issue the UI already renders stale results on every surface, so a
|
|
13
|
+
// provider adopting this helper needs no UI change.
|
|
14
|
+
//
|
|
15
|
+
// Red lines every parameterization must keep (issue #941):
|
|
16
|
+
// 1. actionable auth/config errors punch through — the cache never masks them;
|
|
17
|
+
// 2. a window whose reset has already rolled over is never served as current;
|
|
18
|
+
// 3. windows without a reset stamp are bounded by cached_at + a TTL
|
|
19
|
+
// (the global max-age, or an explicit unknownResetTtlMs);
|
|
20
|
+
// 4. multi-identity providers partition the cache so one account's bars can
|
|
21
|
+
// never be pinned by another's entry.
|
|
22
|
+
|
|
23
|
+
const fs = require("node:fs");
|
|
24
|
+
const os = require("node:os");
|
|
25
|
+
const path = require("node:path");
|
|
26
|
+
|
|
27
|
+
// Clock skew tolerated when a cache stamp lies slightly in the future (copied
|
|
28
|
+
// files, VM pauses): up to one minute past `now` still counts as usable.
|
|
29
|
+
const FUTURE_STAMP_SKEW_MS = 60_000;
|
|
30
|
+
|
|
31
|
+
// ISO-8601 strings only (the shape every provider stamps via
|
|
32
|
+
// `new Date(ms).toISOString()`). Returns ms, or null when absent/unparseable.
|
|
33
|
+
function parseTimeMs(value) {
|
|
34
|
+
if (typeof value !== "string" || !value) return null;
|
|
35
|
+
const ts = Date.parse(value);
|
|
36
|
+
return Number.isFinite(ts) && ts > 0 ? ts : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function resolveTrackerDir({ home, trackerDir } = {}) {
|
|
40
|
+
if (typeof trackerDir === "string" && trackerDir.trim()) return trackerDir.trim();
|
|
41
|
+
return path.join(home || os.homedir(), ".tokentracker", "tracker");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Reads and parses a cache file, collapsing every I/O / JSON error into null —
|
|
45
|
+
// a broken cache file must degrade to "no cache", never to a thrown error.
|
|
46
|
+
function readCacheJson(cachePath) {
|
|
47
|
+
try {
|
|
48
|
+
return JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
49
|
+
} catch (_error) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Atomic write: write a same-directory tmp file, then rename over the target.
|
|
55
|
+
// Readers therefore see either the previous or the next payload, never a torn
|
|
56
|
+
// one. Failures are swallowed by design — a cache write must never turn a
|
|
57
|
+
// successful live read into a failed poll.
|
|
58
|
+
function writeCacheJsonAtomic(cachePath, payload) {
|
|
59
|
+
try {
|
|
60
|
+
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
61
|
+
const tmpPath = `${cachePath}.${process.pid}.tmp`;
|
|
62
|
+
fs.writeFileSync(tmpPath, JSON.stringify(payload, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
63
|
+
fs.renameSync(tmpPath, cachePath);
|
|
64
|
+
} catch (_error) {}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function clearCacheFile(cachePath) {
|
|
68
|
+
try {
|
|
69
|
+
fs.rmSync(cachePath, { force: true });
|
|
70
|
+
} catch (_error) {}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Builds the read/write/clear facade for one provider's last-good cache.
|
|
75
|
+
*
|
|
76
|
+
* Options:
|
|
77
|
+
* file filename, or ({ namespace }) => filename for
|
|
78
|
+
* namespace-partitioned files (qoder / qoder-cn).
|
|
79
|
+
* key payload wrapper key ("kimi" → `{ kimi: {...} }`);
|
|
80
|
+
* null stores the entry flat at the top level (ark).
|
|
81
|
+
* fields { name: kind } — kinds: "string" (write `|| null`,
|
|
82
|
+
* read typeof check), "number" (Number.isFinite both
|
|
83
|
+
* sides), "raw" (`?? null` both sides), "boolTrue"
|
|
84
|
+
* (`=== true` both sides). A field may instead be
|
|
85
|
+
* { read, write } functions for bespoke shapes
|
|
86
|
+
* (claude's weekly_scoped array). A kind may also
|
|
87
|
+
* declare `survivor(value, ctx)` — content counting as
|
|
88
|
+
* window survivors for the write guard and the read
|
|
89
|
+
* survivor gate (claude's scoped-only snapshots).
|
|
90
|
+
* windowKeys object windows gated per-window by their reset stamp;
|
|
91
|
+
* dead windows become null (or vanish — sparseWindowKeys).
|
|
92
|
+
* sparseWindowKeys window keys omitted from the payload entirely when
|
|
93
|
+
* absent/dead (qoder's tertiary_window), instead of
|
|
94
|
+
* being written/read as null.
|
|
95
|
+
* windowResetField per-window reset field name ("reset_at"; claude uses
|
|
96
|
+
* "resets_at").
|
|
97
|
+
* resetTimeMode "iso" (default) | "unix-seconds" (codex).
|
|
98
|
+
* maxAgeMs global fresh-keeping bound; number, (rawEntry) => number
|
|
99
|
+
* for source-dependent bounds (opencode-go), or null for
|
|
100
|
+
* no global gate (ark — undated windows ride
|
|
101
|
+
* unknownResetTtlMs alone). Default: 7 days.
|
|
102
|
+
* maxAgeGate "always" (reject old snapshots up front) |
|
|
103
|
+
* "ifNoDatedSurvivor" (qoder: a snapshot with a
|
|
104
|
+
* future-dated window outlives the global max-age; the
|
|
105
|
+
* gate then only bounds undated survivors).
|
|
106
|
+
* unknownResetTtlMs keep-without-reset bound for windows carrying no reset
|
|
107
|
+
* stamp (12h where used); default: keep (the global
|
|
108
|
+
* max-age already bounds them).
|
|
109
|
+
* stale true (fallback reads — the standard annotation) |
|
|
110
|
+
* false | "byAge" (stale = older than freshTtlMs, qoder)
|
|
111
|
+
* | null (omit the field entirely, antigravity).
|
|
112
|
+
* freshTtlMs required by stale: "byAge".
|
|
113
|
+
* cachedFlag include `cached: true` in reads (antigravity/qoder/ark).
|
|
114
|
+
* source read-side provenance for `source`:
|
|
115
|
+
* { const: "disk-cache" } (ark) | "provenance" (qoder —
|
|
116
|
+
* fresh hits re-serve the written source, older hits
|
|
117
|
+
* report "disk-cache"; #955). The field is written from
|
|
118
|
+
* record.source as a plain string-or-null.
|
|
119
|
+
* identityField entry field holding the partition key ("identity";
|
|
120
|
+
* ark: "profile_identity"). null = unpartitioned.
|
|
121
|
+
* identityRequired true: a read/write without an identity is refused and a
|
|
122
|
+
* stored identity must match (cursor-family, fail-closed).
|
|
123
|
+
* false: mismatch only rejects when the caller could
|
|
124
|
+
* establish an identity at all (ark, fail-open).
|
|
125
|
+
* writeGuard (record) => bool; default: configured, error-free, and
|
|
126
|
+
* carrying at least one window. Ark overrides (its
|
|
127
|
+
* normalizer already threw on windowless payloads).
|
|
128
|
+
* dirOverride option name overriding the tracker dir
|
|
129
|
+
* ("trackerDir", opencode-go).
|
|
130
|
+
*/
|
|
131
|
+
function createLimitsCache(options) {
|
|
132
|
+
const {
|
|
133
|
+
file,
|
|
134
|
+
key,
|
|
135
|
+
fields = {},
|
|
136
|
+
windowKeys = [],
|
|
137
|
+
sparseWindowKeys = [],
|
|
138
|
+
windowResetField = "reset_at",
|
|
139
|
+
resetTimeMode = "iso",
|
|
140
|
+
maxAgeMs = 7 * 24 * 60 * 60 * 1000,
|
|
141
|
+
maxAgeGate = "always",
|
|
142
|
+
unknownResetTtlMs = null,
|
|
143
|
+
stale = true,
|
|
144
|
+
freshTtlMs = null,
|
|
145
|
+
cachedFlag = false,
|
|
146
|
+
source = null,
|
|
147
|
+
identityField = null,
|
|
148
|
+
identityRequired = false,
|
|
149
|
+
writeGuard = null,
|
|
150
|
+
dirOverride = null,
|
|
151
|
+
} = options;
|
|
152
|
+
|
|
153
|
+
// Fields whose content can keep a snapshot usable even when no windowKey
|
|
154
|
+
// does (claude's weekly_scoped array). A kind declares a `survivor(value,
|
|
155
|
+
// ctx) => bool` hook — ctx mirrors the fields.read context — applied to the
|
|
156
|
+
// record on the write guard and to the raw entry on the read survivor gate,
|
|
157
|
+
// so scoped-only snapshots live and expire through the same predicate.
|
|
158
|
+
const survivorFields = Object.entries(fields).filter(
|
|
159
|
+
([, kind]) => typeof kind?.survivor === "function",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
if (stale === "byAge" && !Number.isFinite(freshTtlMs)) {
|
|
163
|
+
throw new Error("limits-cache: stale \"byAge\" requires freshTtlMs");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cachePath({ home, namespace, trackerDir } = {}) {
|
|
167
|
+
const name = typeof file === "function" ? file({ namespace }) : file;
|
|
168
|
+
return path.join(resolveTrackerDir({ home, trackerDir: dirOverride ? trackerDir : undefined }), name);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function hasWindow(record, { nowMs = Date.now() } = {}) {
|
|
172
|
+
if (windowKeys.some((windowKey) => Boolean(record?.[windowKey]))) return true;
|
|
173
|
+
return survivorFields.some(([field, kind]) => Boolean(
|
|
174
|
+
kind.survivor(record?.[field], { nowMs, cachedAtMs: null, isWindowUsable }),
|
|
175
|
+
));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function shouldWrite(record, { nowMs } = {}) {
|
|
179
|
+
if (writeGuard) return writeGuard(record);
|
|
180
|
+
return Boolean(record?.configured) && !record.error && hasWindow(record, { nowMs });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A window whose reset has already passed is stale data, not a usable
|
|
184
|
+
// fallback — the quota rolled over, so serving its old usage would mislead.
|
|
185
|
+
// Windows without a parseable stamp are kept unless unknownResetTtlMs
|
|
186
|
+
// bounds them by the snapshot's age instead.
|
|
187
|
+
function isWindowUsable(window, { cachedAtMs, nowMs } = {}) {
|
|
188
|
+
if (!window || typeof window !== "object") return false;
|
|
189
|
+
let resetAtMs;
|
|
190
|
+
if (resetTimeMode === "unix-seconds") {
|
|
191
|
+
const resetAt = Number(window[windowResetField]);
|
|
192
|
+
resetAtMs = Number.isFinite(resetAt) && resetAt > 0 ? resetAt * 1000 : null;
|
|
193
|
+
} else {
|
|
194
|
+
resetAtMs = parseTimeMs(window[windowResetField]);
|
|
195
|
+
}
|
|
196
|
+
if (resetAtMs !== null) return resetAtMs > nowMs;
|
|
197
|
+
if (unknownResetTtlMs !== null) {
|
|
198
|
+
return Number.isFinite(cachedAtMs) && nowMs - cachedAtMs <= unknownResetTtlMs;
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function writeField(field, kind, record) {
|
|
204
|
+
if (typeof kind === "object" && kind !== null && typeof kind.write === "function") {
|
|
205
|
+
return kind.write(record);
|
|
206
|
+
}
|
|
207
|
+
const value = record?.[field];
|
|
208
|
+
switch (kind) {
|
|
209
|
+
case "string":
|
|
210
|
+
return value || null;
|
|
211
|
+
case "number":
|
|
212
|
+
return Number.isFinite(value) ? value : null;
|
|
213
|
+
case "raw":
|
|
214
|
+
return value ?? null;
|
|
215
|
+
case "boolTrue":
|
|
216
|
+
return value === true;
|
|
217
|
+
default:
|
|
218
|
+
return value ?? null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readField(field, kind, raw, ctx) {
|
|
223
|
+
if (typeof kind === "object" && kind !== null && typeof kind.read === "function") {
|
|
224
|
+
return kind.read(raw, ctx);
|
|
225
|
+
}
|
|
226
|
+
const value = raw?.[field];
|
|
227
|
+
switch (kind) {
|
|
228
|
+
case "string":
|
|
229
|
+
return typeof value === "string" ? value : null;
|
|
230
|
+
case "number":
|
|
231
|
+
return Number.isFinite(value) ? value : null;
|
|
232
|
+
case "raw":
|
|
233
|
+
return value ?? null;
|
|
234
|
+
case "boolTrue":
|
|
235
|
+
return value === true;
|
|
236
|
+
default:
|
|
237
|
+
return value ?? null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Core validator: cached_at gates, identity gate, then the per-window reset
|
|
242
|
+
// gates. Returns the servable record or null — a cache with no surviving
|
|
243
|
+
// window must never pin an error-shaped result in place.
|
|
244
|
+
function normalizeEntry(raw, { nowMs = Date.now(), identity, maxAgeMs: maxAgeOverride, stale: staleOverride } = {}) {
|
|
245
|
+
const cachedAtMs = parseTimeMs(raw?.cached_at);
|
|
246
|
+
if (!Number.isFinite(cachedAtMs)) return null;
|
|
247
|
+
if (cachedAtMs > nowMs + FUTURE_STAMP_SKEW_MS) return null;
|
|
248
|
+
|
|
249
|
+
if (identityField && identityRequired && !identity) return null;
|
|
250
|
+
if (identityField && identity && raw?.[identityField] !== identity) return null;
|
|
251
|
+
|
|
252
|
+
const effectiveMaxAge = typeof maxAgeMs === "function"
|
|
253
|
+
? maxAgeMs(raw)
|
|
254
|
+
: maxAgeMs;
|
|
255
|
+
|
|
256
|
+
// Per-window gates first; dropped windows become null (or vanish for
|
|
257
|
+
// sparse keys) so a partially-dead snapshot still serves its survivors.
|
|
258
|
+
const windows = {};
|
|
259
|
+
let hasSurvivor = false;
|
|
260
|
+
let hasDatedSurvivor = false;
|
|
261
|
+
for (const windowKey of windowKeys) {
|
|
262
|
+
const window = raw?.[windowKey];
|
|
263
|
+
const usable = isWindowUsable(window, { cachedAtMs, nowMs });
|
|
264
|
+
if (sparseWindowKeys.includes(windowKey)) {
|
|
265
|
+
if (usable) {
|
|
266
|
+
windows[windowKey] = window;
|
|
267
|
+
hasSurvivor = true;
|
|
268
|
+
if (parseTimeMs(window?.[windowResetField]) !== null) hasDatedSurvivor = true;
|
|
269
|
+
}
|
|
270
|
+
} else {
|
|
271
|
+
windows[windowKey] = usable ? window : null;
|
|
272
|
+
if (usable) {
|
|
273
|
+
hasSurvivor = true;
|
|
274
|
+
if (parseTimeMs(window?.[windowResetField]) !== null) hasDatedSurvivor = true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Extra survivor fields (claude's weekly_scoped) count as window content:
|
|
279
|
+
// their survivor predicate applies the field's own expiry filter, so a
|
|
280
|
+
// scoped-only snapshot survives while its entries are still live and a
|
|
281
|
+
// windowless shell is still refused when they are not.
|
|
282
|
+
let hasExtraSurvivor = false;
|
|
283
|
+
for (const [field, kind] of survivorFields) {
|
|
284
|
+
if (kind.survivor(raw?.[field], { nowMs, cachedAtMs, isWindowUsable })) {
|
|
285
|
+
hasExtraSurvivor = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (windowKeys.length > 0 && !hasSurvivor && !hasExtraSurvivor) return null;
|
|
289
|
+
|
|
290
|
+
if (effectiveMaxAge !== null) {
|
|
291
|
+
if (maxAgeGate === "ifNoDatedSurvivor") {
|
|
292
|
+
if (!hasDatedSurvivor && nowMs - cachedAtMs > effectiveMaxAge) return null;
|
|
293
|
+
} else if (nowMs - cachedAtMs > (maxAgeOverride ?? effectiveMaxAge)) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const normalized = {
|
|
299
|
+
configured: true,
|
|
300
|
+
error: null,
|
|
301
|
+
};
|
|
302
|
+
for (const [field, kind] of Object.entries(fields)) {
|
|
303
|
+
normalized[field] = readField(field, kind, raw, { nowMs, cachedAtMs, isWindowUsable });
|
|
304
|
+
}
|
|
305
|
+
Object.assign(normalized, windows);
|
|
306
|
+
if (cachedFlag) normalized.cached = true;
|
|
307
|
+
|
|
308
|
+
const effectiveStale = staleOverride ?? stale;
|
|
309
|
+
if (effectiveStale === "byAge") {
|
|
310
|
+
normalized.stale = nowMs - cachedAtMs > freshTtlMs;
|
|
311
|
+
} else if (effectiveStale !== null) {
|
|
312
|
+
normalized.stale = effectiveStale;
|
|
313
|
+
}
|
|
314
|
+
normalized.cached_at = raw.cached_at;
|
|
315
|
+
|
|
316
|
+
if (source !== null) {
|
|
317
|
+
if (source === "provenance") {
|
|
318
|
+
const writtenSource = typeof raw?.source === "string" && raw.source.trim()
|
|
319
|
+
? raw.source.trim()
|
|
320
|
+
: null;
|
|
321
|
+
// Provenance must describe the data's age, not the transport (#955):
|
|
322
|
+
// a fresh hit re-serves the winning source it was written with, an
|
|
323
|
+
// older hit honestly reports the disk cache.
|
|
324
|
+
normalized.source = normalized.stale === false && writtenSource
|
|
325
|
+
? writtenSource
|
|
326
|
+
: "disk-cache";
|
|
327
|
+
} else if (typeof source === "object" && typeof source.const === "string") {
|
|
328
|
+
normalized.source = source.const;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return windowKeys.length > 0 || Object.keys(fields).length > 0 ? normalized : null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function read(opts = {}) {
|
|
336
|
+
const { home, namespace, trackerDir, nowMs = Date.now(), identity, maxAgeMs: maxAgeOverride, stale: staleOverride } = opts;
|
|
337
|
+
if (identityField && identityRequired && !identity) return null;
|
|
338
|
+
const parsed = readCacheJson(cachePath({ home, namespace, trackerDir }));
|
|
339
|
+
const entry = key ? parsed?.[key] : parsed;
|
|
340
|
+
return normalizeEntry(entry, { nowMs, identity, maxAgeMs: maxAgeOverride, stale: staleOverride });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function write(record, opts = {}) {
|
|
344
|
+
const { home, namespace, trackerDir, nowMs = Date.now(), identity } = opts;
|
|
345
|
+
if (identityField && identityRequired && !identity) return;
|
|
346
|
+
if (!shouldWrite(record, { nowMs })) return;
|
|
347
|
+
const entry = {};
|
|
348
|
+
if (identityField && identityRequired) entry[identityField] = identity;
|
|
349
|
+
for (const [field, kind] of Object.entries(fields)) {
|
|
350
|
+
entry[field] = writeField(field, kind, record);
|
|
351
|
+
}
|
|
352
|
+
for (const windowKey of windowKeys) {
|
|
353
|
+
if (sparseWindowKeys.includes(windowKey)) {
|
|
354
|
+
if (record?.[windowKey]) entry[windowKey] = record[windowKey];
|
|
355
|
+
} else {
|
|
356
|
+
entry[windowKey] = record?.[windowKey] || null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
entry.cached_at = new Date(nowMs).toISOString();
|
|
360
|
+
const payload = key ? { [key]: entry } : entry;
|
|
361
|
+
writeCacheJsonAtomic(cachePath({ home, namespace, trackerDir }), payload);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function clear(opts = {}) {
|
|
365
|
+
clearCacheFile(cachePath(opts));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return {
|
|
369
|
+
file,
|
|
370
|
+
key,
|
|
371
|
+
path: cachePath,
|
|
372
|
+
hasWindow,
|
|
373
|
+
isWindowUsable,
|
|
374
|
+
normalize: normalizeEntry,
|
|
375
|
+
read,
|
|
376
|
+
write,
|
|
377
|
+
clear,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Internal cache-plumbing markers ride on provider slot results between the
|
|
382
|
+
// fetcher and the assembly; they are never payload and are stripped before the
|
|
383
|
+
// aggregate is served.
|
|
384
|
+
function stripInternalMarkers(record, markers) {
|
|
385
|
+
if (!record || !markers.some((marker) => marker in record)) return record;
|
|
386
|
+
const payload = { ...record };
|
|
387
|
+
for (const marker of markers) delete payload[marker];
|
|
388
|
+
return payload;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The standard assembly-site integration, shared verbatim by the providers
|
|
393
|
+
* whose fallback is the plain three-way shape:
|
|
394
|
+
*
|
|
395
|
+
* success with windows → stamp fresh, write cache (generation-gated,
|
|
396
|
+
* write-identity hook may refuse);
|
|
397
|
+
* actionable failure → pass through untouched (auth/config errors must
|
|
398
|
+
* never be hidden behind stale cache);
|
|
399
|
+
* transient failure → serve the cache (replace or overlay), else the
|
|
400
|
+
* failed slot itself.
|
|
401
|
+
*
|
|
402
|
+
* Async because at least one provider (copilot) re-derives its cache identity
|
|
403
|
+
* through async storage; a failed poll must not block the embedded server's
|
|
404
|
+
* event loop on that path.
|
|
405
|
+
*/
|
|
406
|
+
async function withLimitsCacheFallback(slot, {
|
|
407
|
+
cache,
|
|
408
|
+
hasWindow = null,
|
|
409
|
+
nowMs = Date.now(),
|
|
410
|
+
home,
|
|
411
|
+
env,
|
|
412
|
+
isCurrentGeneration = () => true,
|
|
413
|
+
resolveWriteIdentity = () => undefined,
|
|
414
|
+
resolveReadIdentity = () => undefined,
|
|
415
|
+
beforeRead = null,
|
|
416
|
+
passthrough = null,
|
|
417
|
+
merge = "replace",
|
|
418
|
+
markers = ["cache_identity", "auth_error"],
|
|
419
|
+
} = {}) {
|
|
420
|
+
const slotHasWindow = hasWindow || cache.hasWindow;
|
|
421
|
+
if (!slot || slot.configured === false) return slot;
|
|
422
|
+
|
|
423
|
+
if (!slot.error && slotHasWindow(slot)) {
|
|
424
|
+
const stamped = {
|
|
425
|
+
...slot,
|
|
426
|
+
stale: false,
|
|
427
|
+
cached_at: new Date(nowMs).toISOString(),
|
|
428
|
+
};
|
|
429
|
+
if (isCurrentGeneration()) {
|
|
430
|
+
const identity = await resolveWriteIdentity(slot);
|
|
431
|
+
// Recheck after the await: a newer poll can bump the generation while
|
|
432
|
+
// the (copilot-style) async identity lookup runs, and the superseded
|
|
433
|
+
// fetch's stamp would then poison the account's current entry.
|
|
434
|
+
if (identity && isCurrentGeneration()) {
|
|
435
|
+
cache.write(stamped, { home, nowMs, identity });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return stripInternalMarkers(stamped, markers);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (passthrough && passthrough(slot)) {
|
|
442
|
+
return stripInternalMarkers(slot, markers);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// The slot's error object loses the fetcher's identity field when the outer
|
|
446
|
+
// timeout race wins (the common failure mode this cache exists for), so the
|
|
447
|
+
// read identity is re-derived here on the failure-only path. beforeRead may
|
|
448
|
+
// be async — copilot re-derives its identity through the keychain and must
|
|
449
|
+
// sample the wall clock only after that lookup (a slow poll can straddle a
|
|
450
|
+
// reset), so its hook returns { identity, nowMs } in one awaited step.
|
|
451
|
+
const readOptions = beforeRead ? await beforeRead(slot) : {};
|
|
452
|
+
if (readOptions === null) return stripInternalMarkers(slot, markers);
|
|
453
|
+
const cached = cache.read({
|
|
454
|
+
home,
|
|
455
|
+
nowMs: readOptions.nowMs ?? nowMs,
|
|
456
|
+
identity: await (readOptions.identity !== undefined
|
|
457
|
+
? Promise.resolve(readOptions.identity)
|
|
458
|
+
: resolveReadIdentity()),
|
|
459
|
+
});
|
|
460
|
+
const merged = cached
|
|
461
|
+
? (merge === "overlay" ? { ...slot, ...cached } : cached)
|
|
462
|
+
: slot;
|
|
463
|
+
return stripInternalMarkers(merged, markers);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
module.exports = {
|
|
467
|
+
parseTimeMs,
|
|
468
|
+
resolveTrackerDir,
|
|
469
|
+
readCacheJson,
|
|
470
|
+
writeCacheJsonAtomic,
|
|
471
|
+
clearCacheFile,
|
|
472
|
+
createLimitsCache,
|
|
473
|
+
stripInternalMarkers,
|
|
474
|
+
withLimitsCacheFallback,
|
|
475
|
+
};
|
|
@@ -335,6 +335,30 @@
|
|
|
335
335
|
"cache_read": 0.05,
|
|
336
336
|
"note": "LiteLLM entry (sourced from https://cloud.google.com/gemini-enterprise-agent-platform/generative-ai/pricing, cached 2026-10-31): $0.50/$3 per MTok, $0.05 cache read, no published cache-write rate. Same rate the edge table already pins; promoted so curated/edge parity guards the key (issue #929)."
|
|
337
337
|
},
|
|
338
|
+
"gemini-3.5-flash": {
|
|
339
|
+
"input": 1.5,
|
|
340
|
+
"output": 9,
|
|
341
|
+
"cache_read": 0.15,
|
|
342
|
+
"note": "Google Gemini 3.5 Flash Global Standard list rate: $1.50/$9.00 per MTok, $0.15 cache read (output includes thinking). No published per-token cache-write rate (context-cache storage is billed separately per hour). Verified 2026-11-08 from https://cloud.google.com/model-garden/pricing; matches the existing LiteLLM snapshot. Promoted so curated/edge stay in parity after the Antigravity 3.x-flash remap was removed (issue #1063). Do not remap this id to gemini-2.5-flash — that SKU is $0.30/$2.50/$0.03."
|
|
343
|
+
},
|
|
344
|
+
"gemini-3.8-flash": {
|
|
345
|
+
"input": 0.75,
|
|
346
|
+
"output": 3.75,
|
|
347
|
+
"cache_read": 0.075,
|
|
348
|
+
"note": "Google Gemini 3.8 Flash Global Standard introductory list rate through 2026-12-31: $0.75/$3.75 per MTok, $0.075 cache read (output includes thinking). Starting 2027-01-01 the published standard list is $1.50/$7.50/$0.15. No published per-token cache-write rate (context-cache storage is billed separately per hour). Verified 2026-11-08 from https://cloud.google.com/model-garden/pricing and https://aistudio.google.com/docs/pricing (issue #1063). Do not remap these ids to gemini-2.5-flash — that SKU is $0.30/$2.50/$0.03."
|
|
349
|
+
},
|
|
350
|
+
"gemini-3.7-flash": {
|
|
351
|
+
"input": 0.75,
|
|
352
|
+
"output": 3.75,
|
|
353
|
+
"cache_read": 0.075,
|
|
354
|
+
"note": "Google Gemini 3.7 Flash Global Standard introductory list rate through 2026-12-31: $0.75/$3.75 per MTok, $0.075 cache read (output includes thinking). Starting 2027-01-01 the published standard list is $1.50/$7.50/$0.15. No published per-token cache-write rate (context-cache storage is billed separately per hour). Verified 2026-11-08 from https://cloud.google.com/model-garden/pricing and https://aistudio.google.com/docs/pricing (issue #1063). Do not remap these ids to gemini-2.5-flash — that SKU is $0.30/$2.50/$0.03."
|
|
355
|
+
},
|
|
356
|
+
"gemini-3.6-flash": {
|
|
357
|
+
"input": 0.75,
|
|
358
|
+
"output": 3.75,
|
|
359
|
+
"cache_read": 0.075,
|
|
360
|
+
"note": "Google Gemini 3.6 Flash Global Standard introductory list rate through 2026-12-31: $0.75/$3.75 per MTok, $0.075 cache read (output includes thinking). Starting 2027-01-01 the published standard list is $1.50/$7.50/$0.15. No published per-token cache-write rate (context-cache storage is billed separately per hour). Verified 2026-11-08 from https://cloud.google.com/model-garden/pricing and https://aistudio.google.com/docs/pricing (issue #1063). Do not remap these ids to gemini-2.5-flash — that SKU is $0.30/$2.50/$0.03."
|
|
361
|
+
},
|
|
338
362
|
"MiniMax-M2.7": {
|
|
339
363
|
"input": 0.3,
|
|
340
364
|
"output": 1.2,
|
|
@@ -47,7 +47,10 @@ function normalizeAntigravityModel(model) {
|
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (/^gemini-3\.\d+-flash-lite/.test(lower)) return "gemini-2.5-flash-lite";
|
|
50
|
-
|
|
50
|
+
// Gemini 3.x Flash models have their own published list rates (3.5 Flash
|
|
51
|
+
// and 3.6/3.7/3.8 Flash are curated so cloud edge stays in parity).
|
|
52
|
+
// Remapping the whole family to gemini-2.5-flash under-counted
|
|
53
|
+
// API-equivalent cost (#1063).
|
|
51
54
|
if (/^gemini-3\.\d+-pro/.test(lower)) return "gemini-2.5-pro";
|
|
52
55
|
if (/^claude-(sonnet|opus|haiku)-4\.\d+/.test(lower)) {
|
|
53
56
|
return lower.replace(/^claude-(sonnet|opus|haiku)-4\.(\d+)/, "claude-$1-4-$2");
|