@tokenzero/cli 0.1.3 → 0.1.5
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 +115 -36
- package/package.json +1 -1
package/bin/tokenzero.mjs
CHANGED
|
@@ -69,7 +69,18 @@ var PRICES = {
|
|
|
69
69
|
"gemini-3-flash": P("gemini", "fast", 0.5, 3, 0.05),
|
|
70
70
|
"gemini-2.5-pro": P("gemini", "frontier", 1.25, 10, 0.125, void 0, { longCtxAt: 2e5, longCtx: { input: 2.5, output: 15, cachedInput: 0.25 } }),
|
|
71
71
|
"gemini-2.5-flash": P("gemini", "fast", 0.3, 2.5, 0.03),
|
|
72
|
-
"gemini-2.5-flash-lite": P("gemini", "fast", 0.1, 0.4, 0.01)
|
|
72
|
+
"gemini-2.5-flash-lite": P("gemini", "fast", 0.1, 0.4, 0.01),
|
|
73
|
+
// ---- xAI Grok ----
|
|
74
|
+
"grok-4": P("xai", "frontier", 3, 15, 0.75),
|
|
75
|
+
"grok-4-fast": P("xai", "fast", 0.2, 0.8, 0.05),
|
|
76
|
+
"grok-4-mini": P("xai", "fast", 0.2, 0.8, 0.05),
|
|
77
|
+
"grok-3": P("xai", "mid", 3, 15, 0.75),
|
|
78
|
+
"grok-3-mini": P("xai", "fast", 0.3, 0.5, 0.075),
|
|
79
|
+
// ---- DeepSeek ----
|
|
80
|
+
"deepseek-v3": P("deepseek", "fast", 0.27, 1.1, 0.07),
|
|
81
|
+
"deepseek-chat": P("deepseek", "fast", 0.27, 1.1, 0.07),
|
|
82
|
+
"deepseek-r1": P("deepseek", "mid", 0.55, 2.19, 0.14),
|
|
83
|
+
"deepseek-reasoner": P("deepseek", "mid", 0.55, 2.19, 0.14)
|
|
73
84
|
};
|
|
74
85
|
var CASCADE_TARGET = {
|
|
75
86
|
"gpt-6-astra": "gpt-6.1-sol",
|
|
@@ -100,13 +111,19 @@ var CASCADE_TARGET = {
|
|
|
100
111
|
"gemini-3.1-pro": "gemini-3.8-flash",
|
|
101
112
|
"gemini-3.8-flash": "gemini-3.5-flash-lite",
|
|
102
113
|
"gemini-2.5-pro": "gemini-2.5-flash",
|
|
103
|
-
"gemini-2.5-flash": "gemini-2.5-flash-lite"
|
|
114
|
+
"gemini-2.5-flash": "gemini-2.5-flash-lite",
|
|
115
|
+
"grok-4": "grok-4-fast",
|
|
116
|
+
"grok-3": "grok-3-mini",
|
|
117
|
+
"deepseek-r1": "deepseek-v3",
|
|
118
|
+
"deepseek-reasoner": "deepseek-chat"
|
|
104
119
|
};
|
|
105
120
|
var KEYS_BY_LENGTH = Object.keys(PRICES).sort((a, b) => b.length - a.length);
|
|
106
121
|
function normalizeModel(model) {
|
|
107
122
|
if (typeof model !== "string" || !model) return null;
|
|
108
123
|
let m = model.toLowerCase().trim();
|
|
109
|
-
m = m.replace(/^(openai|anthropic|google|models|vertex_ai)\//, "");
|
|
124
|
+
m = m.replace(/^(openai|anthropic|google|gemini|x-ai|xai|deepseek|bedrock|models|vertex_ai)\//, "");
|
|
125
|
+
m = m.replace(/^(?:us\.|eu\.|apac\.)?anthropic\./, "");
|
|
126
|
+
m = m.replace(/-v\d+:\d+$/, "");
|
|
110
127
|
m = m.replace(/@\d{8}$/, "");
|
|
111
128
|
m = m.replace(/-(\d{8}|\d{4}-\d{2}-\d{2})$/, "");
|
|
112
129
|
m = m.replace(/-(latest|preview|customtools|thinking)$/, "");
|
|
@@ -118,7 +135,7 @@ function priceFor(model) {
|
|
|
118
135
|
const key = normalizeModel(model);
|
|
119
136
|
return key ? { key, ...PRICES[key] } : null;
|
|
120
137
|
}
|
|
121
|
-
function costOf(model, u) {
|
|
138
|
+
function costOf(model, u, factor = 1) {
|
|
122
139
|
const p = priceFor(model);
|
|
123
140
|
if (!p) return null;
|
|
124
141
|
const tier = p.longCtx && p.longCtxAt && u.input > p.longCtxAt ? { ...p, ...p.longCtx } : p;
|
|
@@ -126,7 +143,7 @@ function costOf(model, u) {
|
|
|
126
143
|
const written = u.cacheWrite ?? 0;
|
|
127
144
|
const fresh = Math.max(0, u.input - cached - written);
|
|
128
145
|
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;
|
|
146
|
+
return factor * (fresh * tier.input / M + cached * (tier.cachedInput ?? tier.input) / M + written * (tier.cacheWrite ?? tier.input) / M + u.output * tier.output / M);
|
|
130
147
|
}
|
|
131
148
|
|
|
132
149
|
// cli/src/tokens-lite.ts
|
|
@@ -224,6 +241,7 @@ function promptLike(t) {
|
|
|
224
241
|
const words = (t.match(/[A-Za-z]{2,}/g) ?? []).length;
|
|
225
242
|
return tags < 4 && cssRules < 3 && codeChars / t.length < 0.03 && words / (t.length / 6) > 0.5;
|
|
226
243
|
}
|
|
244
|
+
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
245
|
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
246
|
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
247
|
var LLM_LIB_FAMILY = /^(@langchain\/[\w-]+|langchain[-_][\w-]+|langgraph[-_][\w-]+|llama[-_]index[-_][\w-]+|@ai-sdk\/[\w-]+|openai[-_]agents[-_][\w-]+)$/i;
|
|
@@ -359,7 +377,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
359
377
|
const { kind, reason } = detectRepoKind(manifests2, sources);
|
|
360
378
|
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
379
|
const promptLibrary = [];
|
|
362
|
-
const repoCaches = sources.some((f) => /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(f.content));
|
|
380
|
+
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
381
|
const PROMPT_FILE = /(^|\/)[^/]*(prompt|instruction|template|persona)s?[^/]*\.[a-z]+$|(^|\/)prompts?\//i;
|
|
364
382
|
const PROMPT_NAME = /\b[\w.]*(prompt|system|instruction|template|persona)\w*\s*(:\s*[\w[\]|. ]+)?\s*[:=]\s*(f|r|rf|fr|b)?$/i;
|
|
365
383
|
for (const f of sources) {
|
|
@@ -372,7 +390,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
372
390
|
const lead = (masked[l.line - 1] ?? "").slice(0, Math.max(0, (masked[l.line - 1] ?? "").search(/["'`]|$/))).trimEnd();
|
|
373
391
|
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
392
|
}
|
|
375
|
-
const hasCacheControl = /cache_control|cacheControl|prompt_cache_key|promptCacheKey|cachedContent|cached_content/.test(src);
|
|
393
|
+
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
394
|
const illustrative = EXAMPLE_PATH.test(f.path);
|
|
377
395
|
let repeatFlagged = false;
|
|
378
396
|
const loc = makeLocator(masked);
|
|
@@ -393,6 +411,16 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
393
411
|
local.push({ ...fd, severity: sev });
|
|
394
412
|
};
|
|
395
413
|
const refs = resolveScopes(loc, masked, scope);
|
|
414
|
+
if (sdk.startsWith("langchain")) {
|
|
415
|
+
const assigned = /^\s*(?:const|let|var|self\.|this\.)?\s*([A-Za-z_]\w*)\s*(?::[^=\n]+)?=/.exec(masked[i])?.[1];
|
|
416
|
+
if (assigned) for (let l = 0; l < masked.length; l++) {
|
|
417
|
+
const inv = new RegExp(`\\b${assigned}\\.(invoke|ainvoke|stream|astream|batch|abatch|call|predict|generate)\\s*\\(`).exec(masked[l]);
|
|
418
|
+
if (!inv) continue;
|
|
419
|
+
const close = matchClose(loc.text, loc.starts[l] + inv.index + inv[0].length - 1);
|
|
420
|
+
const sc = { start: l, end: Math.max(l, loc.lineOf(close)) };
|
|
421
|
+
refs.push(sc, ...resolveScopes(loc, masked, sc));
|
|
422
|
+
}
|
|
423
|
+
}
|
|
396
424
|
const inScopes = (l, scs) => scs.some((sc) => l.line - 1 >= sc.start && l.line - 1 <= sc.end);
|
|
397
425
|
let near = literals.filter((l) => inScopes(l, [scope, ...refs]));
|
|
398
426
|
if (!near.some((l) => l.text.trim().length > 20)) {
|
|
@@ -411,6 +439,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
411
439
|
const prompts = near.filter((l) => promptLike(l.text));
|
|
412
440
|
const codeCtx = masked.slice(Math.max(0, i - 25), i + 25).join("\n");
|
|
413
441
|
const simpleTask = near.some((l) => SIMPLE_TASK.test(l.text));
|
|
442
|
+
const repeatTask = near.some((l) => REPEAT_TASK.test(l.text));
|
|
414
443
|
const isAnthropic = sdk.includes("anthropic") || (model?.startsWith("claude") ?? false);
|
|
415
444
|
const bigTok = prompts.reduce((a, l) => a + countTokens(l.text), 0);
|
|
416
445
|
if (bigTok >= 300 && !hasCacheControl) {
|
|
@@ -440,7 +469,7 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
440
469
|
line: i + 1,
|
|
441
470
|
title: `${key} used for a task whose prompt reads like extraction/classification`,
|
|
442
471
|
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
|
|
472
|
+
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
473
|
est_savings_pct: Math.round((1 - b / a) * 100)
|
|
445
474
|
});
|
|
446
475
|
}
|
|
@@ -451,12 +480,12 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
451
480
|
file: f.path,
|
|
452
481
|
line: i + 1,
|
|
453
482
|
title: "LLM call inside a loop",
|
|
454
|
-
detail:
|
|
455
|
-
fix:
|
|
483
|
+
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.',
|
|
484
|
+
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
485
|
est_savings_pct: 25
|
|
457
486
|
});
|
|
458
487
|
}
|
|
459
|
-
if (
|
|
488
|
+
if (repeatTask && !repeatFlagged && !/seed["']?\s*[:=]|temperature["']?\s*[:=]\s*0\b|X-TokenZero-Cache/i.test(spanText)) {
|
|
460
489
|
repeatFlagged = true;
|
|
461
490
|
push({
|
|
462
491
|
severity: "low",
|
|
@@ -526,17 +555,32 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
526
555
|
function filesFromDiff(diff) {
|
|
527
556
|
const files = [];
|
|
528
557
|
let cur = null;
|
|
558
|
+
let lineNo = 0;
|
|
529
559
|
for (const line of diff.split("\n")) {
|
|
530
560
|
const m = /^\+\+\+ b\/(.+)$/.exec(line);
|
|
531
561
|
if (m) {
|
|
532
562
|
cur = { path: m[1], content: "" };
|
|
533
563
|
files.push(cur);
|
|
564
|
+
lineNo = 0;
|
|
534
565
|
continue;
|
|
535
566
|
}
|
|
536
567
|
if (!cur) continue;
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
568
|
+
const h = /^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line);
|
|
569
|
+
if (h) {
|
|
570
|
+
const start = Number(h[1]);
|
|
571
|
+
while (lineNo < start - 1) {
|
|
572
|
+
cur.content += "\n";
|
|
573
|
+
lineNo++;
|
|
574
|
+
}
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (line.startsWith("+") && !line.startsWith("+++")) {
|
|
578
|
+
cur.content += line.slice(1) + "\n";
|
|
579
|
+
lineNo++;
|
|
580
|
+
} else if (line.startsWith(" ")) {
|
|
581
|
+
cur.content += line.slice(1) + "\n";
|
|
582
|
+
lineNo++;
|
|
583
|
+
} else if (line === "\") continue;
|
|
540
584
|
}
|
|
541
585
|
return files;
|
|
542
586
|
}
|
|
@@ -622,7 +666,7 @@ function canonical(obj) {
|
|
|
622
666
|
}
|
|
623
667
|
|
|
624
668
|
// cli/src/main.ts
|
|
625
|
-
var VERSION = "0.1.
|
|
669
|
+
var VERSION = "0.1.5";
|
|
626
670
|
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
627
671
|
function parseArgs(argv) {
|
|
628
672
|
const a = { _: [], flags: {} };
|
|
@@ -829,42 +873,77 @@ ${bold("Your code")}`);
|
|
|
829
873
|
if (sn) out(` ${s.padEnd(10)} ${sn}`);
|
|
830
874
|
}
|
|
831
875
|
}
|
|
876
|
+
if (a.flags["github-action"]) await setupGithubAction(root, a);
|
|
832
877
|
if (a.flags.mcp) await setupMcp(root, base, a);
|
|
833
878
|
out(`
|
|
834
879
|
${bold("Next")}`);
|
|
835
880
|
if (wsId) out(` dashboard ${base}/dashboard?workspace=${wsId}`);
|
|
836
881
|
out(` usage ${selfCmd(base)} usage`);
|
|
837
882
|
if (wsId) out(` badge turn on public stats in the dashboard, then: [](${base}/dashboard?workspace=${wsId})`);
|
|
883
|
+
if (!a.flags["github-action"]) out(` PR checks ${selfCmd(base)} init --github-action`);
|
|
838
884
|
if (!a.flags.mcp) out(` coding agents: ${selfCmd(base)} init --mcp`);
|
|
839
885
|
}
|
|
840
|
-
var selfCmd = (
|
|
886
|
+
var selfCmd = (_base) => "npx -y @tokenzero/cli";
|
|
841
887
|
var redactKey = (s, key) => key ? s.replace(key, key.slice(0, 12) + "\u2026" + key.slice(-4)) : s;
|
|
842
|
-
|
|
888
|
+
var GH_ACTION_YAML = `name: TokenZero LLM Cost Check
|
|
889
|
+
on: [pull_request]
|
|
890
|
+
permissions:
|
|
891
|
+
contents: read
|
|
892
|
+
pull-requests: write
|
|
893
|
+
jobs:
|
|
894
|
+
cost-audit:
|
|
895
|
+
runs-on: ubuntu-latest
|
|
896
|
+
steps:
|
|
897
|
+
- uses: actions/checkout@v4
|
|
898
|
+
with:
|
|
899
|
+
fetch-depth: 0
|
|
900
|
+
- uses: tokenzero-ai/tokenzero/action@v0
|
|
901
|
+
with:
|
|
902
|
+
github-token: \${{ secrets.GITHUB_TOKEN }}
|
|
903
|
+
`;
|
|
904
|
+
async function setupGithubAction(root, a) {
|
|
843
905
|
out(`
|
|
844
|
-
${bold("
|
|
845
|
-
|
|
846
|
-
const
|
|
847
|
-
const file = path2.join(cursorDir, "mcp.json");
|
|
848
|
-
let cfg = {};
|
|
906
|
+
${bold("GitHub Action")} ${dim("(PR token-waste & model-arbitrage check)")}`);
|
|
907
|
+
const wfDir = path2.join(root, ".github", "workflows");
|
|
908
|
+
const file = path2.join(wfDir, "tokenzero.yml");
|
|
849
909
|
if (fs2.existsSync(file)) {
|
|
850
|
-
|
|
851
|
-
cfg = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
852
|
-
} catch {
|
|
853
|
-
out(yellow(` ! ${file} is not valid JSON; not touching it`));
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (cfg?.mcpServers?.tokenzero?.url === `${base}/mcp`) {
|
|
858
|
-
out(dim(" Cursor: already configured"));
|
|
910
|
+
out(dim(` ${path2.relative(root, file)} already exists`));
|
|
859
911
|
return;
|
|
860
912
|
}
|
|
861
|
-
if (await confirm(` Add
|
|
862
|
-
|
|
863
|
-
fs2.
|
|
864
|
-
fs2.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
913
|
+
if (await confirm(` Add ${path2.relative(root, file)} to audit every PR?`, a)) {
|
|
914
|
+
fs2.mkdirSync(wfDir, { recursive: true });
|
|
915
|
+
fs2.writeFileSync(file, GH_ACTION_YAML);
|
|
865
916
|
out(green(` \u2713 wrote ${path2.relative(root, file)}`));
|
|
866
917
|
}
|
|
867
918
|
}
|
|
919
|
+
async function setupMcp(root, base, a) {
|
|
920
|
+
out(`
|
|
921
|
+
${bold("MCP")} ${dim("(audit_repo_token_waste, optimize_prompt_payload, calculate_model_arbitrage)")}`);
|
|
922
|
+
out(` Claude Code: claude mcp add --transport http tokenzero ${base}/mcp`);
|
|
923
|
+
out(` Local stdio: npx -y @tokenzero/mcp`);
|
|
924
|
+
for (const rel of [path2.join(".cursor", "mcp.json"), ".mcp.json"]) {
|
|
925
|
+
const file = path2.join(root, rel);
|
|
926
|
+
let cfg = {};
|
|
927
|
+
if (fs2.existsSync(file)) {
|
|
928
|
+
try {
|
|
929
|
+
cfg = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
930
|
+
} catch {
|
|
931
|
+
out(yellow(` ! ${file} is not valid JSON; not touching it`));
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (cfg?.mcpServers?.tokenzero?.url === `${base}/mcp`) {
|
|
936
|
+
out(dim(` ${rel}: already configured`));
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (await confirm(` Add TokenZero to ${rel}?`, a)) {
|
|
940
|
+
cfg.mcpServers = { ...cfg.mcpServers ?? {}, tokenzero: { url: `${base}/mcp` } };
|
|
941
|
+
fs2.mkdirSync(path2.dirname(file), { recursive: true });
|
|
942
|
+
fs2.writeFileSync(file, JSON.stringify(cfg, null, 2) + "\n");
|
|
943
|
+
out(green(` \u2713 wrote ${rel}`));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
868
947
|
async function cmdAudit(a) {
|
|
869
948
|
const root = path2.resolve(a._[1] ?? ".");
|
|
870
949
|
let report;
|
|
@@ -980,7 +1059,7 @@ async function cmdVerify(a) {
|
|
|
980
1059
|
function help() {
|
|
981
1060
|
out(`${bold("tokenzero")} v${VERSION} \u2014 cut your LLM bill with a BYOK gateway
|
|
982
1061
|
|
|
983
|
-
${bold("tokenzero init")} [dir] [--url URL] [--name NAME] [--yes] [--no-env] [--mcp]
|
|
1062
|
+
${bold("tokenzero init")} [dir] [--url URL] [--name NAME] [--yes] [--no-env] [--mcp] [--github-action]
|
|
984
1063
|
Audit locally, create a workspace, point OPENAI_BASE_URL / ANTHROPIC_BASE_URL at the gateway.
|
|
985
1064
|
${bold("tokenzero audit")} [dir] [--json | --format markdown] [--diff file|-] [--fail-over PCT]
|
|
986
1065
|
Static token-waste audit. Runs locally; nothing is uploaded. --fail-over makes it a CI gate.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|