@curatedmcp/tokenshield-core 0.2.1 → 0.3.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/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/server.js +27 -0
- package/dist/server.js.map +1 -1
- package/dist/telemetry.d.ts +29 -0
- package/dist/telemetry.js +218 -0
- package/dist/telemetry.js.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +11 -0
- package/src/server.ts +26 -0
- package/src/telemetry.ts +265 -0
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export { StreamUsageAccumulator, usageFromJson } from "./proxy/usage.js";
|
|
|
8
8
|
export { handleAnthropicRequest, setProcessorEnabled, getProcessorEnabledIds, getResponseCacheStats, } from "./proxy/anthropic-passthrough.js";
|
|
9
9
|
export { providerForPath, anthropic } from "./providers/registry.js";
|
|
10
10
|
export type { Provider, Conversation, ConvMessage, ConvBlock, ProviderId } from "./providers/types.js";
|
|
11
|
+
export { telemetry, Telemetry, isTelemetryEnabled, setTelemetryEnabled, isFirstRun, markFirstRunComplete, firstRunBanner, getAnonId, } from "./telemetry.js";
|
|
12
|
+
export type { TelemetryRecord } from "./telemetry.js";
|
|
11
13
|
export { Pipeline } from "./processors/pipeline.js";
|
|
12
14
|
export { conversationDedup } from "./processors/conversation-dedup.js";
|
|
13
15
|
export { ResponseCache } from "./processors/response-cache.js";
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ export { SSEParser } from "./proxy/sse.js";
|
|
|
5
5
|
export { StreamUsageAccumulator, usageFromJson } from "./proxy/usage.js";
|
|
6
6
|
export { handleAnthropicRequest, setProcessorEnabled, getProcessorEnabledIds, getResponseCacheStats, } from "./proxy/anthropic-passthrough.js";
|
|
7
7
|
export { providerForPath, anthropic } from "./providers/registry.js";
|
|
8
|
+
export { telemetry, Telemetry, isTelemetryEnabled, setTelemetryEnabled, isFirstRun, markFirstRunComplete, firstRunBanner, getAnonId, } from "./telemetry.js";
|
|
8
9
|
export { Pipeline } from "./processors/pipeline.js";
|
|
9
10
|
export { conversationDedup } from "./processors/conversation-dedup.js";
|
|
10
11
|
export { ResponseCache } from "./processors/response-cache.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEnD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAErE,OAAO,EACL,SAAS,EACT,SAAS,EACT,kBAAkB,EAClB,mBAAmB,EACnB,UAAU,EACV,oBAAoB,EACpB,cAAc,EACd,SAAS,GACV,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,oCAAoC,CAAC;AACvE,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { handleAnthropicRequest } from "./proxy/anthropic-passthrough.js";
|
|
3
3
|
import { Ledger } from "./ledger.js";
|
|
4
|
+
import { telemetry } from "./telemetry.js";
|
|
4
5
|
export function defaultConfig(overrides = {}) {
|
|
5
6
|
const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".";
|
|
6
7
|
return {
|
|
@@ -29,6 +30,7 @@ async function closeServer(server) {
|
|
|
29
30
|
}
|
|
30
31
|
export async function start(opts) {
|
|
31
32
|
const ledger = new Ledger(opts.config.ledgerPath);
|
|
33
|
+
const isTeamDeployment = opts.config.bind === "0.0.0.0";
|
|
32
34
|
const sink = (r) => {
|
|
33
35
|
try {
|
|
34
36
|
ledger.record(r);
|
|
@@ -36,6 +38,30 @@ export async function start(opts) {
|
|
|
36
38
|
catch {
|
|
37
39
|
// ledger errors must never break the request path
|
|
38
40
|
}
|
|
41
|
+
try {
|
|
42
|
+
// Approximate byte counts from token estimates (industry rule-of-thumb)
|
|
43
|
+
const tokensIn = r.usageRaw.inputTokens + r.usageRaw.cacheReadInputTokens;
|
|
44
|
+
const tokensOut = r.usageSent.inputTokens + r.usageSent.cacheReadInputTokens;
|
|
45
|
+
const TOKEN_TO_BYTE = 3.5;
|
|
46
|
+
const bytesIn = Math.round(tokensIn * TOKEN_TO_BYTE);
|
|
47
|
+
const bytesOut = Math.round(tokensOut * TOKEN_TO_BYTE);
|
|
48
|
+
telemetry.record({
|
|
49
|
+
bytesIn,
|
|
50
|
+
bytesOut,
|
|
51
|
+
bytesSaved: Math.max(0, bytesIn - bytesOut),
|
|
52
|
+
inputTokens: r.usageRaw.inputTokens,
|
|
53
|
+
outputTokens: r.usageRaw.outputTokens,
|
|
54
|
+
dollarsEstimate: r.dollarsRaw,
|
|
55
|
+
dollarsSaved: r.dollarsSaved,
|
|
56
|
+
provider: "anthropic",
|
|
57
|
+
model: r.model,
|
|
58
|
+
client: null,
|
|
59
|
+
teamDeployment: isTeamDeployment,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// telemetry must never break the request path
|
|
64
|
+
}
|
|
39
65
|
opts.onRecord?.(r);
|
|
40
66
|
};
|
|
41
67
|
const proxy = createServer((req, res) => {
|
|
@@ -118,6 +144,7 @@ export async function start(opts) {
|
|
|
118
144
|
ledger,
|
|
119
145
|
close: async () => {
|
|
120
146
|
clearInterval(retentionInterval);
|
|
147
|
+
telemetry.stop();
|
|
121
148
|
await Promise.all([closeServer(proxy), closeServer(dashboard)]);
|
|
122
149
|
ledger.close();
|
|
123
150
|
},
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA2C,MAAM,WAAW,CAAC;AAElF,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare function isTelemetryEnabled(): boolean;
|
|
2
|
+
export declare function setTelemetryEnabled(on: boolean): void;
|
|
3
|
+
export declare function getAnonId(): string;
|
|
4
|
+
export declare function isFirstRun(): boolean;
|
|
5
|
+
export declare function markFirstRunComplete(): void;
|
|
6
|
+
export declare function firstRunBanner(): string;
|
|
7
|
+
export interface TelemetryRecord {
|
|
8
|
+
bytesIn: number;
|
|
9
|
+
bytesOut: number;
|
|
10
|
+
bytesSaved: number;
|
|
11
|
+
inputTokens: number;
|
|
12
|
+
outputTokens: number;
|
|
13
|
+
dollarsEstimate: number;
|
|
14
|
+
dollarsSaved: number;
|
|
15
|
+
provider: "anthropic" | "openai" | "gemini" | null;
|
|
16
|
+
model: string | null;
|
|
17
|
+
client: string | null;
|
|
18
|
+
teamDeployment: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare class Telemetry {
|
|
21
|
+
private counters;
|
|
22
|
+
private timer;
|
|
23
|
+
private flushing;
|
|
24
|
+
start(): void;
|
|
25
|
+
stop(): void;
|
|
26
|
+
record(r: TelemetryRecord): void;
|
|
27
|
+
flush(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare const telemetry: Telemetry;
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { hostname, platform, userInfo } from "node:os";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
const ENDPOINT = process.env["TOKENSHIELD_TELEMETRY_URL"] ?? "https://curatedmcp.com/api/v1/tokenshield/telemetry";
|
|
6
|
+
// Flush every N requests OR every M minutes, whichever first. Keeps the
|
|
7
|
+
// network footprint tiny and ensures dev sessions still report at the end.
|
|
8
|
+
const FLUSH_EVERY_REQUESTS = 50;
|
|
9
|
+
const FLUSH_EVERY_MS = 5 * 60 * 1000;
|
|
10
|
+
function telemetryDir() {
|
|
11
|
+
const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".";
|
|
12
|
+
return join(home, ".tokenshield");
|
|
13
|
+
}
|
|
14
|
+
function settingsFile() {
|
|
15
|
+
return join(telemetryDir(), "settings.json");
|
|
16
|
+
}
|
|
17
|
+
function loadSettings() {
|
|
18
|
+
const file = settingsFile();
|
|
19
|
+
if (existsSync(file)) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = readFileSync(file, "utf8");
|
|
22
|
+
const parsed = JSON.parse(raw);
|
|
23
|
+
return {
|
|
24
|
+
telemetry: parsed.telemetry === "off" ? "off" : "on",
|
|
25
|
+
anonId: parsed.anonId ?? generateAnonId(),
|
|
26
|
+
firstRunCompleted: parsed.firstRunCompleted ?? false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// fall through
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
telemetry: "on",
|
|
35
|
+
anonId: generateAnonId(),
|
|
36
|
+
firstRunCompleted: false,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function saveSettings(s) {
|
|
40
|
+
const dir = telemetryDir();
|
|
41
|
+
if (!existsSync(dir))
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
writeFileSync(settingsFile(), JSON.stringify(s, null, 2), "utf8");
|
|
44
|
+
}
|
|
45
|
+
function generateAnonId() {
|
|
46
|
+
// Deterministic per-machine: sha256 of hostname + username.
|
|
47
|
+
// Cannot be reversed to a person; cannot be cross-correlated with other
|
|
48
|
+
// CuratedMCP products without explicit account linking.
|
|
49
|
+
const seed = `${hostname()}::${userInfo().username}::tokenshield`;
|
|
50
|
+
return createHash("sha256").update(seed).digest("hex");
|
|
51
|
+
}
|
|
52
|
+
export function isTelemetryEnabled() {
|
|
53
|
+
// Hard kill switches — these win over settings file
|
|
54
|
+
if (process.env["TOKENSHIELD_TELEMETRY"] === "0" || process.env["TOKENSHIELD_TELEMETRY"] === "off")
|
|
55
|
+
return false;
|
|
56
|
+
if (process.env["DO_NOT_TRACK"] === "1")
|
|
57
|
+
return false;
|
|
58
|
+
if (process.env["CI"] === "true")
|
|
59
|
+
return false;
|
|
60
|
+
const s = loadSettings();
|
|
61
|
+
return s.telemetry === "on";
|
|
62
|
+
}
|
|
63
|
+
export function setTelemetryEnabled(on) {
|
|
64
|
+
const s = loadSettings();
|
|
65
|
+
s.telemetry = on ? "on" : "off";
|
|
66
|
+
s.firstRunCompleted = true;
|
|
67
|
+
saveSettings(s);
|
|
68
|
+
}
|
|
69
|
+
export function getAnonId() {
|
|
70
|
+
return loadSettings().anonId;
|
|
71
|
+
}
|
|
72
|
+
export function isFirstRun() {
|
|
73
|
+
return !loadSettings().firstRunCompleted;
|
|
74
|
+
}
|
|
75
|
+
export function markFirstRunComplete() {
|
|
76
|
+
const s = loadSettings();
|
|
77
|
+
s.firstRunCompleted = true;
|
|
78
|
+
saveSettings(s);
|
|
79
|
+
}
|
|
80
|
+
export function firstRunBanner() {
|
|
81
|
+
return [
|
|
82
|
+
"",
|
|
83
|
+
" ┌──────────────────────────────────────────────────────────────────┐",
|
|
84
|
+
" │ TokenShield collects anonymous usage stats by default: │",
|
|
85
|
+
" │ • Aggregate token counts and $ saved │",
|
|
86
|
+
" │ • CLI version, Node version, OS │",
|
|
87
|
+
" │ • Provider (anthropic/openai/gemini) + most-used model │",
|
|
88
|
+
" │ │",
|
|
89
|
+
" │ Never sent: prompts, responses, file contents, API keys, │",
|
|
90
|
+
" │ IP address, hostname, username, file paths. │",
|
|
91
|
+
" │ │",
|
|
92
|
+
" │ Disable any time: tokenshield telemetry off │",
|
|
93
|
+
" │ Or via env: TOKENSHIELD_TELEMETRY=0 (or DO_NOT_TRACK=1) │",
|
|
94
|
+
" │ Source: https://github.com/oneprofile-dev/tokenshield │",
|
|
95
|
+
" └──────────────────────────────────────────────────────────────────┘",
|
|
96
|
+
"",
|
|
97
|
+
].join("\n");
|
|
98
|
+
}
|
|
99
|
+
function emptyCounters() {
|
|
100
|
+
return {
|
|
101
|
+
requests: 0,
|
|
102
|
+
bytesIn: 0,
|
|
103
|
+
bytesOut: 0,
|
|
104
|
+
bytesSaved: 0,
|
|
105
|
+
inputTokens: 0,
|
|
106
|
+
outputTokens: 0,
|
|
107
|
+
dollarsEstimate: 0,
|
|
108
|
+
dollarsSaved: 0,
|
|
109
|
+
modelCounts: new Map(),
|
|
110
|
+
provider: null,
|
|
111
|
+
client: null,
|
|
112
|
+
teamDeployment: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
export class Telemetry {
|
|
116
|
+
counters = emptyCounters();
|
|
117
|
+
timer = null;
|
|
118
|
+
flushing = false;
|
|
119
|
+
start() {
|
|
120
|
+
if (!isTelemetryEnabled())
|
|
121
|
+
return;
|
|
122
|
+
if (this.timer)
|
|
123
|
+
return;
|
|
124
|
+
this.timer = setInterval(() => {
|
|
125
|
+
void this.flush();
|
|
126
|
+
}, FLUSH_EVERY_MS);
|
|
127
|
+
this.timer.unref?.();
|
|
128
|
+
}
|
|
129
|
+
stop() {
|
|
130
|
+
if (this.timer) {
|
|
131
|
+
clearInterval(this.timer);
|
|
132
|
+
this.timer = null;
|
|
133
|
+
}
|
|
134
|
+
// Best-effort final flush — fire and forget
|
|
135
|
+
void this.flush();
|
|
136
|
+
}
|
|
137
|
+
record(r) {
|
|
138
|
+
if (!isTelemetryEnabled())
|
|
139
|
+
return;
|
|
140
|
+
const c = this.counters;
|
|
141
|
+
c.requests += 1;
|
|
142
|
+
c.bytesIn += r.bytesIn;
|
|
143
|
+
c.bytesOut += r.bytesOut;
|
|
144
|
+
c.bytesSaved += r.bytesSaved;
|
|
145
|
+
c.inputTokens += r.inputTokens;
|
|
146
|
+
c.outputTokens += r.outputTokens;
|
|
147
|
+
c.dollarsEstimate += r.dollarsEstimate;
|
|
148
|
+
c.dollarsSaved += r.dollarsSaved;
|
|
149
|
+
if (r.provider)
|
|
150
|
+
c.provider = r.provider;
|
|
151
|
+
if (r.client)
|
|
152
|
+
c.client = r.client;
|
|
153
|
+
if (r.teamDeployment)
|
|
154
|
+
c.teamDeployment = true;
|
|
155
|
+
if (r.model)
|
|
156
|
+
c.modelCounts.set(r.model, (c.modelCounts.get(r.model) ?? 0) + 1);
|
|
157
|
+
if (c.requests >= FLUSH_EVERY_REQUESTS) {
|
|
158
|
+
void this.flush();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async flush() {
|
|
162
|
+
if (this.flushing)
|
|
163
|
+
return;
|
|
164
|
+
if (!isTelemetryEnabled())
|
|
165
|
+
return;
|
|
166
|
+
if (this.counters.requests === 0)
|
|
167
|
+
return;
|
|
168
|
+
this.flushing = true;
|
|
169
|
+
const snap = this.counters;
|
|
170
|
+
this.counters = emptyCounters();
|
|
171
|
+
let topModel = null;
|
|
172
|
+
let topModelCount = 0;
|
|
173
|
+
for (const [model, count] of snap.modelCounts) {
|
|
174
|
+
if (count > topModelCount) {
|
|
175
|
+
topModel = model;
|
|
176
|
+
topModelCount = count;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const cliVersion = process.env["npm_package_version"] ?? "unknown";
|
|
180
|
+
const payload = {
|
|
181
|
+
anonId: getAnonId(),
|
|
182
|
+
cliVersion,
|
|
183
|
+
nodeVersion: process.version,
|
|
184
|
+
platform: platform(),
|
|
185
|
+
requests: snap.requests,
|
|
186
|
+
bytesIn: snap.bytesIn,
|
|
187
|
+
bytesOut: snap.bytesOut,
|
|
188
|
+
bytesSaved: snap.bytesSaved,
|
|
189
|
+
inputTokens: snap.inputTokens,
|
|
190
|
+
outputTokens: snap.outputTokens,
|
|
191
|
+
dollarsEstimate: Math.round(snap.dollarsEstimate * 1e6) / 1e6,
|
|
192
|
+
dollarsSaved: Math.round(snap.dollarsSaved * 1e6) / 1e6,
|
|
193
|
+
provider: snap.provider ?? undefined,
|
|
194
|
+
topModel: topModel ?? undefined,
|
|
195
|
+
client: snap.client ?? undefined,
|
|
196
|
+
teamDeployment: snap.teamDeployment,
|
|
197
|
+
};
|
|
198
|
+
try {
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const t = setTimeout(() => controller.abort(), 5000);
|
|
201
|
+
await fetch(ENDPOINT, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/json" },
|
|
204
|
+
body: JSON.stringify(payload),
|
|
205
|
+
signal: controller.signal,
|
|
206
|
+
});
|
|
207
|
+
clearTimeout(t);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Telemetry must NEVER affect user experience. Drop the batch silently.
|
|
211
|
+
}
|
|
212
|
+
finally {
|
|
213
|
+
this.flushing = false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
export const telemetry = new Telemetry();
|
|
218
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -13,6 +13,17 @@ export {
|
|
|
13
13
|
} from "./proxy/anthropic-passthrough.js";
|
|
14
14
|
export { providerForPath, anthropic } from "./providers/registry.js";
|
|
15
15
|
export type { Provider, Conversation, ConvMessage, ConvBlock, ProviderId } from "./providers/types.js";
|
|
16
|
+
export {
|
|
17
|
+
telemetry,
|
|
18
|
+
Telemetry,
|
|
19
|
+
isTelemetryEnabled,
|
|
20
|
+
setTelemetryEnabled,
|
|
21
|
+
isFirstRun,
|
|
22
|
+
markFirstRunComplete,
|
|
23
|
+
firstRunBanner,
|
|
24
|
+
getAnonId,
|
|
25
|
+
} from "./telemetry.js";
|
|
26
|
+
export type { TelemetryRecord } from "./telemetry.js";
|
|
16
27
|
export { Pipeline } from "./processors/pipeline.js";
|
|
17
28
|
export { conversationDedup } from "./processors/conversation-dedup.js";
|
|
18
29
|
export { ResponseCache } from "./processors/response-cache.js";
|
package/src/server.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { createServer, IncomingMessage, ServerResponse, Server } from "node:http
|
|
|
2
2
|
import type { ProxyConfig, RequestRecord } from "./types.js";
|
|
3
3
|
import { handleAnthropicRequest } from "./proxy/anthropic-passthrough.js";
|
|
4
4
|
import { Ledger } from "./ledger.js";
|
|
5
|
+
import { telemetry } from "./telemetry.js";
|
|
5
6
|
|
|
6
7
|
export interface ProxyServerHandle {
|
|
7
8
|
proxy: Server;
|
|
@@ -50,12 +51,36 @@ async function closeServer(server: Server): Promise<void> {
|
|
|
50
51
|
export async function start(opts: StartOptions): Promise<ProxyServerHandle> {
|
|
51
52
|
const ledger = new Ledger(opts.config.ledgerPath);
|
|
52
53
|
|
|
54
|
+
const isTeamDeployment = opts.config.bind === "0.0.0.0";
|
|
53
55
|
const sink = (r: RequestRecord): void => {
|
|
54
56
|
try {
|
|
55
57
|
ledger.record(r);
|
|
56
58
|
} catch {
|
|
57
59
|
// ledger errors must never break the request path
|
|
58
60
|
}
|
|
61
|
+
try {
|
|
62
|
+
// Approximate byte counts from token estimates (industry rule-of-thumb)
|
|
63
|
+
const tokensIn = r.usageRaw.inputTokens + r.usageRaw.cacheReadInputTokens;
|
|
64
|
+
const tokensOut = r.usageSent.inputTokens + r.usageSent.cacheReadInputTokens;
|
|
65
|
+
const TOKEN_TO_BYTE = 3.5;
|
|
66
|
+
const bytesIn = Math.round(tokensIn * TOKEN_TO_BYTE);
|
|
67
|
+
const bytesOut = Math.round(tokensOut * TOKEN_TO_BYTE);
|
|
68
|
+
telemetry.record({
|
|
69
|
+
bytesIn,
|
|
70
|
+
bytesOut,
|
|
71
|
+
bytesSaved: Math.max(0, bytesIn - bytesOut),
|
|
72
|
+
inputTokens: r.usageRaw.inputTokens,
|
|
73
|
+
outputTokens: r.usageRaw.outputTokens,
|
|
74
|
+
dollarsEstimate: r.dollarsRaw,
|
|
75
|
+
dollarsSaved: r.dollarsSaved,
|
|
76
|
+
provider: "anthropic",
|
|
77
|
+
model: r.model,
|
|
78
|
+
client: null,
|
|
79
|
+
teamDeployment: isTeamDeployment,
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
// telemetry must never break the request path
|
|
83
|
+
}
|
|
59
84
|
opts.onRecord?.(r);
|
|
60
85
|
};
|
|
61
86
|
|
|
@@ -142,6 +167,7 @@ export async function start(opts: StartOptions): Promise<ProxyServerHandle> {
|
|
|
142
167
|
ledger,
|
|
143
168
|
close: async () => {
|
|
144
169
|
clearInterval(retentionInterval);
|
|
170
|
+
telemetry.stop();
|
|
145
171
|
await Promise.all([closeServer(proxy), closeServer(dashboard)]);
|
|
146
172
|
ledger.close();
|
|
147
173
|
},
|
package/src/telemetry.ts
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { hostname, platform, userInfo } from "node:os";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
|
|
6
|
+
const ENDPOINT = process.env["TOKENSHIELD_TELEMETRY_URL"] ?? "https://curatedmcp.com/api/v1/tokenshield/telemetry";
|
|
7
|
+
|
|
8
|
+
// Flush every N requests OR every M minutes, whichever first. Keeps the
|
|
9
|
+
// network footprint tiny and ensures dev sessions still report at the end.
|
|
10
|
+
const FLUSH_EVERY_REQUESTS = 50;
|
|
11
|
+
const FLUSH_EVERY_MS = 5 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
function telemetryDir(): string {
|
|
14
|
+
const home = process.env["HOME"] ?? process.env["USERPROFILE"] ?? ".";
|
|
15
|
+
return join(home, ".tokenshield");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function settingsFile(): string {
|
|
19
|
+
return join(telemetryDir(), "settings.json");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface Settings {
|
|
23
|
+
telemetry: "on" | "off";
|
|
24
|
+
anonId: string;
|
|
25
|
+
firstRunCompleted: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function loadSettings(): Settings {
|
|
29
|
+
const file = settingsFile();
|
|
30
|
+
if (existsSync(file)) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = readFileSync(file, "utf8");
|
|
33
|
+
const parsed = JSON.parse(raw) as Partial<Settings>;
|
|
34
|
+
return {
|
|
35
|
+
telemetry: parsed.telemetry === "off" ? "off" : "on",
|
|
36
|
+
anonId: parsed.anonId ?? generateAnonId(),
|
|
37
|
+
firstRunCompleted: parsed.firstRunCompleted ?? false,
|
|
38
|
+
};
|
|
39
|
+
} catch {
|
|
40
|
+
// fall through
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
telemetry: "on",
|
|
45
|
+
anonId: generateAnonId(),
|
|
46
|
+
firstRunCompleted: false,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function saveSettings(s: Settings): void {
|
|
51
|
+
const dir = telemetryDir();
|
|
52
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
53
|
+
writeFileSync(settingsFile(), JSON.stringify(s, null, 2), "utf8");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function generateAnonId(): string {
|
|
57
|
+
// Deterministic per-machine: sha256 of hostname + username.
|
|
58
|
+
// Cannot be reversed to a person; cannot be cross-correlated with other
|
|
59
|
+
// CuratedMCP products without explicit account linking.
|
|
60
|
+
const seed = `${hostname()}::${userInfo().username}::tokenshield`;
|
|
61
|
+
return createHash("sha256").update(seed).digest("hex");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isTelemetryEnabled(): boolean {
|
|
65
|
+
// Hard kill switches — these win over settings file
|
|
66
|
+
if (process.env["TOKENSHIELD_TELEMETRY"] === "0" || process.env["TOKENSHIELD_TELEMETRY"] === "off") return false;
|
|
67
|
+
if (process.env["DO_NOT_TRACK"] === "1") return false;
|
|
68
|
+
if (process.env["CI"] === "true") return false;
|
|
69
|
+
|
|
70
|
+
const s = loadSettings();
|
|
71
|
+
return s.telemetry === "on";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setTelemetryEnabled(on: boolean): void {
|
|
75
|
+
const s = loadSettings();
|
|
76
|
+
s.telemetry = on ? "on" : "off";
|
|
77
|
+
s.firstRunCompleted = true;
|
|
78
|
+
saveSettings(s);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getAnonId(): string {
|
|
82
|
+
return loadSettings().anonId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function isFirstRun(): boolean {
|
|
86
|
+
return !loadSettings().firstRunCompleted;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function markFirstRunComplete(): void {
|
|
90
|
+
const s = loadSettings();
|
|
91
|
+
s.firstRunCompleted = true;
|
|
92
|
+
saveSettings(s);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function firstRunBanner(): string {
|
|
96
|
+
return [
|
|
97
|
+
"",
|
|
98
|
+
" ┌──────────────────────────────────────────────────────────────────┐",
|
|
99
|
+
" │ TokenShield collects anonymous usage stats by default: │",
|
|
100
|
+
" │ • Aggregate token counts and $ saved │",
|
|
101
|
+
" │ • CLI version, Node version, OS │",
|
|
102
|
+
" │ • Provider (anthropic/openai/gemini) + most-used model │",
|
|
103
|
+
" │ │",
|
|
104
|
+
" │ Never sent: prompts, responses, file contents, API keys, │",
|
|
105
|
+
" │ IP address, hostname, username, file paths. │",
|
|
106
|
+
" │ │",
|
|
107
|
+
" │ Disable any time: tokenshield telemetry off │",
|
|
108
|
+
" │ Or via env: TOKENSHIELD_TELEMETRY=0 (or DO_NOT_TRACK=1) │",
|
|
109
|
+
" │ Source: https://github.com/oneprofile-dev/tokenshield │",
|
|
110
|
+
" └──────────────────────────────────────────────────────────────────┘",
|
|
111
|
+
"",
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Batched counters ────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
interface Counters {
|
|
118
|
+
requests: number;
|
|
119
|
+
bytesIn: number;
|
|
120
|
+
bytesOut: number;
|
|
121
|
+
bytesSaved: number;
|
|
122
|
+
inputTokens: number;
|
|
123
|
+
outputTokens: number;
|
|
124
|
+
dollarsEstimate: number;
|
|
125
|
+
dollarsSaved: number;
|
|
126
|
+
modelCounts: Map<string, number>;
|
|
127
|
+
provider: string | null;
|
|
128
|
+
client: string | null;
|
|
129
|
+
teamDeployment: boolean;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function emptyCounters(): Counters {
|
|
133
|
+
return {
|
|
134
|
+
requests: 0,
|
|
135
|
+
bytesIn: 0,
|
|
136
|
+
bytesOut: 0,
|
|
137
|
+
bytesSaved: 0,
|
|
138
|
+
inputTokens: 0,
|
|
139
|
+
outputTokens: 0,
|
|
140
|
+
dollarsEstimate: 0,
|
|
141
|
+
dollarsSaved: 0,
|
|
142
|
+
modelCounts: new Map(),
|
|
143
|
+
provider: null,
|
|
144
|
+
client: null,
|
|
145
|
+
teamDeployment: false,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface TelemetryRecord {
|
|
150
|
+
bytesIn: number;
|
|
151
|
+
bytesOut: number;
|
|
152
|
+
bytesSaved: number;
|
|
153
|
+
inputTokens: number;
|
|
154
|
+
outputTokens: number;
|
|
155
|
+
dollarsEstimate: number;
|
|
156
|
+
dollarsSaved: number;
|
|
157
|
+
provider: "anthropic" | "openai" | "gemini" | null;
|
|
158
|
+
model: string | null;
|
|
159
|
+
client: string | null;
|
|
160
|
+
teamDeployment: boolean;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export class Telemetry {
|
|
164
|
+
private counters = emptyCounters();
|
|
165
|
+
private timer: NodeJS.Timeout | null = null;
|
|
166
|
+
private flushing = false;
|
|
167
|
+
|
|
168
|
+
start(): void {
|
|
169
|
+
if (!isTelemetryEnabled()) return;
|
|
170
|
+
if (this.timer) return;
|
|
171
|
+
this.timer = setInterval(() => {
|
|
172
|
+
void this.flush();
|
|
173
|
+
}, FLUSH_EVERY_MS);
|
|
174
|
+
this.timer.unref?.();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
stop(): void {
|
|
178
|
+
if (this.timer) {
|
|
179
|
+
clearInterval(this.timer);
|
|
180
|
+
this.timer = null;
|
|
181
|
+
}
|
|
182
|
+
// Best-effort final flush — fire and forget
|
|
183
|
+
void this.flush();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
record(r: TelemetryRecord): void {
|
|
187
|
+
if (!isTelemetryEnabled()) return;
|
|
188
|
+
|
|
189
|
+
const c = this.counters;
|
|
190
|
+
c.requests += 1;
|
|
191
|
+
c.bytesIn += r.bytesIn;
|
|
192
|
+
c.bytesOut += r.bytesOut;
|
|
193
|
+
c.bytesSaved += r.bytesSaved;
|
|
194
|
+
c.inputTokens += r.inputTokens;
|
|
195
|
+
c.outputTokens += r.outputTokens;
|
|
196
|
+
c.dollarsEstimate += r.dollarsEstimate;
|
|
197
|
+
c.dollarsSaved += r.dollarsSaved;
|
|
198
|
+
if (r.provider) c.provider = r.provider;
|
|
199
|
+
if (r.client) c.client = r.client;
|
|
200
|
+
if (r.teamDeployment) c.teamDeployment = true;
|
|
201
|
+
if (r.model) c.modelCounts.set(r.model, (c.modelCounts.get(r.model) ?? 0) + 1);
|
|
202
|
+
|
|
203
|
+
if (c.requests >= FLUSH_EVERY_REQUESTS) {
|
|
204
|
+
void this.flush();
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async flush(): Promise<void> {
|
|
209
|
+
if (this.flushing) return;
|
|
210
|
+
if (!isTelemetryEnabled()) return;
|
|
211
|
+
if (this.counters.requests === 0) return;
|
|
212
|
+
|
|
213
|
+
this.flushing = true;
|
|
214
|
+
const snap = this.counters;
|
|
215
|
+
this.counters = emptyCounters();
|
|
216
|
+
|
|
217
|
+
let topModel: string | null = null;
|
|
218
|
+
let topModelCount = 0;
|
|
219
|
+
for (const [model, count] of snap.modelCounts) {
|
|
220
|
+
if (count > topModelCount) {
|
|
221
|
+
topModel = model;
|
|
222
|
+
topModelCount = count;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const cliVersion = process.env["npm_package_version"] ?? "unknown";
|
|
227
|
+
|
|
228
|
+
const payload = {
|
|
229
|
+
anonId: getAnonId(),
|
|
230
|
+
cliVersion,
|
|
231
|
+
nodeVersion: process.version,
|
|
232
|
+
platform: platform(),
|
|
233
|
+
requests: snap.requests,
|
|
234
|
+
bytesIn: snap.bytesIn,
|
|
235
|
+
bytesOut: snap.bytesOut,
|
|
236
|
+
bytesSaved: snap.bytesSaved,
|
|
237
|
+
inputTokens: snap.inputTokens,
|
|
238
|
+
outputTokens: snap.outputTokens,
|
|
239
|
+
dollarsEstimate: Math.round(snap.dollarsEstimate * 1e6) / 1e6,
|
|
240
|
+
dollarsSaved: Math.round(snap.dollarsSaved * 1e6) / 1e6,
|
|
241
|
+
provider: snap.provider ?? undefined,
|
|
242
|
+
topModel: topModel ?? undefined,
|
|
243
|
+
client: snap.client ?? undefined,
|
|
244
|
+
teamDeployment: snap.teamDeployment,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const controller = new AbortController();
|
|
249
|
+
const t = setTimeout(() => controller.abort(), 5000);
|
|
250
|
+
await fetch(ENDPOINT, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers: { "Content-Type": "application/json" },
|
|
253
|
+
body: JSON.stringify(payload),
|
|
254
|
+
signal: controller.signal,
|
|
255
|
+
});
|
|
256
|
+
clearTimeout(t);
|
|
257
|
+
} catch {
|
|
258
|
+
// Telemetry must NEVER affect user experience. Drop the batch silently.
|
|
259
|
+
} finally {
|
|
260
|
+
this.flushing = false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const telemetry = new Telemetry();
|