@tokenzero/mcp 0.2.7 → 0.2.9
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/README.md +2 -0
- package/bin/tokenzero-mcp.mjs +136 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,6 +15,8 @@ claude mcp add tokenzero -- npx -y @tokenzero/mcp # Claude Code
|
|
|
15
15
|
| `audit_repo_token_waste` | locally | Static audit of a local directory (default: the working directory), a public GitHub repo, or a diff. Nothing from a local directory is uploaded. |
|
|
16
16
|
| `calculate_model_arbitrage` | locally | Prices a workload across models at list prices, with and without prompt caching. |
|
|
17
17
|
| `optimize_prompt_payload` | TokenZero service | Rewrites a request body so its static prefix is cacheable. Sends the body you pass to the TokenZero service (`https://tokenzero.sh` unless `TOKENZERO_URL` is set); do not include secrets. |
|
|
18
|
+
| `workspace_metrics` | TokenZero service | Reads a workspace's savings scorecard: hard savings and net ROI per window, each engine's contribution and one-click recommendations. Uses `TOKENZERO_KEY` unless a key is passed; `demo: true` returns a labelled sample. |
|
|
19
|
+
| `provision_workspace` | TokenZero service | Creates a free workspace and key with no form. The key is returned once. |
|
|
18
20
|
|
|
19
21
|
Optional environment: `TOKENZERO_URL` (another gateway for `optimize_prompt_payload`; default `https://tokenzero.sh`), `GITHUB_TOKEN` (raises GitHub rate limits for repo audits).
|
|
20
22
|
|
package/bin/tokenzero-mcp.mjs
CHANGED
|
@@ -9,17 +9,21 @@ 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) ----
|
|
16
16
|
"gpt-6-astra": P("openai", "frontier", 10, 50, 1, 12.5, { ultrafast: true, longCtxAt: 272e3, longCtx: { input: 20, output: 75, cachedInput: 2, cacheWrite: 25 }, minCacheTokens: 1024 }),
|
|
17
17
|
"gpt-6.1-sol": P("openai", "mid", 2, 10, 0.1, 2.5, { ultrafast: true, longCtxAt: 272e3, longCtx: { input: 4, output: 15, cachedInput: 0.2, cacheWrite: 5 }, minCacheTokens: 1024 }),
|
|
18
|
+
"gpt-6.1-terra": P("openai", "mid", 1, 5, 0.05, 1.25, { longCtxAt: 272e3, longCtx: { input: 2, output: 7.5, cachedInput: 0.1, cacheWrite: 2.5 }, minCacheTokens: 1024 }),
|
|
18
19
|
"gpt-6-sol": P("openai", "mid", 2, 10, 0.2, 2.5, { longCtxAt: 272e3, longCtx: { input: 4, output: 15, cachedInput: 0.4, cacheWrite: 5 }, minCacheTokens: 1024 }),
|
|
19
20
|
"gpt-6-luna": P("openai", "fast", 0.1, 0.5, 0.01, 0.125, { longCtxAt: 272e3, longCtx: { input: 0.2, output: 0.75, cachedInput: 0.02, cacheWrite: 0.25 }, minCacheTokens: 1024 }),
|
|
20
21
|
"gpt-5.6-sol": P("openai", "frontier", 4, 20, 0.4, 5, { longCtxAt: 272e3, longCtx: { input: 8, output: 30, cachedInput: 0.8, cacheWrite: 10 }, minCacheTokens: 1024 }),
|
|
21
22
|
"gpt-5.6-terra": P("openai", "mid", 2, 12, 0.2, 2.5, { longCtxAt: 272e3, longCtx: { input: 4, output: 18, cachedInput: 0.4, cacheWrite: 5 }, minCacheTokens: 1024 }),
|
|
22
23
|
"gpt-5.6-luna": P("openai", "fast", 0.2, 1.2, 0.02, 0.25, { longCtxAt: 272e3, longCtx: { input: 0.4, output: 1.8, cachedInput: 0.04, cacheWrite: 0.5 }, minCacheTokens: 1024 }),
|
|
24
|
+
// Daybreak cyber model: access needs OpenAI approval; 400K window
|
|
25
|
+
"gpt-5.6-cyber": P("openai", "frontier", 12.5, 75, 1.25, 15.625, { longCtxAt: 272e3, longCtx: { input: 25, output: 112.5, cachedInput: 2.5, cacheWrite: 31.25 }, minCacheTokens: 1024 }),
|
|
26
|
+
"gpt-5.3-codex": P("openai", "mid", 1.75, 14, 0.175, void 0, { minCacheTokens: 1024 }),
|
|
23
27
|
"gpt-5.5": P("openai", "frontier", 5, 30, 0.5, void 0, { fastFactor: 2.5, longCtxAt: 272e3, longCtx: { input: 10, output: 45, cachedInput: 1 }, minCacheTokens: 1024 }),
|
|
24
28
|
"gpt-5.5-pro": P("openai", "frontier", 30, 180),
|
|
25
29
|
"gpt-5.4": P("openai", "frontier", 2.5, 15, 0.25, void 0, { longCtxAt: 272e3, longCtx: { input: 5, output: 22.5, cachedInput: 0.5 }, minCacheTokens: 1024 }),
|
|
@@ -84,7 +88,8 @@ var PRICES = {
|
|
|
84
88
|
var MODEL_ALIASES = { "deepseek-v4-flash": "deepseek-flash", "deepseek-v4-flash-vision-exp": "deepseek-flash" };
|
|
85
89
|
var CASCADE_TARGET = {
|
|
86
90
|
"gpt-6-astra": "gpt-6.1-sol",
|
|
87
|
-
"gpt-6.1-sol": "gpt-6-
|
|
91
|
+
"gpt-6.1-sol": "gpt-6.1-terra",
|
|
92
|
+
"gpt-6.1-terra": "gpt-6-luna",
|
|
88
93
|
"gpt-6-sol": "gpt-6-luna",
|
|
89
94
|
"gpt-5.6-sol": "gpt-5.6-terra",
|
|
90
95
|
"gpt-5.6-terra": "gpt-5.6-luna",
|
|
@@ -122,7 +127,7 @@ function normalizeModel(model) {
|
|
|
122
127
|
if (typeof model !== "string" || !model) return null;
|
|
123
128
|
let m = model.toLowerCase().trim();
|
|
124
129
|
m = m.replace(/^(openai|anthropic|google|models|vertex_ai|x-ai|xai|deepseek|bedrock|openrouter)\//, "");
|
|
125
|
-
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)\./, "");
|
|
126
131
|
m = m.replace(/-v\d+(?::\d+)?$/, "");
|
|
127
132
|
if (MODEL_ALIASES[m]) m = MODEL_ALIASES[m];
|
|
128
133
|
m = m.replace(/@\d{8}$/, "");
|
|
@@ -157,6 +162,7 @@ var MODEL_SUNSET = {
|
|
|
157
162
|
"gpt-5-nano": { retires: "2026-12-11", replacement: "gpt-5.6-luna", announced: "2026-06-11" },
|
|
158
163
|
"gpt-5-pro": { retires: "2026-12-11", replacement: "gpt-5.6-sol", announced: "2026-06-11" },
|
|
159
164
|
"gpt-5.1": { retires: "2027-04-01", replacement: "gpt-6-sol", announced: "2026-10-01" },
|
|
165
|
+
"gpt-5.3-codex": { retires: "2027-04-01", replacement: "gpt-6-sol", announced: "2026-10-01" },
|
|
160
166
|
"gpt-5.4-nano": { retires: "2027-04-01", replacement: "gpt-6-luna", announced: "2026-10-01" },
|
|
161
167
|
"o1": { retires: "2026-10-23", replacement: "gpt-5.6-sol", announced: "2026-04-22" },
|
|
162
168
|
"o3": { retires: "2026-12-11", replacement: "gpt-5.6-sol", announced: "2026-06-11" },
|
|
@@ -395,6 +401,121 @@ function resolveScopes(loc, masked, from, depth = 2) {
|
|
|
395
401
|
return out;
|
|
396
402
|
}
|
|
397
403
|
var AUDIT_AT = new Date(process.env.TOKENZERO_AUDIT_AT || Date.now());
|
|
404
|
+
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/;
|
|
405
|
+
var MODEL_GATE = /\b(?:if|elif|when|case)\b|&&|\|\||\?/;
|
|
406
|
+
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;
|
|
407
|
+
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\()/;
|
|
408
|
+
var INTERPOLATES = /\bf["']|\$\{|\.format\(|%\s*\(|["']\s*\+|\+\s*["']|`/;
|
|
409
|
+
var SYSTEM_CTX = /\bsystem(?:_?prompt|Prompt|_?message|Message|_?instructions?)?\b\s*[:=(]|["']role["']\s*:\s*["']system["']|\brole\s*:\s*["']system["']|\binstructions\s*[:=]/i;
|
|
410
|
+
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)/;
|
|
411
|
+
var SET_TOOLS = /\btools?\s*=\s*\[[^\]\n]*\bfor\s+\w+\s+in\s+(?:set\(|\w*_?set\b|\w+\.keys\(\)\s*\|)/;
|
|
412
|
+
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/;
|
|
413
|
+
var ANTHROPIC_SDK = /\bfrom\s+anthropic\s+import|\bimport\s+anthropic\b|@anthropic-ai\/sdk|\bAnthropic\(|messages\.create\(/;
|
|
414
|
+
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;
|
|
415
|
+
var USER_ROLE = /["']?role["']?\s*:\s*["']user["']/;
|
|
416
|
+
var USAGE_FIELD = /\b(?:input_tokens|prompt_tokens|promptTokenCount|inputTokens|promptTokens|prompt_token_count)\b/;
|
|
417
|
+
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/;
|
|
418
|
+
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;
|
|
419
|
+
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|&&))/;
|
|
420
|
+
var isComment = (raw) => /^\s*(?:#|\/\/|\*|\/\*)/.test(raw);
|
|
421
|
+
function cacheLint(path3, lines, masked) {
|
|
422
|
+
const out = [];
|
|
423
|
+
const seen = /* @__PURE__ */ new Set();
|
|
424
|
+
const add = (k, fd) => {
|
|
425
|
+
if (!seen.has(k)) {
|
|
426
|
+
seen.add(k);
|
|
427
|
+
out.push({ file: path3, ...fd });
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
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));
|
|
431
|
+
const src = lines.join("\n");
|
|
432
|
+
const hasDirective = CACHE_DIRECTIVE.test(src);
|
|
433
|
+
const nonClaudeViaCompat = OPENAI_COMPAT.test(src) && !ANTHROPIC_SDK.test(src) && NON_CLAUDE_MODEL.test(src);
|
|
434
|
+
const ownerOf = (i) => {
|
|
435
|
+
for (let k = i; k >= Math.max(0, i - 6); k--) {
|
|
436
|
+
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*:/);
|
|
437
|
+
if (u >= 0 || p >= 0) return u >= 0 && (p < 0 || (k === i ? u < l.search(/cache_control|cacheControl/) && u > p : u > p)) ? "user" : "prefix";
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
};
|
|
441
|
+
const markers = lines.map((l, i) => !isComment(l) && /cache_control|cacheControl|cachePoint/.test(l) ? i : -1).filter((i) => i >= 0);
|
|
442
|
+
const systemMarked = markers.some((i) => ownerOf(i) === "prefix");
|
|
443
|
+
const USER_MARK = /["']?role["']?\s*:\s*["']user["']|\bcontents\s*[:=]|\bmessages\s*[:=]|\binput\s*[:=]/;
|
|
444
|
+
const inSystemValue = (i) => {
|
|
445
|
+
const raw = lines[i], v = raw.search(VOLATILE_CALL), sp = raw.search(SYSTEM_CTX), up = raw.search(USER_MARK);
|
|
446
|
+
if (sp >= 0 && sp < v) return !(up > sp && up < v);
|
|
447
|
+
if (up >= 0 && up < v) return false;
|
|
448
|
+
for (let k = i - 1; k >= Math.max(0, i - 4); k--) {
|
|
449
|
+
if (isComment(lines[k])) continue;
|
|
450
|
+
const ks = lines[k].search(SYSTEM_CTX), ku = lines[k].search(USER_MARK);
|
|
451
|
+
if (ks >= 0) return !(ku > ks);
|
|
452
|
+
if (ku >= 0) return false;
|
|
453
|
+
}
|
|
454
|
+
return false;
|
|
455
|
+
};
|
|
456
|
+
lines.forEach((raw, i) => {
|
|
457
|
+
if (isComment(raw)) return;
|
|
458
|
+
const code = masked[i] ?? "";
|
|
459
|
+
if (hasDirective && MODEL_GATE.test(code + raw) && MODEL_NAME_TEST.test(raw) && near(i, CACHE_DIRECTIVE, 3, 10)) add("gating", {
|
|
460
|
+
severity: "medium",
|
|
461
|
+
line: i + 1,
|
|
462
|
+
title: "Prompt caching gated on a hardcoded model-name check",
|
|
463
|
+
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).',
|
|
464
|
+
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.',
|
|
465
|
+
est_savings_pct: 10
|
|
466
|
+
});
|
|
467
|
+
if (VOLATILE_CALL.test(raw) && INTERPOLATES.test(raw) && inSystemValue(i) && !RANDOM_ID.test(raw)) add("volatile", {
|
|
468
|
+
severity: "medium",
|
|
469
|
+
line: i + 1,
|
|
470
|
+
title: "Volatile value (time, date or random id) interpolated into the system prompt",
|
|
471
|
+
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).",
|
|
472
|
+
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).",
|
|
473
|
+
est_savings_pct: 15
|
|
474
|
+
});
|
|
475
|
+
if (RANDOM_ID.test(raw) || SET_TOOLS.test(raw)) add("serialization", {
|
|
476
|
+
severity: "low",
|
|
477
|
+
line: i + 1,
|
|
478
|
+
title: RANDOM_ID.test(raw) ? "Random ids generated for messages or tool calls in the prompt" : "Tool list built from an unordered set",
|
|
479
|
+
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).",
|
|
480
|
+
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.",
|
|
481
|
+
est_savings_pct: 5
|
|
482
|
+
});
|
|
483
|
+
if (nonClaudeViaCompat && /cache_control|cacheControl/.test(raw)) add("marker", {
|
|
484
|
+
severity: "medium",
|
|
485
|
+
line: i + 1,
|
|
486
|
+
title: "Anthropic cache_control marker sent through an OpenAI-compatible client to non-Claude models",
|
|
487
|
+
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).",
|
|
488
|
+
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.",
|
|
489
|
+
est_savings_pct: 0
|
|
490
|
+
});
|
|
491
|
+
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", {
|
|
492
|
+
severity: "low",
|
|
493
|
+
line: i + 1,
|
|
494
|
+
title: "Cache breakpoint on per-request user content, none on the system prompt or tools",
|
|
495
|
+
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.",
|
|
496
|
+
fix: "Add a breakpoint after the last tool or the system prompt; keep the per-turn one only for long conversations.",
|
|
497
|
+
est_savings_pct: 5
|
|
498
|
+
});
|
|
499
|
+
if (ZEROED_CACHE_USAGE.test(code)) add("usage", {
|
|
500
|
+
severity: "medium",
|
|
501
|
+
line: i + 1,
|
|
502
|
+
title: "Cached-token usage hardcoded to zero",
|
|
503
|
+
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).",
|
|
504
|
+
fix: "Read `cache_read_input_tokens` / `cache_creation_input_tokens` (Anthropic), `prompt_tokens_details.cached_tokens` (OpenAI) or `cachedContentTokenCount` (Gemini) from the usage object.",
|
|
505
|
+
est_savings_pct: 0
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
const costLine = lines.findIndex((l) => !isComment(l) && COST_MATH.test(l));
|
|
509
|
+
if (costLine >= 0 && !CACHE_USAGE_FIELD.test(src)) add("usage", {
|
|
510
|
+
severity: "medium",
|
|
511
|
+
line: costLine + 1,
|
|
512
|
+
title: "Cost math from token usage ignores cached tokens",
|
|
513
|
+
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).",
|
|
514
|
+
fix: "Price cache reads, cache writes and uncached input separately from the provider usage fields, or read the per-request cost from TokenZero receipts.",
|
|
515
|
+
est_savings_pct: 0
|
|
516
|
+
});
|
|
517
|
+
return out;
|
|
518
|
+
}
|
|
398
519
|
function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
399
520
|
const findings = [];
|
|
400
521
|
const callSites = [];
|
|
@@ -557,6 +678,9 @@ function analyzeFiles(files, repo = null, manifests2 = []) {
|
|
|
557
678
|
findings.push(...local);
|
|
558
679
|
siteFindings.push(local);
|
|
559
680
|
});
|
|
681
|
+
if (callLines.length || hasCacheControl || USAGE_FIELD.test(src)) {
|
|
682
|
+
for (const fd of cacheLint(f.path, lines, masked)) findings.push({ ...fd, severity: kind === "library" || illustrative ? DOWN[fd.severity] : fd.severity });
|
|
683
|
+
}
|
|
560
684
|
}
|
|
561
685
|
const libTokens = promptLibrary.reduce((a, p) => a + p.tokens, 0);
|
|
562
686
|
if (callSites.length && !repoCaches && libTokens >= 1024 && !findings.some((f) => /static prompt tokens/.test(f.title))) {
|
|
@@ -637,7 +761,7 @@ async function fetchRepoFiles(repo, opts = {}) {
|
|
|
637
761
|
const meta = await fetch(`${GH}/repos/${repo}`, { headers: ghHeaders(opts.token) });
|
|
638
762
|
if (meta.status === 404) throw new Error("repository not found (private repos are not supported)");
|
|
639
763
|
if (!meta.ok) throw new Error(`GitHub API ${meta.status}: ${(await meta.text()).slice(0, 200)}`);
|
|
640
|
-
const
|
|
764
|
+
const metaJson = await meta.json(), branch = metaJson.default_branch, fullName = typeof metaJson.full_name === "string" ? metaJson.full_name : repo;
|
|
641
765
|
const tree = await fetch(`${GH}/repos/${repo}/git/trees/${encodeURIComponent(branch)}?recursive=1`, { headers: ghHeaders(opts.token) });
|
|
642
766
|
if (!tree.ok) throw new Error(`GitHub tree ${tree.status}`);
|
|
643
767
|
const treeAll = (await tree.json()).tree;
|
|
@@ -661,14 +785,15 @@ async function fetchRepoFiles(repo, opts = {}) {
|
|
|
661
785
|
const r = await fetch(`https://raw.githubusercontent.com/${repo}/${branch}/${n}`, { headers: { "user-agent": UA } });
|
|
662
786
|
return r.ok ? { path: n, content: await r.text() } : null;
|
|
663
787
|
}))).filter((m) => !!m);
|
|
664
|
-
return { files, manifests: manifests2, branch, total_candidates: entries.length };
|
|
788
|
+
return { files, manifests: manifests2, branch, total_candidates: entries.length, full_name: fullName };
|
|
665
789
|
}
|
|
666
790
|
|
|
667
791
|
// src/arbitrage.ts
|
|
792
|
+
var ARBITRAGE_DEFAULTS = { input_tokens: 2e3, output_tokens: 400 };
|
|
668
793
|
function modelArbitrage(args) {
|
|
669
794
|
const key = normalizeModel(args.model);
|
|
670
795
|
if (!key) throw new Error(`unknown model ${args.model}; known: ${Object.keys(PRICES).join(", ")}`);
|
|
671
|
-
const n = args.requests_per_month ?? 1e4, inp = args.input_tokens, out = args.output_tokens, st = Math.min(inp, args.static_tokens ?? 0);
|
|
796
|
+
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);
|
|
672
797
|
const rowFor = (m) => {
|
|
673
798
|
const p = priceFor(m);
|
|
674
799
|
const base = costOf(m, { input: inp, output: out });
|
|
@@ -679,6 +804,7 @@ function modelArbitrage(args) {
|
|
|
679
804
|
const rows = [...new Set(models)].map(rowFor);
|
|
680
805
|
return {
|
|
681
806
|
pricing_version: PRICING_VERSION,
|
|
807
|
+
workload: { input_tokens: inp, output_tokens: out, requests_per_month: n, ...args.input_tokens == null || args.output_tokens == null ? { defaults_used: true } : {} },
|
|
682
808
|
requested: rows[0],
|
|
683
809
|
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 })),
|
|
684
810
|
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."
|
|
@@ -754,7 +880,7 @@ function manifests(root) {
|
|
|
754
880
|
}
|
|
755
881
|
|
|
756
882
|
// mcp/src/main.ts
|
|
757
|
-
var VERSION = "0.2.
|
|
883
|
+
var VERSION = "0.2.9";
|
|
758
884
|
var BAKED_URL = true ? "https://tokenzero.sh" : "__TOKENZERO_DEFAULT_URL__";
|
|
759
885
|
var SUPPORTED = [...MCP_LEGACY_VERSIONS];
|
|
760
886
|
var SERVER = { name: "tokenzero", version: VERSION };
|
|
@@ -766,15 +892,15 @@ var TOOLS = [
|
|
|
766
892
|
name: "audit_repo_token_waste",
|
|
767
893
|
title: "Audit a repo or directory for LLM token waste",
|
|
768
894
|
annotations: { title: "Audit a repo or directory for LLM token waste", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
769
|
-
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.",
|
|
895
|
+
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.",
|
|
770
896
|
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 }
|
|
771
897
|
},
|
|
772
898
|
{
|
|
773
899
|
name: "calculate_model_arbitrage",
|
|
774
900
|
title: "Price a workload across models",
|
|
775
901
|
annotations: { title: "Price a workload across models", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
776
|
-
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.`,
|
|
777
|
-
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"
|
|
902
|
+
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.`,
|
|
903
|
+
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 }
|
|
778
904
|
},
|
|
779
905
|
{
|
|
780
906
|
name: "optimize_prompt_payload",
|
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.9",
|
|
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",
|