tokendiet 1.4.0 → 1.6.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.
Potentially problematic release.
This version of tokendiet might be problematic. Click here for more details.
- package/dist/cli.js +3 -0
- package/dist/commands/start.d.ts +1 -0
- package/dist/commands/start.js +2 -0
- package/dist/main.js +1 -1
- package/dist/proxy/analytics.service.d.ts +1 -0
- package/dist/proxy/analytics.service.js +3 -0
- package/dist/proxy/cli-output.d.ts +2 -2
- package/dist/proxy/cli-output.js +17 -8
- package/dist/proxy/cloud-api.service.d.ts +1 -0
- package/dist/proxy/cloud-api.service.js +2 -1
- package/dist/proxy/proxy.controller.d.ts +1 -0
- package/dist/proxy/proxy.controller.js +19 -3
- package/dist/proxy/token-count.service.d.ts +19 -1
- package/dist/proxy/token-count.service.js +6 -1
- package/dist/token-stripper/provider-detector.util.d.ts +0 -5
- package/dist/token-stripper/provider-detector.util.js +0 -32
- package/dist/utils/message-stripper.d.ts +2 -0
- package/dist/utils/message-stripper.js +75 -29
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -26,6 +26,7 @@ program
|
|
|
26
26
|
.option('--cloud-url <url>', 'Cloud API URL', 'https://tokendiet-production.up.railway.app')
|
|
27
27
|
.option('--port <port>', 'Port to listen on (default: 3100)', '3100')
|
|
28
28
|
.option('--verbose', 'Show full NestJS logs')
|
|
29
|
+
.option('--dry-run', 'Disable all stripping (baseline measurement)')
|
|
29
30
|
.action(async (opts) => {
|
|
30
31
|
let orgToken = await resolveToken(opts.orgToken);
|
|
31
32
|
if (!orgToken) {
|
|
@@ -37,6 +38,7 @@ program
|
|
|
37
38
|
cloudUrl: opts.cloudUrl,
|
|
38
39
|
port: parseInt(opts.port, 10),
|
|
39
40
|
verbose: opts.verbose ?? false,
|
|
41
|
+
dryRun: opts.dryRun ?? false,
|
|
40
42
|
});
|
|
41
43
|
while (result.unauthorized) {
|
|
42
44
|
(0, utils_1.clearSavedToken)();
|
|
@@ -51,6 +53,7 @@ program
|
|
|
51
53
|
cloudUrl: opts.cloudUrl,
|
|
52
54
|
port: parseInt(opts.port, 10),
|
|
53
55
|
verbose: opts.verbose ?? false,
|
|
56
|
+
dryRun: opts.dryRun ?? false,
|
|
54
57
|
});
|
|
55
58
|
}
|
|
56
59
|
if (result.success) {
|
package/dist/commands/start.d.ts
CHANGED
package/dist/commands/start.js
CHANGED
|
@@ -125,6 +125,8 @@ async function startGateway(options) {
|
|
|
125
125
|
};
|
|
126
126
|
if (options.verbose)
|
|
127
127
|
env.TOKENDIET_VERBOSE = '1';
|
|
128
|
+
if (options.dryRun)
|
|
129
|
+
env.TOKENDIET_DRY_RUN = '1';
|
|
128
130
|
if (gatewayRunId)
|
|
129
131
|
env.TOKENDIET_GATEWAY_RUN_ID = gatewayRunId;
|
|
130
132
|
const claudeSettings = (0, utils_1.readJsonFile)(utils_1.claudeSettingsPath);
|
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@ async function bootstrap() {
|
|
|
10
10
|
const port = process.env.PORT ?? 3100;
|
|
11
11
|
await app.listen(port);
|
|
12
12
|
if (!process.env.TOKENDIET_VERBOSE) {
|
|
13
|
-
(0, cli_output_1.printBanner)(Number(port));
|
|
13
|
+
(0, cli_output_1.printBanner)(Number(port), process.env.TOKENDIET_DRY_RUN === '1');
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
bootstrap();
|
|
@@ -25,6 +25,7 @@ export declare class AnalyticsService {
|
|
|
25
25
|
private readonly gatewayRunId;
|
|
26
26
|
private readonly upstreamUrl;
|
|
27
27
|
private readonly analyticsPath;
|
|
28
|
+
private readonly isDryRun;
|
|
28
29
|
private sessionTotals;
|
|
29
30
|
constructor(config: ConfigService, cloudApiService: CloudApiService);
|
|
30
31
|
getSessionTotals(): SessionTotals;
|
|
@@ -71,6 +71,7 @@ let AnalyticsService = AnalyticsService_1 = class AnalyticsService {
|
|
|
71
71
|
gatewayRunId;
|
|
72
72
|
upstreamUrl;
|
|
73
73
|
analyticsPath;
|
|
74
|
+
isDryRun;
|
|
74
75
|
sessionTotals = { tokensSaved: 0, dollarSavings: 0, requestCount: 0 };
|
|
75
76
|
constructor(config, cloudApiService) {
|
|
76
77
|
this.config = config;
|
|
@@ -78,6 +79,7 @@ let AnalyticsService = AnalyticsService_1 = class AnalyticsService {
|
|
|
78
79
|
this.sessionId = this.config.get('TOKENDIET_SESSION_ID') ?? null;
|
|
79
80
|
this.gatewayRunId = this.config.get('TOKENDIET_GATEWAY_RUN_ID') ?? null;
|
|
80
81
|
this.upstreamUrl = this.config.get('PROXY_TARGET_URL') ?? DEFAULT_UPSTREAM;
|
|
82
|
+
this.isDryRun = this.config.get('TOKENDIET_DRY_RUN') === '1';
|
|
81
83
|
this.analyticsPath = ANALYTICS_PATH;
|
|
82
84
|
}
|
|
83
85
|
getSessionTotals() {
|
|
@@ -99,6 +101,7 @@ let AnalyticsService = AnalyticsService_1 = class AnalyticsService {
|
|
|
99
101
|
strippingApplied: this.buildStrippingEntries(preStripResult, usage),
|
|
100
102
|
originalRequestTokenCount: preStripResult.input_tokens,
|
|
101
103
|
countMethod: preStripResult.countMethod,
|
|
104
|
+
isDryRun: this.isDryRun,
|
|
102
105
|
upstreamResponseUsage: {
|
|
103
106
|
input_tokens: usage.input_tokens,
|
|
104
107
|
output_tokens: usage.output_tokens ?? 0,
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export declare function printBanner(port: number): void;
|
|
1
|
+
export declare function printBanner(port: number, dryRun?: boolean): void;
|
|
2
2
|
interface Totals {
|
|
3
3
|
tokensSaved: number;
|
|
4
4
|
dollarSavings: number;
|
|
5
5
|
requestCount: number;
|
|
6
6
|
}
|
|
7
|
-
export declare function printRequestSavings(model: string, tokensSaved: number, costSaved: number, totals: Totals): void;
|
|
7
|
+
export declare function printRequestSavings(model: string, tokensSaved: number, costSaved: number, originalTokenCount: number, totals: Totals, dryRun?: boolean): void;
|
|
8
8
|
export declare function _reset(): void;
|
|
9
9
|
export {};
|
package/dist/proxy/cli-output.js
CHANGED
|
@@ -11,12 +11,14 @@ const MAX_VISIBLE = 5;
|
|
|
11
11
|
const COL_MODEL = 30;
|
|
12
12
|
const COL_TOKENS = 12;
|
|
13
13
|
const COL_COST = 10;
|
|
14
|
-
const
|
|
14
|
+
const COL_PCT = 6;
|
|
15
|
+
const TABLE_WIDTH = 2 + COL_MODEL + 2 + COL_TOKENS + 2 + COL_COST + 2 + COL_PCT + 2;
|
|
15
16
|
const recentRequests = [];
|
|
16
17
|
let linesWritten = 0;
|
|
17
|
-
function printBanner(port) {
|
|
18
|
+
function printBanner(port, dryRun = false) {
|
|
18
19
|
console.log('');
|
|
19
|
-
|
|
20
|
+
const tag = dryRun ? chalk_1.default.bold.yellow(' [DRY RUN]') : '';
|
|
21
|
+
console.log(chalk_1.default.bold.green(' TokenDiet') + tag + ' running on port ' + chalk_1.default.cyan(String(port)));
|
|
20
22
|
console.log('');
|
|
21
23
|
}
|
|
22
24
|
function formatNumber(n) {
|
|
@@ -34,7 +36,8 @@ function formatHeader() {
|
|
|
34
36
|
return chalk_1.default.dim(' ' +
|
|
35
37
|
'Model'.padEnd(COL_MODEL) + ' ' +
|
|
36
38
|
'Tokens saved'.padStart(COL_TOKENS) + ' ' +
|
|
37
|
-
'Cost saved'.padStart(COL_COST)
|
|
39
|
+
'Cost saved'.padStart(COL_COST) + ' ' +
|
|
40
|
+
'%'.padStart(COL_PCT));
|
|
38
41
|
}
|
|
39
42
|
function formatSeparator() {
|
|
40
43
|
return chalk_1.default.dim(' ' + '\u2500'.repeat(TABLE_WIDTH - 2));
|
|
@@ -43,12 +46,14 @@ function formatRequest(entry, dim) {
|
|
|
43
46
|
const model = truncateModel(entry.model);
|
|
44
47
|
const tokens = formatNumber(entry.tokensSaved);
|
|
45
48
|
const cost = formatCost(entry.costSaved);
|
|
49
|
+
const pct = entry.pctSaved.toFixed(2) + '%';
|
|
46
50
|
const wrap = dim ? chalk_1.default.dim : (s) => s;
|
|
47
51
|
const arrow = dim ? chalk_1.default.dim('\u2193 ') : chalk_1.default.green('\u2193 ');
|
|
48
52
|
return (arrow +
|
|
49
53
|
wrap(model.padEnd(COL_MODEL)) + ' ' +
|
|
50
54
|
(dim ? chalk_1.default.dim : chalk_1.default.yellow)(tokens.padStart(COL_TOKENS)) + ' ' +
|
|
51
|
-
(dim ? chalk_1.default.dim : chalk_1.default.green)(cost.padStart(COL_COST))
|
|
55
|
+
(dim ? chalk_1.default.dim : chalk_1.default.green)(cost.padStart(COL_COST)) + ' ' +
|
|
56
|
+
(dim ? chalk_1.default.dim : chalk_1.default.cyan)(pct.padStart(COL_PCT)));
|
|
52
57
|
}
|
|
53
58
|
function formatTotals(totals) {
|
|
54
59
|
const label = `Total (${totals.requestCount} reqs)`;
|
|
@@ -58,13 +63,17 @@ function formatTotals(totals) {
|
|
|
58
63
|
chalk_1.default.yellow(tokens.padStart(COL_TOKENS)) + ' ' +
|
|
59
64
|
chalk_1.default.green(cost.padStart(COL_COST)));
|
|
60
65
|
}
|
|
61
|
-
function printRequestSavings(model, tokensSaved, costSaved, totals) {
|
|
62
|
-
|
|
66
|
+
function printRequestSavings(model, tokensSaved, costSaved, originalTokenCount, totals, dryRun = false) {
|
|
67
|
+
const pctSaved = originalTokenCount > 0 ? (tokensSaved / originalTokenCount) * 100 : 0;
|
|
68
|
+
recentRequests.unshift({ model, tokensSaved, costSaved, pctSaved });
|
|
63
69
|
if (recentRequests.length > MAX_VISIBLE)
|
|
64
70
|
recentRequests.length = MAX_VISIBLE;
|
|
65
71
|
if (linesWritten > 0) {
|
|
66
72
|
process.stdout.write(`\x1b[${linesWritten}A\x1b[0J`);
|
|
67
73
|
}
|
|
74
|
+
if (dryRun) {
|
|
75
|
+
console.log(chalk_1.default.yellow.bold(' [DRY RUN] ') + chalk_1.default.dim('Stripping disabled — baseline measurement'));
|
|
76
|
+
}
|
|
68
77
|
console.log(formatHeader());
|
|
69
78
|
console.log(formatSeparator());
|
|
70
79
|
for (let i = 0; i < recentRequests.length; i++) {
|
|
@@ -72,7 +81,7 @@ function printRequestSavings(model, tokensSaved, costSaved, totals) {
|
|
|
72
81
|
}
|
|
73
82
|
console.log(formatSeparator());
|
|
74
83
|
console.log(formatTotals(totals));
|
|
75
|
-
linesWritten = recentRequests.length + 4;
|
|
84
|
+
linesWritten = recentRequests.length + 4 + (dryRun ? 1 : 0);
|
|
76
85
|
}
|
|
77
86
|
function _reset() {
|
|
78
87
|
recentRequests.length = 0;
|
|
@@ -40,7 +40,8 @@ let CloudApiService = CloudApiService_1 = class CloudApiService {
|
|
|
40
40
|
this.logger.warn(`Cloud API returned ${response.status}`);
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
const body = (await response.json());
|
|
44
|
+
return body;
|
|
44
45
|
}
|
|
45
46
|
catch (err) {
|
|
46
47
|
this.logger.warn(`Cloud API request failed: ${err.message}`);
|
|
@@ -19,6 +19,7 @@ export declare class ProxyController {
|
|
|
19
19
|
private readonly analyticsService;
|
|
20
20
|
private readonly logger;
|
|
21
21
|
private readonly proxy;
|
|
22
|
+
private readonly isDryRun;
|
|
22
23
|
constructor(config: ConfigService, tokenCountService: TokenCountService, analyticsService: AnalyticsService);
|
|
23
24
|
messages(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
24
25
|
countTokens(req: Request, res: Response, next: NextFunction): Promise<void>;
|
|
@@ -81,10 +81,12 @@ let ProxyController = ProxyController_1 = class ProxyController {
|
|
|
81
81
|
analyticsService;
|
|
82
82
|
logger = new common_1.Logger(ProxyController_1.name);
|
|
83
83
|
proxy;
|
|
84
|
+
isDryRun;
|
|
84
85
|
constructor(config, tokenCountService, analyticsService) {
|
|
85
86
|
this.config = config;
|
|
86
87
|
this.tokenCountService = tokenCountService;
|
|
87
88
|
this.analyticsService = analyticsService;
|
|
89
|
+
this.isDryRun = this.config.get('TOKENDIET_DRY_RUN') === '1';
|
|
88
90
|
this.proxy = (0, http_proxy_middleware_1.createProxyMiddleware)({
|
|
89
91
|
target: this.config.get('PROXY_TARGET_URL') ?? DEFAULT_UPSTREAM,
|
|
90
92
|
selfHandleResponse: true,
|
|
@@ -95,8 +97,11 @@ let ProxyController = ProxyController_1 = class ProxyController {
|
|
|
95
97
|
req.__requestedAt = new Date().toISOString();
|
|
96
98
|
req.__requestPath = req.originalUrl ?? req.path;
|
|
97
99
|
req.__preStripCountPromise = this.tokenCountService.count(req.__originalBody);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
+
if (!this.isDryRun) {
|
|
101
|
+
req.body = (0, message_stripper_1.stripMessageBody)(req.body);
|
|
102
|
+
req.body = (0, system_tools_stripper_1.stripSystemAndTools)(req.body);
|
|
103
|
+
}
|
|
104
|
+
req.__localDelta = this.tokenCountService.countLocalDelta(req.__originalBody, req.body);
|
|
100
105
|
(0, http_proxy_middleware_1.fixRequestBody)(_proxyReq, req);
|
|
101
106
|
},
|
|
102
107
|
proxyRes: (0, http_proxy_middleware_1.responseInterceptor)(async (responseBuffer, proxyRes, req) => {
|
|
@@ -120,6 +125,17 @@ let ProxyController = ProxyController_1 = class ProxyController {
|
|
|
120
125
|
}
|
|
121
126
|
if (!preStripResult)
|
|
122
127
|
return responseBuffer;
|
|
128
|
+
this.logger.debug(`countMethod: ${preStripResult.countMethod}`);
|
|
129
|
+
const localDelta = req.__localDelta;
|
|
130
|
+
if (preStripResult.countMethod === 'local' && localDelta) {
|
|
131
|
+
const usageTotal = usage.input_tokens
|
|
132
|
+
+ (usage.cache_read_input_tokens ?? 0)
|
|
133
|
+
+ (usage.cache_creation_input_tokens ?? 0);
|
|
134
|
+
preStripResult = {
|
|
135
|
+
input_tokens: usageTotal + localDelta.delta,
|
|
136
|
+
countMethod: 'delta',
|
|
137
|
+
};
|
|
138
|
+
}
|
|
123
139
|
try {
|
|
124
140
|
const cloudResponse = await this.analyticsService.record({
|
|
125
141
|
model,
|
|
@@ -132,7 +148,7 @@ let ProxyController = ProxyController_1 = class ProxyController {
|
|
|
132
148
|
requestedAt,
|
|
133
149
|
});
|
|
134
150
|
if (cloudResponse) {
|
|
135
|
-
(0, cli_output_1.printRequestSavings)(model, cloudResponse.savings?.tokensSaved ?? 0, cloudResponse.savings?.costSaved ?? 0, this.analyticsService.getSessionTotals());
|
|
151
|
+
(0, cli_output_1.printRequestSavings)(model, cloudResponse.savings?.tokensSaved ?? 0, cloudResponse.savings?.costSaved ?? 0, cloudResponse.originalRequestTokenCount ?? 0, this.analyticsService.getSessionTotals(), this.isDryRun);
|
|
136
152
|
}
|
|
137
153
|
}
|
|
138
154
|
catch (err) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConfigService } from '@nestjs/config';
|
|
2
|
-
export type CountMethod = 'api' | 'local';
|
|
2
|
+
export type CountMethod = 'api' | 'local' | 'delta';
|
|
3
3
|
export interface TokenCountResult {
|
|
4
4
|
input_tokens: number;
|
|
5
5
|
countMethod: CountMethod;
|
|
@@ -28,6 +28,24 @@ export declare class TokenCountService {
|
|
|
28
28
|
system?: any;
|
|
29
29
|
tools?: any;
|
|
30
30
|
}): Promise<TokenCountResult>;
|
|
31
|
+
countLocalDelta(originalBody: {
|
|
32
|
+
messages?: Array<{
|
|
33
|
+
role: string;
|
|
34
|
+
content: string | any[];
|
|
35
|
+
}>;
|
|
36
|
+
system?: any;
|
|
37
|
+
tools?: any;
|
|
38
|
+
}, strippedBody: {
|
|
39
|
+
messages?: Array<{
|
|
40
|
+
role: string;
|
|
41
|
+
content: string | any[];
|
|
42
|
+
}>;
|
|
43
|
+
system?: any;
|
|
44
|
+
tools?: any;
|
|
45
|
+
}): {
|
|
46
|
+
delta: number;
|
|
47
|
+
countMethod: 'delta';
|
|
48
|
+
};
|
|
31
49
|
countLocally(body: {
|
|
32
50
|
messages?: Array<{
|
|
33
51
|
role: string;
|
|
@@ -65,9 +65,14 @@ let TokenCountService = TokenCountService_1 = class TokenCountService {
|
|
|
65
65
|
params.tools = sanitized.tools;
|
|
66
66
|
}
|
|
67
67
|
const result = await this.client.messages.countTokens(params);
|
|
68
|
-
this.logger.
|
|
68
|
+
this.logger.debug(`count_tokens response: ${JSON.stringify(result)}`);
|
|
69
69
|
return { input_tokens: result.input_tokens, countMethod: 'api' };
|
|
70
70
|
}
|
|
71
|
+
countLocalDelta(originalBody, strippedBody) {
|
|
72
|
+
const originalCount = this.countLocally(originalBody).input_tokens;
|
|
73
|
+
const strippedCount = this.countLocally(strippedBody).input_tokens;
|
|
74
|
+
return { delta: Math.max(0, originalCount - strippedCount), countMethod: 'delta' };
|
|
75
|
+
}
|
|
71
76
|
countLocally(body) {
|
|
72
77
|
const { encode } = require('gpt-tokenizer');
|
|
73
78
|
let total = 0;
|
|
@@ -9,8 +9,3 @@ export interface LLMProvider {
|
|
|
9
9
|
modelInUrlPath?: boolean;
|
|
10
10
|
modelUrlPattern?: RegExp;
|
|
11
11
|
}
|
|
12
|
-
export declare class ProviderDetector {
|
|
13
|
-
detect(hostname: string, path: string): LLMProvider | null;
|
|
14
|
-
isLLMHost(hostname: string): boolean;
|
|
15
|
-
getAllHosts(): string[];
|
|
16
|
-
}
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
-
};
|
|
8
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.ProviderDetector = void 0;
|
|
10
|
-
const common_1 = require("@nestjs/common");
|
|
11
3
|
const KNOWN_PROVIDERS = [
|
|
12
4
|
{
|
|
13
5
|
name: 'anthropic',
|
|
@@ -93,28 +85,4 @@ const KNOWN_PROVIDERS = [
|
|
|
93
85
|
modelUrlPattern: /\/v1\/engines\/([^/]+)\/completions/,
|
|
94
86
|
},
|
|
95
87
|
];
|
|
96
|
-
let ProviderDetector = class ProviderDetector {
|
|
97
|
-
detect(hostname, path) {
|
|
98
|
-
for (const provider of KNOWN_PROVIDERS) {
|
|
99
|
-
if (!hostname.includes(provider.host)) {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
const pathMatches = provider.requestPaths.some((validPath) => path.startsWith(validPath));
|
|
103
|
-
if (pathMatches) {
|
|
104
|
-
return provider;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
isLLMHost(hostname) {
|
|
110
|
-
return KNOWN_PROVIDERS.some((p) => hostname.includes(p.host));
|
|
111
|
-
}
|
|
112
|
-
getAllHosts() {
|
|
113
|
-
return KNOWN_PROVIDERS.map((p) => p.host);
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
exports.ProviderDetector = ProviderDetector;
|
|
117
|
-
exports.ProviderDetector = ProviderDetector = __decorate([
|
|
118
|
-
(0, common_1.Injectable)()
|
|
119
|
-
], ProviderDetector);
|
|
120
88
|
//# sourceMappingURL=provider-detector.util.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare function isCatNFormatted(text: string): boolean;
|
|
2
|
+
export declare function stripReadPrefixes(text: string): string;
|
|
1
3
|
export declare function normalizeWhitespace(text: string): string;
|
|
2
4
|
export declare function normalizeToolResultWhitespace(text: string): string;
|
|
3
5
|
export declare function stripMessageBody(body: unknown): unknown;
|
|
@@ -1,8 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isCatNFormatted = isCatNFormatted;
|
|
4
|
+
exports.stripReadPrefixes = stripReadPrefixes;
|
|
3
5
|
exports.normalizeWhitespace = normalizeWhitespace;
|
|
4
6
|
exports.normalizeToolResultWhitespace = normalizeToolResultWhitespace;
|
|
5
7
|
exports.stripMessageBody = stripMessageBody;
|
|
8
|
+
const CAT_N_PREFIX = /^[ \t]*\d+[\t→]/;
|
|
9
|
+
function isCatNFormatted(text) {
|
|
10
|
+
const lines = text.split('\n');
|
|
11
|
+
let checked = 0;
|
|
12
|
+
for (const line of lines) {
|
|
13
|
+
if (line.trim() === '')
|
|
14
|
+
continue;
|
|
15
|
+
if (!CAT_N_PREFIX.test(line))
|
|
16
|
+
return false;
|
|
17
|
+
checked++;
|
|
18
|
+
if (checked >= 3)
|
|
19
|
+
break;
|
|
20
|
+
}
|
|
21
|
+
return checked > 0;
|
|
22
|
+
}
|
|
23
|
+
function stripReadPrefixes(text) {
|
|
24
|
+
if (!isCatNFormatted(text))
|
|
25
|
+
return normalizeToolResultWhitespace(text);
|
|
26
|
+
return text
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((line) => line.replace(CAT_N_PREFIX, ''))
|
|
29
|
+
.join('\n')
|
|
30
|
+
.replace(/[ \t]+$/gm, '')
|
|
31
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
32
|
+
.replace(/^\n+/, '')
|
|
33
|
+
.replace(/\n+$/, '');
|
|
34
|
+
}
|
|
6
35
|
function normalizeWhitespace(text) {
|
|
7
36
|
return text
|
|
8
37
|
.replace(/[ \t]+/g, ' ')
|
|
@@ -22,7 +51,9 @@ function stripMessageBody(body) {
|
|
|
22
51
|
const obj = body;
|
|
23
52
|
if (!Array.isArray(obj.messages))
|
|
24
53
|
return body;
|
|
25
|
-
|
|
54
|
+
let catNStripped = 0;
|
|
55
|
+
let toolResultsTotal = 0;
|
|
56
|
+
const result = {
|
|
26
57
|
...obj,
|
|
27
58
|
messages: obj.messages.map((msg) => {
|
|
28
59
|
if (!msg || typeof msg !== 'object')
|
|
@@ -32,40 +63,55 @@ function stripMessageBody(body) {
|
|
|
32
63
|
return { ...m, content: normalizeWhitespace(m.content) };
|
|
33
64
|
}
|
|
34
65
|
if (Array.isArray(m.content)) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
return
|
|
66
|
+
const normalized = m.content.map((block) => {
|
|
67
|
+
if (!block || typeof block !== 'object')
|
|
68
|
+
return block;
|
|
69
|
+
const b = block;
|
|
70
|
+
if (b.type === 'text') {
|
|
71
|
+
const text = normalizeWhitespace(b.text);
|
|
72
|
+
if (text === '')
|
|
73
|
+
return null;
|
|
74
|
+
return { ...b, text };
|
|
75
|
+
}
|
|
76
|
+
if (b.type === 'tool_result') {
|
|
77
|
+
toolResultsTotal++;
|
|
78
|
+
if (typeof b.content === 'string') {
|
|
79
|
+
const wasCatN = isCatNFormatted(b.content);
|
|
80
|
+
if (wasCatN)
|
|
81
|
+
catNStripped++;
|
|
82
|
+
return { ...b, content: stripReadPrefixes(b.content) };
|
|
43
83
|
}
|
|
44
|
-
if (b.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
61
|
-
}
|
|
84
|
+
if (Array.isArray(b.content)) {
|
|
85
|
+
return {
|
|
86
|
+
...b,
|
|
87
|
+
content: b.content.map((sub) => {
|
|
88
|
+
if (sub &&
|
|
89
|
+
typeof sub === 'object' &&
|
|
90
|
+
sub.type === 'text' &&
|
|
91
|
+
typeof sub.text === 'string') {
|
|
92
|
+
const wasCatN = isCatNFormatted(sub.text);
|
|
93
|
+
if (wasCatN)
|
|
94
|
+
catNStripped++;
|
|
95
|
+
return { ...sub, text: stripReadPrefixes(sub.text) };
|
|
96
|
+
}
|
|
97
|
+
return sub;
|
|
98
|
+
}),
|
|
99
|
+
};
|
|
62
100
|
}
|
|
63
|
-
|
|
64
|
-
|
|
101
|
+
}
|
|
102
|
+
return block;
|
|
103
|
+
}).filter((block) => block !== null);
|
|
104
|
+
return {
|
|
105
|
+
...m,
|
|
106
|
+
content: normalized,
|
|
65
107
|
};
|
|
66
108
|
}
|
|
67
109
|
return msg;
|
|
68
110
|
}),
|
|
69
111
|
};
|
|
112
|
+
if (toolResultsTotal > 0) {
|
|
113
|
+
console.log(`[stripMessageBody] tool_results: ${toolResultsTotal}, cat-n stripped: ${catNStripped}`);
|
|
114
|
+
}
|
|
115
|
+
return result;
|
|
70
116
|
}
|
|
71
117
|
//# sourceMappingURL=message-stripper.js.map
|