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.
- package/LICENSE +21 -0
- package/README.md +312 -0
- package/dist/adapters/anthropic.d.ts +5 -0
- package/dist/adapters/anthropic.js +34 -0
- package/dist/adapters/gemini.d.ts +5 -0
- package/dist/adapters/gemini.js +42 -0
- package/dist/adapters/grok.d.ts +6 -0
- package/dist/adapters/grok.js +33 -0
- package/dist/adapters/index.d.ts +5 -0
- package/dist/adapters/index.js +18 -0
- package/dist/adapters/kimi.d.ts +6 -0
- package/dist/adapters/kimi.js +33 -0
- package/dist/adapters/openai.d.ts +5 -0
- package/dist/adapters/openai.js +46 -0
- package/dist/core/budgetManager.d.ts +40 -0
- package/dist/core/budgetManager.js +116 -0
- package/dist/core/costEngine.d.ts +6 -0
- package/dist/core/costEngine.js +30 -0
- package/dist/core/pricingRegistry.d.ts +27 -0
- package/dist/core/pricingRegistry.js +75 -0
- package/dist/core/types.d.ts +66 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +134 -0
- package/dist/interceptors/fetchInterceptor.d.ts +13 -0
- package/dist/interceptors/fetchInterceptor.js +73 -0
- package/dist/interceptors/sdkInterceptor.d.ts +9 -0
- package/dist/interceptors/sdkInterceptor.js +37 -0
- package/dist/introspection/contextRegistry.d.ts +30 -0
- package/dist/introspection/contextRegistry.js +81 -0
- package/dist/introspection/modelLister.d.ts +22 -0
- package/dist/introspection/modelLister.js +190 -0
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +31 -0
- package/dist/registry.d.ts +18 -0
- package/dist/registry.js +38 -0
- package/package.json +46 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BudgetManager = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Budget manager - encapsulates all budget tracking state
|
|
6
|
+
*/
|
|
7
|
+
class BudgetManager {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.totalSpent = 0;
|
|
10
|
+
this.trackingLock = Promise.resolve();
|
|
11
|
+
// Validate monthly limit
|
|
12
|
+
if (typeof options.monthlyLimit !== 'number' || isNaN(options.monthlyLimit) || !isFinite(options.monthlyLimit)) {
|
|
13
|
+
throw new Error('TokenFirewall: monthlyLimit must be a valid number');
|
|
14
|
+
}
|
|
15
|
+
if (options.monthlyLimit <= 0) {
|
|
16
|
+
throw new Error('TokenFirewall: monthlyLimit must be greater than 0');
|
|
17
|
+
}
|
|
18
|
+
// Validate mode
|
|
19
|
+
const mode = options.mode ?? "block";
|
|
20
|
+
if (mode !== "warn" && mode !== "block") {
|
|
21
|
+
throw new Error('TokenFirewall: mode must be either "warn" or "block"');
|
|
22
|
+
}
|
|
23
|
+
this.limit = options.monthlyLimit;
|
|
24
|
+
this.mode = mode;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Track a new cost and enforce budget limits
|
|
28
|
+
* Uses locking to prevent race conditions
|
|
29
|
+
* @throws Error if budget exceeded in block mode
|
|
30
|
+
*/
|
|
31
|
+
async track(cost) {
|
|
32
|
+
// Validate cost parameter
|
|
33
|
+
if (typeof cost !== 'number' || isNaN(cost) || !isFinite(cost)) {
|
|
34
|
+
throw new Error('TokenFirewall: Cost must be a valid number');
|
|
35
|
+
}
|
|
36
|
+
if (cost < 0) {
|
|
37
|
+
throw new Error('TokenFirewall: Cost cannot be negative');
|
|
38
|
+
}
|
|
39
|
+
// Wait for any pending tracking to complete
|
|
40
|
+
await this.trackingLock;
|
|
41
|
+
// Create new lock for this tracking operation
|
|
42
|
+
let releaseLock;
|
|
43
|
+
this.trackingLock = new Promise((resolve) => {
|
|
44
|
+
releaseLock = resolve;
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
// Check if this would exceed budget BEFORE adding
|
|
48
|
+
const projectedTotal = this.totalSpent + cost;
|
|
49
|
+
if (projectedTotal > this.limit) {
|
|
50
|
+
const message = `TokenFirewall: Budget exceeded! Would spend $${projectedTotal.toFixed(4)} of $${this.limit.toFixed(2)} limit`;
|
|
51
|
+
if (this.mode === "block") {
|
|
52
|
+
// In block mode, DON'T track the cost and throw error
|
|
53
|
+
throw new Error(message);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
console.warn(message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Only commit the cost if we didn't throw
|
|
60
|
+
this.totalSpent += cost;
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
// Release the lock
|
|
64
|
+
releaseLock();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get current budget status
|
|
69
|
+
*/
|
|
70
|
+
getStatus() {
|
|
71
|
+
const remaining = Math.max(0, this.limit - this.totalSpent);
|
|
72
|
+
const percentageUsed = (this.totalSpent / this.limit) * 100;
|
|
73
|
+
return {
|
|
74
|
+
totalSpent: this.totalSpent,
|
|
75
|
+
limit: this.limit,
|
|
76
|
+
remaining,
|
|
77
|
+
percentageUsed,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Reset budget tracking (useful for monthly resets)
|
|
82
|
+
*/
|
|
83
|
+
reset() {
|
|
84
|
+
this.totalSpent = 0;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Export budget state for persistence
|
|
88
|
+
*/
|
|
89
|
+
exportState() {
|
|
90
|
+
return {
|
|
91
|
+
totalSpent: this.totalSpent,
|
|
92
|
+
limit: this.limit,
|
|
93
|
+
mode: this.mode,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Import budget state from persistence
|
|
98
|
+
* Validates state before importing
|
|
99
|
+
*/
|
|
100
|
+
importState(state) {
|
|
101
|
+
// Validate totalSpent
|
|
102
|
+
if (typeof state.totalSpent !== 'number' || isNaN(state.totalSpent) || !isFinite(state.totalSpent)) {
|
|
103
|
+
throw new Error('TokenFirewall: Invalid budget state - totalSpent must be a valid number');
|
|
104
|
+
}
|
|
105
|
+
if (state.totalSpent < 0) {
|
|
106
|
+
throw new Error('TokenFirewall: Invalid budget state - totalSpent cannot be negative');
|
|
107
|
+
}
|
|
108
|
+
// Allow importing state that exceeds limit (user may have increased limit)
|
|
109
|
+
// Just warn if it's suspicious
|
|
110
|
+
if (state.totalSpent > this.limit * 10) {
|
|
111
|
+
console.warn(`TokenFirewall: Imported totalSpent (${state.totalSpent}) is much larger than current limit (${this.limit})`);
|
|
112
|
+
}
|
|
113
|
+
this.totalSpent = state.totalSpent;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
exports.BudgetManager = BudgetManager;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.calculateCost = calculateCost;
|
|
4
|
+
const pricingRegistry_1 = require("./pricingRegistry");
|
|
5
|
+
/**
|
|
6
|
+
* Pure function to calculate cost from normalized usage
|
|
7
|
+
* Pricing is per 1M tokens
|
|
8
|
+
*/
|
|
9
|
+
function calculateCost(usage) {
|
|
10
|
+
try {
|
|
11
|
+
const pricing = pricingRegistry_1.pricingRegistry.getPricing(usage.provider, usage.model);
|
|
12
|
+
const inputCost = (usage.inputTokens / 1000000) * pricing.input;
|
|
13
|
+
const outputCost = (usage.outputTokens / 1000000) * pricing.output;
|
|
14
|
+
const totalCost = inputCost + outputCost;
|
|
15
|
+
return {
|
|
16
|
+
inputCost,
|
|
17
|
+
outputCost,
|
|
18
|
+
totalCost,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// If pricing not found, log warning and return zero cost
|
|
23
|
+
console.warn(`TokenFirewall: ${error instanceof Error ? error.message : 'Unknown pricing error'} - tracking with $0 cost`);
|
|
24
|
+
return {
|
|
25
|
+
inputCost: 0,
|
|
26
|
+
outputCost: 0,
|
|
27
|
+
totalCost: 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ModelPricing } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Centralized pricing registry for all LLM providers
|
|
4
|
+
*/
|
|
5
|
+
declare class PricingRegistry {
|
|
6
|
+
private pricing;
|
|
7
|
+
constructor();
|
|
8
|
+
/**
|
|
9
|
+
* Initialize default pricing for supported providers
|
|
10
|
+
*/
|
|
11
|
+
private initializeDefaultPricing;
|
|
12
|
+
/**
|
|
13
|
+
* Register pricing for a provider and model
|
|
14
|
+
*/
|
|
15
|
+
register(provider: string, model: string, pricing: ModelPricing): void;
|
|
16
|
+
/**
|
|
17
|
+
* Get pricing for a specific provider and model
|
|
18
|
+
* @throws Error if pricing not found
|
|
19
|
+
*/
|
|
20
|
+
getPricing(provider: string, model: string): ModelPricing;
|
|
21
|
+
/**
|
|
22
|
+
* Check if pricing exists for a provider and model
|
|
23
|
+
*/
|
|
24
|
+
hasPricing(provider: string, model: string): boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare const pricingRegistry: PricingRegistry;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.pricingRegistry = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Centralized pricing registry for all LLM providers
|
|
6
|
+
*/
|
|
7
|
+
class PricingRegistry {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.pricing = new Map();
|
|
10
|
+
this.initializeDefaultPricing();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initialize default pricing for supported providers
|
|
14
|
+
*/
|
|
15
|
+
initializeDefaultPricing() {
|
|
16
|
+
// OpenAI pricing (per 1M tokens)
|
|
17
|
+
this.register("openai", "gpt-4o", { input: 2.5, output: 10.0 });
|
|
18
|
+
this.register("openai", "gpt-4o-mini", { input: 0.15, output: 0.6 });
|
|
19
|
+
this.register("openai", "gpt-4-turbo", { input: 10.0, output: 30.0 });
|
|
20
|
+
this.register("openai", "gpt-3.5-turbo", { input: 0.5, output: 1.5 });
|
|
21
|
+
// Anthropic pricing (per 1M tokens)
|
|
22
|
+
this.register("anthropic", "claude-3-5-sonnet-20241022", { input: 3.0, output: 15.0 });
|
|
23
|
+
this.register("anthropic", "claude-3-5-haiku-20241022", { input: 0.8, output: 4.0 });
|
|
24
|
+
this.register("anthropic", "claude-3-opus-20240229", { input: 15.0, output: 75.0 });
|
|
25
|
+
// Google Gemini pricing (per 1M tokens)
|
|
26
|
+
this.register("gemini", "gemini-2.0-flash-exp", { input: 0.0, output: 0.0 });
|
|
27
|
+
this.register("gemini", "gemini-1.5-pro", { input: 1.25, output: 5.0 });
|
|
28
|
+
this.register("gemini", "gemini-1.5-flash", { input: 0.075, output: 0.3 });
|
|
29
|
+
// Grok pricing (per 1M tokens)
|
|
30
|
+
this.register("grok", "grok-beta", { input: 5.0, output: 15.0 });
|
|
31
|
+
this.register("grok", "grok-vision-beta", { input: 5.0, output: 15.0 });
|
|
32
|
+
this.register("grok", "grok-2-1212", { input: 2.0, output: 10.0 });
|
|
33
|
+
this.register("grok", "grok-2-vision-1212", { input: 2.0, output: 10.0 });
|
|
34
|
+
// Llama models via Grok API (per 1M tokens)
|
|
35
|
+
this.register("grok", "llama-3.1-70b", { input: 0.5, output: 0.8 });
|
|
36
|
+
this.register("grok", "llama-3.1-8b", { input: 0.1, output: 0.1 });
|
|
37
|
+
this.register("grok", "llama-3.2-90b-vision", { input: 0.6, output: 0.9 });
|
|
38
|
+
this.register("grok", "llama-3.3-70b", { input: 0.5, output: 0.8 });
|
|
39
|
+
// Kimi pricing (per 1M tokens)
|
|
40
|
+
this.register("kimi", "moonshot-v1-8k", { input: 0.12, output: 0.12 });
|
|
41
|
+
this.register("kimi", "moonshot-v1-32k", { input: 0.24, output: 0.24 });
|
|
42
|
+
this.register("kimi", "moonshot-v1-128k", { input: 0.6, output: 0.6 });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Register pricing for a provider and model
|
|
46
|
+
*/
|
|
47
|
+
register(provider, model, pricing) {
|
|
48
|
+
if (!this.pricing.has(provider)) {
|
|
49
|
+
this.pricing.set(provider, new Map());
|
|
50
|
+
}
|
|
51
|
+
this.pricing.get(provider).set(model, pricing);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get pricing for a specific provider and model
|
|
55
|
+
* @throws Error if pricing not found
|
|
56
|
+
*/
|
|
57
|
+
getPricing(provider, model) {
|
|
58
|
+
const providerPricing = this.pricing.get(provider);
|
|
59
|
+
if (!providerPricing) {
|
|
60
|
+
throw new Error(`TokenFirewall: No pricing found for provider "${provider}"`);
|
|
61
|
+
}
|
|
62
|
+
const modelPricing = providerPricing.get(model);
|
|
63
|
+
if (!modelPricing) {
|
|
64
|
+
throw new Error(`TokenFirewall: No pricing found for model "${model}" from provider "${provider}"`);
|
|
65
|
+
}
|
|
66
|
+
return modelPricing;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Check if pricing exists for a provider and model
|
|
70
|
+
*/
|
|
71
|
+
hasPricing(provider, model) {
|
|
72
|
+
return this.pricing.get(provider)?.has(model) ?? false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
exports.pricingRegistry = new PricingRegistry();
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified usage contract that all providers must normalize to
|
|
3
|
+
*/
|
|
4
|
+
export interface NormalizedUsage {
|
|
5
|
+
provider: string;
|
|
6
|
+
model: string;
|
|
7
|
+
inputTokens: number;
|
|
8
|
+
outputTokens: number;
|
|
9
|
+
totalTokens: number;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Cost breakdown for a single LLM call
|
|
13
|
+
*/
|
|
14
|
+
export interface CostBreakdown {
|
|
15
|
+
inputCost: number;
|
|
16
|
+
outputCost: number;
|
|
17
|
+
totalCost: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Provider adapter interface - all adapters must implement this
|
|
21
|
+
*/
|
|
22
|
+
export interface ProviderAdapter {
|
|
23
|
+
name: string;
|
|
24
|
+
detect(response: unknown): boolean;
|
|
25
|
+
normalize(response: unknown, request?: unknown): NormalizedUsage;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Pricing configuration for a specific model
|
|
29
|
+
*/
|
|
30
|
+
export interface ModelPricing {
|
|
31
|
+
input: number;
|
|
32
|
+
output: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Budget guard configuration
|
|
36
|
+
*/
|
|
37
|
+
export interface BudgetGuardOptions {
|
|
38
|
+
monthlyLimit: number;
|
|
39
|
+
mode?: "warn" | "block";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Budget status information
|
|
43
|
+
*/
|
|
44
|
+
export interface BudgetStatus {
|
|
45
|
+
totalSpent: number;
|
|
46
|
+
limit: number;
|
|
47
|
+
remaining: number;
|
|
48
|
+
percentageUsed: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Model information with context limits and budget usage
|
|
52
|
+
*/
|
|
53
|
+
export interface ModelInfo {
|
|
54
|
+
model: string;
|
|
55
|
+
contextLimit?: number;
|
|
56
|
+
budgetUsagePercentage?: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Options for listing available models
|
|
60
|
+
*/
|
|
61
|
+
export interface ListModelsOptions {
|
|
62
|
+
provider: string;
|
|
63
|
+
apiKey: string;
|
|
64
|
+
baseURL?: string;
|
|
65
|
+
includeBudgetUsage?: boolean;
|
|
66
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { BudgetGuardOptions, ProviderAdapter, ModelPricing, BudgetStatus } from "./core/types";
|
|
2
|
+
import { BudgetManager } from "./core/budgetManager";
|
|
3
|
+
import { patchGlobalFetch } from "./interceptors/fetchInterceptor";
|
|
4
|
+
import { patchProvider } from "./interceptors/sdkInterceptor";
|
|
5
|
+
import { listAvailableModels, ModelInfo, ListModelsOptions } from "./introspection/modelLister";
|
|
6
|
+
/**
|
|
7
|
+
* Create and configure a budget guard
|
|
8
|
+
* @param options - Budget configuration options
|
|
9
|
+
* @returns Budget manager instance
|
|
10
|
+
*/
|
|
11
|
+
export declare function createBudgetGuard(options: BudgetGuardOptions): BudgetManager;
|
|
12
|
+
/**
|
|
13
|
+
* Patch global fetch to intercept LLM API calls
|
|
14
|
+
*/
|
|
15
|
+
export { patchGlobalFetch };
|
|
16
|
+
/**
|
|
17
|
+
* Patch a specific provider SDK
|
|
18
|
+
* @param providerName - Name of the provider to patch
|
|
19
|
+
*/
|
|
20
|
+
export { patchProvider };
|
|
21
|
+
/**
|
|
22
|
+
* Register a custom provider adapter
|
|
23
|
+
* @param adapter - Provider adapter implementation
|
|
24
|
+
*/
|
|
25
|
+
export declare function registerAdapter(adapter: ProviderAdapter): void;
|
|
26
|
+
/**
|
|
27
|
+
* Register custom pricing for a provider and model
|
|
28
|
+
* @param provider - Provider name
|
|
29
|
+
* @param model - Model name
|
|
30
|
+
* @param pricing - Pricing configuration (per 1M tokens)
|
|
31
|
+
*/
|
|
32
|
+
export declare function registerPricing(provider: string, model: string, pricing: ModelPricing): void;
|
|
33
|
+
/**
|
|
34
|
+
* Register custom context limit for a provider and model
|
|
35
|
+
* @param provider - Provider name
|
|
36
|
+
* @param model - Model name
|
|
37
|
+
* @param contextLimit - Context window size in tokens
|
|
38
|
+
*/
|
|
39
|
+
export declare function registerContextLimit(provider: string, model: string, contextLimit: number): void;
|
|
40
|
+
/**
|
|
41
|
+
* Get current budget status
|
|
42
|
+
* @returns Budget status or null if no budget guard is active
|
|
43
|
+
*/
|
|
44
|
+
export declare function getBudgetStatus(): BudgetStatus | null;
|
|
45
|
+
/**
|
|
46
|
+
* Reset budget tracking
|
|
47
|
+
*/
|
|
48
|
+
export declare function resetBudget(): void;
|
|
49
|
+
/**
|
|
50
|
+
* Export budget state for persistence
|
|
51
|
+
*/
|
|
52
|
+
export declare function exportBudgetState(): {
|
|
53
|
+
totalSpent: number;
|
|
54
|
+
limit: number;
|
|
55
|
+
mode: string;
|
|
56
|
+
} | null;
|
|
57
|
+
/**
|
|
58
|
+
* Import budget state from persistence
|
|
59
|
+
*/
|
|
60
|
+
export declare function importBudgetState(state: {
|
|
61
|
+
totalSpent: number;
|
|
62
|
+
}): void;
|
|
63
|
+
/**
|
|
64
|
+
* List available models for a provider with context limits and budget usage
|
|
65
|
+
* @param options - Provider configuration and options
|
|
66
|
+
* @returns Array of model information
|
|
67
|
+
*/
|
|
68
|
+
export declare function listModels(options: Omit<ListModelsOptions, 'budgetManager'>): Promise<ModelInfo[]>;
|
|
69
|
+
export { listAvailableModels };
|
|
70
|
+
export type { BudgetGuardOptions, ProviderAdapter, ModelPricing, NormalizedUsage, CostBreakdown, BudgetStatus, ModelInfo, ListModelsOptions, } from "./core/types";
|
|
71
|
+
export type { ModelInfo as ModelInfoType, ListModelsOptions as ListModelsOptionsType } from "./introspection/modelLister";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listAvailableModels = exports.patchProvider = exports.patchGlobalFetch = void 0;
|
|
4
|
+
exports.createBudgetGuard = createBudgetGuard;
|
|
5
|
+
exports.registerAdapter = registerAdapter;
|
|
6
|
+
exports.registerPricing = registerPricing;
|
|
7
|
+
exports.registerContextLimit = registerContextLimit;
|
|
8
|
+
exports.getBudgetStatus = getBudgetStatus;
|
|
9
|
+
exports.resetBudget = resetBudget;
|
|
10
|
+
exports.exportBudgetState = exportBudgetState;
|
|
11
|
+
exports.importBudgetState = importBudgetState;
|
|
12
|
+
exports.listModels = listModels;
|
|
13
|
+
const budgetManager_1 = require("./core/budgetManager");
|
|
14
|
+
const pricingRegistry_1 = require("./core/pricingRegistry");
|
|
15
|
+
const registry_1 = require("./registry");
|
|
16
|
+
const fetchInterceptor_1 = require("./interceptors/fetchInterceptor");
|
|
17
|
+
Object.defineProperty(exports, "patchGlobalFetch", { enumerable: true, get: function () { return fetchInterceptor_1.patchGlobalFetch; } });
|
|
18
|
+
const sdkInterceptor_1 = require("./interceptors/sdkInterceptor");
|
|
19
|
+
Object.defineProperty(exports, "patchProvider", { enumerable: true, get: function () { return sdkInterceptor_1.patchProvider; } });
|
|
20
|
+
const modelLister_1 = require("./introspection/modelLister");
|
|
21
|
+
Object.defineProperty(exports, "listAvailableModels", { enumerable: true, get: function () { return modelLister_1.listAvailableModels; } });
|
|
22
|
+
const contextRegistry_1 = require("./introspection/contextRegistry");
|
|
23
|
+
let globalBudgetManager = null;
|
|
24
|
+
/**
|
|
25
|
+
* Create and configure a budget guard
|
|
26
|
+
* @param options - Budget configuration options
|
|
27
|
+
* @returns Budget manager instance
|
|
28
|
+
*/
|
|
29
|
+
function createBudgetGuard(options) {
|
|
30
|
+
// Warn if overwriting existing budget guard
|
|
31
|
+
if (globalBudgetManager) {
|
|
32
|
+
console.warn('TokenFirewall: Creating new budget guard will replace existing one. Previous budget state will be lost.');
|
|
33
|
+
}
|
|
34
|
+
const manager = new budgetManager_1.BudgetManager(options);
|
|
35
|
+
globalBudgetManager = manager;
|
|
36
|
+
(0, fetchInterceptor_1.setBudgetManager)(manager);
|
|
37
|
+
return manager;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Register a custom provider adapter
|
|
41
|
+
* @param adapter - Provider adapter implementation
|
|
42
|
+
*/
|
|
43
|
+
function registerAdapter(adapter) {
|
|
44
|
+
registry_1.adapterRegistry.register(adapter);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Register custom pricing for a provider and model
|
|
48
|
+
* @param provider - Provider name
|
|
49
|
+
* @param model - Model name
|
|
50
|
+
* @param pricing - Pricing configuration (per 1M tokens)
|
|
51
|
+
*/
|
|
52
|
+
function registerPricing(provider, model, pricing) {
|
|
53
|
+
// Validate inputs
|
|
54
|
+
if (!provider || typeof provider !== 'string') {
|
|
55
|
+
throw new Error('TokenFirewall: Provider must be a non-empty string');
|
|
56
|
+
}
|
|
57
|
+
if (!model || typeof model !== 'string') {
|
|
58
|
+
throw new Error('TokenFirewall: Model must be a non-empty string');
|
|
59
|
+
}
|
|
60
|
+
if (!pricing || typeof pricing !== 'object') {
|
|
61
|
+
throw new Error('TokenFirewall: Pricing must be an object');
|
|
62
|
+
}
|
|
63
|
+
if (typeof pricing.input !== 'number' || pricing.input < 0 || !isFinite(pricing.input)) {
|
|
64
|
+
throw new Error('TokenFirewall: Pricing input must be a non-negative number');
|
|
65
|
+
}
|
|
66
|
+
if (typeof pricing.output !== 'number' || pricing.output < 0 || !isFinite(pricing.output)) {
|
|
67
|
+
throw new Error('TokenFirewall: Pricing output must be a non-negative number');
|
|
68
|
+
}
|
|
69
|
+
pricingRegistry_1.pricingRegistry.register(provider, model, pricing);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Register custom context limit for a provider and model
|
|
73
|
+
* @param provider - Provider name
|
|
74
|
+
* @param model - Model name
|
|
75
|
+
* @param contextLimit - Context window size in tokens
|
|
76
|
+
*/
|
|
77
|
+
function registerContextLimit(provider, model, contextLimit) {
|
|
78
|
+
// Validate inputs
|
|
79
|
+
if (!provider || typeof provider !== 'string') {
|
|
80
|
+
throw new Error('TokenFirewall: Provider must be a non-empty string');
|
|
81
|
+
}
|
|
82
|
+
if (!model || typeof model !== 'string') {
|
|
83
|
+
throw new Error('TokenFirewall: Model must be a non-empty string');
|
|
84
|
+
}
|
|
85
|
+
if (typeof contextLimit !== 'number' || contextLimit <= 0 || !isFinite(contextLimit)) {
|
|
86
|
+
throw new Error('TokenFirewall: Context limit must be a positive number');
|
|
87
|
+
}
|
|
88
|
+
contextRegistry_1.contextRegistry.register(provider, model, { tokens: contextLimit });
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Get current budget status
|
|
92
|
+
* @returns Budget status or null if no budget guard is active
|
|
93
|
+
*/
|
|
94
|
+
function getBudgetStatus() {
|
|
95
|
+
return globalBudgetManager ? globalBudgetManager.getStatus() : null;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Reset budget tracking
|
|
99
|
+
*/
|
|
100
|
+
function resetBudget() {
|
|
101
|
+
if (globalBudgetManager) {
|
|
102
|
+
globalBudgetManager.reset();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Export budget state for persistence
|
|
107
|
+
*/
|
|
108
|
+
function exportBudgetState() {
|
|
109
|
+
if (globalBudgetManager) {
|
|
110
|
+
return globalBudgetManager.exportState();
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Import budget state from persistence
|
|
116
|
+
*/
|
|
117
|
+
function importBudgetState(state) {
|
|
118
|
+
if (!globalBudgetManager) {
|
|
119
|
+
throw new Error('TokenFirewall: Cannot import budget state - no budget guard exists. Call createBudgetGuard() first.');
|
|
120
|
+
}
|
|
121
|
+
globalBudgetManager.importState(state);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* List available models for a provider with context limits and budget usage
|
|
125
|
+
* @param options - Provider configuration and options
|
|
126
|
+
* @returns Array of model information
|
|
127
|
+
*/
|
|
128
|
+
async function listModels(options) {
|
|
129
|
+
// Pass the global budget manager to avoid circular dependency
|
|
130
|
+
return (0, modelLister_1.listAvailableModels)({
|
|
131
|
+
...options,
|
|
132
|
+
budgetManager: globalBudgetManager
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { BudgetManager } from "../core/budgetManager";
|
|
2
|
+
/**
|
|
3
|
+
* Set the budget manager for fetch interception
|
|
4
|
+
*/
|
|
5
|
+
export declare function setBudgetManager(manager: BudgetManager | null): void;
|
|
6
|
+
/**
|
|
7
|
+
* Patch global fetch to intercept LLM API calls
|
|
8
|
+
*/
|
|
9
|
+
export declare function patchGlobalFetch(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Restore original fetch
|
|
12
|
+
*/
|
|
13
|
+
export declare function unpatchGlobalFetch(): void;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setBudgetManager = setBudgetManager;
|
|
4
|
+
exports.patchGlobalFetch = patchGlobalFetch;
|
|
5
|
+
exports.unpatchGlobalFetch = unpatchGlobalFetch;
|
|
6
|
+
const registry_1 = require("../registry");
|
|
7
|
+
const costEngine_1 = require("../core/costEngine");
|
|
8
|
+
const logger_1 = require("../logger");
|
|
9
|
+
let isPatched = false;
|
|
10
|
+
let budgetManager = null;
|
|
11
|
+
const originalFetch = globalThis.fetch;
|
|
12
|
+
/**
|
|
13
|
+
* Set the budget manager for fetch interception
|
|
14
|
+
*/
|
|
15
|
+
function setBudgetManager(manager) {
|
|
16
|
+
budgetManager = manager;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Patch global fetch to intercept LLM API calls
|
|
20
|
+
*/
|
|
21
|
+
function patchGlobalFetch() {
|
|
22
|
+
if (isPatched) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const interceptedFetch = async function (input, init) {
|
|
26
|
+
const response = await originalFetch(input, init);
|
|
27
|
+
// Try to clone response for tracking (may fail for some responses)
|
|
28
|
+
let clonedResponse;
|
|
29
|
+
try {
|
|
30
|
+
clonedResponse = response.clone();
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// If cloning fails, just return original response without tracking
|
|
34
|
+
console.warn('TokenFirewall: Failed to clone response for tracking');
|
|
35
|
+
return response;
|
|
36
|
+
}
|
|
37
|
+
// Process response and track budget BEFORE returning
|
|
38
|
+
try {
|
|
39
|
+
const responseData = await clonedResponse.json();
|
|
40
|
+
// Try to process with adapter registry
|
|
41
|
+
const normalizedUsage = registry_1.adapterRegistry.process(responseData);
|
|
42
|
+
if (normalizedUsage) {
|
|
43
|
+
// Calculate cost
|
|
44
|
+
const cost = (0, costEngine_1.calculateCost)(normalizedUsage);
|
|
45
|
+
// Track budget if manager exists - MUST await to enforce blocking
|
|
46
|
+
if (budgetManager) {
|
|
47
|
+
await budgetManager.track(cost.totalCost);
|
|
48
|
+
}
|
|
49
|
+
// Log usage
|
|
50
|
+
logger_1.logger.logUsage(normalizedUsage, cost);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
// If it's a budget error, re-throw it
|
|
55
|
+
if (error instanceof Error && error.message.includes('TokenFirewall: Budget exceeded')) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
// Otherwise, not JSON or not an LLM response - ignore silently
|
|
59
|
+
}
|
|
60
|
+
return response;
|
|
61
|
+
};
|
|
62
|
+
globalThis.fetch = interceptedFetch;
|
|
63
|
+
isPatched = true;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Restore original fetch
|
|
67
|
+
*/
|
|
68
|
+
function unpatchGlobalFetch() {
|
|
69
|
+
if (isPatched) {
|
|
70
|
+
globalThis.fetch = originalFetch;
|
|
71
|
+
isPatched = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-specific interceptors for providers that need direct patching
|
|
3
|
+
* Currently placeholder - most providers work via fetch interception
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Patch a specific provider SDK
|
|
7
|
+
* @param providerName - Name of the provider to patch
|
|
8
|
+
*/
|
|
9
|
+
export declare function patchProvider(providerName: string): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* SDK-specific interceptors for providers that need direct patching
|
|
4
|
+
* Currently placeholder - most providers work via fetch interception
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.patchProvider = patchProvider;
|
|
8
|
+
const patchedProviders = new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Patch a specific provider SDK
|
|
11
|
+
* @param providerName - Name of the provider to patch
|
|
12
|
+
*/
|
|
13
|
+
function patchProvider(providerName) {
|
|
14
|
+
if (patchedProviders.has(providerName)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
switch (providerName.toLowerCase()) {
|
|
18
|
+
case "openai":
|
|
19
|
+
// OpenAI SDK uses fetch internally - covered by fetch interceptor
|
|
20
|
+
break;
|
|
21
|
+
case "anthropic":
|
|
22
|
+
// Anthropic SDK uses fetch internally - covered by fetch interceptor
|
|
23
|
+
break;
|
|
24
|
+
case "gemini":
|
|
25
|
+
// Gemini SDK uses fetch internally - covered by fetch interceptor
|
|
26
|
+
break;
|
|
27
|
+
case "grok":
|
|
28
|
+
// Grok uses fetch - covered by fetch interceptor
|
|
29
|
+
break;
|
|
30
|
+
case "kimi":
|
|
31
|
+
// Kimi uses fetch - covered by fetch interceptor
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
console.warn(`TokenFirewall: Provider "${providerName}" not recognized`);
|
|
35
|
+
}
|
|
36
|
+
patchedProviders.add(providerName);
|
|
37
|
+
}
|