@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.
Files changed (2) hide show
  1. package/bin/tokenzero.mjs +39 -12
  2. 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, with parity reported in the ledger, or switch the model here behind an eval.`,
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: "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; if the prefix is shared, caching pays off immediately.",
455
- fix: "Batch where possible; otherwise keep the shared prefix cache-formatted (TokenZero does this automatically).",
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 (simpleTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
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
- if (line.startsWith("+") && !line.startsWith("+++")) cur.content += line.slice(1) + "\n";
538
- else if (line.startsWith(" ")) cur.content += line.slice(1) + "\n";
539
- else if (line.startsWith("@@")) cur.content += "\n";
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.3";
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",
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",