tokentracker-cli 0.5.79 → 0.5.81
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/{Card-Cv4eTIKD.js → Card-CvRCwmts.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BLFOnvMn.js → DashboardPage-Q3m4isBc.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CIeY4GXM.js → FadeIn-7Sp96sG1.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-IsYc44dj.js → IpCheckPage-CU5zi7km.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-Su4flsQc.js → LeaderboardPage-C9g0lTkN.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfilePage-bt2zrvtb.js → LeaderboardProfilePage-U3Oww62u.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CojL1PZS.js → LimitsPage-DW-j3hsV.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-C2Qp69XI.js → ProviderIcon-BJl3TO7B.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-FSVM_ozY.js → SettingsPage-DnSupHC-.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CeLvw5tR.js → WidgetsPage-DwSoxQql.js} +1 -1
- package/dashboard/dist/assets/{download-BK4EqMpL.js → download-DtuvJNeF.js} +1 -1
- package/dashboard/dist/assets/{leaderboard-columns-CxdAz5_V.js → leaderboard-columns-B1_Q_WZh.js} +1 -1
- package/dashboard/dist/assets/{main-CPsqG3PW.js → main-MimZZsgW.js} +188 -188
- package/dashboard/dist/assets/{use-limits-display-prefs-C-Y8vFA9.js → use-limits-display-prefs-CPfPbjXC.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CiHD5lbg.js → use-usage-limits-BvCjl0IL.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/serve.js +14 -4
- package/src/lib/local-api.js +146 -27
- package/src/lib/rollout.js +19 -20
- package/src/lib/static-server.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r as a}from"./main-
|
|
1
|
+
import{r as a}from"./main-MimZZsgW.js";const d=["claude","codex","cursor","gemini","kiro","copilot","antigravity"],v={claude:"Claude",codex:"Codex",cursor:"Cursor",gemini:"Gemini",kiro:"Kiro",copilot:"GitHub Copilot",antigravity:"Antigravity"},k={claude:"/brand-logos/claude-code.svg",codex:"/brand-logos/codex.svg",cursor:"/brand-logos/cursor.svg",gemini:"/brand-logos/gemini.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 y(){if(typeof window>"u")return[...d];try{const r=window.localStorage.getItem(l);if(!r)return[...d];const s=JSON.parse(r);if(!Array.isArray(s))return[...d];const n=s.filter(c=>d.includes(c));for(const c of d)n.includes(c)||n.push(c);return n}catch{return[...d]}}function m(){const r=Object.fromEntries(d.map(s=>[s,!0]));if(typeof window>"u")return r;try{const s=window.localStorage.getItem(g);if(!s)return r;const n=JSON.parse(s);if(!n||typeof n!="object")return r;const c={...r};for(const u of d)typeof n[u]=="boolean"&&(c[u]=n[u]);return c}catch{return r}}function C(){const[r,s]=a.useState(y),[n,c]=a.useState(m);a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(l,JSON.stringify(r))}catch{}},[r]),a.useEffect(()=>{if(!(typeof window>"u"))try{window.localStorage.setItem(g,JSON.stringify(n))}catch{}},[n]),a.useEffect(()=>{if(typeof window>"u")return;const o=t=>{t.key===l&&s(y()),t.key===g&&c(m())};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 i=[...t];return[i[e-1],i[e]]=[i[e],i[e-1]],i})},[]),p=a.useCallback(o=>{s(t=>{const e=t.indexOf(o);if(e<0||e>=t.length-1)return t;const i=[...t];return[i[e],i[e+1]]=[i[e+1],i[e]],i})},[]),O=a.useCallback((o,t)=>{o!==t&&s(e=>{const i=e.indexOf(o),w=e.indexOf(t);if(i<0||w<0)return e;const f=[...e],[E]=f.splice(i,1);return f.splice(w,0,E),f})},[]),x=a.useCallback(()=>{s([...d]),c(Object.fromEntries(d.map(o=>[o,!0])))},[]),I=a.useMemo(()=>r.filter(o=>n[o]!==!1),[r,n]);return{order:r,visibility:n,visibleOrdered:I,toggle:u,moveUp:b,moveDown:p,moveToward:O,reset:x}}export{k as L,v as a,C as u};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{r,aM as l}from"./main-
|
|
1
|
+
import{r,aM as l}from"./main-MimZZsgW.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};
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
]
|
|
136
136
|
}
|
|
137
137
|
</script>
|
|
138
|
-
<script type="module" crossorigin src="/assets/main-
|
|
138
|
+
<script type="module" crossorigin src="/assets/main-MimZZsgW.js"></script>
|
|
139
139
|
<link rel="stylesheet" crossorigin href="/assets/main-DRf20yyJ.css">
|
|
140
140
|
</head>
|
|
141
141
|
<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-MimZZsgW.js"></script>
|
|
55
55
|
<link rel="stylesheet" crossorigin href="/assets/main-DRf20yyJ.css">
|
|
56
56
|
</head>
|
|
57
57
|
<body>
|
package/package.json
CHANGED
package/src/commands/serve.js
CHANGED
|
@@ -11,11 +11,16 @@ const { openInBrowser } = require("../lib/browser-auth");
|
|
|
11
11
|
|
|
12
12
|
const DEFAULT_PORT = 7680;
|
|
13
13
|
const NPM_PACKAGE_NAME = "tokentracker-cli";
|
|
14
|
+
const LOCAL_BIND_HOST = "127.0.0.1";
|
|
14
15
|
|
|
15
16
|
function buildPortInUseHint(port) {
|
|
16
17
|
return `Port ${port} is still in use after cleanup. Try: npx ${NPM_PACKAGE_NAME} serve --port ${port + 1}\n`;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
function getLocalServerUrl(port) {
|
|
21
|
+
return `http://${LOCAL_BIND_HOST}:${port}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
async function cmdServe(argv) {
|
|
20
25
|
const opts = parseArgs(argv);
|
|
21
26
|
|
|
@@ -85,7 +90,6 @@ async function cmdServe(argv) {
|
|
|
85
90
|
// CORS preflight
|
|
86
91
|
if (req.method === "OPTIONS") {
|
|
87
92
|
res.writeHead(204, {
|
|
88
|
-
"Access-Control-Allow-Origin": "*",
|
|
89
93
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
90
94
|
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
91
95
|
});
|
|
@@ -116,8 +120,8 @@ async function cmdServe(argv) {
|
|
|
116
120
|
// 4. Listen (kill stale process on same port if needed)
|
|
117
121
|
const port = opts.port;
|
|
118
122
|
await ensurePortFree(port);
|
|
119
|
-
server.listen(port, () => {
|
|
120
|
-
const url =
|
|
123
|
+
server.listen(port, LOCAL_BIND_HOST, () => {
|
|
124
|
+
const url = getLocalServerUrl(port);
|
|
121
125
|
process.stdout.write(
|
|
122
126
|
[
|
|
123
127
|
"",
|
|
@@ -226,4 +230,10 @@ function parseArgs(argv) {
|
|
|
226
230
|
return opts;
|
|
227
231
|
}
|
|
228
232
|
|
|
229
|
-
module.exports = {
|
|
233
|
+
module.exports = {
|
|
234
|
+
cmdServe,
|
|
235
|
+
buildPortInUseHint,
|
|
236
|
+
NPM_PACKAGE_NAME,
|
|
237
|
+
LOCAL_BIND_HOST,
|
|
238
|
+
getLocalServerUrl,
|
|
239
|
+
};
|
package/src/lib/local-api.js
CHANGED
|
@@ -2,6 +2,8 @@ const fs = require("node:fs");
|
|
|
2
2
|
const os = require("node:os");
|
|
3
3
|
const path = require("node:path");
|
|
4
4
|
const { spawn } = require("node:child_process");
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
const { DEFAULT_BASE_URL, resolveRuntimeConfig } = require("./runtime-config");
|
|
5
7
|
|
|
6
8
|
const SYNC_TIMEOUT_MS = 120_000;
|
|
7
9
|
const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
|
|
@@ -106,12 +108,22 @@ function getModelPricing(model) {
|
|
|
106
108
|
|
|
107
109
|
function computeRowCost(row) {
|
|
108
110
|
const pricing = getModelPricing(row.model);
|
|
111
|
+
// For OpenAI/Codex-family rollouts, `output_tokens` already includes any
|
|
112
|
+
// reasoning tokens (the OpenAI API's `completion_tokens` is inclusive),
|
|
113
|
+
// so adding a separate `reasoning_output_tokens * output_rate` term
|
|
114
|
+
// double-charges that slice. ccusage models this the same way. For other
|
|
115
|
+
// sources we keep the explicit reasoning term because `reasoning` is not
|
|
116
|
+
// guaranteed to be folded into `output_tokens`.
|
|
117
|
+
const reasoningIncludedInOutput = row.source === "codex" || row.source === "every-code";
|
|
118
|
+
const reasoningCost = reasoningIncludedInOutput
|
|
119
|
+
? 0
|
|
120
|
+
: (row.reasoning_output_tokens || 0) * (pricing.output || 0);
|
|
109
121
|
return (
|
|
110
122
|
((row.input_tokens || 0) * (pricing.input || 0) +
|
|
111
123
|
(row.output_tokens || 0) * (pricing.output || 0) +
|
|
112
124
|
(row.cached_input_tokens || 0) * (pricing.cache_read || 0) +
|
|
113
125
|
(row.cache_creation_input_tokens || 0) * (pricing.cache_write || 0) +
|
|
114
|
-
|
|
126
|
+
reasoningCost) /
|
|
115
127
|
1_000_000
|
|
116
128
|
);
|
|
117
129
|
}
|
|
@@ -149,6 +161,29 @@ function readProjectQueueData(projectQueuePath) {
|
|
|
149
161
|
return Array.from(seen.values());
|
|
150
162
|
}
|
|
151
163
|
|
|
164
|
+
function isLegacyInclusiveCodexRow(row) {
|
|
165
|
+
if (!row || (row.source !== "codex" && row.source !== "every-code")) return false;
|
|
166
|
+
const inputTokens = Number(row.input_tokens || 0);
|
|
167
|
+
const cachedInputTokens = Number(row.cached_input_tokens || 0);
|
|
168
|
+
const outputTokens = Number(row.output_tokens || 0);
|
|
169
|
+
const totalTokens = Number(row.total_tokens || 0);
|
|
170
|
+
if (!Number.isFinite(inputTokens) || !Number.isFinite(cachedInputTokens)) return false;
|
|
171
|
+
if (cachedInputTokens <= 0 || inputTokens < cachedInputTokens) return false;
|
|
172
|
+
// Legacy Codex queue rows stored input inclusive of cache reads, while
|
|
173
|
+
// total_tokens remained input + output. Canonical rows keep input as pure
|
|
174
|
+
// non-cached input, so cache-heavy legacy rows can be identified by this
|
|
175
|
+
// exact invariant.
|
|
176
|
+
return totalTokens === inputTokens + outputTokens;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeQueueRow(row) {
|
|
180
|
+
if (!isLegacyInclusiveCodexRow(row)) return row;
|
|
181
|
+
return {
|
|
182
|
+
...row,
|
|
183
|
+
input_tokens: Number(row.input_tokens || 0) - Number(row.cached_input_tokens || 0),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
152
187
|
function readQueueData(queuePath) {
|
|
153
188
|
let raw;
|
|
154
189
|
try {
|
|
@@ -184,7 +219,7 @@ function readQueueData(queuePath) {
|
|
|
184
219
|
const seen = new Map();
|
|
185
220
|
for (const row of parsed) {
|
|
186
221
|
const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
|
|
187
|
-
seen.set(key, row);
|
|
222
|
+
seen.set(key, normalizeQueueRow(row));
|
|
188
223
|
}
|
|
189
224
|
return Array.from(seen.values());
|
|
190
225
|
}
|
|
@@ -351,6 +386,67 @@ function trimOutput(value, max = 4000) {
|
|
|
351
386
|
return t.length <= max ? t : t.slice(t.length - max);
|
|
352
387
|
}
|
|
353
388
|
|
|
389
|
+
function normalizeRemoteHttpBaseUrl(value) {
|
|
390
|
+
if (typeof value !== "string") return null;
|
|
391
|
+
const trimmed = value.trim();
|
|
392
|
+
if (!trimmed) return null;
|
|
393
|
+
try {
|
|
394
|
+
const url = new URL(trimmed);
|
|
395
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
396
|
+
url.username = "";
|
|
397
|
+
url.password = "";
|
|
398
|
+
url.hash = "";
|
|
399
|
+
return url.toString().replace(/\/$/, "");
|
|
400
|
+
} catch (_e) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function resolveAllowedInsforgeBaseUrl(value) {
|
|
406
|
+
const requested = normalizeRemoteHttpBaseUrl(value);
|
|
407
|
+
if (!requested) return null;
|
|
408
|
+
|
|
409
|
+
const runtime = resolveRuntimeConfig();
|
|
410
|
+
const allowed = new Set(
|
|
411
|
+
[runtime.baseUrl, DEFAULT_BASE_URL]
|
|
412
|
+
.map((entry) => normalizeRemoteHttpBaseUrl(entry))
|
|
413
|
+
.filter(Boolean),
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
return allowed.has(requested) ? requested : null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function parseCookieHeader(value) {
|
|
420
|
+
const out = new Map();
|
|
421
|
+
if (typeof value !== "string" || !value.trim()) return out;
|
|
422
|
+
for (const part of value.split(";")) {
|
|
423
|
+
const idx = part.indexOf("=");
|
|
424
|
+
if (idx < 1) continue;
|
|
425
|
+
const key = part.slice(0, idx).trim();
|
|
426
|
+
const rawValue = part.slice(idx + 1).trim();
|
|
427
|
+
if (key) out.set(key, rawValue);
|
|
428
|
+
}
|
|
429
|
+
return out;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isLoopbackHostname(hostname) {
|
|
433
|
+
return hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1" || hostname === "[::1]";
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function hasAllowedLoopbackOrigin(headers = {}) {
|
|
437
|
+
const candidates = [headers.origin, headers.referer];
|
|
438
|
+
for (const raw of candidates) {
|
|
439
|
+
if (raw == null || raw === "") continue;
|
|
440
|
+
try {
|
|
441
|
+
const url = new URL(String(raw));
|
|
442
|
+
if (url.protocol !== "http:" || !isLoopbackHostname(url.hostname)) return false;
|
|
443
|
+
} catch (_e) {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
|
|
354
450
|
function readJsonBody(req) {
|
|
355
451
|
return new Promise((resolve, reject) => {
|
|
356
452
|
const chunks = [];
|
|
@@ -516,7 +612,7 @@ function scanClaudeProjects(projectMap) {
|
|
|
516
612
|
// ---------------------------------------------------------------------------
|
|
517
613
|
|
|
518
614
|
function json(res, data, status) {
|
|
519
|
-
res.writeHead(status || 200, { "Content-Type": "application/json"
|
|
615
|
+
res.writeHead(status || 200, { "Content-Type": "application/json" });
|
|
520
616
|
res.end(JSON.stringify(data));
|
|
521
617
|
}
|
|
522
618
|
|
|
@@ -531,6 +627,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
531
627
|
// so that both browser and WKWebView share the same login session via the proxy.
|
|
532
628
|
// Persisted to disk so cookies survive server restarts.
|
|
533
629
|
let relayCookies = new Map();
|
|
630
|
+
const localAuthToken = crypto.randomBytes(24).toString("hex");
|
|
534
631
|
const trackerDataDir = path.join(os.homedir(), ".tokentracker", "tracker");
|
|
535
632
|
const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
|
|
536
633
|
|
|
@@ -643,13 +740,40 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
643
740
|
let _nativeAuthPending = false;
|
|
644
741
|
let _nativeAuthExpiry = 0;
|
|
645
742
|
|
|
743
|
+
function isAuthorizedLocalMutation(req) {
|
|
744
|
+
const headerToken = req?.headers?.["x-tokentracker-local-auth"];
|
|
745
|
+
const cookieToken = parseCookieHeader(req?.headers?.cookie).get("tokentracker_local_auth");
|
|
746
|
+
const token = typeof headerToken === "string" && headerToken.trim()
|
|
747
|
+
? headerToken.trim()
|
|
748
|
+
: cookieToken || "";
|
|
749
|
+
if (!token || token !== localAuthToken) return false;
|
|
750
|
+
return hasAllowedLoopbackOrigin(req?.headers || {});
|
|
751
|
+
}
|
|
752
|
+
|
|
646
753
|
return async function handleLocalApi(req, res, url) {
|
|
647
754
|
const p = url.pathname;
|
|
648
755
|
|
|
756
|
+
if (p === "/api/local-auth") {
|
|
757
|
+
if (String(req.method || "GET").toUpperCase() !== "GET") {
|
|
758
|
+
json(res, { error: "Method Not Allowed" }, 405);
|
|
759
|
+
return true;
|
|
760
|
+
}
|
|
761
|
+
res.writeHead(200, {
|
|
762
|
+
"Content-Type": "application/json",
|
|
763
|
+
"Cache-Control": "no-store",
|
|
764
|
+
});
|
|
765
|
+
res.end(JSON.stringify({ token: localAuthToken }));
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
|
|
649
769
|
// --- Auth bridge: native OAuth flag (WebView ↔ system browser) ---
|
|
650
770
|
if (p === "/api/auth-bridge/verifier") {
|
|
651
771
|
const method = String(req.method || "GET").toUpperCase();
|
|
652
772
|
if (method === "PUT" || method === "POST") {
|
|
773
|
+
if (!isAuthorizedLocalMutation(req)) {
|
|
774
|
+
json(res, { error: "Unauthorized" }, 401);
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
653
777
|
const body = await readJsonBody(req);
|
|
654
778
|
_nativeAuthPending = Boolean(body?.native);
|
|
655
779
|
_nativeAuthExpiry = Date.now() + 5 * 60 * 1000; // 5 min TTL
|
|
@@ -669,20 +793,8 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
669
793
|
|
|
670
794
|
// --- auth proxy: forward /api/auth/* to InsForge cloud ---
|
|
671
795
|
if (p.startsWith("/api/auth/")) {
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|| process.env.INSFORGE_BASE_URL
|
|
675
|
-
|| "";
|
|
676
|
-
if (!insforgeBase) {
|
|
677
|
-
try {
|
|
678
|
-
const cfgPath = path.join(os.homedir(), ".tokentracker", "tracker", "config.json");
|
|
679
|
-
const cfg = JSON.parse(fs.readFileSync(cfgPath, "utf8"));
|
|
680
|
-
insforgeBase = cfg?.baseUrl || "";
|
|
681
|
-
} catch { /* ignore */ }
|
|
682
|
-
}
|
|
683
|
-
if (!insforgeBase) {
|
|
684
|
-
insforgeBase = DEFAULT_BASE_URL;
|
|
685
|
-
}
|
|
796
|
+
const runtime = resolveRuntimeConfig();
|
|
797
|
+
const insforgeBase = runtime.baseUrl || DEFAULT_BASE_URL;
|
|
686
798
|
try {
|
|
687
799
|
const targetUrl = `${insforgeBase.replace(/\/$/, "")}${p}${url.search || ""}`;
|
|
688
800
|
const proxyHeaders = {};
|
|
@@ -748,6 +860,10 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
748
860
|
json(res, { ok: false, error: "Method Not Allowed" }, 405);
|
|
749
861
|
return true;
|
|
750
862
|
}
|
|
863
|
+
if (!isAuthorizedLocalMutation(req)) {
|
|
864
|
+
json(res, { ok: false, error: "Unauthorized" }, 401);
|
|
865
|
+
return true;
|
|
866
|
+
}
|
|
751
867
|
try {
|
|
752
868
|
let body = {};
|
|
753
869
|
try {
|
|
@@ -759,8 +875,13 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
759
875
|
if (typeof body.deviceToken === "string" && body.deviceToken.trim()) {
|
|
760
876
|
extraEnv.TOKENTRACKER_DEVICE_TOKEN = body.deviceToken.trim();
|
|
761
877
|
}
|
|
762
|
-
if (
|
|
763
|
-
|
|
878
|
+
if (body.insforgeBaseUrl != null) {
|
|
879
|
+
const allowedBaseUrl = resolveAllowedInsforgeBaseUrl(body.insforgeBaseUrl);
|
|
880
|
+
if (!allowedBaseUrl) {
|
|
881
|
+
json(res, { ok: false, error: "Unsupported insforgeBaseUrl override" }, 400);
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
extraEnv.TOKENTRACKER_INSFORGE_BASE_URL = allowedBaseUrl;
|
|
764
885
|
}
|
|
765
886
|
const result = await runSyncCommand(extraEnv);
|
|
766
887
|
try {
|
|
@@ -939,14 +1060,11 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
939
1060
|
const sources = Array.from(bySource.values()).map((s) => {
|
|
940
1061
|
s.models = Array.from(s.models.values())
|
|
941
1062
|
.map((m) => {
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
(m.totals.cache_creation_input_tokens || 0) * (p.cache_write || 0) +
|
|
948
|
-
(m.totals.reasoning_output_tokens || 0) * (p.output || 0)) /
|
|
949
|
-
1_000_000;
|
|
1063
|
+
const cost = computeRowCost({
|
|
1064
|
+
...m.totals,
|
|
1065
|
+
model: m.model,
|
|
1066
|
+
source: s.source,
|
|
1067
|
+
});
|
|
950
1068
|
return { ...m, totals: { ...m.totals, total_cost_usd: cost.toFixed(6) } };
|
|
951
1069
|
})
|
|
952
1070
|
.sort((a, b) => b.totals.total_tokens - a.totals.total_tokens);
|
|
@@ -1109,6 +1227,7 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
1109
1227
|
|
|
1110
1228
|
module.exports = {
|
|
1111
1229
|
createLocalApiHandler,
|
|
1230
|
+
resolveAllowedInsforgeBaseUrl,
|
|
1112
1231
|
resolveQueuePath,
|
|
1113
1232
|
// Exported for cross-consumer tests (pricing + native contract lock).
|
|
1114
1233
|
MODEL_PRICING,
|
package/src/lib/rollout.js
CHANGED
|
@@ -2150,12 +2150,12 @@ function pickDelta(lastUsage, totalUsage, prevTotals) {
|
|
|
2150
2150
|
const hasTotal = isNonEmptyObject(totalUsage);
|
|
2151
2151
|
const hasPrevTotals = isNonEmptyObject(prevTotals);
|
|
2152
2152
|
|
|
2153
|
-
//
|
|
2154
|
-
//
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2153
|
+
// NOTE: We used to guard against "duplicate token_count records where
|
|
2154
|
+
// total_token_usage is unchanged" by returning null here. We removed that
|
|
2155
|
+
// guard to align token counts with ccusage exactly (audited against 10 days
|
|
2156
|
+
// of real rollouts). When last_token_usage is present we trust it as the
|
|
2157
|
+
// per-turn delta; when it's absent the cumulative-subtract path naturally
|
|
2158
|
+
// yields an all-zero delta on duplicates and is still filtered below.
|
|
2159
2159
|
if (!hasLast && hasTotal && hasPrevTotals && totalsReset(totalUsage, prevTotals)) {
|
|
2160
2160
|
const normalized = normalizeUsage(totalUsage);
|
|
2161
2161
|
return isAllZeroUsage(normalized) ? null : normalized;
|
|
@@ -2203,6 +2203,19 @@ function normalizeUsage(u) {
|
|
|
2203
2203
|
const n = Number(u[k] || 0);
|
|
2204
2204
|
out[k] = Number.isFinite(n) && n >= 0 ? Math.floor(n) : 0;
|
|
2205
2205
|
}
|
|
2206
|
+
// Codex rollouts (and Every Code, which shares the format) report
|
|
2207
|
+
// `input_tokens` as the TOTAL prompt, with `cached_input_tokens` as the
|
|
2208
|
+
// cached subset — i.e. the cached slice is INSIDE the input count. Our
|
|
2209
|
+
// queue schema (CLAUDE.md → Token Normalization Convention) stores
|
|
2210
|
+
// `input_tokens` as pure non-cached input and `cached_input_tokens`
|
|
2211
|
+
// separately. Without this subtraction the cost formula bills the cached
|
|
2212
|
+
// bytes twice: once at the full input rate and again at the cache_read
|
|
2213
|
+
// rate, producing ~6–7x cost inflation on cache-heavy Codex sessions
|
|
2214
|
+
// (verified against ccusage's per-day numbers on the same rollouts).
|
|
2215
|
+
// We intentionally leave `total_tokens` unchanged: Codex reports
|
|
2216
|
+
// total = input(inclusive of cached) + output, which numerically equals
|
|
2217
|
+
// our schema's non_cached + cached + output + 0 (cache_creation=0 here).
|
|
2218
|
+
out.input_tokens = Math.max(0, out.input_tokens - out.cached_input_tokens);
|
|
2206
2219
|
return out;
|
|
2207
2220
|
}
|
|
2208
2221
|
|
|
@@ -2241,20 +2254,6 @@ function isAllZeroUsage(u) {
|
|
|
2241
2254
|
return true;
|
|
2242
2255
|
}
|
|
2243
2256
|
|
|
2244
|
-
function sameUsage(a, b) {
|
|
2245
|
-
for (const k of [
|
|
2246
|
-
"input_tokens",
|
|
2247
|
-
"cached_input_tokens",
|
|
2248
|
-
"cache_creation_input_tokens",
|
|
2249
|
-
"output_tokens",
|
|
2250
|
-
"reasoning_output_tokens",
|
|
2251
|
-
"total_tokens",
|
|
2252
|
-
]) {
|
|
2253
|
-
if (toNonNegativeInt(a?.[k]) !== toNonNegativeInt(b?.[k])) return false;
|
|
2254
|
-
}
|
|
2255
|
-
return true;
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
2257
|
function totalsReset(curr, prev) {
|
|
2259
2258
|
const currTotal = curr?.total_tokens;
|
|
2260
2259
|
const prevTotal = prev?.total_tokens;
|
package/src/lib/static-server.js
CHANGED
|
@@ -46,7 +46,6 @@ async function serveStaticFile(baseDir, pathname, res) {
|
|
|
46
46
|
"Content-Type": contentType,
|
|
47
47
|
"Content-Length": stat.size,
|
|
48
48
|
"Cache-Control": isHtml ? "no-cache" : "public, max-age=31536000, immutable",
|
|
49
|
-
"Access-Control-Allow-Origin": "*",
|
|
50
49
|
});
|
|
51
50
|
|
|
52
51
|
const stream = fs.createReadStream(filePath);
|