@tokenzero/mcp 0.2.8 → 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.
- package/bin/tokenzero-mcp.mjs +205 -10
- package/package.json +1 -1
package/bin/tokenzero-mcp.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import readline from "node:readline";
|
|
|
9
9
|
import crypto from "node:crypto";
|
|
10
10
|
|
|
11
11
|
// src/pricing.ts
|
|
12
|
-
var PRICING_VERSION = "2026-10-
|
|
12
|
+
var PRICING_VERSION = "2026-10-28";
|
|
13
13
|
var P = (provider, tier, input, output, cachedInput, cacheWrite, extra = {}) => ({ provider, tier, input, output, cachedInput, cacheWrite, ...extra });
|
|
14
14
|
var PRICES = {
|
|
15
15
|
// ---- OpenAI (standard tier, ≤272K; cache write = 30m TTL rate) ----
|
|
@@ -127,7 +127,7 @@ function normalizeModel(model) {
|
|
|
127
127
|
if (typeof model !== "string" || !model) return null;
|
|
128
128
|
let m = model.toLowerCase().trim();
|
|
129
129
|
m = m.replace(/^(openai|anthropic|google|models|vertex_ai|x-ai|xai|deepseek|bedrock|openrouter)\//, "");
|
|
130
|
-
m = m.replace(/^(us|eu|apac|global)\./, "").replace(/^(anthropic|amazon|meta|mistral)\./, "");
|
|
130
|
+
m = m.replace(/^(us|eu|apac|global)\./, "").replace(/^(anthropic|amazon|meta|mistral|openai)\./, "");
|
|
131
131
|
m = m.replace(/-v\d+(?::\d+)?$/, "");
|
|
132
132
|
if (MODEL_ALIASES[m]) m = MODEL_ALIASES[m];
|
|
133
133
|
m = m.replace(/@\d{8}$/, "");
|
|
@@ -137,8 +137,15 @@ function normalizeModel(model) {
|
|
|
137
137
|
for (const k of KEYS_BY_LENGTH) if (m.startsWith(k + "-") || m === k) return k;
|
|
138
138
|
return null;
|
|
139
139
|
}
|
|
140
|
+
var isFreeModel = (model) => typeof model === "string" && (/:free$/i.test(model.trim()) || /^openrouter\/free$/i.test(model.trim()));
|
|
141
|
+
var dynamicPrice = null;
|
|
142
|
+
var FREE_PRICE = { provider: "openrouter", tier: "fast", input: 0, output: 0, cachedInput: 0 };
|
|
140
143
|
function priceFor(model) {
|
|
144
|
+
if (isFreeModel(model)) return { key: String(model).trim().toLowerCase(), ...FREE_PRICE };
|
|
141
145
|
const key = normalizeModel(model);
|
|
146
|
+
if (key && PRICES[key]) return { key, ...PRICES[key] };
|
|
147
|
+
const dyn = typeof model === "string" && dynamicPrice ? dynamicPrice(model) : null;
|
|
148
|
+
if (dyn) return { key: String(model).trim().toLowerCase(), ...dyn };
|
|
142
149
|
return key ? { key, ...PRICES[key] } : null;
|
|
143
150
|
}
|
|
144
151
|
function costOf(model, u, factor = 1) {
|
|
@@ -338,7 +345,7 @@ function makeLocator(masked) {
|
|
|
338
345
|
};
|
|
339
346
|
return { text, starts, lineOf };
|
|
340
347
|
}
|
|
341
|
-
var ASSIGN = (name) => new RegExp(
|
|
348
|
+
var ASSIGN = (name) => new RegExp(`^[ \\t]*(?:export[ \\t]+)?(?:(?:const|let|var|final|static|private|public|readonly)[ \\t]+)*(?:self\\.|this\\.)?${name}[ \\t]*(?::[^=\\n]{0,200})?=(?![=>])`);
|
|
342
349
|
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"]);
|
|
343
350
|
function assignmentScope(loc, masked, line) {
|
|
344
351
|
const eq = masked[line].search(/=(?![=>])/);
|
|
@@ -401,6 +408,154 @@ function resolveScopes(loc, masked, from, depth = 2) {
|
|
|
401
408
|
return out;
|
|
402
409
|
}
|
|
403
410
|
var AUDIT_AT = new Date(process.env.TOKENZERO_AUDIT_AT || Date.now());
|
|
411
|
+
var CACHE_DIRECTIVE = /cache_control|cacheControl|cachePoint|cache_point|prompt_cache_key|promptCacheKey|cachedContent|cached_content|supports?_?(?:prompt_?)?caching|supports?(?:Prompt)?Caching|enable_?(?:prompt_?)?cach(?:e|ing)|(?:prompt_?)?cache_?enabled|(?:prompt)?[cC]acheEnabled/;
|
|
412
|
+
var MODEL_GATE = /\b(?:if|elif|when|case)\b|&&|\|\||\?/;
|
|
413
|
+
var MODEL_NAME_TEST = /\b(?:model|model_?id|model_?name|modelId|modelName|deployment)\w*\s*(?:\.(?:startswith|startsWith|endswith|endsWith|includes|lower\(\)\.startswith|toLowerCase\(\)\.(?:startsWith|includes))\s*\(\s*\(?\s*["'`](?:claude|anthropic|gpt|gemini|o\d|deepseek|grok|qwen|minimax)|(?:===?|==)\s*["'`](?:claude|anthropic|gpt|gemini|deepseek|grok)[\w.-]*["'`])|["'`](?:claude|anthropic|gemini|gpt)[\w.-]*["'`]\s+in\s+(?:\w+\.)*(?:model|model_?id|model_?name)\b|\/\^?(?:claude|anthropic|gpt|gemini)[^/\n]*\/[a-z]*\.test\(\s*(?:\w+\.)*(?:model|modelId|modelName)/i;
|
|
414
|
+
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\()/;
|
|
415
|
+
var INTERPOLATES = /\bf["']|\$\{|\.format\(|%\s*\(|["']\s*\+|\+\s*["']|`/;
|
|
416
|
+
var SYSTEM_CTX = /\bsystem(?:_?prompt|Prompt|_?message|Message|_?instructions?)?\b\s*[:=(]|["']role["']\s*:\s*["']system["']|\brole\s*:\s*["']system["']|\binstructions\s*[:=]/i;
|
|
417
|
+
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})["'`]/;
|
|
418
|
+
var KEY_PLACEHOLDER = /x{6,}|X{6,}|0{8,}|1234567|abcdef|your|example|placeholder|dummy|fake|test|redacted|\.\.\./i;
|
|
419
|
+
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";
|
|
420
|
+
var PREFIX_DYNAMIC = /\bf(?:"""|'''|"|')\s{0,4}\{[A-Za-z_][\w.\[\]'"]{0,60}\}|`\s{0,4}\$\{[^}\n]{1,80}\}/;
|
|
421
|
+
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)/;
|
|
422
|
+
var SET_TOOLS = /\btools?\s*=\s*\[[^\]\n]*\bfor\s+\w+\s+in\s+(?:set\(|\w*_?set\b|\w+\.keys\(\)\s*\|)/;
|
|
423
|
+
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/;
|
|
424
|
+
var ANTHROPIC_SDK = /\bfrom\s+anthropic\s+import|\bimport\s+anthropic\b|@anthropic-ai\/sdk|\bAnthropic\(|messages\.create\(/;
|
|
425
|
+
var NON_CLAUDE_MODEL = /["'`](?:openai\/|google\/|deepseek\/|x-ai\/|meta-llama\/|mistralai\/|qwen\/)?(?:gpt-[\w.-]+|o[1-9][\w.-]*|gemini-[\w.-]+|deepseek-[\w.-]+|grok-[\w.-]+|llama[\w.-]*|mistral[\w.-]*|qwen[\w.-]*)["'`]/i;
|
|
426
|
+
var USER_ROLE = /["']?role["']?\s*:\s*["']user["']/;
|
|
427
|
+
var USAGE_FIELD = /\b(?:input_tokens|prompt_tokens|promptTokenCount|inputTokens|promptTokens|prompt_token_count)\b/;
|
|
428
|
+
var CACHE_USAGE_FIELD = /cache_read_input_tokens|cache_creation_input_tokens|cacheReadInputTokens|cacheCreationInputTokens|cached_tokens|cachedTokens|cachedContentTokenCount|cached_content_token_count|prompt_tokens_details|promptTokensDetails|cache_read|cacheRead|cache_write|cacheWrite|cacheReadTokens|cacheWriteTokens/;
|
|
429
|
+
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;
|
|
430
|
+
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|&&))/;
|
|
431
|
+
var isComment = (raw) => /^\s*(?:#|\/\/|\*|\/\*)/.test(raw);
|
|
432
|
+
function secretLint(path3, lines) {
|
|
433
|
+
const out = [];
|
|
434
|
+
for (let i = 0; i < lines.length && out.length < 5; i++) {
|
|
435
|
+
const raw = lines[i];
|
|
436
|
+
if (raw.length > 2e4) continue;
|
|
437
|
+
const km = HARDCODED_KEY.exec(raw);
|
|
438
|
+
if (!km || KEY_PLACEHOLDER.test(km[1])) continue;
|
|
439
|
+
const who = keyProvider(km[1]);
|
|
440
|
+
out.push({
|
|
441
|
+
file: path3,
|
|
442
|
+
severity: "high",
|
|
443
|
+
line: i + 1,
|
|
444
|
+
title: `Hard-coded ${who} API key in source`,
|
|
445
|
+
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.`,
|
|
446
|
+
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.",
|
|
447
|
+
est_savings_pct: 0
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
function cacheLint(path3, lines, masked) {
|
|
453
|
+
const out = [];
|
|
454
|
+
const seen = /* @__PURE__ */ new Set();
|
|
455
|
+
const add = (k, fd) => {
|
|
456
|
+
if (!seen.has(k)) {
|
|
457
|
+
seen.add(k);
|
|
458
|
+
out.push({ file: path3, ...fd });
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
const near = (i, re, before, after) => lines.slice(Math.max(0, i - before), Math.min(lines.length, i + after + 1)).some((l) => !isComment(l) && re.test(l));
|
|
462
|
+
const src = lines.join("\n");
|
|
463
|
+
const hasDirective = CACHE_DIRECTIVE.test(src);
|
|
464
|
+
const nonClaudeViaCompat = OPENAI_COMPAT.test(src) && !ANTHROPIC_SDK.test(src) && NON_CLAUDE_MODEL.test(src);
|
|
465
|
+
const ownerOf = (i) => {
|
|
466
|
+
for (let k = i; k >= Math.max(0, i - 6); k--) {
|
|
467
|
+
const l = lines[k], u = l.search(USER_ROLE), p = l.search(/["']role["']\s*:\s*["'](?:system|developer)["']|\brole\s*:\s*["'](?:system|developer)["']|\bsystem\s*[=:]|\btools?\s*[=:]|["'](?:system|tools)["']\s*:/);
|
|
468
|
+
if (u >= 0 || p >= 0) return u >= 0 && (p < 0 || (k === i ? u < l.search(/cache_control|cacheControl/) && u > p : u > p)) ? "user" : "prefix";
|
|
469
|
+
}
|
|
470
|
+
return null;
|
|
471
|
+
};
|
|
472
|
+
const markers = lines.map((l, i) => !isComment(l) && /cache_control|cacheControl|cachePoint/.test(l) ? i : -1).filter((i) => i >= 0);
|
|
473
|
+
const systemMarked = markers.some((i) => ownerOf(i) === "prefix");
|
|
474
|
+
const USER_MARK = /["']?role["']?\s*:\s*["']user["']|\bcontents\s*[:=]|\bmessages\s*[:=]|\binput\s*[:=]/;
|
|
475
|
+
const inSystemValue = (i) => {
|
|
476
|
+
const raw = lines[i], v = raw.search(VOLATILE_CALL), sp = raw.search(SYSTEM_CTX), up = raw.search(USER_MARK);
|
|
477
|
+
if (sp >= 0 && sp < v) return !(up > sp && up < v);
|
|
478
|
+
if (up >= 0 && up < v) return false;
|
|
479
|
+
for (let k = i - 1; k >= Math.max(0, i - 4); k--) {
|
|
480
|
+
if (isComment(lines[k])) continue;
|
|
481
|
+
const ks = lines[k].search(SYSTEM_CTX), ku = lines[k].search(USER_MARK);
|
|
482
|
+
if (ks >= 0) return !(ku > ks);
|
|
483
|
+
if (ku >= 0) return false;
|
|
484
|
+
}
|
|
485
|
+
return false;
|
|
486
|
+
};
|
|
487
|
+
lines.forEach((raw, i) => {
|
|
488
|
+
if (isComment(raw)) return;
|
|
489
|
+
const code = masked[i] ?? "";
|
|
490
|
+
if (hasDirective && MODEL_GATE.test(code + raw) && MODEL_NAME_TEST.test(raw) && near(i, CACHE_DIRECTIVE, 3, 10)) add("gating", {
|
|
491
|
+
severity: "medium",
|
|
492
|
+
line: i + 1,
|
|
493
|
+
title: "Prompt caching gated on a hardcoded model-name check",
|
|
494
|
+
detail: 'Caching is turned on only when the model id matches a fixed name or prefix. When a provider renames a model, adds a "-latest" alias or you switch to a new cacheable model, the check stops matching and every call quietly pays the full input rate. This is the most common prompt-caching defect in agent code (31% of 120 studied defects, arXiv:2610.34817).',
|
|
495
|
+
fix: 'Decide caching from the provider (or a capability table that defaults to "on"), not from the model string; or route through TokenZero, which applies the right cache markers for every priced model.',
|
|
496
|
+
est_savings_pct: 10
|
|
497
|
+
});
|
|
498
|
+
if (VOLATILE_CALL.test(raw) && INTERPOLATES.test(raw) && inSystemValue(i) && !RANDOM_ID.test(raw)) add("volatile", {
|
|
499
|
+
severity: "medium",
|
|
500
|
+
line: i + 1,
|
|
501
|
+
title: "Volatile value (time, date or random id) interpolated into the system prompt",
|
|
502
|
+
detail: "A value that changes on every request is written into the system prompt, which sits at the start of the cached prefix. Provider caches match byte-for-byte from the start, so no request ever reuses the previous one and the cache discount is lost on the whole prompt (11% of studied defects, arXiv:2610.34817).",
|
|
503
|
+
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).",
|
|
504
|
+
est_savings_pct: 15
|
|
505
|
+
});
|
|
506
|
+
const pm = PREFIX_DYNAMIC.exec(raw);
|
|
507
|
+
if (pm && SYSTEM_CTX.test(raw.slice(0, pm.index + 1)) && raw.length - pm.index >= 160) add("prefix-order", {
|
|
508
|
+
severity: "medium",
|
|
509
|
+
line: i + 1,
|
|
510
|
+
title: "Per-request value at the start of the system prompt (everything after it misses the cache)",
|
|
511
|
+
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.",
|
|
512
|
+
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.",
|
|
513
|
+
est_savings_pct: 20
|
|
514
|
+
});
|
|
515
|
+
if (RANDOM_ID.test(raw) || SET_TOOLS.test(raw)) add("serialization", {
|
|
516
|
+
severity: "low",
|
|
517
|
+
line: i + 1,
|
|
518
|
+
title: RANDOM_ID.test(raw) ? "Random ids generated for messages or tool calls in the prompt" : "Tool list built from an unordered set",
|
|
519
|
+
detail: RANDOM_ID.test(raw) ? "An id minted with a random generator is written into the conversation. If it is regenerated when the history is rebuilt, the same turn serializes to different bytes and the cached prefix stops matching (nondeterministic serialization, 8% of studied defects, arXiv:2610.34817)." : "Iterating a set gives no stable order, so the same tools can serialize in a different order across processes and break the cached prefix (nondeterministic serialization, 8% of studied defects, arXiv:2610.34817).",
|
|
520
|
+
fix: RANDOM_ID.test(raw) ? "Mint ids once, store them with the message, and reuse the stored value; never regenerate them per request." : "Sort the tools (for example by name) before sending them.",
|
|
521
|
+
est_savings_pct: 5
|
|
522
|
+
});
|
|
523
|
+
if (nonClaudeViaCompat && /cache_control|cacheControl/.test(raw)) add("marker", {
|
|
524
|
+
severity: "medium",
|
|
525
|
+
line: i + 1,
|
|
526
|
+
title: "Anthropic cache_control marker sent through an OpenAI-compatible client to non-Claude models",
|
|
527
|
+
detail: "This file uses an OpenAI-compatible client and names non-Claude models, but attaches Anthropic-style `cache_control` markers. Providers that do not accept the field reject the request or ignore it, so calls fail or run uncached (incompatible marker, 8% of studied defects, arXiv:2610.34817).",
|
|
528
|
+
fix: "Attach `cache_control` only for Claude models; OpenAI, Gemini and DeepSeek cache stable prefixes automatically (use `prompt_cache_key` on OpenAI). TokenZero adds the right marker per provider.",
|
|
529
|
+
est_savings_pct: 0
|
|
530
|
+
});
|
|
531
|
+
if (!systemMarked && /cache_control|cacheControl/.test(raw) && ownerOf(i) === "user" && lines.slice(Math.max(0, i - 4), i + 2).some((l) => INTERPOLATES.test(l) || /\b(?:user_input|query|question|request|input)\b/.test(l))) add("misplaced", {
|
|
532
|
+
severity: "low",
|
|
533
|
+
line: i + 1,
|
|
534
|
+
title: "Cache breakpoint on per-request user content, none on the system prompt or tools",
|
|
535
|
+
detail: "The only `cache_control` marker is on a user message whose text changes per request, while the static system prompt and tools are unmarked. Each call writes a new cache entry (billed at the cache-write surcharge) that the next call cannot read (misplaced marker, 3% of studied defects, arXiv:2610.34817). In multi-turn chats a breakpoint on the latest turn is fine as long as the system prompt or tools also carry one.",
|
|
536
|
+
fix: "Add a breakpoint after the last tool or the system prompt; keep the per-turn one only for long conversations.",
|
|
537
|
+
est_savings_pct: 5
|
|
538
|
+
});
|
|
539
|
+
if (ZEROED_CACHE_USAGE.test(code)) add("usage", {
|
|
540
|
+
severity: "medium",
|
|
541
|
+
line: i + 1,
|
|
542
|
+
title: "Cached-token usage hardcoded to zero",
|
|
543
|
+
detail: "Cache-read or cache-write token counts are set to 0 instead of read from the response, so cost reports and budgets misstate spend (usage miscount, 19% of studied defects, arXiv:2610.34817).",
|
|
544
|
+
fix: "Read `cache_read_input_tokens` / `cache_creation_input_tokens` (Anthropic), `prompt_tokens_details.cached_tokens` (OpenAI) or `cachedContentTokenCount` (Gemini) from the usage object.",
|
|
545
|
+
est_savings_pct: 0
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
const costLine = lines.findIndex((l) => !isComment(l) && COST_MATH.test(l));
|
|
549
|
+
if (costLine >= 0 && !CACHE_USAGE_FIELD.test(src)) add("usage", {
|
|
550
|
+
severity: "medium",
|
|
551
|
+
line: costLine + 1,
|
|
552
|
+
title: "Cost math from token usage ignores cached tokens",
|
|
553
|
+
detail: "Spend is computed from input/prompt token counts with no reference to cached tokens anywhere in this file. Anthropic reports cache reads and writes separately from `input_tokens` (so this undercounts writes and misses the read discount); OpenAI includes cached tokens in `prompt_tokens` (so this overcounts). Either way reported cost is wrong (usage miscount, 19% of studied defects, arXiv:2610.34817).",
|
|
554
|
+
fix: "Price cache reads, cache writes and uncached input separately from the provider usage fields, or read the per-request cost from TokenZero receipts.",
|
|
555
|
+
est_savings_pct: 0
|
|
556
|
+
});
|
|
557
|
+
return out;
|
|
558
|
+
}
|
|
404
559
|
function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
405
560
|
const findings = [];
|
|
406
561
|
const callSites = [];
|
|
@@ -563,6 +718,10 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
563
718
|
findings.push(...local);
|
|
564
719
|
siteFindings.push(local);
|
|
565
720
|
});
|
|
721
|
+
findings.push(...secretLint(f.path, lines));
|
|
722
|
+
if (callLines.length || hasCacheControl || USAGE_FIELD.test(src)) {
|
|
723
|
+
for (const fd of cacheLint(f.path, lines, masked)) findings.push({ ...fd, severity: kind === "library" || illustrative ? DOWN[fd.severity] : fd.severity });
|
|
724
|
+
}
|
|
566
725
|
}
|
|
567
726
|
const libTokens = promptLibrary.reduce((a, p) => a + p.tokens, 0);
|
|
568
727
|
if (callSites.length && !repoCaches && libTokens >= 1024 && !findings.some((f) => /static prompt tokens/.test(f.title))) {
|
|
@@ -671,10 +830,11 @@ async function fetchRepoFiles(repo, opts = {}) {
|
|
|
671
830
|
}
|
|
672
831
|
|
|
673
832
|
// src/arbitrage.ts
|
|
833
|
+
var ARBITRAGE_DEFAULTS = { input_tokens: 2e3, output_tokens: 400 };
|
|
674
834
|
function modelArbitrage(args) {
|
|
675
835
|
const key = normalizeModel(args.model);
|
|
676
836
|
if (!key) throw new Error(`unknown model ${args.model}; known: ${Object.keys(PRICES).join(", ")}`);
|
|
677
|
-
const n = args.requests_per_month ?? 1e4, inp = args.input_tokens, out = args.output_tokens, st = Math.min(inp, args.static_tokens ?? 0);
|
|
837
|
+
const n = args.requests_per_month ?? 1e4, inp = args.input_tokens ?? ARBITRAGE_DEFAULTS.input_tokens, out = args.output_tokens ?? ARBITRAGE_DEFAULTS.output_tokens, st = Math.min(inp, args.static_tokens ?? 0);
|
|
678
838
|
const rowFor = (m) => {
|
|
679
839
|
const p = priceFor(m);
|
|
680
840
|
const base = costOf(m, { input: inp, output: out });
|
|
@@ -685,6 +845,7 @@ function modelArbitrage(args) {
|
|
|
685
845
|
const rows = [...new Set(models)].map(rowFor);
|
|
686
846
|
return {
|
|
687
847
|
pricing_version: PRICING_VERSION,
|
|
848
|
+
workload: { input_tokens: inp, output_tokens: out, requests_per_month: n, ...args.input_tokens == null || args.output_tokens == null ? { defaults_used: true } : {} },
|
|
688
849
|
requested: rows[0],
|
|
689
850
|
alternatives: rows.slice(1).map((r) => ({ ...r, savings_pct_vs_requested: Math.round((1 - r.per_request_usd / rows[0].per_request_usd) * 1e3) / 10 })),
|
|
690
851
|
caveat: "List prices. Cheaper models need an eval before swapping; TokenZero cascade routing is opt-in and advisory by default; an opt-in shadow parity monitor is coming."
|
|
@@ -760,27 +921,52 @@ function manifests(root) {
|
|
|
760
921
|
}
|
|
761
922
|
|
|
762
923
|
// mcp/src/main.ts
|
|
763
|
-
var VERSION = "0.2.
|
|
924
|
+
var VERSION = "0.2.10";
|
|
764
925
|
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
765
926
|
var SUPPORTED = [...MCP_LEGACY_VERSIONS];
|
|
766
927
|
var SERVER = { name: "tokenzero", version: VERSION };
|
|
767
|
-
var CAPABILITIES = { tools: { listChanged: false } };
|
|
768
|
-
var
|
|
928
|
+
var CAPABILITIES = { tools: { listChanged: false }, prompts: { listChanged: false } };
|
|
929
|
+
var PROMPTS = [
|
|
930
|
+
{ name: "cut-my-llm-bill", title: "Cut my LLM bill", description: "Find token waste in this project (scanned locally, nothing uploaded) and show the top fixes with estimated savings. No API key needed.", arguments: [{ name: "repo", description: "owner/name of a public GitHub repo instead of the local project", required: false }] },
|
|
931
|
+
{ name: "price-my-workload", title: "Price my LLM workload", description: "Price a workload across models at list prices, with the cheaper sibling and the effect of prompt caching. No API key needed.", arguments: [{ name: "model", description: "model id, e.g. gpt-6.1-sol or claude-sonnet-5-5", required: true }, { name: "requests_per_month", description: "monthly request volume (default 10000)", required: false }] },
|
|
932
|
+
{ name: "try-tokenzero", title: "Try TokenZero on one call", description: "Run one priced call through TokenZero with no key or setup and show the saving and its signed receipt.", arguments: [] }
|
|
933
|
+
];
|
|
934
|
+
function promptMessages(name, args = {}) {
|
|
935
|
+
const p = PROMPTS.find((x) => x.name === name);
|
|
936
|
+
if (!p) throw new Error(`unknown prompt ${name}; available: ${PROMPTS.map((x) => x.name).join(", ")}`);
|
|
937
|
+
const say = (text) => ({ description: p.description, messages: [{ role: "user", content: { type: "text", text } }] });
|
|
938
|
+
const repo = typeof args?.repo === "string" && /^[\w.-]+\/[\w.-]+$/.test(args.repo.trim()) ? args.repo.trim() : "";
|
|
939
|
+
if (name === "cut-my-llm-bill") return say(`Use the TokenZero MCP tools to find where this project wastes LLM tokens. Call audit_repo_token_waste with ${repo ? `repo "${repo}"` : 'path "." (it scans this machine; nothing is uploaded)'}. Then list the top 3 findings with file:line, the estimated saving and the fix, and finish with the one-line change that applies the fixes automatically (base_url ${serviceUrl() || "https://api.tokenzero.sh"}/v1, my provider key stays mine). If nothing calls an LLM here, call try_tokenzero instead and show what it measured.`);
|
|
940
|
+
if (name === "price-my-workload") {
|
|
941
|
+
const model = typeof args?.model === "string" && args.model.trim() ? args.model.trim().slice(0, 80) : "gpt-6.1-sol";
|
|
942
|
+
const n = Number(args?.requests_per_month) > 0 ? Math.min(1e9, Math.round(Number(args.requests_per_month))) : 1e4;
|
|
943
|
+
return say(`Call the TokenZero tool calculate_model_arbitrage with {"model": "${model}", "requests_per_month": ${n}}. Show the monthly cost on ${model}, on its cheaper sibling, and with prompt caching, as a short table, then one sentence on which change saves the most.`);
|
|
944
|
+
}
|
|
945
|
+
return say("Call the TokenZero tool try_tokenzero with no arguments. Show what happened, the saving it measured, and the signed receipt id, then the keyless curl it returns so I can try a real call.");
|
|
946
|
+
}
|
|
947
|
+
var INSTRUCTIONS = "Start with try_tokenzero (no key, no setup). Use provision_workspace to create a TokenZero workspace and key with no form; audit_repo_token_waste to find LLM cost waste in the current project (local, nothing uploaded), calculate_model_arbitrage to price model alternatives, optimize_prompt_payload to make a request body cache-friendly.";
|
|
769
948
|
var serviceUrl = () => (process.env.TOKENZERO_URL || (BAKED_URL.startsWith("http") ? BAKED_URL : "")).replace(/\/+$/, "");
|
|
770
949
|
var TOOLS = [
|
|
950
|
+
{
|
|
951
|
+
name: "try_tokenzero",
|
|
952
|
+
title: "Try TokenZero (no key needed)",
|
|
953
|
+
annotations: { title: "Try TokenZero (no key needed)", readOnlyHint: true, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
954
|
+
description: 'Call this first: no API key, no signup, no setup. Shows what TokenZero would do to one of your LLM calls: it runs the four savings engines (prompt-cache formatting, edge cache, cascade advice, signed receipt) on a sample or your own prompt and model, and returns the list-price saving, the signed receipt and a copy-paste keyless curl that makes a real call on a free model. Pass {} for the default, or {"model": "gpt-6.1-sol", "system": "...", "prompt": "..."}. Runs in the TokenZero service; send no secrets in the prompt.',
|
|
955
|
+
inputSchema: { type: "object", properties: { model: { type: "string", description: "a priced model id; default gpt-6.1-sol" }, system: { type: "string", description: "your system prompt (optional)" }, prompt: { type: "string", description: "your user message (optional)" } }, additionalProperties: false }
|
|
956
|
+
},
|
|
771
957
|
{
|
|
772
958
|
name: "audit_repo_token_waste",
|
|
773
959
|
title: "Audit a repo or directory for LLM token waste",
|
|
774
960
|
annotations: { title: "Audit a repo or directory for LLM token waste", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
775
|
-
description: "Static audit for LLM token waste: uncached static prompts and prompt templates, frontier models on extraction/classification prompts, calls in loops, unpriced models. Give `path` for a local directory (scanned on this machine, nothing uploaded; defaults to the working directory), `repo` (owner/name) for a public GitHub repo, or `diff` for a unified diff. Results are static heuristics, not measurements.",
|
|
961
|
+
description: "No API key needed. Static audit for LLM token waste: uncached static prompts and prompt templates, frontier models on extraction/classification prompts, calls in loops, unpriced models, and prompt-caching mis-integration (caching gated on model names, volatile values in the system prompt, random ids in history, wrong-provider cache markers, misplaced breakpoints, cost math that ignores cached tokens). Give `path` for a local directory (scanned on this machine, nothing uploaded; defaults to the working directory), `repo` (owner/name) for a public GitHub repo, or `diff` for a unified diff. Results are static heuristics, not measurements.",
|
|
776
962
|
inputSchema: { type: "object", properties: { path: { type: "string", description: "local directory; defaults to the current working directory" }, repo: { type: "string", description: "public GitHub owner/name" }, diff: { type: "string", description: "unified diff text, e.g. from git diff" } }, additionalProperties: false }
|
|
777
963
|
},
|
|
778
964
|
{
|
|
779
965
|
name: "calculate_model_arbitrage",
|
|
780
966
|
title: "Price a workload across models",
|
|
781
967
|
annotations: { title: "Price a workload across models", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
782
|
-
description: `Price a workload at list prices (table ${PRICING_VERSION}) for the requested model, its cheaper sibling and any extra models, with and without prompt caching. Runs locally.`,
|
|
783
|
-
inputSchema: { type: "object", properties: { model: { type: "string" }, input_tokens: { type: "number" }, output_tokens: { type: "number" }, requests_per_month: { type: "number" }, static_tokens: { type: "number", description: "portion of input_tokens that is a fixed prefix (system + tools)" }, compare_to: { type: "array", items: { type: "string" } } }, required: ["model"
|
|
968
|
+
description: `No API key needed; only \`model\` is required (defaults 2,000 tokens in and 400 out). Price a workload at list prices (table ${PRICING_VERSION}) for the requested model, its cheaper sibling and any extra models, with and without prompt caching. Runs locally.`,
|
|
969
|
+
inputSchema: { type: "object", properties: { model: { type: "string", description: "model id, e.g. gpt-6.1-sol or claude-sonnet-5-5" }, input_tokens: { type: "number", description: "per request; default 2000" }, output_tokens: { type: "number", description: "per request; default 400" }, requests_per_month: { type: "number" }, static_tokens: { type: "number", description: "portion of input_tokens that is a fixed prefix (system + tools)" }, compare_to: { type: "array", items: { type: "string" } } }, required: ["model"], additionalProperties: false }
|
|
784
970
|
},
|
|
785
971
|
{
|
|
786
972
|
name: "optimize_prompt_payload",
|
|
@@ -818,6 +1004,7 @@ async function callTool(name, args) {
|
|
|
818
1004
|
}
|
|
819
1005
|
case "calculate_model_arbitrage":
|
|
820
1006
|
return modelArbitrage(args);
|
|
1007
|
+
case "try_tokenzero":
|
|
821
1008
|
case "optimize_prompt_payload": {
|
|
822
1009
|
const base = serviceUrl();
|
|
823
1010
|
if (!base) throw new Error("set TOKENZERO_URL to use this tool (the cache formatter runs in the TokenZero service)");
|
|
@@ -899,6 +1086,10 @@ async function handle(m) {
|
|
|
899
1086
|
return reply(discoverResult(SERVER, CAPABILITIES, INSTRUCTIONS));
|
|
900
1087
|
case "tools/list":
|
|
901
1088
|
return reply(modernResult({ tools: TOOLS }, SERVER, true));
|
|
1089
|
+
case "prompts/list":
|
|
1090
|
+
return reply(modernResult({ prompts: PROMPTS }, SERVER, true));
|
|
1091
|
+
case "prompts/get":
|
|
1092
|
+
return reply(modernResult(promptMessages(String(m.params?.name ?? ""), m.params?.arguments), SERVER));
|
|
902
1093
|
case "tools/call":
|
|
903
1094
|
return reply(modernResult(await call(), SERVER));
|
|
904
1095
|
default:
|
|
@@ -918,6 +1109,10 @@ async function handle(m) {
|
|
|
918
1109
|
return reply(discoverResult(SERVER, CAPABILITIES, INSTRUCTIONS));
|
|
919
1110
|
case "tools/list":
|
|
920
1111
|
return reply({ tools: TOOLS });
|
|
1112
|
+
case "prompts/list":
|
|
1113
|
+
return reply({ prompts: PROMPTS });
|
|
1114
|
+
case "prompts/get":
|
|
1115
|
+
return reply(promptMessages(String(m.params?.name ?? ""), m.params?.arguments));
|
|
921
1116
|
case "tools/call":
|
|
922
1117
|
return reply(await call());
|
|
923
1118
|
default:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenzero/mcp",
|
|
3
3
|
"mcpName": "sh.tokenzero/mcp",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.10",
|
|
5
5
|
"description": "TokenZero MCP server (stdio): audit LLM token waste in your project locally, price model alternatives, and make prompts cache-friendly from Claude Code, Cursor, Windsurf or Cline.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "Apache-2.0",
|