@tokenzero/cli 0.1.3 → 0.1.4
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/bin/tokenzero.mjs +39 -12
- package/package.json +1 -1
package/bin/tokenzero.mjs
CHANGED
|
@@ -118,7 +118,7 @@ function priceFor(model) {
|
|
|
118
118
|
const key = normalizeModel(model);
|
|
119
119
|
return key ? { key, ...PRICES[key] } : null;
|
|
120
120
|
}
|
|
121
|
-
function costOf(model, u) {
|
|
121
|
+
function costOf(model, u, factor = 1) {
|
|
122
122
|
const p = priceFor(model);
|
|
123
123
|
if (!p) return null;
|
|
124
124
|
const tier = p.longCtx && p.longCtxAt && u.input > p.longCtxAt ? { ...p, ...p.longCtx } : p;
|
|
@@ -126,7 +126,7 @@ function costOf(model, u) {
|
|
|
126
126
|
const written = u.cacheWrite ?? 0;
|
|
127
127
|
const fresh = Math.max(0, u.input - cached - written);
|
|
128
128
|
const M = 1e6;
|
|
129
|
-
return fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M;
|
|
129
|
+
return factor * (fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M);
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
// cli/src/tokens-lite.ts
|
|
@@ -224,6 +224,7 @@ function promptLike(t) {
|
|
|
224
224
|
const words = (t.match(/[A-Za-z]{2,}/g) ?? []).length;
|
|
225
225
|
return tags < 4 && cssRules < 3 && codeChars / t.length < 0.03 && words / (t.length / 6) > 0.5;
|
|
226
226
|
}
|
|
227
|
+
var REPEAT_TASK = /\b(classify|classification|categori[sz]e|sentiment|label (this|the|each)|is (this|it) spam|yes or no|true or false)\b/i;
|
|
227
228
|
var SIMPLE_TASK = /\b(classify|classification|categori[sz]e|sentiment|extract (the|all|every|any)|return (only |just )?(valid |a )?json|respond (only )?(with|in) (valid )?json|output (only )?(valid )?json|translate (this|the following|into)|summari[sz]e (this|the following|in \d+)|label (this|the|each)|is (this|it) spam|yes or no|true or false)\b/i;
|
|
228
229
|
var LLM_LIB_EXACT = /* @__PURE__ */ new Set(["openai", "anthropic", "@anthropic-ai/sdk", "ai", "litellm", "openai-agents", "@openai/agents", "instructor", "dspy", "dspy-ai", "guidance", "semantic-kernel", "haystack-ai", "autogen", "pyautogen", "autogen-agentchat", "crewai", "google-genai", "@google/genai", "mistralai", "@mistralai/mistralai", "cohere", "cohere-ai", "llama-index", "llama_index", "llamaindex", "langchain", "langgraph", "@langchain/core", "groq-sdk", "together-ai", "ollama"]);
|
|
229
230
|
var LLM_LIB_FAMILY = /^(@langchain\/[\w-]+|langchain[-_][\w-]+|langgraph[-_][\w-]+|llama[-_]index[-_][\w-]+|@ai-sdk\/[\w-]+|openai[-_]agents[-_][\w-]+)$/i;
|
|
@@ -359,7 +360,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
359
360
|
const { kind, reason } = detectRepoKind(manifests2, sources);
|
|
360
361
|
if (kind === "library") notes.push(`Library/SDK layout detected (${reason}): call sites are most likely pass-through wrappers where the caller chooses the model and prompt, so findings are downgraded one level and the estimate is discounted.`);
|
|
361
362
|
const promptLibrary = [];
|
|
362
|
-
const repoCaches = sources.some((f) => /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(f.content));
|
|
363
|
+
const repoCaches = sources.some((f) => /cache_control|cacheControl|prompt_cache_key|promptCacheKey|prompt_cache_options|promptCacheOptions|prompt_cache_breakpoint|promptCacheBreakpoint|prompt_cache_retention|promptCacheRetention|cachedContent|cached_content/.test(f.content));
|
|
363
364
|
const PROMPT_FILE = /(^|\/)[^/]*(prompt|instruction|template|persona)s?[^/]*\.[a-z]+$|(^|\/)prompts?\//i;
|
|
364
365
|
const PROMPT_NAME = /\b[\w.]*(prompt|system|instruction|template|persona)\w*\s*(:\s*[\w[\]|. ]+)?\s*[:=]\s*(f|r|rf|fr|b)?$/i;
|
|
365
366
|
for (const f of sources) {
|
|
@@ -372,7 +373,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
372
373
|
const lead = (masked[l.line - 1] ?? "").slice(0, Math.max(0, (masked[l.line - 1] ?? "").search(/["'`]|$/))).trimEnd();
|
|
373
374
|
if (PROMPT_FILE.test(f.path) || PROMPT_NAME.test(lead) || PROMPT_NAME.test((masked[l.line - 2] ?? "").trimEnd())) promptLibrary.push({ file: f.path, line: l.line, tokens });
|
|
374
375
|
}
|
|
375
|
-
const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(src);
|
|
376
|
+
const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|prompt_cache_options|promptCacheOptions|prompt_cache_breakpoint|promptCacheBreakpoint|prompt_cache_retention|promptCacheRetention|cachedContent|cached_content/.test(src);
|
|
376
377
|
const illustrative = EXAMPLE_PATH.test(f.path);
|
|
377
378
|
let repeatFlagged = false;
|
|
378
379
|
const loc = makeLocator(masked);
|
|
@@ -393,6 +394,16 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
393
394
|
local.push({ ...fd, severity: sev });
|
|
394
395
|
};
|
|
395
396
|
const refs = resolveScopes(loc, masked, scope);
|
|
397
|
+
if (sdk.startsWith("langchain")) {
|
|
398
|
+
const assigned = /^\s*(?:const|let|var|self\.|this\.)?\s*([A-Za-z_]\w*)\s*(?::[^=\n]+)?=/.exec(masked[i])?.[1];
|
|
399
|
+
if (assigned) for (let l = 0; l < masked.length; l++) {
|
|
400
|
+
const inv = new RegExp(`\\b${assigned}\\.(invoke|ainvoke|stream|astream|batch|abatch|call|predict|generate)\\s*\\(`).exec(masked[l]);
|
|
401
|
+
if (!inv) continue;
|
|
402
|
+
const close = matchClose(loc.text, loc.starts[l] + inv.index + inv[0].length - 1);
|
|
403
|
+
const sc = { start: l, end: Math.max(l, loc.lineOf(close)) };
|
|
404
|
+
refs.push(sc, ...resolveScopes(loc, masked, sc));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
396
407
|
const inScopes = (l, scs) => scs.some((sc) => l.line - 1 >= sc.start && l.line - 1 <= sc.end);
|
|
397
408
|
let near = literals.filter((l) => inScopes(l, [scope, ...refs]));
|
|
398
409
|
if (!near.some((l) => l.text.trim().length > 20)) {
|
|
@@ -411,6 +422,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
411
422
|
const prompts = near.filter((l) => promptLike(l.text));
|
|
412
423
|
const codeCtx = masked.slice(Math.max(0, i - 25), i + 25).join("\n");
|
|
413
424
|
const simpleTask = near.some((l) => SIMPLE_TASK.test(l.text));
|
|
425
|
+
const repeatTask = near.some((l) => REPEAT_TASK.test(l.text));
|
|
414
426
|
const isAnthropic = sdk.includes("anthropic") || (model?.startsWith("claude") ?? false);
|
|
415
427
|
const bigTok = prompts.reduce((a, l) => a + countTokens(l.text), 0);
|
|
416
428
|
if (bigTok >= 300 && !hasCacheControl) {
|
|
@@ -440,7 +452,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
440
452
|
line: i + 1,
|
|
441
453
|
title: `${key} used for a task whose prompt reads like extraction/classification`,
|
|
442
454
|
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.`,
|
|
443
|
-
fix: `Opt into TokenZero cascade routing (\`X-TokenZero-Cascade: auto\`) so this call is routed to ${to} only when the classifier is confident
|
|
455
|
+
fix: `Opt into TokenZero cascade routing (\`X-TokenZero-Cascade: auto\`) so this call is routed to ${to} only when the classifier is confident (an opt-in shadow parity monitor is coming; nothing is promised), or switch the model here behind an eval.`,
|
|
444
456
|
est_savings_pct: Math.round((1 - b / a) * 100)
|
|
445
457
|
});
|
|
446
458
|
}
|
|
@@ -451,12 +463,12 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
451
463
|
file: f.path,
|
|
452
464
|
line: i + 1,
|
|
453
465
|
title: "LLM call inside a loop",
|
|
454
|
-
detail:
|
|
455
|
-
fix:
|
|
466
|
+
detail: 'Per-item calls repeat the full prompt prefix for every element. If items are independent and latency-tolerant, the provider Batch API is 50% cheaper, and OpenAI Flex processing (`service_tier: "flex"`) gives the same 50% on ordinary calls without restructuring; if the prefix is shared, caching pays off immediately.',
|
|
467
|
+
fix: 'Batch where possible, or set `service_tier: "flex"` on OpenAI calls that can wait; otherwise keep the shared prefix cache-formatted (TokenZero does this automatically).',
|
|
456
468
|
est_savings_pct: 25
|
|
457
469
|
});
|
|
458
470
|
}
|
|
459
|
-
if (
|
|
471
|
+
if (repeatTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
|
|
460
472
|
repeatFlagged = true;
|
|
461
473
|
push({
|
|
462
474
|
severity: "low",
|
|
@@ -526,17 +538,32 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
526
538
|
function filesFromDiff(diff) {
|
|
527
539
|
const files = [];
|
|
528
540
|
let cur = null;
|
|
541
|
+
let lineNo = 0;
|
|
529
542
|
for (const line of diff.split("\n")) {
|
|
530
543
|
const m = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
531
544
|
if (m) {
|
|
532
545
|
cur = { path: m[1], content: "" };
|
|
533
546
|
files.push(cur);
|
|
547
|
+
lineNo = 0;
|
|
534
548
|
continue;
|
|
535
549
|
}
|
|
536
550
|
if (!cur) continue;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
551
|
+
const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
552
|
+
if (h) {
|
|
553
|
+
const start = Number(h[1]);
|
|
554
|
+
while (lineNo < start - 1) {
|
|
555
|
+
cur.content += "\n";
|
|
556
|
+
lineNo++;
|
|
557
|
+
}
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
561
|
+
cur.content += line.slice(1) + "\n";
|
|
562
|
+
lineNo++;
|
|
563
|
+
} else if (line.startsWith(" ")) {
|
|
564
|
+
cur.content += line.slice(1) + "\n";
|
|
565
|
+
lineNo++;
|
|
566
|
+
} else if (line === "\") continue;
|
|
540
567
|
}
|
|
541
568
|
return files;
|
|
542
569
|
}
|
|
@@ -622,7 +649,7 @@ function canonical(obj) {
|
|
|
622
649
|
}
|
|
623
650
|
|
|
624
651
|
// cli/src/main.ts
|
|
625
|
-
var VERSION = "0.1.
|
|
652
|
+
var VERSION = "0.1.4";
|
|
626
653
|
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
627
654
|
function parseArgs(argv) {
|
|
628
655
|
const a = { _: [], flags: {} };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|