@tokenzero/cli 0.2.9 → 0.2.10

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 +43 -2
  2. package/package.json +1 -1
package/bin/tokenzero.mjs CHANGED
@@ -139,8 +139,15 @@ function normalizeModel(model) {
139
139
  for (const k of KEYS_BY_LENGTH) if (m.startsWith(k + "-") || m === k) return k;
140
140
  return null;
141
141
  }
142
+ var isFreeModel = (model) => typeof model === "string" && (/:free$/i.test(model.trim()) || /^openrouter\/free$/i.test(model.trim()));
143
+ var dynamicPrice = null;
144
+ var FREE_PRICE = { provider: "openrouter", tier: "fast", input: 0, output: 0, cachedInput: 0 };
142
145
  function priceFor(model) {
146
+ if (isFreeModel(model)) return { key: String(model).trim().toLowerCase(), ...FREE_PRICE };
143
147
  const key = normalizeModel(model);
148
+ if (key && PRICES[key]) return { key, ...PRICES[key] };
149
+ const dyn = typeof model === "string" && dynamicPrice ? dynamicPrice(model) : null;
150
+ if (dyn) return { key: String(model).trim().toLowerCase(), ...dyn };
144
151
  return key ? { key, ...PRICES[key] } : null;
145
152
  }
146
153
  function costOf(model, u, factor = 1) {
@@ -340,7 +347,7 @@ function makeLocator(masked) {
340
347
  };
341
348
  return { text, starts, lineOf };
342
349
  }
343
- var ASSIGN = (name) => new RegExp(`^\\s*(?:export\\s+)?(?:const|let|var|final|static|private|public|readonly|\\s)*\\s*(?:self\\.|this\\.)?${name}\\s*(?::[^=\\n]+)?=(?![=>])`);
350
+ var ASSIGN = (name) => new RegExp(`^[ \\t]*(?:export[ \\t]+)?(?:(?:const|let|var|final|static|private|public|readonly)[ \\t]+)*(?:self\\.|this\\.)?${name}[ \\t]*(?::[^=\\n]{0,200})?=(?![=>])`);
344
351
  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"]);
345
352
  function assignmentScope(loc, masked, line) {
346
353
  const eq = masked[line].search(/=(?![=>])/);
@@ -409,6 +416,10 @@ var MODEL_NAME_TEST = /\b(?:model|model_?id|model_?name|modelId|modelName|deploy
409
416
  var VOLATILE_CALL = /\b(?:datetime\.(?:now|utcnow)\(|date\.today\(|time\.time\(|time\.strftime\(|Date\.now\(|new Date\(\s*\)|\.toISOString\(|\.isoformat\(|\.toLocale(?:Date|Time)?String\(|uuid\.?uuid4\(|uuid4\(|randomUUID\(|nanoid\(|Math\.random\(|secrets\.token_hex\()/;
410
417
  var INTERPOLATES = /\bf["']|\$\{|\.format\(|%\s*\(|["']\s*\+|\+\s*["']|`/;
411
418
  var SYSTEM_CTX = /\bsystem(?:_?prompt|Prompt|_?message|Message|_?instructions?)?\b\s*[:=(]|["']role["']\s*:\s*["']system["']|\brole\s*:\s*["']system["']|\binstructions\s*[:=]/i;
419
+ var HARDCODED_KEY = /["'`]((?:sk-ant-api\d\d-|sk-ant-|sk-proj-|sk-or-v1-|sk-svcacct-)[A-Za-z0-9_-]{20,200}|sk-[A-Za-z0-9]{40,200}|AIza[0-9A-Za-z_-]{35}|xai-[A-Za-z0-9]{40,200}|gsk_[A-Za-z0-9]{40,200}|csk-[A-Za-z0-9]{30,200}|tz_live_[A-Za-z0-9_-]{24,200})["'`]/;
420
+ var KEY_PLACEHOLDER = /x{6,}|X{6,}|0{8,}|1234567|abcdef|your|example|placeholder|dummy|fake|test|redacted|\.\.\./i;
421
+ var keyProvider = (k) => k.startsWith("sk-ant-") ? "Anthropic" : k.startsWith("sk-or-") ? "OpenRouter" : k.startsWith("AIza") ? "Google" : k.startsWith("xai-") ? "xAI" : k.startsWith("gsk_") ? "Groq" : k.startsWith("csk-") ? "Cerebras" : k.startsWith("tz_live_") ? "TokenZero" : "OpenAI";
422
+ var PREFIX_DYNAMIC = /\bf(?:"""|'''|"|')\s{0,4}\{[A-Za-z_][\w.\[\]'"]{0,60}\}|`\s{0,4}\$\{[^}\n]{1,80}\}/;
412
423
  var RANDOM_ID = /(?:\b(?:tool_call_id|toolCallId|call_id|callId|tool_use_id|toolUseId)\b|["']id["'])\s*[:=]\s*[^,\n]*?(?:uuid\.?uuid4\(|uuid4\(|randomUUID\(|nanoid\(|Math\.random\(|secrets\.token_hex\(|shortuuid)/;
413
424
  var SET_TOOLS = /\btools?\s*=\s*\[[^\]\n]*\bfor\s+\w+\s+in\s+(?:set\(|\w*_?set\b|\w+\.keys\(\)\s*\|)/;
414
425
  var OPENAI_COMPAT = /\bfrom\s+openai\s+import|\bimport\s+openai\b|from\s+["']openai["']|require\(\s*["']openai["']\s*\)|\bOpenAI\(|openrouter\.ai|chat\.completions\.create|litellm\.completion/;
@@ -420,6 +431,26 @@ var CACHE_USAGE_FIELD = /cache_read_input_tokens|cache_creation_input_tokens|cac
420
431
  var COST_MATH = /\b(?:input_tokens|prompt_tokens|promptTokenCount|inputTokens|promptTokens|prompt_token_count)\b[^;\n]*(?:\*\s*[\w.[\]'"]*(?:price|rate|cost|per_?m|perM|usd)|\/\s*1[_,]?000[_,]?000|\/\s*1e6)|(?:price|rate|cost|per_?m|perM|usd)\w*\s*\*\s*[\w.[\]'"]*\b(?:input_tokens|prompt_tokens|promptTokenCount|inputTokens|promptTokens)\b/i;
421
432
  var ZEROED_CACHE_USAGE = /\b(?:cache_creation_input_tokens|cache_read_input_tokens|cacheCreationInputTokens|cacheReadInputTokens|cached_tokens|cachedTokens)["']?\s*[:=]\s*0\b(?!\s*(?:if|\?|\|\||or\b|&&))/;
422
433
  var isComment = (raw) => /^\s*(?:#|\/\/|\*|\/\*)/.test(raw);
434
+ function secretLint(path3, lines) {
435
+ const out2 = [];
436
+ for (let i = 0; i < lines.length && out2.length < 5; i++) {
437
+ const raw = lines[i];
438
+ if (raw.length > 2e4) continue;
439
+ const km = HARDCODED_KEY.exec(raw);
440
+ if (!km || KEY_PLACEHOLDER.test(km[1])) continue;
441
+ const who = keyProvider(km[1]);
442
+ out2.push({
443
+ file: path3,
444
+ severity: "high",
445
+ line: i + 1,
446
+ title: `Hard-coded ${who} API key in source`,
447
+ detail: `A string that looks like a live ${who} key (${km[1].slice(0, 7)}\u2026) is committed in this file. Anyone who can read the repository or its history can bill your account; a key committed to a public repo and never revoked is how the October 2026 LiteLLM supply-chain compromise began.`,
448
+ fix: "Revoke the key at the provider now (removing it from the file does not remove it from git history), then load it from the environment or a secret manager. TokenZero never needs your key in code: SDKs read OPENAI_API_KEY / ANTHROPIC_API_KEY as usual.",
449
+ est_savings_pct: 0
450
+ });
451
+ }
452
+ return out2;
453
+ }
423
454
  function cacheLint(path3, lines, masked) {
424
455
  const out2 = [];
425
456
  const seen = /* @__PURE__ */ new Set();
@@ -474,6 +505,15 @@ function cacheLint(path3, lines, masked) {
474
505
  fix: "Keep the system prompt static. Put the current time or request id in the last user message (or round it to the day if it must stay in the system prompt).",
475
506
  est_savings_pct: 15
476
507
  });
508
+ const pm = PREFIX_DYNAMIC.exec(raw);
509
+ if (pm && SYSTEM_CTX.test(raw.slice(0, pm.index + 1)) && raw.length - pm.index >= 160) add("prefix-order", {
510
+ severity: "medium",
511
+ line: i + 1,
512
+ title: "Per-request value at the start of the system prompt (everything after it misses the cache)",
513
+ detail: "The system prompt opens with an interpolated value and continues with a long fixed block. Provider caches match byte-for-byte from the first token, so when the opening varies per request the fixed block behind it is re-billed at the full input rate every time. Moving per-job details below the shared prompt took a published AI code-review pipeline from $2.43 to $0.77 per run.",
514
+ fix: "Put the fixed instructions first and the per-request details (names, ids, dates, file lists) at the end, ideally in the last user message.",
515
+ est_savings_pct: 20
516
+ });
477
517
  if (RANDOM_ID.test(raw) || SET_TOOLS.test(raw)) add("serialization", {
478
518
  severity: "low",
479
519
  line: i + 1,
@@ -680,6 +720,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
680
720
  findings.push(...local);
681
721
  siteFindings.push(local);
682
722
  });
723
+ findings.push(...secretLint(f.path, lines));
683
724
  if (callLines.length || hasCacheControl || USAGE_FIELD.test(src)) {
684
725
  for (const fd of cacheLint(f.path, lines, masked)) findings.push({ ...fd, severity: kind === "library" || illustrative ? DOWN[fd.severity] : fd.severity });
685
726
  }
@@ -834,7 +875,7 @@ function canonical(obj) {
834
875
  }
835
876
 
836
877
  // cli/src/main.ts
837
- var VERSION = "0.2.9";
878
+ var VERSION = "0.2.10";
838
879
  var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
839
880
  function parseArgs(argv) {
840
881
  const a = { _: [], flags: {} };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenzero/cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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",