token-guard 0.1.0
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 +109 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +141 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -0
- package/dist/display.d.ts +10 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +139 -0
- package/dist/display.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/monitor.d.ts +20 -0
- package/dist/monitor.d.ts.map +1 -0
- package/dist/monitor.js +160 -0
- package/dist/monitor.js.map +1 -0
- package/dist/pricing.d.ts +6 -0
- package/dist/pricing.d.ts.map +1 -0
- package/dist/pricing.js +60 -0
- package/dist/pricing.js.map +1 -0
- package/dist/tokenizer.d.ts +3 -0
- package/dist/tokenizer.d.ts.map +1 -0
- package/dist/tokenizer.js +54 -0
- package/dist/tokenizer.js.map +1 -0
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +43 -0
- package/src/cli.ts +166 -0
- package/src/config.ts +44 -0
- package/src/display.ts +160 -0
- package/src/index.ts +7 -0
- package/src/monitor.ts +189 -0
- package/src/pricing.ts +66 -0
- package/src/tokenizer.ts +57 -0
- package/src/types.ts +33 -0
- package/tsconfig.json +18 -0
package/src/display.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import type { UsageStats, TokenGuardOptions } from './types.js';
|
|
2
|
+
import { formatTokens } from './tokenizer.js';
|
|
3
|
+
|
|
4
|
+
const COLORS = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
bold: '\x1b[1m',
|
|
7
|
+
dim: '\x1b[2m',
|
|
8
|
+
red: '\x1b[31m',
|
|
9
|
+
green: '\x1b[32m',
|
|
10
|
+
yellow: '\x1b[33m',
|
|
11
|
+
blue: '\x1b[34m',
|
|
12
|
+
magenta: '\x1b[35m',
|
|
13
|
+
cyan: '\x1b[36m',
|
|
14
|
+
white: '\x1b[37m',
|
|
15
|
+
bgRed: '\x1b[41m',
|
|
16
|
+
bgYellow: '\x1b[43m',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export function clearLine(): void {
|
|
20
|
+
process.stderr.write('\x1b[2K\x1b[G');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function moveCursorUp(lines: number): void {
|
|
24
|
+
process.stderr.write(`\x1b[${lines}A`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function progressBar(current: number, max: number, width: number = 20): string {
|
|
28
|
+
const percent = Math.min(current / max, 1);
|
|
29
|
+
const filled = Math.round(width * percent);
|
|
30
|
+
const empty = width - filled;
|
|
31
|
+
|
|
32
|
+
let color = COLORS.green;
|
|
33
|
+
if (percent >= 0.9) color = COLORS.red;
|
|
34
|
+
else if (percent >= 0.8) color = COLORS.yellow;
|
|
35
|
+
|
|
36
|
+
return `${color}${'█'.repeat(filled)}${COLORS.dim}${'░'.repeat(empty)}${COLORS.reset}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatCost(cost: number): string {
|
|
40
|
+
return `$${cost.toFixed(4)}`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderStatus(
|
|
44
|
+
stats: UsageStats,
|
|
45
|
+
options: TokenGuardOptions,
|
|
46
|
+
inline: boolean = true
|
|
47
|
+
): string {
|
|
48
|
+
const { budget, costLimit, warnPercent } = options;
|
|
49
|
+
const { inputTokens, outputTokens, totalTokens, estimatedCost, model } = stats;
|
|
50
|
+
|
|
51
|
+
if (options.quiet) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const lines: string[] = [];
|
|
56
|
+
|
|
57
|
+
if (inline) {
|
|
58
|
+
// Compact inline display
|
|
59
|
+
let status = `${COLORS.cyan}◆${COLORS.reset} `;
|
|
60
|
+
status += `${COLORS.bold}${formatTokens(totalTokens)}${COLORS.reset} tokens `;
|
|
61
|
+
status += `${COLORS.dim}(in:${formatTokens(inputTokens)} out:${formatTokens(outputTokens)})${COLORS.reset} `;
|
|
62
|
+
status += `${COLORS.green}${formatCost(estimatedCost)}${COLORS.reset} `;
|
|
63
|
+
|
|
64
|
+
if (budget) {
|
|
65
|
+
const percent = (totalTokens / budget) * 100;
|
|
66
|
+
if (percent >= 100) {
|
|
67
|
+
status += `${COLORS.bgRed}${COLORS.white} BUDGET EXCEEDED ${COLORS.reset}`;
|
|
68
|
+
} else if (percent >= warnPercent) {
|
|
69
|
+
status += `${COLORS.yellow}⚠ ${percent.toFixed(0)}% of budget${COLORS.reset}`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (costLimit) {
|
|
74
|
+
const percent = (estimatedCost / costLimit) * 100;
|
|
75
|
+
if (percent >= 100) {
|
|
76
|
+
status += `${COLORS.bgRed}${COLORS.white} COST LIMIT EXCEEDED ${COLORS.reset}`;
|
|
77
|
+
} else if (percent >= warnPercent) {
|
|
78
|
+
status += `${COLORS.yellow}⚠ ${percent.toFixed(0)}% of cost limit${COLORS.reset}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return status;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Box display
|
|
86
|
+
const width = 47;
|
|
87
|
+
const border = '─'.repeat(width - 2);
|
|
88
|
+
|
|
89
|
+
lines.push(`${COLORS.dim}┌${border}┐${COLORS.reset}`);
|
|
90
|
+
lines.push(`${COLORS.dim}│${COLORS.reset} ${COLORS.bold}${COLORS.cyan}token-guard${COLORS.reset} v0.1.0${' '.repeat(width - 24)}${COLORS.dim}│${COLORS.reset}`);
|
|
91
|
+
|
|
92
|
+
if (budget) {
|
|
93
|
+
const budgetLine = ` Budget: ${formatTokens(budget)} tokens | Warn: ${warnPercent}%`;
|
|
94
|
+
lines.push(`${COLORS.dim}│${COLORS.reset}${budgetLine}${' '.repeat(width - budgetLine.length - 2)}${COLORS.dim}│${COLORS.reset}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
lines.push(`${COLORS.dim}├${border}┤${COLORS.reset}`);
|
|
98
|
+
|
|
99
|
+
// Progress bar
|
|
100
|
+
if (budget) {
|
|
101
|
+
const bar = progressBar(totalTokens, budget, 20);
|
|
102
|
+
const counts = `${formatTokens(totalTokens)} / ${formatTokens(budget)}`;
|
|
103
|
+
lines.push(`${COLORS.dim}│${COLORS.reset} ${bar} ${counts}${' '.repeat(width - 30 - counts.length)}${COLORS.dim}│${COLORS.reset}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Token breakdown
|
|
107
|
+
const breakdown = ` Input: ${formatTokens(inputTokens)} | Output: ${formatTokens(outputTokens)}`;
|
|
108
|
+
lines.push(`${COLORS.dim}│${COLORS.reset}${breakdown}${' '.repeat(width - breakdown.length - 2)}${COLORS.dim}│${COLORS.reset}`);
|
|
109
|
+
|
|
110
|
+
// Cost
|
|
111
|
+
const costLine = ` Est. Cost: ${formatCost(estimatedCost)} (${model})`;
|
|
112
|
+
lines.push(`${COLORS.dim}│${COLORS.reset}${costLine}${' '.repeat(width - costLine.length - 2)}${COLORS.dim}│${COLORS.reset}`);
|
|
113
|
+
|
|
114
|
+
lines.push(`${COLORS.dim}└${border}┘${COLORS.reset}`);
|
|
115
|
+
|
|
116
|
+
return lines.join('\n');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function renderFinalReport(stats: UsageStats, options: TokenGuardOptions): string {
|
|
120
|
+
const lines: string[] = [];
|
|
121
|
+
const duration = stats.endTime
|
|
122
|
+
? ((stats.endTime.getTime() - stats.startTime.getTime()) / 1000).toFixed(1)
|
|
123
|
+
: '?';
|
|
124
|
+
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push(`${COLORS.bold}${COLORS.cyan}═══ token-guard Report ═══${COLORS.reset}`);
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(`${COLORS.bold}Command:${COLORS.reset} ${stats.command}`);
|
|
129
|
+
lines.push(`${COLORS.bold}Model:${COLORS.reset} ${stats.model}`);
|
|
130
|
+
lines.push(`${COLORS.bold}Duration:${COLORS.reset} ${duration}s`);
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push(`${COLORS.bold}Tokens:${COLORS.reset}`);
|
|
133
|
+
lines.push(` Input: ${formatTokens(stats.inputTokens).padStart(10)}`);
|
|
134
|
+
lines.push(` Output: ${formatTokens(stats.outputTokens).padStart(10)}`);
|
|
135
|
+
lines.push(` Total: ${formatTokens(stats.totalTokens).padStart(10)}`);
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push(`${COLORS.bold}Estimated Cost:${COLORS.reset} ${COLORS.green}${formatCost(stats.estimatedCost)}${COLORS.reset}`);
|
|
138
|
+
|
|
139
|
+
if (stats.budgetExceeded) {
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push(`${COLORS.bgRed}${COLORS.white} BUDGET EXCEEDED - Process terminated ${COLORS.reset}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (stats.costExceeded) {
|
|
145
|
+
lines.push('');
|
|
146
|
+
lines.push(`${COLORS.bgRed}${COLORS.white} COST LIMIT EXCEEDED - Process terminated ${COLORS.reset}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
lines.push('');
|
|
150
|
+
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function warn(message: string): void {
|
|
155
|
+
console.error(`${COLORS.yellow}⚠ token-guard:${COLORS.reset} ${message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function error(message: string): void {
|
|
159
|
+
console.error(`${COLORS.red}✖ token-guard:${COLORS.reset} ${message}`);
|
|
160
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// token-guard - Monitor and limit token usage for AI coding assistants
|
|
2
|
+
|
|
3
|
+
export { TokenMonitor } from './monitor.js';
|
|
4
|
+
export { countTokens, formatTokens } from './tokenizer.js';
|
|
5
|
+
export { calculateCost, getPricing, detectModel, MODEL_PRICING } from './pricing.js';
|
|
6
|
+
export { loadConfig, getConfigPath } from './config.js';
|
|
7
|
+
export type { TokenGuardOptions, UsageStats, ModelPricing, Config } from './types.js';
|
package/src/monitor.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { writeFileSync } from 'node:fs';
|
|
3
|
+
import type { TokenGuardOptions, UsageStats } from './types.js';
|
|
4
|
+
import { countTokens } from './tokenizer.js';
|
|
5
|
+
import { calculateCost, detectModel } from './pricing.js';
|
|
6
|
+
import { renderStatus, renderFinalReport, clearLine, warn, error } from './display.js';
|
|
7
|
+
|
|
8
|
+
export class TokenMonitor {
|
|
9
|
+
private stats: UsageStats;
|
|
10
|
+
private options: TokenGuardOptions;
|
|
11
|
+
private process: ChildProcess | null = null;
|
|
12
|
+
private updateInterval: NodeJS.Timeout | null = null;
|
|
13
|
+
private lastStatusLength = 0;
|
|
14
|
+
|
|
15
|
+
constructor(command: string, options: TokenGuardOptions) {
|
|
16
|
+
const model = options.model || detectModel(command);
|
|
17
|
+
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.stats = {
|
|
20
|
+
inputTokens: 0,
|
|
21
|
+
outputTokens: 0,
|
|
22
|
+
totalTokens: 0,
|
|
23
|
+
estimatedCost: 0,
|
|
24
|
+
model,
|
|
25
|
+
startTime: new Date(),
|
|
26
|
+
command,
|
|
27
|
+
budgetExceeded: false,
|
|
28
|
+
costExceeded: false,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async run(args: string[]): Promise<number> {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const [cmd, ...cmdArgs] = args;
|
|
35
|
+
|
|
36
|
+
this.process = spawn(cmd, cmdArgs, {
|
|
37
|
+
stdio: ['inherit', 'pipe', 'pipe'],
|
|
38
|
+
shell: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Track input (what we send to the process)
|
|
42
|
+
// For now, we count the command itself as input
|
|
43
|
+
this.addInputTokens(args.join(' '));
|
|
44
|
+
|
|
45
|
+
// Track stdout
|
|
46
|
+
this.process.stdout?.on('data', (data: Buffer) => {
|
|
47
|
+
const text = data.toString();
|
|
48
|
+
this.addOutputTokens(text);
|
|
49
|
+
process.stdout.write(data);
|
|
50
|
+
this.checkLimits();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Track stderr
|
|
54
|
+
this.process.stderr?.on('data', (data: Buffer) => {
|
|
55
|
+
const text = data.toString();
|
|
56
|
+
this.addOutputTokens(text);
|
|
57
|
+
process.stderr.write(data);
|
|
58
|
+
this.checkLimits();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Periodic status update
|
|
62
|
+
if (!this.options.quiet) {
|
|
63
|
+
this.updateInterval = setInterval(() => {
|
|
64
|
+
this.showStatus();
|
|
65
|
+
}, 1000);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.process.on('close', (code) => {
|
|
69
|
+
this.cleanup();
|
|
70
|
+
this.stats.endTime = new Date();
|
|
71
|
+
|
|
72
|
+
if (!this.options.quiet) {
|
|
73
|
+
console.error(renderFinalReport(this.stats, this.options));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (this.options.output) {
|
|
77
|
+
this.saveReport();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
resolve(code ?? 0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.process.on('error', (err) => {
|
|
84
|
+
this.cleanup();
|
|
85
|
+
error(`Failed to start command: ${err.message}`);
|
|
86
|
+
resolve(1);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private addInputTokens(text: string): void {
|
|
92
|
+
const tokens = countTokens(text);
|
|
93
|
+
this.stats.inputTokens += tokens;
|
|
94
|
+
this.updateTotals();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private addOutputTokens(text: string): void {
|
|
98
|
+
const tokens = countTokens(text);
|
|
99
|
+
this.stats.outputTokens += tokens;
|
|
100
|
+
this.updateTotals();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private updateTotals(): void {
|
|
104
|
+
this.stats.totalTokens = this.stats.inputTokens + this.stats.outputTokens;
|
|
105
|
+
this.stats.estimatedCost = calculateCost(
|
|
106
|
+
this.stats.inputTokens,
|
|
107
|
+
this.stats.outputTokens,
|
|
108
|
+
this.stats.model
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private checkLimits(): void {
|
|
113
|
+
const { budget, costLimit, warnPercent } = this.options;
|
|
114
|
+
|
|
115
|
+
// Check token budget
|
|
116
|
+
if (budget) {
|
|
117
|
+
const percent = (this.stats.totalTokens / budget) * 100;
|
|
118
|
+
|
|
119
|
+
if (percent >= 100) {
|
|
120
|
+
this.stats.budgetExceeded = true;
|
|
121
|
+
this.terminate('Token budget exceeded');
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (percent >= warnPercent && percent < warnPercent + 5) {
|
|
126
|
+
warn(`${percent.toFixed(0)}% of token budget used`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check cost limit
|
|
131
|
+
if (costLimit) {
|
|
132
|
+
const percent = (this.stats.estimatedCost / costLimit) * 100;
|
|
133
|
+
|
|
134
|
+
if (percent >= 100) {
|
|
135
|
+
this.stats.costExceeded = true;
|
|
136
|
+
this.terminate('Cost limit exceeded');
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (percent >= warnPercent && percent < warnPercent + 5) {
|
|
141
|
+
warn(`${percent.toFixed(0)}% of cost limit used`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private terminate(reason: string): void {
|
|
147
|
+
error(reason);
|
|
148
|
+
if (this.process) {
|
|
149
|
+
this.process.kill('SIGTERM');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private showStatus(): void {
|
|
154
|
+
if (this.options.quiet) return;
|
|
155
|
+
|
|
156
|
+
const status = renderStatus(this.stats, this.options, true);
|
|
157
|
+
clearLine();
|
|
158
|
+
process.stderr.write(status);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private cleanup(): void {
|
|
162
|
+
if (this.updateInterval) {
|
|
163
|
+
clearInterval(this.updateInterval);
|
|
164
|
+
this.updateInterval = null;
|
|
165
|
+
}
|
|
166
|
+
clearLine();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private saveReport(): void {
|
|
170
|
+
if (!this.options.output) return;
|
|
171
|
+
|
|
172
|
+
const report = {
|
|
173
|
+
...this.stats,
|
|
174
|
+
startTime: this.stats.startTime.toISOString(),
|
|
175
|
+
endTime: this.stats.endTime?.toISOString(),
|
|
176
|
+
options: this.options,
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
writeFileSync(this.options.output, JSON.stringify(report, null, 2));
|
|
181
|
+
} catch (err) {
|
|
182
|
+
error(`Failed to save report: ${err}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getStats(): UsageStats {
|
|
187
|
+
return { ...this.stats };
|
|
188
|
+
}
|
|
189
|
+
}
|
package/src/pricing.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import type { ModelPricing } from './types.js';
|
|
2
|
+
|
|
3
|
+
// Pricing per 1M tokens (as of Jan 2026)
|
|
4
|
+
export const MODEL_PRICING: Record<string, ModelPricing> = {
|
|
5
|
+
// Claude models
|
|
6
|
+
'claude-opus-4-5': { input: 15.00, output: 75.00 },
|
|
7
|
+
'claude-opus-4': { input: 15.00, output: 75.00 },
|
|
8
|
+
'claude-sonnet-4': { input: 3.00, output: 15.00 },
|
|
9
|
+
'claude-3-opus': { input: 15.00, output: 75.00 },
|
|
10
|
+
'claude-3-sonnet': { input: 3.00, output: 15.00 },
|
|
11
|
+
'claude-3-haiku': { input: 0.25, output: 1.25 },
|
|
12
|
+
|
|
13
|
+
// OpenAI models
|
|
14
|
+
'gpt-4o': { input: 2.50, output: 10.00 },
|
|
15
|
+
'gpt-4-turbo': { input: 10.00, output: 30.00 },
|
|
16
|
+
'gpt-4': { input: 30.00, output: 60.00 },
|
|
17
|
+
'gpt-3.5-turbo': { input: 0.50, output: 1.50 },
|
|
18
|
+
'o1': { input: 15.00, output: 60.00 },
|
|
19
|
+
'o1-mini': { input: 3.00, output: 12.00 },
|
|
20
|
+
|
|
21
|
+
// Default fallback (sonnet-level pricing)
|
|
22
|
+
'default': { input: 3.00, output: 15.00 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function getPricing(model: string): ModelPricing {
|
|
26
|
+
const normalizedModel = model.toLowerCase().replace(/[_-]/g, '-');
|
|
27
|
+
|
|
28
|
+
for (const [key, pricing] of Object.entries(MODEL_PRICING)) {
|
|
29
|
+
if (normalizedModel.includes(key.replace(/[_-]/g, '-'))) {
|
|
30
|
+
return pricing;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return MODEL_PRICING['default'];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function calculateCost(
|
|
38
|
+
inputTokens: number,
|
|
39
|
+
outputTokens: number,
|
|
40
|
+
model: string
|
|
41
|
+
): number {
|
|
42
|
+
const pricing = getPricing(model);
|
|
43
|
+
const inputCost = (inputTokens / 1_000_000) * pricing.input;
|
|
44
|
+
const outputCost = (outputTokens / 1_000_000) * pricing.output;
|
|
45
|
+
return inputCost + outputCost;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function detectModel(command: string): string {
|
|
49
|
+
const lowerCommand = command.toLowerCase();
|
|
50
|
+
|
|
51
|
+
if (lowerCommand.includes('claude')) {
|
|
52
|
+
if (lowerCommand.includes('opus')) return 'claude-opus-4';
|
|
53
|
+
if (lowerCommand.includes('haiku')) return 'claude-3-haiku';
|
|
54
|
+
return 'claude-sonnet-4'; // default for claude commands
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (lowerCommand.includes('aider')) {
|
|
58
|
+
// Aider defaults vary, assume sonnet
|
|
59
|
+
return 'claude-sonnet-4';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (lowerCommand.includes('gpt-4')) return 'gpt-4o';
|
|
63
|
+
if (lowerCommand.includes('openai')) return 'gpt-4o';
|
|
64
|
+
|
|
65
|
+
return 'default';
|
|
66
|
+
}
|
package/src/tokenizer.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Simple token estimation without external dependencies
|
|
2
|
+
// Uses GPT-4 style tokenization rules (~4 chars per token)
|
|
3
|
+
|
|
4
|
+
const CHARS_PER_TOKEN = 4;
|
|
5
|
+
|
|
6
|
+
export function countTokens(text: string): number {
|
|
7
|
+
if (!text) return 0;
|
|
8
|
+
|
|
9
|
+
// More accurate estimation:
|
|
10
|
+
// - Split on whitespace and punctuation
|
|
11
|
+
// - Count special tokens for code
|
|
12
|
+
|
|
13
|
+
// Count words
|
|
14
|
+
const words = text.split(/\s+/).filter(w => w.length > 0);
|
|
15
|
+
|
|
16
|
+
// Estimate tokens (rough approximation)
|
|
17
|
+
// - Short words (1-4 chars) = 1 token
|
|
18
|
+
// - Medium words (5-8 chars) = 1-2 tokens
|
|
19
|
+
// - Long words (9+ chars) = 2+ tokens
|
|
20
|
+
// - Code symbols often get their own tokens
|
|
21
|
+
|
|
22
|
+
let tokens = 0;
|
|
23
|
+
|
|
24
|
+
for (const word of words) {
|
|
25
|
+
if (word.length <= 4) {
|
|
26
|
+
tokens += 1;
|
|
27
|
+
} else if (word.length <= 8) {
|
|
28
|
+
tokens += Math.ceil(word.length / 4);
|
|
29
|
+
} else {
|
|
30
|
+
tokens += Math.ceil(word.length / 3);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Add tokens for punctuation and special characters
|
|
35
|
+
const specialChars = text.match(/[{}()\[\]<>:;,."'`~!@#$%^&*+=|\\/?-]/g);
|
|
36
|
+
if (specialChars) {
|
|
37
|
+
tokens += Math.ceil(specialChars.length * 0.5);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Add tokens for newlines (often separate tokens)
|
|
41
|
+
const newlines = text.match(/\n/g);
|
|
42
|
+
if (newlines) {
|
|
43
|
+
tokens += newlines.length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return Math.max(1, Math.round(tokens));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function formatTokens(count: number): string {
|
|
50
|
+
if (count >= 1_000_000) {
|
|
51
|
+
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
52
|
+
}
|
|
53
|
+
if (count >= 1_000) {
|
|
54
|
+
return `${(count / 1_000).toFixed(1)}k`;
|
|
55
|
+
}
|
|
56
|
+
return count.toString();
|
|
57
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface TokenGuardOptions {
|
|
2
|
+
budget?: number;
|
|
3
|
+
costLimit?: number;
|
|
4
|
+
model?: string;
|
|
5
|
+
warnPercent: number;
|
|
6
|
+
quiet: boolean;
|
|
7
|
+
output?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ModelPricing {
|
|
11
|
+
input: number; // per 1M tokens
|
|
12
|
+
output: number; // per 1M tokens
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UsageStats {
|
|
16
|
+
inputTokens: number;
|
|
17
|
+
outputTokens: number;
|
|
18
|
+
totalTokens: number;
|
|
19
|
+
estimatedCost: number;
|
|
20
|
+
model: string;
|
|
21
|
+
startTime: Date;
|
|
22
|
+
endTime?: Date;
|
|
23
|
+
command: string;
|
|
24
|
+
budgetExceeded: boolean;
|
|
25
|
+
costExceeded: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Config {
|
|
29
|
+
defaultBudget?: number;
|
|
30
|
+
defaultCostLimit?: number;
|
|
31
|
+
warnPercent: number;
|
|
32
|
+
models: Record<string, ModelPricing>;
|
|
33
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*"],
|
|
17
|
+
"exclude": ["node_modules", "dist"]
|
|
18
|
+
}
|