@curatedmcp/tokenshield-core 0.2.0 → 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/README.md +93 -0
- 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/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @curatedmcp/tokenshield-core
|
|
2
|
+
|
|
3
|
+
**The proxy engine behind [TokenShield](https://www.npmjs.com/package/@curatedmcp/tokenshield).** Anthropic API-layer middleware with token accounting, conversation dedup, and response cache.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@curatedmcp/tokenshield-core)
|
|
6
|
+
[](https://github.com/oneprofile-dev/tokenshield/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
> **You probably want [`@curatedmcp/tokenshield`](https://www.npmjs.com/package/@curatedmcp/tokenshield) instead.** That's the CLI. This package is the embeddable engine — use it if you're integrating TokenShield directly into another Node server.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install @curatedmcp/tokenshield-core
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## What's in the box
|
|
19
|
+
|
|
20
|
+
- **`Pipeline`** — middleware runner with fail-open semantics and per-processor circuit breakers
|
|
21
|
+
- **`conversationDedup`** — content-hash `tool_result` blocks; replace 2nd+ with deterministic pointer stubs
|
|
22
|
+
- **`responseCache`** — LRU+TTL cache for `temperature === 0 && stream === false` requests
|
|
23
|
+
- **`anthropic`** — provider adapter (URL matching, SSE usage parsing, message conversion)
|
|
24
|
+
- **`Ledger`** — SQLite-backed request log using Node 22's built-in `node:sqlite` (zero native deps)
|
|
25
|
+
- **`dollarsFor()`** — accurate pricing math across all Anthropic models (Opus 4.7, Sonnet 4.6, Haiku 4.5, …)
|
|
26
|
+
|
|
27
|
+
## Minimal example
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { Pipeline, conversationDedup, anthropic } from "@curatedmcp/tokenshield-core";
|
|
31
|
+
|
|
32
|
+
const pipeline = new Pipeline({
|
|
33
|
+
processors: [conversationDedup],
|
|
34
|
+
enabled: new Set(["conversation-dedup"]),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const conv = anthropic.toConversation(anthropicMessagesRequest);
|
|
38
|
+
const result = pipeline.run(conv, {
|
|
39
|
+
providerId: "anthropic",
|
|
40
|
+
conversationFingerprint: "...",
|
|
41
|
+
inboundBytes: 12_345,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const compressedRequest = anthropic.applyConversation(anthropicMessagesRequest, result.conversation);
|
|
45
|
+
// → send compressedRequest to api.anthropic.com
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Provider adapter interface
|
|
49
|
+
|
|
50
|
+
If you want to add OpenAI, Gemini, or a custom provider, implement:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
interface Provider {
|
|
54
|
+
id: "anthropic" | "openai" | "gemini" | string;
|
|
55
|
+
matches(url: URL, headers: Record<string, string>): boolean;
|
|
56
|
+
parseUsageFromJson(body: unknown): { usage: UsageCounts; model: string | null };
|
|
57
|
+
parseUsageFromSSE(event: SSEEvent, acc: UsageAccumulator): void;
|
|
58
|
+
extractModel(requestBody: unknown): string;
|
|
59
|
+
toConversation(requestBody: unknown): Conversation | null;
|
|
60
|
+
applyConversation(requestBody: unknown, conv: Conversation): unknown;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
OpenAI lands in v1.1, Gemini in v1.2 — or send a PR.
|
|
65
|
+
|
|
66
|
+
## Privacy guarantees
|
|
67
|
+
|
|
68
|
+
- All work happens in-process. Nothing leaves the host that calls this library.
|
|
69
|
+
- No network I/O except the upstream provider call you make yourself.
|
|
70
|
+
- The optional ledger writes to a SQLite file you control (`~/.tokenshield/ledger.db` by default).
|
|
71
|
+
|
|
72
|
+
## Architecture & full docs
|
|
73
|
+
|
|
74
|
+
This is the engine. For the full CLI, dashboard, integrations, and end-user docs, see:
|
|
75
|
+
|
|
76
|
+
- **CLI:** [`@curatedmcp/tokenshield`](https://www.npmjs.com/package/@curatedmcp/tokenshield)
|
|
77
|
+
- **Source:** [github.com/oneprofile-dev/tokenshield](https://github.com/oneprofile-dev/tokenshield)
|
|
78
|
+
- **Whitepaper:** [docs/whitepaper.md](https://github.com/oneprofile-dev/tokenshield/blob/main/docs/whitepaper.md)
|
|
79
|
+
- **Website:** [curatedmcp.com/tokenshield](https://curatedmcp.com/tokenshield)
|
|
80
|
+
|
|
81
|
+
## Part of the CuratedMCP control plane
|
|
82
|
+
|
|
83
|
+
TokenShield is one of three products at **[curatedmcp.com](https://curatedmcp.com)** — the MCP governance control plane for engineering organizations:
|
|
84
|
+
|
|
85
|
+
- 🛡️ **TokenShield** — cut your Claude Code bill 40–70%
|
|
86
|
+
- 🔍 **[MCP Auditor](https://curatedmcp.com/auditor)** — static analysis for MCP server security
|
|
87
|
+
- 📊 **[Sentinel](https://curatedmcp.com/sentinel)** — runtime anomaly detection
|
|
88
|
+
|
|
89
|
+
[Start an enterprise pilot →](https://curatedmcp.com/enterprise/pilot)
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
MIT — see [LICENSE](https://github.com/oneprofile-dev/tokenshield/blob/main/LICENSE).
|
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();
|