slash-tokens 1.5.4 → 1.5.6
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/dist/cli.js +48 -6
- package/dist/patterns.d.ts +2 -0
- package/dist/patterns.js +25 -0
- package/dist/preflight.d.ts +10 -3
- package/dist/preflight.js +15 -3
- package/dist/report.js +26 -5
- package/dist/scanner.d.ts +4 -0
- package/dist/scanner.js +4 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -90,6 +90,13 @@ var AI_PATTERNS = [
|
|
|
90
90
|
{ name: "Cohere", regex: /from\s+['"]cohere|CohereClient|cohere\.chat/g },
|
|
91
91
|
{ name: "Mistral", regex: /from\s+['"]@mistralai|MistralClient/g }
|
|
92
92
|
];
|
|
93
|
+
var SDK_REPRESENTATIVE_MODEL = {
|
|
94
|
+
Anthropic: "claude-sonnet",
|
|
95
|
+
OpenAI: "gpt-5.4",
|
|
96
|
+
Gemini: "gemini-3.1-pro",
|
|
97
|
+
Grok: "grok-4.20"
|
|
98
|
+
};
|
|
99
|
+
var UNKNOWN_SDK_REPRESENTATIVE_MODEL = "claude-sonnet";
|
|
93
100
|
var SKIP_DIRS = new Set([
|
|
94
101
|
"node_modules",
|
|
95
102
|
".git",
|
|
@@ -159,13 +166,15 @@ function findCallSites(filePath, content) {
|
|
|
159
166
|
const endLine = Math.min(lines.length, lineNum + 20);
|
|
160
167
|
const context = lines.slice(startLine, endLine).join(`
|
|
161
168
|
`);
|
|
162
|
-
const
|
|
169
|
+
const estimatedModel = SDK_REPRESENTATIVE_MODEL[pattern.name] ?? UNKNOWN_SDK_REPRESENTATIVE_MODEL;
|
|
170
|
+
const tokensPerCall = slash(context, estimatedModel);
|
|
163
171
|
if (!sites.some((s) => s.line === lineNum && s.sdk === pattern.name)) {
|
|
164
172
|
sites.push({
|
|
165
173
|
file: filePath,
|
|
166
174
|
line: lineNum,
|
|
167
175
|
sdk: pattern.name,
|
|
168
|
-
tokensPerCall
|
|
176
|
+
tokensPerCall,
|
|
177
|
+
estimatedModel
|
|
169
178
|
});
|
|
170
179
|
}
|
|
171
180
|
}
|
|
@@ -193,6 +202,24 @@ function scan(dir) {
|
|
|
193
202
|
};
|
|
194
203
|
}
|
|
195
204
|
|
|
205
|
+
// src/models.ts
|
|
206
|
+
var MODELS = {
|
|
207
|
+
"claude-opus": { input: 5, output: 25, context: 1e6 },
|
|
208
|
+
"claude-opus-4.7": { input: 5, output: 25, context: 1e6 },
|
|
209
|
+
"claude-sonnet": { input: 2, output: 10, context: 1e6 },
|
|
210
|
+
"claude-haiku": { input: 1, output: 5, context: 200000 },
|
|
211
|
+
"grok-4.20": { input: 1.25, output: 2.5, context: 1e6 },
|
|
212
|
+
"grok-4-1-fast": { input: 1.25, output: 2.5, context: 1e6 },
|
|
213
|
+
"gemini-3.1-pro": { input: 2, output: 12, context: 1e6 },
|
|
214
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5, context: 1e6 },
|
|
215
|
+
"gpt-5.4": { input: 2.5, output: 15, context: 1e6 },
|
|
216
|
+
"gpt-5.4-mini": { input: 0.75, output: 4.5, context: 128000 },
|
|
217
|
+
"gpt-5.4-nano": { input: 0.2, output: 1.25, context: 128000 }
|
|
218
|
+
};
|
|
219
|
+
function getModel(name) {
|
|
220
|
+
return MODELS[name] || MODELS[name.toLowerCase()];
|
|
221
|
+
}
|
|
222
|
+
|
|
196
223
|
// src/report.ts
|
|
197
224
|
var R = "\x1B[0m";
|
|
198
225
|
var B = "\x1B[1m";
|
|
@@ -231,9 +258,21 @@ function printReport(sites, filesScanned, timeMs, cwd) {
|
|
|
231
258
|
const dailyInput = sites.reduce((sum, s) => sum + s.tokensPerCall * callsPerDay, 0);
|
|
232
259
|
const dailyOutput = dailyInput * 2;
|
|
233
260
|
const dailyTotal = dailyInput + dailyOutput;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
261
|
+
let monthlyCostInput = 0;
|
|
262
|
+
let monthlyCostOutput = 0;
|
|
263
|
+
let unknownProviderSites = 0;
|
|
264
|
+
for (const site of sites) {
|
|
265
|
+
const info = getModel(site.estimatedModel);
|
|
266
|
+
if (!info)
|
|
267
|
+
continue;
|
|
268
|
+
if (site.estimatedModel === UNKNOWN_SDK_REPRESENTATIVE_MODEL && !["Anthropic"].includes(site.sdk)) {
|
|
269
|
+
unknownProviderSites++;
|
|
270
|
+
}
|
|
271
|
+
const siteDailyInput = site.tokensPerCall * callsPerDay;
|
|
272
|
+
const siteDailyOutput = siteDailyInput * 2;
|
|
273
|
+
monthlyCostInput += siteDailyInput * 30 / 1e6 * info.input;
|
|
274
|
+
monthlyCostOutput += siteDailyOutput * 30 / 1e6 * info.output;
|
|
275
|
+
}
|
|
237
276
|
const monthlyCost = monthlyCostInput + monthlyCostOutput;
|
|
238
277
|
console.log(`${ORANGE} DAILY TOKEN VOLUME ${GRAY}(${callsPerDay} calls/site/day)${R}`);
|
|
239
278
|
console.log(`${DIM} ${"─".repeat(60)}${R}`);
|
|
@@ -241,11 +280,14 @@ function printReport(sites, filesScanned, timeMs, cwd) {
|
|
|
241
280
|
console.log(`${WHITE} Output tokens: ${B}${formatNum(dailyOutput)}${R} ${GRAY}(estimated)${R}`);
|
|
242
281
|
console.log(`${WHITE} Total: ${ORANGE}${B}${formatNum(dailyTotal)} tokens/day${R}`);
|
|
243
282
|
console.log("");
|
|
244
|
-
console.log(`${ORANGE} MONTHLY COST ${GRAY}(
|
|
283
|
+
console.log(`${ORANGE} MONTHLY COST ${GRAY}(real per-provider pricing)${R}`);
|
|
245
284
|
console.log(`${DIM} ${"─".repeat(60)}${R}`);
|
|
246
285
|
console.log(`${WHITE} Input: ${B}$${monthlyCostInput.toFixed(2)}/mo${R}`);
|
|
247
286
|
console.log(`${WHITE} Output: ${B}$${monthlyCostOutput.toFixed(2)}/mo${R}`);
|
|
248
287
|
console.log(`${WHITE} Total: ${ORANGE}${B}$${monthlyCost.toFixed(2)}/mo${R}`);
|
|
288
|
+
if (unknownProviderSites > 0) {
|
|
289
|
+
console.log(`${GRAY} (${unknownProviderSites} call site${unknownProviderSites === 1 ? "" : "s"} use an assumed price — the detected SDK doesn't reveal the exact provider)${R}`);
|
|
290
|
+
}
|
|
249
291
|
console.log("");
|
|
250
292
|
const recoveryPct = 10;
|
|
251
293
|
const monthlyRecovered = monthlyCost * recoveryPct / 100;
|
package/dist/patterns.d.ts
CHANGED
|
@@ -3,5 +3,7 @@ export interface Pattern {
|
|
|
3
3
|
regex: RegExp;
|
|
4
4
|
}
|
|
5
5
|
export declare const AI_PATTERNS: Pattern[];
|
|
6
|
+
export declare const SDK_REPRESENTATIVE_MODEL: Record<string, string>;
|
|
7
|
+
export declare const UNKNOWN_SDK_REPRESENTATIVE_MODEL = "claude-sonnet";
|
|
6
8
|
export declare const SKIP_DIRS: Set<string>;
|
|
7
9
|
export declare const SCAN_EXTENSIONS: Set<string>;
|
package/dist/patterns.js
CHANGED
|
@@ -20,6 +20,31 @@ export const AI_PATTERNS = [
|
|
|
20
20
|
// Mistral
|
|
21
21
|
{ name: 'Mistral', regex: /from\s+['"]@mistralai|MistralClient/g },
|
|
22
22
|
];
|
|
23
|
+
// Representative model per detected SDK — the scanner only ever sees an
|
|
24
|
+
// SDK name (a regex match on import/call syntax), never the actual model
|
|
25
|
+
// string passed at runtime, so there's no way to know the exact model a
|
|
26
|
+
// found call site uses. Fixed 2026-08-23: this used to mean every SDK got
|
|
27
|
+
// the raw, uncalibrated token estimate AND one flat hardcoded $3/$15
|
|
28
|
+
// "GPT-4o class" price regardless of provider — the product's own
|
|
29
|
+
// top-of-funnel "here's what you'd save" numbers were both uncalibrated
|
|
30
|
+
// and mispriced for every provider except whichever one happened to match
|
|
31
|
+
// that flat rate by coincidence. Mapping each identified SDK to one real,
|
|
32
|
+
// representative model lets the scanner apply that model's real
|
|
33
|
+
// calibration factor and real price — still an approximation (we don't
|
|
34
|
+
// know the exact model), but a real per-provider one instead of a single
|
|
35
|
+
// guess applied to everyone.
|
|
36
|
+
export const SDK_REPRESENTATIVE_MODEL = {
|
|
37
|
+
'Anthropic': 'claude-sonnet',
|
|
38
|
+
'OpenAI': 'gpt-5.4',
|
|
39
|
+
'Gemini': 'gemini-3.1-pro',
|
|
40
|
+
'Grok': 'grok-4.20',
|
|
41
|
+
};
|
|
42
|
+
// Fallback for SDKs that don't map to one specific provider (Vercel AI,
|
|
43
|
+
// LangChain, and Bedrock can all wrap any underlying provider; raw
|
|
44
|
+
// fetch-to-AI-endpoint and Cohere/Mistral have no pricing data in MODELS
|
|
45
|
+
// at all). claude-sonnet is used as a documented, honest middle-of-the-
|
|
46
|
+
// road placeholder — not a claim about which model is actually running.
|
|
47
|
+
export const UNKNOWN_SDK_REPRESENTATIVE_MODEL = 'claude-sonnet';
|
|
23
48
|
export const SKIP_DIRS = new Set([
|
|
24
49
|
'node_modules', '.git', 'dist', 'build', '.next', '.nuxt', '.svelte-kit',
|
|
25
50
|
'coverage', '.turbo', '.cache', '__pycache__', '.venv', 'venv',
|
package/dist/preflight.d.ts
CHANGED
|
@@ -48,8 +48,15 @@ export declare function preflight(content: string, model: string): PreflightResu
|
|
|
48
48
|
* - Cheapest SAME-PROVIDER alternative by input price. If two alternatives
|
|
49
49
|
* tie on price (unlikely but possible), returns the first encountered in
|
|
50
50
|
* PROVIDER_MODELS order.
|
|
51
|
-
* - Must agree with intercept.ts findCheapestRoute for identical inputs
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* - Must agree with intercept.ts findCheapestRoute for identical inputs,
|
|
52
|
+
* INCLUDING respecting the same init() config gates: shouldRoute()
|
|
53
|
+
* (init({route: false}) must make this always return null, matching
|
|
54
|
+
* patchFetch() never routing) and isModelAllowed() (init({models: [...]})
|
|
55
|
+
* must exclude any candidate not in that list, matching
|
|
56
|
+
* findCheapestRoute's real filtering). Fixed 2026-08-23 — this
|
|
57
|
+
* function used to ignore both, so a call site previewing "what will
|
|
58
|
+
* Slash route to" via preflightRoute() would show a route recommendation
|
|
59
|
+
* that real traffic through patchFetch() would never actually take
|
|
60
|
+
* under the same config, silently breaking the "matches exactly" promise.
|
|
54
61
|
*/
|
|
55
62
|
export declare function preflightRoute(content: string, model: string): Alternative | null;
|
package/dist/preflight.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { slash } from './slash.js';
|
|
2
2
|
import { getModel, MODELS } from './models.js';
|
|
3
3
|
import { PROVIDER_MODELS, providerOf } from './providers.js';
|
|
4
|
+
import { shouldRoute, isModelAllowed } from './config.js';
|
|
4
5
|
/**
|
|
5
6
|
* Compute cost of a prompt of `tokens` tokens on the given ModelInfo.
|
|
6
7
|
* Uses input-token price only (output is not known at preflight time).
|
|
@@ -69,11 +70,20 @@ export function preflight(content, model) {
|
|
|
69
70
|
* - Cheapest SAME-PROVIDER alternative by input price. If two alternatives
|
|
70
71
|
* tie on price (unlikely but possible), returns the first encountered in
|
|
71
72
|
* PROVIDER_MODELS order.
|
|
72
|
-
* - Must agree with intercept.ts findCheapestRoute for identical inputs
|
|
73
|
-
*
|
|
74
|
-
*
|
|
73
|
+
* - Must agree with intercept.ts findCheapestRoute for identical inputs,
|
|
74
|
+
* INCLUDING respecting the same init() config gates: shouldRoute()
|
|
75
|
+
* (init({route: false}) must make this always return null, matching
|
|
76
|
+
* patchFetch() never routing) and isModelAllowed() (init({models: [...]})
|
|
77
|
+
* must exclude any candidate not in that list, matching
|
|
78
|
+
* findCheapestRoute's real filtering). Fixed 2026-08-23 — this
|
|
79
|
+
* function used to ignore both, so a call site previewing "what will
|
|
80
|
+
* Slash route to" via preflightRoute() would show a route recommendation
|
|
81
|
+
* that real traffic through patchFetch() would never actually take
|
|
82
|
+
* under the same config, silently breaking the "matches exactly" promise.
|
|
75
83
|
*/
|
|
76
84
|
export function preflightRoute(content, model) {
|
|
85
|
+
if (!shouldRoute())
|
|
86
|
+
return null;
|
|
77
87
|
const tokens = slash(content, model);
|
|
78
88
|
const info = getModel(model);
|
|
79
89
|
if (!info)
|
|
@@ -89,6 +99,8 @@ export function preflightRoute(content, model) {
|
|
|
89
99
|
for (const m of providerModels) {
|
|
90
100
|
if (m === model)
|
|
91
101
|
continue;
|
|
102
|
+
if (!isModelAllowed(m))
|
|
103
|
+
continue; // user excluded this model
|
|
92
104
|
const altInfo = getModel(m);
|
|
93
105
|
if (!altInfo)
|
|
94
106
|
continue;
|
package/dist/report.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getModel } from './models.js';
|
|
2
|
+
import { UNKNOWN_SDK_REPRESENTATIVE_MODEL } from './patterns.js';
|
|
1
3
|
// ANSI — zero deps
|
|
2
4
|
const R = '\x1b[0m';
|
|
3
5
|
const B = '\x1b[1m';
|
|
@@ -38,10 +40,26 @@ export function printReport(sites, filesScanned, timeMs, cwd) {
|
|
|
38
40
|
const dailyInput = sites.reduce((sum, s) => sum + s.tokensPerCall * callsPerDay, 0);
|
|
39
41
|
const dailyOutput = dailyInput * 2; // Estimate output at 2x input
|
|
40
42
|
const dailyTotal = dailyInput + dailyOutput;
|
|
41
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
// Cost per site using its own representative model's REAL current price
|
|
44
|
+
// (see patterns.ts SDK_REPRESENTATIVE_MODEL) — fixed 2026-08-23: this
|
|
45
|
+
// used to be one flat hardcoded $3/$15 "GPT-4o class" rate applied to
|
|
46
|
+
// every site regardless of detected provider, understating cost by up
|
|
47
|
+
// to ~67% for premium providers and overstating it for cheap ones.
|
|
48
|
+
let monthlyCostInput = 0;
|
|
49
|
+
let monthlyCostOutput = 0;
|
|
50
|
+
let unknownProviderSites = 0;
|
|
51
|
+
for (const site of sites) {
|
|
52
|
+
const info = getModel(site.estimatedModel);
|
|
53
|
+
if (!info)
|
|
54
|
+
continue; // shouldn't happen — estimatedModel always maps to a real MODELS entry
|
|
55
|
+
if (site.estimatedModel === UNKNOWN_SDK_REPRESENTATIVE_MODEL && !['Anthropic'].includes(site.sdk)) {
|
|
56
|
+
unknownProviderSites++;
|
|
57
|
+
}
|
|
58
|
+
const siteDailyInput = site.tokensPerCall * callsPerDay;
|
|
59
|
+
const siteDailyOutput = siteDailyInput * 2;
|
|
60
|
+
monthlyCostInput += (siteDailyInput * 30 / 1000000) * info.input;
|
|
61
|
+
monthlyCostOutput += (siteDailyOutput * 30 / 1000000) * info.output;
|
|
62
|
+
}
|
|
45
63
|
const monthlyCost = monthlyCostInput + monthlyCostOutput;
|
|
46
64
|
console.log(`${ORANGE} DAILY TOKEN VOLUME ${GRAY}(${callsPerDay} calls/site/day)${R}`);
|
|
47
65
|
console.log(`${DIM} ${'─'.repeat(60)}${R}`);
|
|
@@ -49,11 +67,14 @@ export function printReport(sites, filesScanned, timeMs, cwd) {
|
|
|
49
67
|
console.log(`${WHITE} Output tokens: ${B}${formatNum(dailyOutput)}${R} ${GRAY}(estimated)${R}`);
|
|
50
68
|
console.log(`${WHITE} Total: ${ORANGE}${B}${formatNum(dailyTotal)} tokens/day${R}`);
|
|
51
69
|
console.log('');
|
|
52
|
-
console.log(`${ORANGE} MONTHLY COST ${GRAY}(
|
|
70
|
+
console.log(`${ORANGE} MONTHLY COST ${GRAY}(real per-provider pricing)${R}`);
|
|
53
71
|
console.log(`${DIM} ${'─'.repeat(60)}${R}`);
|
|
54
72
|
console.log(`${WHITE} Input: ${B}$${monthlyCostInput.toFixed(2)}/mo${R}`);
|
|
55
73
|
console.log(`${WHITE} Output: ${B}$${monthlyCostOutput.toFixed(2)}/mo${R}`);
|
|
56
74
|
console.log(`${WHITE} Total: ${ORANGE}${B}$${monthlyCost.toFixed(2)}/mo${R}`);
|
|
75
|
+
if (unknownProviderSites > 0) {
|
|
76
|
+
console.log(`${GRAY} (${unknownProviderSites} call site${unknownProviderSites === 1 ? '' : 's'} use an assumed price — the detected SDK doesn't reveal the exact provider)${R}`);
|
|
77
|
+
}
|
|
57
78
|
console.log('');
|
|
58
79
|
// Slash — sunk cost recovery (conservative 10% gate efficiency)
|
|
59
80
|
const recoveryPct = 10;
|
package/dist/scanner.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export interface CallSite {
|
|
|
3
3
|
line: number;
|
|
4
4
|
sdk: string;
|
|
5
5
|
tokensPerCall: number;
|
|
6
|
+
/** The representative model used to calibrate/price this site — see
|
|
7
|
+
* patterns.ts SDK_REPRESENTATIVE_MODEL. Not the actual runtime model
|
|
8
|
+
* (the scanner can't know that from source alone). */
|
|
9
|
+
estimatedModel: string;
|
|
6
10
|
}
|
|
7
11
|
export declare function scan(dir: string): {
|
|
8
12
|
sites: CallSite[];
|
package/dist/scanner.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
2
2
|
import { join, extname } from 'path';
|
|
3
3
|
import { slash } from './slash.js';
|
|
4
|
-
import { AI_PATTERNS, SKIP_DIRS, SCAN_EXTENSIONS } from './patterns.js';
|
|
4
|
+
import { AI_PATTERNS, SKIP_DIRS, SCAN_EXTENSIONS, SDK_REPRESENTATIVE_MODEL, UNKNOWN_SDK_REPRESENTATIVE_MODEL, } from './patterns.js';
|
|
5
5
|
function walkDir(dir, files) {
|
|
6
6
|
let entries;
|
|
7
7
|
try {
|
|
@@ -44,7 +44,8 @@ function findCallSites(filePath, content) {
|
|
|
44
44
|
const startLine = Math.max(0, lineNum - 5);
|
|
45
45
|
const endLine = Math.min(lines.length, lineNum + 20);
|
|
46
46
|
const context = lines.slice(startLine, endLine).join('\n');
|
|
47
|
-
const
|
|
47
|
+
const estimatedModel = SDK_REPRESENTATIVE_MODEL[pattern.name] ?? UNKNOWN_SDK_REPRESENTATIVE_MODEL;
|
|
48
|
+
const tokensPerCall = slash(context, estimatedModel);
|
|
48
49
|
// Avoid duplicates at same location
|
|
49
50
|
if (!sites.some(s => s.line === lineNum && s.sdk === pattern.name)) {
|
|
50
51
|
sites.push({
|
|
@@ -52,6 +53,7 @@ function findCallSites(filePath, content) {
|
|
|
52
53
|
line: lineNum,
|
|
53
54
|
sdk: pattern.name,
|
|
54
55
|
tokensPerCall,
|
|
56
|
+
estimatedModel,
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
}
|