@tokenzero/cli 0.1.1 → 0.1.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/README.md +1 -1
- package/bin/tokenzero.mjs +148 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ Audits are **static heuristics**: they read code, not traffic, and report estima
|
|
|
13
13
|
|
|
14
14
|
## Connecting to the gateway
|
|
15
15
|
|
|
16
|
-
These commands
|
|
16
|
+
These commands talk to the hosted gateway at `https://tokenzero.sh`. To use another deployment, pass `--url` or set `TOKENZERO_URL`.
|
|
17
17
|
|
|
18
18
|
```sh
|
|
19
19
|
npx -y @tokenzero/cli init # audit, create a workspace, write base URLs to .env (asks first)
|
package/bin/tokenzero.mjs
CHANGED
|
@@ -203,7 +203,9 @@ function lex(src, path3) {
|
|
|
203
203
|
j++;
|
|
204
204
|
}
|
|
205
205
|
const end = Math.min(j, src.length);
|
|
206
|
-
|
|
206
|
+
const rep = (py ? /^\s*\*\s*(\d{1,6})\b/ : /^\.repeat\(\s*(\d{1,6})\s*\)/).exec(src.slice(end + 1, end + 40));
|
|
207
|
+
const body = src.slice(i + 1, end);
|
|
208
|
+
literals.push({ text: rep ? body.repeat(Math.max(1, Math.min(Number(rep[1]), Math.floor(2e5 / Math.max(1, body.length))))) : body, line });
|
|
207
209
|
blank(i + 1, end);
|
|
208
210
|
advance(i, end + 1);
|
|
209
211
|
i = end + 1;
|
|
@@ -254,6 +256,98 @@ function detectRepoKind(manifests2, files) {
|
|
|
254
256
|
return { kind: "unknown", reason: "no manifest signal" };
|
|
255
257
|
}
|
|
256
258
|
var DOWN = { high: "medium", medium: "low", low: "info", info: "info" };
|
|
259
|
+
var OPEN = "([{";
|
|
260
|
+
var CLOSE = ")]}";
|
|
261
|
+
function matchClose(m, from, limit = 4e4) {
|
|
262
|
+
let depth = 0;
|
|
263
|
+
for (let k = from; k < Math.min(m.length, from + limit); k++) {
|
|
264
|
+
const c2 = m[k];
|
|
265
|
+
if (OPEN.includes(c2)) depth++;
|
|
266
|
+
else if (CLOSE.includes(c2) && --depth === 0) return k;
|
|
267
|
+
}
|
|
268
|
+
return Math.min(m.length, from + limit) - 1;
|
|
269
|
+
}
|
|
270
|
+
function makeLocator(masked) {
|
|
271
|
+
const text = masked.join("\n");
|
|
272
|
+
const starts = [];
|
|
273
|
+
let o = 0;
|
|
274
|
+
for (const l of masked) {
|
|
275
|
+
starts.push(o);
|
|
276
|
+
o += l.length + 1;
|
|
277
|
+
}
|
|
278
|
+
const lineOf = (off) => {
|
|
279
|
+
let lo = 0, hi = starts.length - 1;
|
|
280
|
+
while (lo < hi) {
|
|
281
|
+
const mid = lo + hi + 1 >> 1;
|
|
282
|
+
if (starts[mid] <= off) lo = mid;
|
|
283
|
+
else hi = mid - 1;
|
|
284
|
+
}
|
|
285
|
+
return lo;
|
|
286
|
+
};
|
|
287
|
+
return { text, starts, lineOf };
|
|
288
|
+
}
|
|
289
|
+
var ASSIGN = (name) => new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var|final|static|private|public|readonly|\\s)*\\s*(?:self\\.|this\\.)?${name}\\s*(?::[^=\\n]+)?=(?![=>])`);
|
|
290
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["self", "this", "await", "async", "return", "model", "messages", "role", "content", "system", "user", "true", "false", "True", "False", "None", "null", "undefined", "new", "max_tokens", "temperature", "client", "resp", "response"]);
|
|
291
|
+
function assignmentScope(loc, masked, line) {
|
|
292
|
+
const eq = masked[line].search(/=(?![=>])/);
|
|
293
|
+
let end = line;
|
|
294
|
+
let k = loc.starts[line] + eq + 1;
|
|
295
|
+
for (; ; ) {
|
|
296
|
+
const lineEnd = loc.starts[end] + masked[end].length;
|
|
297
|
+
let opened = -1;
|
|
298
|
+
for (let q = k; q < lineEnd; q++) if (OPEN.includes(loc.text[q])) {
|
|
299
|
+
opened = q;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
if (opened >= 0) {
|
|
303
|
+
const close = matchClose(loc.text, opened, 2e4);
|
|
304
|
+
end = loc.lineOf(close);
|
|
305
|
+
k = close + 1;
|
|
306
|
+
if (k < loc.starts[end] + masked[end].length) continue;
|
|
307
|
+
}
|
|
308
|
+
const tail = masked[end].trimEnd();
|
|
309
|
+
if (end + 1 < masked.length && (/[+\\,(]$/.test(tail) || /^\s*[+.]/.test(masked[end + 1] ?? "")) && end - line < 60) {
|
|
310
|
+
end++;
|
|
311
|
+
k = loc.starts[end];
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
return { start: line, end };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function identifiersIn(masked, sc) {
|
|
318
|
+
const ids = /* @__PURE__ */ new Set();
|
|
319
|
+
for (let l = sc.start; l <= sc.end; l++) for (const m of (masked[l] ?? "").matchAll(/\b([A-Za-z_]\w{1,60})\b/g)) if (!KEYWORDS.has(m[1])) ids.add(m[1]);
|
|
320
|
+
return ids;
|
|
321
|
+
}
|
|
322
|
+
function resolveScopes(loc, masked, from, depth = 2) {
|
|
323
|
+
const out2 = [], seen = /* @__PURE__ */ new Set();
|
|
324
|
+
let frontier = [from];
|
|
325
|
+
for (let d = 0; d < depth && frontier.length; d++) {
|
|
326
|
+
const next = [];
|
|
327
|
+
for (const sc of frontier) for (const id of identifiersIn(masked, sc)) {
|
|
328
|
+
if (seen.has(id)) continue;
|
|
329
|
+
seen.add(id);
|
|
330
|
+
const re = ASSIGN(id);
|
|
331
|
+
let found = -1;
|
|
332
|
+
for (let l = sc.start; l >= 0; l--) if (re.test(masked[l])) {
|
|
333
|
+
found = l;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
if (found < 0) {
|
|
337
|
+
for (let l = sc.end + 1; l < masked.length; l++) if (re.test(masked[l])) {
|
|
338
|
+
found = l;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (found < 0 || found >= from.start && found <= from.end) continue;
|
|
343
|
+
const a = assignmentScope(loc, masked, found);
|
|
344
|
+
out2.push(a);
|
|
345
|
+
next.push(a);
|
|
346
|
+
}
|
|
347
|
+
frontier = next;
|
|
348
|
+
}
|
|
349
|
+
return out2;
|
|
350
|
+
}
|
|
257
351
|
function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
258
352
|
const findings = [];
|
|
259
353
|
const callSites = [];
|
|
@@ -281,9 +375,16 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
281
375
|
const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(src);
|
|
282
376
|
const illustrative = EXAMPLE_PATH.test(f.path);
|
|
283
377
|
let repeatFlagged = false;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
378
|
+
const loc = makeLocator(masked);
|
|
379
|
+
const callLines = [];
|
|
380
|
+
masked.forEach((code2, i) => {
|
|
381
|
+
const hit = CALL_RE.find(([re]) => re.test(code2));
|
|
382
|
+
if (!hit) return;
|
|
383
|
+
const m = hit[0].exec(code2);
|
|
384
|
+
const close = matchClose(loc.text, loc.starts[i] + m.index + m[0].length - 1);
|
|
385
|
+
callLines.push({ line: i, sdk: hit[1], scope: { start: i, end: Math.max(i, loc.lineOf(close)) } });
|
|
386
|
+
});
|
|
387
|
+
callLines.forEach(({ line: i, sdk, scope }, n) => {
|
|
287
388
|
const local = [];
|
|
288
389
|
const push = (fd) => {
|
|
289
390
|
let sev = fd.severity;
|
|
@@ -291,13 +392,22 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
291
392
|
if (illustrative) sev = DOWN[sev];
|
|
292
393
|
local.push({ ...fd, severity: sev });
|
|
293
394
|
};
|
|
294
|
-
const
|
|
295
|
-
const
|
|
395
|
+
const refs = resolveScopes(loc, masked, scope);
|
|
396
|
+
const inScopes = (l, scs) => scs.some((sc) => l.line - 1 >= sc.start && l.line - 1 <= sc.end);
|
|
397
|
+
let near = literals.filter((l) => inScopes(l, [scope, ...refs]));
|
|
398
|
+
if (!near.some((l) => l.text.trim().length > 20)) {
|
|
399
|
+
const prevEnd = n > 0 ? callLines[n - 1].scope.end : -1;
|
|
400
|
+
near = near.concat(literals.filter((l) => l.line - 1 > prevEnd && l.line - 1 >= i - 40 && l.line - 1 < i));
|
|
401
|
+
}
|
|
402
|
+
const spanText = lines.slice(scope.start, scope.end + 1).join("\n");
|
|
403
|
+
const nextStart = n + 1 < callLines.length ? callLines[n + 1].line : lines.length;
|
|
404
|
+
const fallbackWindow = lines.slice(i, Math.min(nextStart, i + 30)).join("\n");
|
|
405
|
+
const modelFrom = (t) => [...t.matchAll(MODEL_RE)].map((m) => m[1])[0] ?? null;
|
|
406
|
+
const model = modelFrom(spanText) ?? modelFrom(refs.map((r) => lines.slice(r.start, r.end + 1).join("\n")).join("\n")) ?? modelFrom(fallbackWindow);
|
|
296
407
|
callSites.push({ file: f.path, line: i + 1, model, sdk });
|
|
297
408
|
if (model) models[model] = (models[model] ?? 0) + 1;
|
|
298
409
|
const key = normalizeModel(model);
|
|
299
410
|
const price = key ? priceFor(key) : null;
|
|
300
|
-
const near = literals.filter((l) => l.line >= i + 1 - 40 && l.line <= i + 1 + 40);
|
|
301
411
|
const prompts = near.filter((l) => promptLike(l.text));
|
|
302
412
|
const codeCtx = masked.slice(Math.max(0, i - 25), i + 25).join("\n");
|
|
303
413
|
const simpleTask = near.some((l) => SIMPLE_TASK.test(l.text));
|
|
@@ -311,23 +421,25 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
311
421
|
severity: isAnthropic ? bigTok >= 1500 ? "high" : cacheable ? "medium" : "low" : cacheable ? "medium" : "low",
|
|
312
422
|
file: f.path,
|
|
313
423
|
line: prompts[0].line,
|
|
314
|
-
title: `~${bigTok.toLocaleString()} static prompt tokens re-billed at full price on every call`,
|
|
315
|
-
detail: isAnthropic ? `This call sends a large fixed prompt but never marks a cache breakpoint, so Anthropic bills all ${bigTok} tokens at the full input rate each time instead of the cache-read rate (${readRate}% cheaper).` : `A fixed
|
|
424
|
+
title: isAnthropic ? `~${bigTok.toLocaleString()} static prompt tokens re-billed at full price on every call` : `~${bigTok.toLocaleString()} static prompt tokens sent on every call; only a stable prefix gets the cache discount`,
|
|
425
|
+
detail: (isAnthropic ? `This call sends a large fixed prompt but never marks a cache breakpoint, so Anthropic bills all ${bigTok} tokens at the full input rate each time instead of the cache-read rate (${readRate}% cheaper).` : `A fixed block of ~${bigTok} tokens goes out with every call. ${model ? model : "This provider"} discounts cached input ${readRate}%, but only for a byte-identical prefix of at least ${min} tokens that is routed to the same cache; anything volatile (dates, ids, user text) placed before or inside it, or traffic spread across cache shards, means paying full price.`) + (prompts[0].line !== i + 1 ? ` Sent by the call at ${f.path}:${i + 1}.` : ""),
|
|
316
426
|
fix: isAnthropic ? 'Route through TokenZero (breakpoints are injected automatically) or add `cache_control: {type: "ephemeral"}` to the last system block and the last tool.' : "Route through TokenZero to get a stable `prompt_cache_key`, or move dynamic content below the static prefix and set `prompt_cache_key` yourself.",
|
|
317
427
|
// OpenAI/Gemini already cache stable prefixes automatically; the recoverable part
|
|
318
428
|
// is prefix hygiene and cache routing, not the whole cache discount.
|
|
319
429
|
est_savings_pct: !cacheable ? 0 : isAnthropic ? Math.min(85, Math.round(bigTok / (bigTok + 400) * readRate)) : 15
|
|
320
430
|
});
|
|
321
431
|
}
|
|
322
|
-
|
|
323
|
-
|
|
432
|
+
const cascadeTo = key ? CASCADE_TARGET[key] : void 0;
|
|
433
|
+
const ratio = cascadeTo ? costOf(cascadeTo, { input: 1500, output: 200 }) / costOf(key, { input: 1500, output: 200 }) : 1;
|
|
434
|
+
if (key && price && cascadeTo && simpleTask && (price.tier === "frontier" || ratio <= 0.5)) {
|
|
435
|
+
const to = cascadeTo;
|
|
324
436
|
const a = costOf(key, { input: 1500, output: 200 }), b = costOf(to, { input: 1500, output: 200 });
|
|
325
437
|
push({
|
|
326
|
-
severity: "high",
|
|
438
|
+
severity: price.tier === "frontier" || ratio <= 0.2 ? "high" : "medium",
|
|
327
439
|
file: f.path,
|
|
328
440
|
line: i + 1,
|
|
329
441
|
title: `${key} used for a task whose prompt reads like extraction/classification`,
|
|
330
|
-
detail:
|
|
442
|
+
detail: `${price.tier === "frontier" ? "Frontier" : "Full-size"} pricing ($${price.input}/M in, $${price.output}/M out) on a constrained task. ${to} costs ~${Math.round((1 - b / a) * 100)}% less per call at this shape.`,
|
|
331
443
|
fix: `Opt into TokenZero cascade routing (\`X-TokenZero-Cascade: auto\`) so this call is routed to ${to} only when the classifier is confident, with parity reported in the ledger, or switch the model here behind an eval.`,
|
|
332
444
|
est_savings_pct: Math.round((1 - b / a) * 100)
|
|
333
445
|
});
|
|
@@ -344,10 +456,10 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
344
456
|
est_savings_pct: 25
|
|
345
457
|
});
|
|
346
458
|
}
|
|
347
|
-
if (simpleTask && !repeatFlagged && !/seed
|
|
459
|
+
if (simpleTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
|
|
348
460
|
repeatFlagged = true;
|
|
349
461
|
push({
|
|
350
|
-
severity: "
|
|
462
|
+
severity: "low",
|
|
351
463
|
file: f.path,
|
|
352
464
|
line: i + 1,
|
|
353
465
|
title: "Repeat-prone task with no response-cache opt-in",
|
|
@@ -356,7 +468,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
356
468
|
est_savings_pct: 10
|
|
357
469
|
});
|
|
358
470
|
}
|
|
359
|
-
if (isAnthropic && /\btools\s*[:=]/.test(masked.slice(
|
|
471
|
+
if (isAnthropic && /\btools\s*[:=]/.test(masked.slice(scope.start, scope.end + 1).join("\n")) && !hasCacheControl) {
|
|
360
472
|
push({
|
|
361
473
|
severity: "low",
|
|
362
474
|
file: f.path,
|
|
@@ -428,14 +540,23 @@ function filesFromDiff(diff) {
|
|
|
428
540
|
}
|
|
429
541
|
return files;
|
|
430
542
|
}
|
|
431
|
-
|
|
543
|
+
var PR_COMMENT_MARKER = "<!-- tokenzero-cost-check -->";
|
|
544
|
+
var PUBLIC_REPO = "https://github.com/tokenzero-ai/tokenzero";
|
|
545
|
+
var inert = (t) => t.replace(/[\\`*_[\]<>|]/g, (c2) => "\\" + c2).replace(/\r?\n/g, " ");
|
|
546
|
+
var pipeSafe = (t) => t.replace(/\|/g, "\\|");
|
|
547
|
+
var code = (t) => "`" + t.replace(/`/g, "'").replace(/\|/g, "\\|").replace(/\r?\n/g, " ").slice(0, 200) + "`";
|
|
548
|
+
var usd2 = (n) => "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
|
549
|
+
function prComment(report, opts = {}) {
|
|
432
550
|
const c2 = report.cost_per_10k_runs;
|
|
433
|
-
const head = c2 ? `**TokenZero
|
|
434
|
-
const rows = report.findings.slice(0, 8).map((f) => `| ${f.severity} |
|
|
551
|
+
const head = c2 ? `**TokenZero cost check:** estimated cost per 10k runs ${code(usd2(c2.before_usd))} \u2192 ${code(usd2(c2.after_usd))} (${report.estimated_waste_pct}% recoverable) on ${code(c2.model)}` : `**TokenZero cost check:** ${report.findings.length} finding(s), ${report.call_sites.length} LLM call site(s)`;
|
|
552
|
+
const rows = report.findings.slice(0, 8).map((f) => `| ${f.severity} | ${code(`${f.file}:${f.line}`)} | ${pipeSafe(f.title)} | ${f.severity === "info" ? "not counted" : f.est_savings_pct + "%"} |`).join("\n");
|
|
553
|
+
const g = opts.gatewayUrl?.replace(/\/+$/, "");
|
|
554
|
+
const fix = g ? `One-line fix for everything above: point your SDK at ${code(g + "/v1")} (BYOK; your keys pass straight through).${opts.hostedReport ? ` [Full report](${g}/audit/${report.id})` : ""}` : `How to fix: route calls through the TokenZero gateway, or apply the fixes above by hand. [TokenZero tools](${PUBLIC_REPO})`;
|
|
435
555
|
return [
|
|
556
|
+
PR_COMMENT_MARKER,
|
|
436
557
|
head,
|
|
437
558
|
"",
|
|
438
|
-
report.findings.length ? "| severity | where | finding |
|
|
559
|
+
report.findings.length ? "| severity | where | finding | savings on that call |\n|---|---|---|---|\n" + rows : "_No token-waste patterns found in the changed lines._",
|
|
439
560
|
"",
|
|
440
561
|
report.findings.slice(0, 3).map((f) => `<details><summary>${f.title}</summary>
|
|
441
562
|
|
|
@@ -444,10 +565,10 @@ ${f.detail}
|
|
|
444
565
|
**Fix:** ${f.fix}
|
|
445
566
|
</details>`).join("\n"),
|
|
446
567
|
"",
|
|
447
|
-
|
|
448
|
-
...report.notes.map((n) => `> ${n}`),
|
|
568
|
+
fix,
|
|
569
|
+
...report.notes.map((n) => `> ${inert(n)}`),
|
|
449
570
|
"",
|
|
450
|
-
"<sub>
|
|
571
|
+
"<sub>Static heuristics at list prices, not measurements. Each row is the saving on the call it points at; the headline weights rows by confidence and averages over every call site in the change. Verified numbers come from the signed ledger once traffic flows.</sub>"
|
|
451
572
|
].join("\n");
|
|
452
573
|
}
|
|
453
574
|
|
|
@@ -501,8 +622,8 @@ function canonical(obj) {
|
|
|
501
622
|
}
|
|
502
623
|
|
|
503
624
|
// cli/src/main.ts
|
|
504
|
-
var VERSION = "0.1.
|
|
505
|
-
var BAKED_URL =
|
|
625
|
+
var VERSION = "0.1.3";
|
|
626
|
+
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
506
627
|
function parseArgs(argv) {
|
|
507
628
|
const a = { _: [], flags: {} };
|
|
508
629
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -523,7 +644,7 @@ function parseArgs(argv) {
|
|
|
523
644
|
}
|
|
524
645
|
var VALUE_FLAGS = /* @__PURE__ */ new Set(["url", "name", "key", "format", "diff", "fail-over", "env-file"]);
|
|
525
646
|
var tty = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
526
|
-
var c = (
|
|
647
|
+
var c = (code2) => (s) => tty ? `\x1B[${code2}m${s}\x1B[0m` : s;
|
|
527
648
|
var bold = c("1");
|
|
528
649
|
var dim = c("2");
|
|
529
650
|
var green = c("32");
|
|
@@ -753,7 +874,7 @@ async function cmdAudit(a) {
|
|
|
753
874
|
} else report = analyzeFiles(walk(root), null, manifests(root));
|
|
754
875
|
const fmt = a.flags.format ?? (a.flags.json ? "json" : "text");
|
|
755
876
|
if (fmt === "json") out(JSON.stringify(report, null, 2));
|
|
756
|
-
else if (fmt === "markdown") out(prComment(report, baseUrl(a)
|
|
877
|
+
else if (fmt === "markdown") out(prComment(report, { gatewayUrl: baseUrl(a) }));
|
|
757
878
|
else {
|
|
758
879
|
summarize(report, 100);
|
|
759
880
|
for (const f of report.findings.slice(0, 3)) out(`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "TokenZero CLI: connect a project to the TokenZero BYOK gateway, audit LLM token waste locally, check usage and verify signed savings receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|