tokentracker-cli 0.17.1 → 0.18.0
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 +2 -1
- package/README.zh-CN.md +4 -3
- package/dashboard/dist/assets/{Card-_mkOA-Yb.js → Card-DaQruAEZ.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-B1ZCjfgB.js +1 -0
- package/dashboard/dist/assets/{FadeIn-DE080VXe.js → FadeIn-BYn5mp_C.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CNcMxVdQ.js → HeaderGithubStar-DnKHfm5F.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-FJu0bz4h.js → IpCheckPage-BtYkItYr.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-JrvoIz9p.js → LandingPage-C4Ukq_ho.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BqX6r-GM.js → LeaderboardPage-yWE-j66g.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-6jCFHBAe.js → LeaderboardProfilePage-C3UVE-YJ.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-DnbpuDKl.js → LimitsPage-DH2yQZwk.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CFbIWMeO.js → LoginPage-BbEBnz58.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-IjHNNvv0.js → PopoverPopup-B6sNFXGZ.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-PdI5oN6o.js → ProviderIcon-CDVWc_TN.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-DVbufjmp.js → SettingsPage-CJ2KJuU5.js} +1 -1
- package/dashboard/dist/assets/{SkillsPage-CKq5nkgS.js → SkillsPage-BQHojjxy.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-7SMsCD-g.js → WidgetsPage-DuUOC5LD.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BgXcvAfn.js → chevron-down-CkMLZDuK.js} +1 -1
- package/dashboard/dist/assets/{download-DVeetMVb.js → download-BjbAqWCN.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-CCE6c95H.js → leaderboard-columns-m4UzCZhg.js} +1 -1
- package/dashboard/dist/assets/{main-BOniLrlw.js → main-Dgiw16jZ.js} +2 -2
- package/dashboard/dist/assets/{use-limits-display-prefs-DGG_4FCk.js → use-limits-display-prefs-DvnaGeUj.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-CMabb_uJ.js → use-native-settings-BPjPcvON.js} +1 -1
- package/dashboard/dist/assets/{use-reduced-motion-BooeKSc0.js → use-reduced-motion-D1akEyuH.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BLD7HPLY.js → use-usage-limits-BmK4zEAz.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/commands/sync.js +30 -0
- package/src/lib/claude-categorizer.js +80 -15
- package/src/lib/pricing/curated-overrides.json +2 -1
- package/src/lib/pricing/index.js +12 -4
- package/src/lib/pricing/matcher.js +45 -10
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +374 -0
- package/src/lib/usage-limits.js +2 -1
- package/dashboard/dist/assets/DashboardPage-BrFLyy1z.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as a}from"./main-
|
|
1
|
+
import{r as a}from"./main-Dgiw16jZ.js";const d=["claude","codex","cursor","gemini","kimi","kiro","copilot","antigravity"],S={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kimi:"Kimi",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},v={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.svg",kimi:"/brand-logos/kimi.svg",kiro:"/brand-logos/kiro.svg",copilot:"/brand-logos/copilot.svg",antigravity:"/brand-logos/antigravity.svg"},l="tt.limits.providerOrder",g="tt.limits.providerVisibility";function w(){if(typeof window>"u")return[...d];try{const i=window.localStorage.getItem(l);if(!i)return[...d];const s=JSON.parse(i);if(!Array.isArray(s))return[...d];const r=s.filter(c=>d.includes(c));for(const c of d)r.includes(c)||r.push(c);return r}catch{return[...d]}}function y(){const i=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return i;try{const s=window.localStorage.getItem(g);if(!s)return i;const r=JSON.parse(s);if(!r||typeof r!="object")return i;const c={...i};for(const u of d)typeof r[u]=="boolean"&&(c[u]=r[u]);return c}catch{return i}}function C(){const[i,s]=a.useState(w),[r,c]=a.useState(y);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(i))}catch{}},[i]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(w()),t.key===g&&c(y())};return window.addEventListener("storage",o),()=>window.removeEventListener("storage",o)},[]);const u=a.useCallback(o=>{c(t=>({...t,[o]:!t[o]}))},[]),b=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<=0)return t;const n=[...t];return[n[e-1],n[e]]=[n[e],n[e-1]],n})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const n=[...t];return[n[e],n[e+1]]=[n[e+1],n[e]],n})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const n=e.indexOf(o),m=e.indexOf(t);if(n<0||m<0)return e;const f=[...e],[I]=f.splice(n,1);return f.splice(m,0,I),f})},[]),k=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),x=a.useMemo(()=>i.filter(o=>r[o]!==!1),[i,r]);return{order:i,visibility:r,visibleOrdered:x,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:k}}export{v as L,S as a,C as u};
|
package/dashboard/dist/assets/{use-native-settings-CMabb_uJ.js → use-native-settings-BPjPcvON.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{D as a,a5 as x,r as n,aB as g,aC as m,aD as b,aE as u,aF as f,az as h}from"./main-
|
|
1
|
+
import{D as a,a5 as x,r as n,aB as g,aC as m,aD as b,aE as u,aF as f,az as h}from"./main-Dgiw16jZ.js";import{C as y}from"./Card-DaQruAEZ.js";function p({checked:s,onChange:t,disabled:e,ariaLabel:i}){return a.jsx("button",{type:"button",role:"switch","aria-checked":s,"aria-label":i,onClick:t,disabled:e,className:x("relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-oai-brand-500 disabled:opacity-50 disabled:cursor-not-allowed",s?"bg-oai-brand-500":"bg-oai-gray-300 dark:bg-oai-gray-700"),children:a.jsx("span",{className:x("inline-block h-3.5 w-3.5 rounded-full bg-white transition-transform",s?"translate-x-[18px]":"translate-x-[3px]")})})}function j({label:s,hint:t,control:e}){return a.jsxs("div",{className:"flex items-center justify-between gap-4 py-3",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("div",{className:"text-sm text-oai-gray-900 dark:text-oai-gray-200",children:s}),t?a.jsx("div",{className:"mt-0.5 text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),a.jsx("div",{className:"shrink-0",children:e})]})}function N({title:s,subtitle:t,action:e,children:i}){return a.jsxs(y,{children:[a.jsxs("div",{className:"mb-3 flex items-start justify-between gap-4",children:[a.jsxs("div",{className:"min-w-0 flex-1",children:[a.jsx("h2",{className:"text-sm font-medium text-oai-gray-500 dark:text-oai-gray-300 uppercase tracking-wide",children:s}),t?a.jsx("p",{className:"mt-1 truncate text-xs text-oai-gray-500 dark:text-oai-gray-400",children:t}):null]}),e?a.jsx("div",{className:"shrink-0",children:e}):null]}),a.jsx("div",{className:"-mb-3 divide-y divide-oai-gray-200/60 dark:divide-oai-gray-800/60",children:i})]})}function w({options:s,value:t,onChange:e}){return a.jsx("div",{className:"inline-flex items-center rounded-lg border border-oai-gray-200 bg-oai-gray-50 p-0.5 dark:border-oai-gray-800 dark:bg-oai-gray-900",children:s.map(({value:i,label:d,Icon:l})=>{const r=t===i;return a.jsxs("button",{type:"button",onClick:()=>e(i),"aria-pressed":r,className:x("inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-colors",r?"bg-white text-oai-black shadow-sm dark:bg-oai-gray-800 dark:text-white":"text-oai-gray-500 hover:text-oai-black dark:text-oai-gray-400 dark:hover:text-white"),children:[l?a.jsx(l,{className:"h-3.5 w-3.5","aria-hidden":!0}):null,a.jsx("span",{children:d})]},i)})})}function S(){const[s,t]=n.useState(null),e=g()&&m();n.useEffect(()=>{if(!e)return;const r=b(o=>t(o));return u(),r},[e]);const i=n.useCallback((r,o)=>{e&&(t(c=>c&&{...c,[r]:o}),f(r,o))},[e]),d=n.useCallback(r=>{e&&h(r)},[e]),l=n.useCallback(()=>{e&&u()},[e]);return{available:e,settings:s,setSetting:i,runAction:d,refresh:l}}export{N as S,p as T,j as a,w as b,S as u};
|
package/dashboard/dist/assets/{use-reduced-motion-BooeKSc0.js → use-reduced-motion-D1akEyuH.js}
RENAMED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{aG as t,aH as o,r,aI as s}from"./main-
|
|
1
|
+
import{aG as t,aH as o,r,aI as s}from"./main-Dgiw16jZ.js";function u(){!t.current&&o();const[e]=r.useState(s.current);return e}export{u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r,aj as l}from"./main-
|
|
1
|
+
import{r,aj as l}from"./main-Dgiw16jZ.js";function y(i){const[o,a]=r.useState(null),[u,s]=r.useState(null),[c,f]=r.useState(!0),n=!!i?.initialRefresh,g=r.useCallback(async()=>{try{const e=await l({refresh:!0});a(e&&typeof e=="object"?e:null),s(null)}catch(e){s(e?.message||String(e))}},[]);return r.useEffect(()=>{let e=!1;return(async()=>{try{const t=await l(n?{refresh:!0}:{});if(e)return;a(t&&typeof t=="object"?t:null),s(null)}catch(t){if(e)return;s(t?.message||String(t))}finally{e||f(!1)}})(),()=>{e=!0}},[n]),{data:o,error:u,isLoading:c,refresh:g}}export{y as u};
|
|
@@ -210,7 +210,7 @@
|
|
|
210
210
|
]
|
|
211
211
|
}
|
|
212
212
|
</script>
|
|
213
|
-
<script type="module" crossorigin src="/assets/main-
|
|
213
|
+
<script type="module" crossorigin src="/assets/main-Dgiw16jZ.js"></script>
|
|
214
214
|
<link rel="stylesheet" crossorigin href="/assets/main-CITVpx5B.css">
|
|
215
215
|
</head>
|
|
216
216
|
<body>
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"description": "Shareable Token Tracker dashboard snapshot."
|
|
52
52
|
}
|
|
53
53
|
</script>
|
|
54
|
-
<script type="module" crossorigin src="/assets/main-
|
|
54
|
+
<script type="module" crossorigin src="/assets/main-Dgiw16jZ.js"></script>
|
|
55
55
|
<link rel="stylesheet" crossorigin href="/assets/main-CITVpx5B.css">
|
|
56
56
|
</head>
|
|
57
57
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tokentracker-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, Grok Build, oh-my-pi, pi, Craft Agents, Kilo CLI, Kilo Code)",
|
|
5
5
|
"main": "src/cli.js",
|
|
6
6
|
"bin": {
|
package/src/commands/sync.js
CHANGED
|
@@ -37,6 +37,8 @@ const {
|
|
|
37
37
|
parseCraftIncremental,
|
|
38
38
|
resolveGrokBuildSessions,
|
|
39
39
|
parseGrokBuildIncremental,
|
|
40
|
+
listAntigravityTranscripts,
|
|
41
|
+
parseAntigravityIncremental,
|
|
40
42
|
resolveCodebuddyProjectFiles,
|
|
41
43
|
parseCodebuddyIncremental,
|
|
42
44
|
resolveKiroCliSessionFiles,
|
|
@@ -285,6 +287,32 @@ async function cmdSync(argv) {
|
|
|
285
287
|
});
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
const antigravityFiles = await listAntigravityTranscripts(geminiHome);
|
|
291
|
+
let antigravityResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
292
|
+
if (antigravityFiles.length > 0) {
|
|
293
|
+
if (progress?.enabled) {
|
|
294
|
+
progress.start(
|
|
295
|
+
`Parsing Antigravity ${renderBar(0)} 0/${formatNumber(antigravityFiles.length)} files | buckets 0`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
antigravityResult = await parseAntigravityIncremental({
|
|
299
|
+
sessionFiles: antigravityFiles,
|
|
300
|
+
cursors,
|
|
301
|
+
queuePath,
|
|
302
|
+
projectQueuePath,
|
|
303
|
+
onProgress: (p) => {
|
|
304
|
+
if (!progress?.enabled) return;
|
|
305
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
306
|
+
progress.update(
|
|
307
|
+
`Parsing Antigravity ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(
|
|
308
|
+
p.bucketsQueued,
|
|
309
|
+
)}`,
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
source: "antigravity",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
288
316
|
const opencodeFiles = await listOpencodeMessageFiles(opencodeStorageDir);
|
|
289
317
|
let opencodeResult = { filesProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
290
318
|
if (opencodeFiles.length > 0) {
|
|
@@ -805,6 +833,7 @@ async function cmdSync(argv) {
|
|
|
805
833
|
openclawResult.filesProcessed +
|
|
806
834
|
claudeResult.filesProcessed +
|
|
807
835
|
geminiResult.filesProcessed +
|
|
836
|
+
antigravityResult.filesProcessed +
|
|
808
837
|
opencodeResult.filesProcessed +
|
|
809
838
|
cursorResult.recordsProcessed +
|
|
810
839
|
kiroResult.recordsProcessed +
|
|
@@ -824,6 +853,7 @@ async function cmdSync(argv) {
|
|
|
824
853
|
openclawResult.bucketsQueued +
|
|
825
854
|
claudeResult.bucketsQueued +
|
|
826
855
|
geminiResult.bucketsQueued +
|
|
856
|
+
antigravityResult.bucketsQueued +
|
|
827
857
|
opencodeResult.bucketsQueued +
|
|
828
858
|
cursorResult.bucketsQueued +
|
|
829
859
|
kiroResult.bucketsQueued +
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const fssync = require("node:fs");
|
|
14
14
|
const os = require("node:os");
|
|
15
15
|
const path = require("node:path");
|
|
16
|
+
const crypto = require("node:crypto");
|
|
16
17
|
const readline = require("node:readline");
|
|
17
18
|
|
|
18
19
|
const {
|
|
@@ -514,11 +515,56 @@ function dayKeyToIsoBounds(from, to) {
|
|
|
514
515
|
};
|
|
515
516
|
}
|
|
516
517
|
|
|
517
|
-
// Cache: keyed on (rootDir|from|to|maxMtime).
|
|
518
|
-
//
|
|
518
|
+
// Cache: keyed on (rootDir|from|to|files.length|maxMtime). The key already
|
|
519
|
+
// changes the moment any session file is touched, so a long TTL is safe — we
|
|
520
|
+
// don't need a short window as a "safety net". Mirror the in-memory cache
|
|
521
|
+
// to disk so a fresh tracker process (e.g. menu bar app restart) doesn't pay
|
|
522
|
+
// the cold-scan cost again.
|
|
519
523
|
const CACHE = new Map();
|
|
520
|
-
const CACHE_TTL_MS =
|
|
521
|
-
const CACHE_SCHEMA_VERSION = "skills-exec-
|
|
524
|
+
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6h
|
|
525
|
+
const CACHE_SCHEMA_VERSION = "skills-exec-v3";
|
|
526
|
+
const DISK_CACHE_DIR = path.join(os.homedir(), ".tokentracker", "cache", "claude-categorizer");
|
|
527
|
+
|
|
528
|
+
function cacheKeyHash(key) {
|
|
529
|
+
return crypto.createHash("sha1").update(key).digest("hex").slice(0, 32);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function readDiskCache(key) {
|
|
533
|
+
try {
|
|
534
|
+
const fp = path.join(DISK_CACHE_DIR, `${cacheKeyHash(key)}.json`);
|
|
535
|
+
const raw = fssync.readFileSync(fp, "utf8");
|
|
536
|
+
const obj = JSON.parse(raw);
|
|
537
|
+
if (!obj || obj.schemaVersion !== CACHE_SCHEMA_VERSION) return null;
|
|
538
|
+
if (Date.now() - Number(obj.at || 0) >= CACHE_TTL_MS) return null;
|
|
539
|
+
return obj.value;
|
|
540
|
+
} catch (_e) {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function writeDiskCache(key, value) {
|
|
546
|
+
try {
|
|
547
|
+
fssync.mkdirSync(DISK_CACHE_DIR, { recursive: true });
|
|
548
|
+
const fp = path.join(DISK_CACHE_DIR, `${cacheKeyHash(key)}.json`);
|
|
549
|
+
const payload = { schemaVersion: CACHE_SCHEMA_VERSION, at: Date.now(), value };
|
|
550
|
+
fssync.writeFileSync(fp, JSON.stringify(payload));
|
|
551
|
+
// Bound on-disk size; categorizer is cheap to recompute when miss.
|
|
552
|
+
try {
|
|
553
|
+
const entries = fssync.readdirSync(DISK_CACHE_DIR)
|
|
554
|
+
.filter((n) => n.endsWith(".json"))
|
|
555
|
+
.map((n) => {
|
|
556
|
+
const p = path.join(DISK_CACHE_DIR, n);
|
|
557
|
+
let mtime = 0;
|
|
558
|
+
try { mtime = fssync.statSync(p).mtimeMs; } catch (_e) {}
|
|
559
|
+
return { p, mtime };
|
|
560
|
+
})
|
|
561
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
562
|
+
for (const e of entries.slice(16)) {
|
|
563
|
+
try { fssync.unlinkSync(e.p); } catch (_e) {}
|
|
564
|
+
}
|
|
565
|
+
} catch (_e) {}
|
|
566
|
+
} catch (_e) {}
|
|
567
|
+
}
|
|
522
568
|
|
|
523
569
|
function maxMtimeMs(files) {
|
|
524
570
|
let max = 0;
|
|
@@ -556,6 +602,11 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
|
|
|
556
602
|
if (cached && Date.now() - cached.at < CACHE_TTL_MS) {
|
|
557
603
|
return cached.value;
|
|
558
604
|
}
|
|
605
|
+
const onDisk = readDiskCache(cacheKey);
|
|
606
|
+
if (onDisk) {
|
|
607
|
+
CACHE.set(cacheKey, { at: Date.now(), value: onDisk });
|
|
608
|
+
return onDisk;
|
|
609
|
+
}
|
|
559
610
|
|
|
560
611
|
const { fromIso, toIso } = dayKeyToIsoBounds(from, to);
|
|
561
612
|
const breakdown = emptyCategoryMap();
|
|
@@ -575,18 +626,31 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
|
|
|
575
626
|
by_exit: new Map(),
|
|
576
627
|
};
|
|
577
628
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
629
|
+
// Process files with bounded parallelism. CPU-bound (JSON.parse per line)
|
|
630
|
+
// limits the win, but overlapping the per-file fs.open + first-block read
|
|
631
|
+
// I/O behind the previous file's parsing still shaves ~15% off cold scans.
|
|
632
|
+
// `seenHashes`/`breakdown`/ledgers are mutated only inside synchronous
|
|
633
|
+
// sections of `classifyOneMessage`; `for await` in `categorizeSessionFile`
|
|
634
|
+
// only yields between lines, so workers can't tear shared state.
|
|
635
|
+
const SCAN_CONCURRENCY = 4;
|
|
636
|
+
let cursor = 0;
|
|
637
|
+
async function worker() {
|
|
638
|
+
while (cursor < files.length) {
|
|
639
|
+
const idx = cursor++;
|
|
640
|
+
if (idx >= files.length) return;
|
|
641
|
+
const counted = await categorizeSessionFile(
|
|
642
|
+
files[idx],
|
|
643
|
+
{ fromIso, toIso, seenHashes },
|
|
644
|
+
breakdown,
|
|
645
|
+
toolLedger,
|
|
646
|
+
skillLedger,
|
|
647
|
+
execLedger,
|
|
648
|
+
);
|
|
649
|
+
if (counted > 0) sessionCount += 1;
|
|
650
|
+
messageCount += counted;
|
|
651
|
+
}
|
|
589
652
|
}
|
|
653
|
+
await Promise.all(Array.from({ length: SCAN_CONCURRENCY }, () => worker()));
|
|
590
654
|
|
|
591
655
|
const totals = emptyTotals();
|
|
592
656
|
for (const key of CATEGORY_KEYS) addInto(totals, breakdown[key]);
|
|
@@ -612,6 +676,7 @@ async function computeClaudeCategoryBreakdown({ from = null, to = null, rootDir
|
|
|
612
676
|
};
|
|
613
677
|
|
|
614
678
|
CACHE.set(cacheKey, { at: Date.now(), value: result });
|
|
679
|
+
writeDiskCache(cacheKey, result);
|
|
615
680
|
// Bound cache size — categorizer is cheap to recompute, no point hoarding.
|
|
616
681
|
while (CACHE.size > 32) {
|
|
617
682
|
const oldest = [...CACHE.entries()].sort((a, b) => a[1].at - b[1].at)[0];
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"nemotron-3-super-free":{ "input": 0, "output": 0, "cache_read": 0 },
|
|
35
35
|
"mimo-v2-pro-free": { "input": 0, "output": 0, "cache_read": 0 },
|
|
36
36
|
"minimax-m2.1-free":{ "input": 0, "output": 0, "cache_read": 0 },
|
|
37
|
-
"MiniMax-M2.1": { "input": 0.5, "output": 3, "cache_read": 0.05 }
|
|
37
|
+
"MiniMax-M2.1": { "input": 0.5, "output": 3, "cache_read": 0.05 },
|
|
38
|
+
"antigravity-gpt-oss-120b": { "input": 2.5, "output": 10, "cache_read": 0, "note": "Antigravity bridge alias. Approximate with GPT-4o-class pricing until Google exposes route-specific billing metadata." }
|
|
38
39
|
},
|
|
39
40
|
"alias": {
|
|
40
41
|
"auto": "composer-1"
|
package/src/lib/pricing/index.js
CHANGED
|
@@ -82,17 +82,25 @@ function resetPricingForTests() {
|
|
|
82
82
|
state.negativeCache.clear();
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
function getModelPricing(model) {
|
|
85
|
+
function getModelPricing(model, opts = {}) {
|
|
86
86
|
if (!model) return ZERO_PRICING;
|
|
87
|
-
|
|
87
|
+
let lookupSource = null;
|
|
88
|
+
if (typeof opts === "string") {
|
|
89
|
+
lookupSource = opts.toLowerCase();
|
|
90
|
+
} else if (typeof opts.source === "string") {
|
|
91
|
+
lookupSource = opts.source.toLowerCase();
|
|
92
|
+
}
|
|
93
|
+
const cacheKey = lookupSource ? `${lookupSource}\0${model}` : model;
|
|
94
|
+
if (state.negativeCache.has(cacheKey)) return ZERO_PRICING;
|
|
88
95
|
|
|
89
96
|
const result = lookupPricing(model, {
|
|
90
97
|
curated: curatedOverrides,
|
|
91
98
|
litellm: state.litellmPerMillionMap,
|
|
99
|
+
source: lookupSource,
|
|
92
100
|
});
|
|
93
101
|
if (result.hit) return result.value;
|
|
94
102
|
|
|
95
|
-
state.negativeCache.add(
|
|
103
|
+
state.negativeCache.add(cacheKey);
|
|
96
104
|
return ZERO_PRICING;
|
|
97
105
|
}
|
|
98
106
|
|
|
@@ -100,7 +108,7 @@ function getModelPricing(model) {
|
|
|
100
108
|
// computeRowCost in src/lib/local-api.js. Moved here so vite mock + local
|
|
101
109
|
// server share one source of truth.
|
|
102
110
|
function computeRowCost(row) {
|
|
103
|
-
const pricing = getModelPricing(row.model);
|
|
111
|
+
const pricing = getModelPricing(row.model, { source: row.source });
|
|
104
112
|
const reasoningIncludedInOutput = row.source === "codex" || row.source === "every-code";
|
|
105
113
|
const reasoningCost = reasoningIncludedInOutput
|
|
106
114
|
? 0
|
|
@@ -28,6 +28,37 @@ function stripReasoningSuffix(model) {
|
|
|
28
28
|
return model;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
function normalizeAntigravityModel(model) {
|
|
32
|
+
if (!model || typeof model !== "string") return model;
|
|
33
|
+
let lower = model
|
|
34
|
+
.trim()
|
|
35
|
+
.replace(/\([^)]*\)/g, " ")
|
|
36
|
+
.replace(/\b(thinking|xhigh|high|medium|low|fast)\b/gi, " ")
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9.]+/g, "-")
|
|
39
|
+
.replace(/^-+|-+$/g, "")
|
|
40
|
+
.replace(/-{2,}/g, "-");
|
|
41
|
+
lower = stripReasoningSuffix(lower);
|
|
42
|
+
|
|
43
|
+
if (lower.startsWith("gemini-claude-") || lower.startsWith("gemini-gpt-")) {
|
|
44
|
+
lower = lower.substring(7);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (/^gemini-3\.\d+-flash-lite/.test(lower)) return "gemini-2.5-flash-lite";
|
|
48
|
+
if (/^gemini-3\.\d+-flash/.test(lower)) return "gemini-2.5-flash";
|
|
49
|
+
if (/^gemini-3\.\d+-pro/.test(lower)) return "gemini-2.5-pro";
|
|
50
|
+
if (/^claude-(sonnet|opus|haiku)-4\.\d+/.test(lower)) {
|
|
51
|
+
return lower.replace(/^claude-(sonnet|opus|haiku)-4\.(\d+)/, "claude-$1-4-$2");
|
|
52
|
+
}
|
|
53
|
+
if (lower.startsWith("gpt-oss-120b")) return "antigravity-gpt-oss-120b";
|
|
54
|
+
|
|
55
|
+
return lower;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function shouldNormalizeAntigravity(source) {
|
|
59
|
+
return typeof source === "string" && source.toLowerCase() === "antigravity";
|
|
60
|
+
}
|
|
61
|
+
|
|
31
62
|
// Memoise the sorted-by-length LiteLLM key list. Reverse-substring scan walks
|
|
32
63
|
// this once per uncached model; ~2k keys × negligible per-iteration cost, but
|
|
33
64
|
// computing the sort on every call would add up across a sync.
|
|
@@ -41,28 +72,31 @@ function getSortedKeys(litellm) {
|
|
|
41
72
|
return cached;
|
|
42
73
|
}
|
|
43
74
|
|
|
44
|
-
function lookupPricing(model, { curated, litellm }) {
|
|
75
|
+
function lookupPricing(model, { curated, litellm, source } = {}) {
|
|
45
76
|
if (!model || typeof model !== "string") {
|
|
46
77
|
return { hit: false, source: "empty", value: null };
|
|
47
78
|
}
|
|
48
|
-
const
|
|
79
|
+
const lookupModel = shouldNormalizeAntigravity(source)
|
|
80
|
+
? normalizeAntigravityModel(model)
|
|
81
|
+
: model;
|
|
82
|
+
const lower = lookupModel.toLowerCase();
|
|
49
83
|
|
|
50
84
|
// 1. CURATED exact
|
|
51
|
-
if (curated.exact && curated.exact[
|
|
52
|
-
return { hit: true, source: "curated:exact", value: curated.exact[
|
|
85
|
+
if (curated.exact && curated.exact[lookupModel]) {
|
|
86
|
+
return { hit: true, source: "curated:exact", value: curated.exact[lookupModel] };
|
|
53
87
|
}
|
|
54
88
|
|
|
55
89
|
// 2. LiteLLM exact
|
|
56
|
-
if (litellm && litellm[
|
|
57
|
-
return { hit: true, source: "litellm:exact", value: litellm[
|
|
90
|
+
if (litellm && litellm[lookupModel]) {
|
|
91
|
+
return { hit: true, source: "litellm:exact", value: litellm[lookupModel] };
|
|
58
92
|
}
|
|
59
93
|
|
|
60
94
|
// 3. CURATED alias (literal mapping like "auto" -> "composer-1")
|
|
61
|
-
if (curated.alias && curated.alias[
|
|
95
|
+
if (curated.alias && curated.alias[lookupModel] && curated.exact[curated.alias[lookupModel]]) {
|
|
62
96
|
return {
|
|
63
97
|
hit: true,
|
|
64
98
|
source: "curated:alias",
|
|
65
|
-
value: curated.exact[curated.alias[
|
|
99
|
+
value: curated.exact[curated.alias[lookupModel]],
|
|
66
100
|
};
|
|
67
101
|
}
|
|
68
102
|
|
|
@@ -78,8 +112,8 @@ function lookupPricing(model, { curated, litellm }) {
|
|
|
78
112
|
|
|
79
113
|
// 5. LiteLLM suffix-strip
|
|
80
114
|
if (litellm) {
|
|
81
|
-
const stripped = stripReasoningSuffix(
|
|
82
|
-
if (stripped !==
|
|
115
|
+
const stripped = stripReasoningSuffix(lookupModel);
|
|
116
|
+
if (stripped !== lookupModel && litellm[stripped]) {
|
|
83
117
|
return { hit: true, source: "litellm:strip", value: litellm[stripped] };
|
|
84
118
|
}
|
|
85
119
|
}
|
|
@@ -144,6 +178,7 @@ function buildLitellmPerMillionMap(rawData) {
|
|
|
144
178
|
module.exports = {
|
|
145
179
|
lookupPricing,
|
|
146
180
|
stripReasoningSuffix,
|
|
181
|
+
normalizeAntigravityModel,
|
|
147
182
|
convertLitellmEntry,
|
|
148
183
|
buildLitellmPerMillionMap,
|
|
149
184
|
};
|