tokenfirewall 1.0.1

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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/dist/adapters/anthropic.d.ts +5 -0
  4. package/dist/adapters/anthropic.js +34 -0
  5. package/dist/adapters/gemini.d.ts +5 -0
  6. package/dist/adapters/gemini.js +42 -0
  7. package/dist/adapters/grok.d.ts +6 -0
  8. package/dist/adapters/grok.js +33 -0
  9. package/dist/adapters/index.d.ts +5 -0
  10. package/dist/adapters/index.js +18 -0
  11. package/dist/adapters/kimi.d.ts +6 -0
  12. package/dist/adapters/kimi.js +33 -0
  13. package/dist/adapters/openai.d.ts +5 -0
  14. package/dist/adapters/openai.js +46 -0
  15. package/dist/core/budgetManager.d.ts +40 -0
  16. package/dist/core/budgetManager.js +116 -0
  17. package/dist/core/costEngine.d.ts +6 -0
  18. package/dist/core/costEngine.js +30 -0
  19. package/dist/core/pricingRegistry.d.ts +27 -0
  20. package/dist/core/pricingRegistry.js +75 -0
  21. package/dist/core/types.d.ts +66 -0
  22. package/dist/core/types.js +2 -0
  23. package/dist/index.d.ts +71 -0
  24. package/dist/index.js +134 -0
  25. package/dist/interceptors/fetchInterceptor.d.ts +13 -0
  26. package/dist/interceptors/fetchInterceptor.js +73 -0
  27. package/dist/interceptors/sdkInterceptor.d.ts +9 -0
  28. package/dist/interceptors/sdkInterceptor.js +37 -0
  29. package/dist/introspection/contextRegistry.d.ts +30 -0
  30. package/dist/introspection/contextRegistry.js +81 -0
  31. package/dist/introspection/modelLister.d.ts +22 -0
  32. package/dist/introspection/modelLister.js +190 -0
  33. package/dist/logger.d.ts +11 -0
  34. package/dist/logger.js +31 -0
  35. package/dist/registry.d.ts +18 -0
  36. package/dist/registry.js +38 -0
  37. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tokenfirewall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,312 @@
1
+ # tokenfirewall
2
+
3
+ Production-grade LLM cost enforcement middleware for Node.js with automatic tracking, budget management, and model discovery.
4
+
5
+ ## Features
6
+
7
+ - **Multi-provider support**: OpenAI, Anthropic, Gemini, Grok, Kimi
8
+ - **Budget enforcement**: Warn or block when limits exceeded
9
+ - **Automatic cost tracking**: Real-time usage monitoring
10
+ - **Model discovery**: List available models with context limits
11
+ - **Context intelligence**: Budget-aware model selection
12
+ - **Extensible**: Add custom providers easily
13
+ - **Type-safe**: Full TypeScript support
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install tokenfirewall
19
+ ```
20
+
21
+ ## Quick Start
22
+
23
+ ```javascript
24
+ const { createBudgetGuard, patchGlobalFetch } = require("tokenfirewall");
25
+
26
+ // Set budget limit
27
+ createBudgetGuard({
28
+ monthlyLimit: 100, // $100 USD
29
+ mode: "block" // or "warn"
30
+ });
31
+
32
+ // Enable tracking
33
+ patchGlobalFetch();
34
+
35
+ // Use any LLM API - tokenfirewall tracks everything
36
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
37
+ method: "POST",
38
+ headers: {
39
+ "Authorization": `Bearer ${process.env.OPENAI_API_KEY}`,
40
+ "Content-Type": "application/json"
41
+ },
42
+ body: JSON.stringify({
43
+ model: "gpt-4o",
44
+ messages: [{ role: "user", content: "Hello!" }]
45
+ })
46
+ });
47
+ ```
48
+
49
+ ## API Reference
50
+
51
+ ### Budget Management
52
+
53
+ #### `createBudgetGuard(options)`
54
+
55
+ Initialize budget protection.
56
+
57
+ ```javascript
58
+ createBudgetGuard({
59
+ monthlyLimit: 100, // Required: monthly budget in USD
60
+ mode: "block" // Optional: "block" (default) or "warn"
61
+ });
62
+ ```
63
+
64
+ #### `getBudgetStatus()`
65
+
66
+ Get current budget information.
67
+
68
+ ```javascript
69
+ const status = getBudgetStatus();
70
+ // {
71
+ // totalSpent: 45.23,
72
+ // limit: 100,
73
+ // remaining: 54.77,
74
+ // percentageUsed: 45.23
75
+ // }
76
+ ```
77
+
78
+ #### `resetBudget()`
79
+
80
+ Reset budget tracking (useful for monthly resets).
81
+
82
+ ```javascript
83
+ resetBudget();
84
+ ```
85
+
86
+ ### Interception
87
+
88
+ #### `patchGlobalFetch()`
89
+
90
+ Intercept all fetch calls to track LLM usage.
91
+
92
+ ```javascript
93
+ patchGlobalFetch();
94
+ ```
95
+
96
+ #### `patchProvider(providerName)`
97
+
98
+ Patch specific provider SDK (most use fetch internally).
99
+
100
+ ```javascript
101
+ patchProvider("openai");
102
+ ```
103
+
104
+ ### Model Discovery
105
+
106
+ #### `listAvailableModels(options)`
107
+
108
+ Discover available models with context limits and budget usage.
109
+
110
+ ```javascript
111
+ const models = await listAvailableModels({
112
+ provider: "openai",
113
+ apiKey: process.env.OPENAI_API_KEY,
114
+ includeBudgetUsage: true // Optional
115
+ });
116
+
117
+ // Returns:
118
+ // [
119
+ // {
120
+ // model: "gpt-4o",
121
+ // contextLimit: 128000,
122
+ // budgetUsagePercentage: 32.4
123
+ // }
124
+ // ]
125
+ ```
126
+
127
+ ### Extensibility
128
+
129
+ #### `registerAdapter(adapter)`
130
+
131
+ Add custom LLM provider.
132
+
133
+ ```javascript
134
+ registerAdapter({
135
+ name: "custom",
136
+ detect: (response) => /* detection logic */,
137
+ normalize: (response) => /* normalization logic */
138
+ });
139
+ ```
140
+
141
+ #### `registerPricing(provider, model, pricing)`
142
+
143
+ Add custom pricing (per 1M tokens).
144
+
145
+ ```javascript
146
+ registerPricing("custom", "model-name", {
147
+ input: 0.001,
148
+ output: 0.002
149
+ });
150
+ ```
151
+
152
+ #### `registerContextLimit(provider, model, contextLimit)`
153
+
154
+ Add custom context limit.
155
+
156
+ ```javascript
157
+ registerContextLimit("custom", "model-name", 131072);
158
+ ```
159
+
160
+ ## Supported Providers
161
+
162
+ | Provider | Models | Context Limits |
163
+ |----------|--------|----------------|
164
+ | OpenAI | gpt-4o, gpt-4o-mini, gpt-3.5-turbo | 16K - 128K |
165
+ | Anthropic | claude-3-5-sonnet, claude-3-5-haiku | 200K |
166
+ | Gemini | gemini-2.5-pro, gemini-2.5-flash | 1M - 2M |
167
+ | Grok | grok-beta, llama-3.3-70b | 131K |
168
+ | Kimi | moonshot-v1-8k/32k/128k | 8K - 128K |
169
+
170
+ ## Usage Examples
171
+
172
+ ### Basic Usage
173
+
174
+ ```javascript
175
+ const { createBudgetGuard, patchGlobalFetch } = require("tokenfirewall");
176
+
177
+ createBudgetGuard({ monthlyLimit: 100, mode: "block" });
178
+ patchGlobalFetch();
179
+
180
+ // Make LLM calls as usual - automatically tracked
181
+ ```
182
+
183
+ ### Budget-Aware Model Selection
184
+
185
+ ```javascript
186
+ const { listAvailableModels, getBudgetStatus } = require("tokenfirewall");
187
+
188
+ const models = await listAvailableModels({
189
+ provider: "openai",
190
+ apiKey: process.env.OPENAI_API_KEY,
191
+ includeBudgetUsage: true
192
+ });
193
+
194
+ const status = getBudgetStatus();
195
+ if (status.remaining < 10) {
196
+ console.log("Low budget - use cheaper models");
197
+ const cheapModels = models.filter(m => m.model.includes("mini"));
198
+ }
199
+ ```
200
+
201
+ ### Context-Aware Routing
202
+
203
+ ```javascript
204
+ const { listAvailableModels } = require("tokenfirewall");
205
+
206
+ const models = await listAvailableModels({
207
+ provider: "openai",
208
+ apiKey: process.env.OPENAI_API_KEY
209
+ });
210
+
211
+ // Find model with sufficient context
212
+ const suitable = models.find(m =>
213
+ m.contextLimit && m.contextLimit >= promptTokens * 1.5
214
+ );
215
+ ```
216
+
217
+ ### Custom Provider
218
+
219
+ ```javascript
220
+ const { registerAdapter, registerPricing } = require("tokenfirewall");
221
+
222
+ // Add Ollama support
223
+ registerAdapter({
224
+ name: "ollama",
225
+ detect: (response) => response?.model && response?.prompt_eval_count !== undefined,
226
+ normalize: (response) => ({
227
+ provider: "ollama",
228
+ model: response.model,
229
+ inputTokens: response.prompt_eval_count || 0,
230
+ outputTokens: response.eval_count || 0,
231
+ totalTokens: (response.prompt_eval_count || 0) + (response.eval_count || 0)
232
+ })
233
+ });
234
+
235
+ registerPricing("ollama", "llama3.2", { input: 0, output: 0 });
236
+ ```
237
+
238
+ ## TypeScript Support
239
+
240
+ Full type definitions included:
241
+
242
+ ```typescript
243
+ import {
244
+ createBudgetGuard,
245
+ listAvailableModels,
246
+ BudgetGuardOptions,
247
+ ModelInfo,
248
+ ListModelsOptions
249
+ } from "tokenfirewall";
250
+
251
+ const options: BudgetGuardOptions = {
252
+ monthlyLimit: 100,
253
+ mode: "block"
254
+ };
255
+
256
+ const models: ModelInfo[] = await listAvailableModels({
257
+ provider: "openai",
258
+ apiKey: process.env.OPENAI_API_KEY!
259
+ });
260
+ ```
261
+
262
+ ## Architecture
263
+
264
+ ```
265
+ tokenfirewall/
266
+ ├── core/ # Provider-agnostic logic
267
+ ├── adapters/ # Provider-specific normalization
268
+ ├── interceptors/ # Request/response capture
269
+ ├── introspection/ # Model discovery
270
+ └── registry/ # Adapter management
271
+ ```
272
+
273
+ Adding a new provider requires only creating an adapter file - no core changes needed.
274
+
275
+ ## Examples
276
+
277
+ See the `examples/` directory for complete working examples:
278
+
279
+ - `basic-usage.js` - Simple OpenAI example
280
+ - `multiple-providers.js` - Track multiple providers
281
+ - `with-sdk.js` - Use with official SDKs
282
+ - `model-discovery.js` - Model discovery
283
+ - `context-aware-routing.js` - Intelligent routing
284
+ - `custom-provider.js` - Add custom provider
285
+ - `gemini-complete-demo.js` - Complete Gemini demo
286
+
287
+ ## Best Practices
288
+
289
+ 1. **Set realistic budgets**: Start with a conservative limit
290
+ 2. **Use warn mode in development**: Switch to block in production
291
+ 3. **Reset monthly**: Automate budget resets with cron
292
+ 4. **Cache model lists**: Model availability doesn't change often
293
+ 5. **Monitor logs**: Review structured JSON output regularly
294
+
295
+ ## Limitations
296
+
297
+ - In-memory tracking only (no persistence in V1)
298
+ - No streaming support yet
299
+ - Context limits are static (not from provider APIs)
300
+ - Budget tracking is local only (not provider-side billing)
301
+
302
+ ## License
303
+
304
+ MIT
305
+
306
+ ## Contributing
307
+
308
+ Contributions welcome! Please open an issue or PR.
309
+
310
+ ## Support
311
+
312
+ For issues and questions, please open a GitHub issue.
@@ -0,0 +1,5 @@
1
+ import { ProviderAdapter } from "../core/types";
2
+ /**
3
+ * Anthropic adapter - normalizes Anthropic API responses
4
+ */
5
+ export declare const anthropicAdapter: ProviderAdapter;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.anthropicAdapter = void 0;
4
+ /**
5
+ * Anthropic adapter - normalizes Anthropic API responses
6
+ */
7
+ exports.anthropicAdapter = {
8
+ name: "anthropic",
9
+ detect(response) {
10
+ if (!response || typeof response !== "object") {
11
+ return false;
12
+ }
13
+ const resp = response;
14
+ return (typeof resp.id === "string" &&
15
+ resp.type === "message" &&
16
+ typeof resp.model === "string" &&
17
+ resp.usage !== undefined);
18
+ },
19
+ normalize(response) {
20
+ const resp = response;
21
+ if (!resp.usage || !resp.model) {
22
+ throw new Error("TokenFirewall: Invalid Anthropic response format");
23
+ }
24
+ const inputTokens = resp.usage.input_tokens ?? 0;
25
+ const outputTokens = resp.usage.output_tokens ?? 0;
26
+ return {
27
+ provider: "anthropic",
28
+ model: resp.model,
29
+ inputTokens,
30
+ outputTokens,
31
+ totalTokens: inputTokens + outputTokens,
32
+ };
33
+ },
34
+ };
@@ -0,0 +1,5 @@
1
+ import { ProviderAdapter } from "../core/types";
2
+ /**
3
+ * Gemini adapter - normalizes Google Gemini API responses
4
+ */
5
+ export declare const geminiAdapter: ProviderAdapter;
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.geminiAdapter = void 0;
4
+ /**
5
+ * Gemini adapter - normalizes Google Gemini API responses
6
+ */
7
+ exports.geminiAdapter = {
8
+ name: "gemini",
9
+ detect(response) {
10
+ if (!response || typeof response !== "object") {
11
+ return false;
12
+ }
13
+ const resp = response;
14
+ return (Array.isArray(resp.candidates) &&
15
+ resp.usageMetadata !== undefined &&
16
+ typeof resp.usageMetadata === "object");
17
+ },
18
+ normalize(response, request) {
19
+ const resp = response;
20
+ if (!resp.usageMetadata) {
21
+ throw new Error("TokenFirewall: Invalid Gemini response format");
22
+ }
23
+ // Extract model from request or use default
24
+ let model = "gemini-1.5-flash";
25
+ if (request && typeof request === "object") {
26
+ const req = request;
27
+ if (req.model) {
28
+ model = req.model;
29
+ }
30
+ }
31
+ if (resp.modelVersion) {
32
+ model = resp.modelVersion;
33
+ }
34
+ return {
35
+ provider: "gemini",
36
+ model,
37
+ inputTokens: resp.usageMetadata.promptTokenCount ?? 0,
38
+ outputTokens: resp.usageMetadata.candidatesTokenCount ?? 0,
39
+ totalTokens: resp.usageMetadata.totalTokenCount ?? 0,
40
+ };
41
+ },
42
+ };
@@ -0,0 +1,6 @@
1
+ import { ProviderAdapter } from "../core/types";
2
+ /**
3
+ * Grok adapter - normalizes Grok API responses
4
+ * Grok uses OpenAI-compatible format and supports both Grok and Llama models
5
+ */
6
+ export declare const grokAdapter: ProviderAdapter;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.grokAdapter = void 0;
4
+ /**
5
+ * Grok adapter - normalizes Grok API responses
6
+ * Grok uses OpenAI-compatible format and supports both Grok and Llama models
7
+ */
8
+ exports.grokAdapter = {
9
+ name: "grok",
10
+ detect(response) {
11
+ if (!response || typeof response !== "object") {
12
+ return false;
13
+ }
14
+ const resp = response;
15
+ return (typeof resp.id === "string" &&
16
+ typeof resp.model === "string" &&
17
+ (resp.model.startsWith("grok") || resp.model.includes("llama")) &&
18
+ resp.usage !== undefined);
19
+ },
20
+ normalize(response) {
21
+ const resp = response;
22
+ if (!resp.usage || !resp.model) {
23
+ throw new Error("TokenFirewall: Invalid Grok response format");
24
+ }
25
+ return {
26
+ provider: "grok",
27
+ model: resp.model,
28
+ inputTokens: resp.usage.prompt_tokens ?? 0,
29
+ outputTokens: resp.usage.completion_tokens ?? 0,
30
+ totalTokens: resp.usage.total_tokens ?? 0,
31
+ };
32
+ },
33
+ };
@@ -0,0 +1,5 @@
1
+ import { ProviderAdapter } from "../core/types";
2
+ /**
3
+ * Registry of all provider adapters
4
+ */
5
+ export declare const adapters: ProviderAdapter[];
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.adapters = void 0;
4
+ const openai_1 = require("./openai");
5
+ const anthropic_1 = require("./anthropic");
6
+ const gemini_1 = require("./gemini");
7
+ const grok_1 = require("./grok");
8
+ const kimi_1 = require("./kimi");
9
+ /**
10
+ * Registry of all provider adapters
11
+ */
12
+ exports.adapters = [
13
+ openai_1.openaiAdapter,
14
+ anthropic_1.anthropicAdapter,
15
+ gemini_1.geminiAdapter,
16
+ grok_1.grokAdapter,
17
+ kimi_1.kimiAdapter,
18
+ ];
@@ -0,0 +1,6 @@
1
+ import { ProviderAdapter } from "../core/types";
2
+ /**
3
+ * Kimi adapter - normalizes Kimi (Moonshot AI) API responses
4
+ * Kimi uses OpenAI-compatible format
5
+ */
6
+ export declare const kimiAdapter: ProviderAdapter;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.kimiAdapter = void 0;
4
+ /**
5
+ * Kimi adapter - normalizes Kimi (Moonshot AI) API responses
6
+ * Kimi uses OpenAI-compatible format
7
+ */
8
+ exports.kimiAdapter = {
9
+ name: "kimi",
10
+ detect(response) {
11
+ if (!response || typeof response !== "object") {
12
+ return false;
13
+ }
14
+ const resp = response;
15
+ return (typeof resp.id === "string" &&
16
+ typeof resp.model === "string" &&
17
+ resp.model.startsWith("moonshot") &&
18
+ resp.usage !== undefined);
19
+ },
20
+ normalize(response) {
21
+ const resp = response;
22
+ if (!resp.usage || !resp.model) {
23
+ throw new Error("TokenFirewall: Invalid Kimi response format");
24
+ }
25
+ return {
26
+ provider: "kimi",
27
+ model: resp.model,
28
+ inputTokens: resp.usage.prompt_tokens ?? 0,
29
+ outputTokens: resp.usage.completion_tokens ?? 0,
30
+ totalTokens: resp.usage.total_tokens ?? 0,
31
+ };
32
+ },
33
+ };
@@ -0,0 +1,5 @@
1
+ import { ProviderAdapter } from "../core/types";
2
+ /**
3
+ * OpenAI adapter - normalizes OpenAI API responses
4
+ */
5
+ export declare const openaiAdapter: ProviderAdapter;
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.openaiAdapter = void 0;
4
+ /**
5
+ * OpenAI adapter - normalizes OpenAI API responses
6
+ */
7
+ exports.openaiAdapter = {
8
+ name: "openai",
9
+ detect(response) {
10
+ if (!response || typeof response !== "object") {
11
+ return false;
12
+ }
13
+ const resp = response;
14
+ // Check basic OpenAI structure
15
+ const hasBasicStructure = (typeof resp.id === "string" &&
16
+ typeof resp.object === "string" &&
17
+ typeof resp.model === "string" &&
18
+ resp.usage !== undefined &&
19
+ typeof resp.usage === "object");
20
+ if (!hasBasicStructure) {
21
+ return false;
22
+ }
23
+ // Exclude Grok and Llama models (they have their own adapter)
24
+ if (resp.model && (resp.model.startsWith("grok") || resp.model.includes("llama"))) {
25
+ return false;
26
+ }
27
+ // Exclude Kimi models (they have their own adapter)
28
+ if (resp.model && resp.model.startsWith("moonshot")) {
29
+ return false;
30
+ }
31
+ return true;
32
+ },
33
+ normalize(response) {
34
+ const resp = response;
35
+ if (!resp.usage || !resp.model) {
36
+ throw new Error("TokenFirewall: Invalid OpenAI response format");
37
+ }
38
+ return {
39
+ provider: "openai",
40
+ model: resp.model,
41
+ inputTokens: resp.usage.prompt_tokens ?? 0,
42
+ outputTokens: resp.usage.completion_tokens ?? 0,
43
+ totalTokens: resp.usage.total_tokens ?? 0,
44
+ };
45
+ },
46
+ };
@@ -0,0 +1,40 @@
1
+ import { BudgetGuardOptions, BudgetStatus } from "./types";
2
+ /**
3
+ * Budget manager - encapsulates all budget tracking state
4
+ */
5
+ export declare class BudgetManager {
6
+ private totalSpent;
7
+ private limit;
8
+ private mode;
9
+ private trackingLock;
10
+ constructor(options: BudgetGuardOptions);
11
+ /**
12
+ * Track a new cost and enforce budget limits
13
+ * Uses locking to prevent race conditions
14
+ * @throws Error if budget exceeded in block mode
15
+ */
16
+ track(cost: number): Promise<void>;
17
+ /**
18
+ * Get current budget status
19
+ */
20
+ getStatus(): BudgetStatus;
21
+ /**
22
+ * Reset budget tracking (useful for monthly resets)
23
+ */
24
+ reset(): void;
25
+ /**
26
+ * Export budget state for persistence
27
+ */
28
+ exportState(): {
29
+ totalSpent: number;
30
+ limit: number;
31
+ mode: string;
32
+ };
33
+ /**
34
+ * Import budget state from persistence
35
+ * Validates state before importing
36
+ */
37
+ importState(state: {
38
+ totalSpent: number;
39
+ }): void;
40
+ }