tokenfirewall 1.0.2 → 2.0.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.
@@ -13,16 +13,64 @@ class PricingRegistry {
13
13
  * Initialize default pricing for supported providers
14
14
  */
15
15
  initializeDefaultPricing() {
16
+ // =========================
16
17
  // OpenAI pricing (per 1M tokens)
18
+ // =========================
19
+ // ===== GPT-5 (Latest Flagship) =====
20
+ this.register("openai", "gpt-5", { input: 5.0, output: 15.0 });
21
+ this.register("openai", "gpt-5-mini", { input: 1.5, output: 5.0 });
22
+ // ===== GPT-4.1 Series =====
23
+ this.register("openai", "gpt-4.1", { input: 3.0, output: 12.0 });
24
+ this.register("openai", "gpt-4.1-mini", { input: 0.8, output: 3.0 });
25
+ // ===== GPT-4o (Balanced Multimodal) =====
17
26
  this.register("openai", "gpt-4o", { input: 2.5, output: 10.0 });
18
27
  this.register("openai", "gpt-4o-mini", { input: 0.15, output: 0.6 });
28
+ // ===== Reasoning Models =====
29
+ this.register("openai", "o1", { input: 6.0, output: 18.0 });
30
+ this.register("openai", "o1-mini", { input: 2.0, output: 6.0 });
31
+ // ===== Image Generation =====
32
+ this.register("openai", "gpt-image-1", { input: 5.0, output: 0.0 });
33
+ // ===== Legacy Models =====
19
34
  this.register("openai", "gpt-4-turbo", { input: 10.0, output: 30.0 });
20
35
  this.register("openai", "gpt-3.5-turbo", { input: 0.5, output: 1.5 });
36
+ // =========================
21
37
  // Anthropic pricing (per 1M tokens)
38
+ // =========================
39
+ // ===== Claude 4.5 (Newer Improved) =====
40
+ this.register("anthropic", "claude-opus-4.5", { input: 17.0, output: 85.0 });
41
+ this.register("anthropic", "claude-sonnet-4.5", { input: 4.0, output: 20.0 });
42
+ this.register("anthropic", "claude-haiku-4.5", { input: 1.2, output: 6.0 });
43
+ // ===== Classic Claude 4 =====
44
+ this.register("anthropic", "claude-4-opus", { input: 15.0, output: 75.0 });
45
+ this.register("anthropic", "claude-sonnet-4", { input: 3.0, output: 15.0 });
46
+ this.register("anthropic", "claude-haiku-4", { input: 1.0, output: 5.0 });
47
+ // ===== Stable Claude 3.5 Fallback =====
48
+ this.register("anthropic", "claude-3-5-sonnet-latest", { input: 3.0, output: 15.0 });
49
+ this.register("anthropic", "claude-3-5-haiku-latest", { input: 0.8, output: 4.0 });
50
+ // ===== Legacy Models =====
22
51
  this.register("anthropic", "claude-3-5-sonnet-20241022", { input: 3.0, output: 15.0 });
23
52
  this.register("anthropic", "claude-3-5-haiku-20241022", { input: 0.8, output: 4.0 });
24
53
  this.register("anthropic", "claude-3-opus-20240229", { input: 15.0, output: 75.0 });
54
+ // =========================
25
55
  // Google Gemini pricing (per 1M tokens)
56
+ // =========================
57
+ // ===== Gemini 3 (Latest Generation) =====
58
+ this.register("gemini", "gemini-3-pro", { input: 3.5, output: 14.0 });
59
+ this.register("gemini", "gemini-3.1-pro", { input: 4.0, output: 16.0 });
60
+ this.register("gemini", "gemini-3-flash", { input: 0.35, output: 1.5 });
61
+ this.register("gemini", "gemini-3-flash-lite", { input: 0.15, output: 0.6 });
62
+ // ===== Image Models (Nano Banana Series) =====
63
+ this.register("gemini", "gemini-3-pro-image", { input: 5.0, output: 0.0 });
64
+ this.register("gemini", "gemini-3.1-flash-image", { input: 0.75, output: 0.0 });
65
+ // ===== Gemini 2.5 (Stable Production Tier) =====
66
+ this.register("gemini", "gemini-2.5-pro", { input: 2.5, output: 10.0 });
67
+ this.register("gemini", "gemini-2.5-flash", { input: 0.30, output: 1.2 });
68
+ this.register("gemini", "gemini-2.5-flash-lite", { input: 0.10, output: 0.4 });
69
+ // Image-capable 2.5
70
+ this.register("gemini", "gemini-2.5-flash-image", { input: 0.5, output: 0.0 });
71
+ // ===== Ultra-light / Experimental =====
72
+ this.register("gemini", "gemini-nano-banana", { input: 0.05, output: 0.2 });
73
+ // ===== Legacy Models =====
26
74
  this.register("gemini", "gemini-2.0-flash-exp", { input: 0.0, output: 0.0 });
27
75
  this.register("gemini", "gemini-1.5-pro", { input: 1.25, output: 5.0 });
28
76
  this.register("gemini", "gemini-1.5-flash", { input: 0.075, output: 0.3 });
@@ -45,17 +93,19 @@ class PricingRegistry {
45
93
  * Register pricing for a provider and model
46
94
  */
47
95
  register(provider, model, pricing) {
48
- if (!this.pricing.has(provider)) {
49
- this.pricing.set(provider, new Map());
96
+ const normalizedProvider = provider.toLowerCase();
97
+ if (!this.pricing.has(normalizedProvider)) {
98
+ this.pricing.set(normalizedProvider, new Map());
50
99
  }
51
- this.pricing.get(provider).set(model, pricing);
100
+ this.pricing.get(normalizedProvider).set(model, pricing);
52
101
  }
53
102
  /**
54
103
  * Get pricing for a specific provider and model
55
104
  * @throws Error if pricing not found
56
105
  */
57
106
  getPricing(provider, model) {
58
- const providerPricing = this.pricing.get(provider);
107
+ const normalizedProvider = provider.toLowerCase();
108
+ const providerPricing = this.pricing.get(normalizedProvider);
59
109
  if (!providerPricing) {
60
110
  throw new Error(`TokenFirewall: No pricing found for provider "${provider}"`);
61
111
  }
@@ -69,7 +119,8 @@ class PricingRegistry {
69
119
  * Check if pricing exists for a provider and model
70
120
  */
71
121
  hasPricing(provider, model) {
72
- return this.pricing.get(provider)?.has(model) ?? false;
122
+ const normalizedProvider = provider.toLowerCase();
123
+ return this.pricing.get(normalizedProvider)?.has(model) ?? false;
73
124
  }
74
125
  }
75
126
  exports.pricingRegistry = new PricingRegistry();
package/dist/index.d.ts CHANGED
@@ -3,6 +3,8 @@ import { BudgetManager } from "./core/budgetManager";
3
3
  import { patchGlobalFetch } from "./interceptors/fetchInterceptor";
4
4
  import { patchProvider } from "./interceptors/sdkInterceptor";
5
5
  import { listAvailableModels, ModelInfo, ListModelsOptions } from "./introspection/modelLister";
6
+ import { ModelRouter } from "./router/modelRouter";
7
+ import { ModelRouterOptions } from "./router/types";
6
8
  /**
7
9
  * Create and configure a budget guard
8
10
  * @param options - Budget configuration options
@@ -37,6 +39,17 @@ export declare function registerPricing(provider: string, model: string, pricing
37
39
  * @param contextLimit - Context window size in tokens
38
40
  */
39
41
  export declare function registerContextLimit(provider: string, model: string, contextLimit: number): void;
42
+ /**
43
+ * Register multiple models for a provider at once
44
+ * Useful for dynamic model discovery or custom provider setup
45
+ * @param provider - Provider name
46
+ * @param models - Array of model configurations
47
+ */
48
+ export declare function registerModels(provider: string, models: Array<{
49
+ name: string;
50
+ contextLimit?: number;
51
+ pricing?: ModelPricing;
52
+ }>): void;
40
53
  /**
41
54
  * Get current budget status
42
55
  * @returns Budget status or null if no budget guard is active
@@ -60,6 +73,16 @@ export declare function exportBudgetState(): {
60
73
  export declare function importBudgetState(state: {
61
74
  totalSpent: number;
62
75
  }): void;
76
+ /**
77
+ * Create and configure a model router for automatic retries and fallbacks
78
+ * @param options - Router configuration options
79
+ * @returns Model router instance
80
+ */
81
+ export declare function createModelRouter(options: ModelRouterOptions): ModelRouter;
82
+ /**
83
+ * Disable model router
84
+ */
85
+ export declare function disableModelRouter(): void;
63
86
  /**
64
87
  * List available models for a provider with context limits and budget usage
65
88
  * @param options - Provider configuration and options
@@ -69,3 +92,12 @@ export declare function listModels(options: Omit<ListModelsOptions, 'budgetManag
69
92
  export { listAvailableModels };
70
93
  export type { BudgetGuardOptions, ProviderAdapter, ModelPricing, NormalizedUsage, CostBreakdown, BudgetStatus, ModelInfo, ListModelsOptions, } from "./core/types";
71
94
  export type { ModelInfo as ModelInfoType, ListModelsOptions as ListModelsOptionsType } from "./introspection/modelLister";
95
+ export type { ModelRouterOptions, RoutingStrategy, FailureType, FailureContext, RoutingDecision, RouterEvent } from "./router/types";
96
+ /**
97
+ * Model configuration for bulk registration
98
+ */
99
+ export interface ModelConfig {
100
+ name: string;
101
+ contextLimit?: number;
102
+ pricing?: ModelPricing;
103
+ }
package/dist/index.js CHANGED
@@ -5,10 +5,13 @@ exports.createBudgetGuard = createBudgetGuard;
5
5
  exports.registerAdapter = registerAdapter;
6
6
  exports.registerPricing = registerPricing;
7
7
  exports.registerContextLimit = registerContextLimit;
8
+ exports.registerModels = registerModels;
8
9
  exports.getBudgetStatus = getBudgetStatus;
9
10
  exports.resetBudget = resetBudget;
10
11
  exports.exportBudgetState = exportBudgetState;
11
12
  exports.importBudgetState = importBudgetState;
13
+ exports.createModelRouter = createModelRouter;
14
+ exports.disableModelRouter = disableModelRouter;
12
15
  exports.listModels = listModels;
13
16
  const budgetManager_1 = require("./core/budgetManager");
14
17
  const pricingRegistry_1 = require("./core/pricingRegistry");
@@ -20,7 +23,9 @@ Object.defineProperty(exports, "patchProvider", { enumerable: true, get: functio
20
23
  const modelLister_1 = require("./introspection/modelLister");
21
24
  Object.defineProperty(exports, "listAvailableModels", { enumerable: true, get: function () { return modelLister_1.listAvailableModels; } });
22
25
  const contextRegistry_1 = require("./introspection/contextRegistry");
26
+ const modelRouter_1 = require("./router/modelRouter");
23
27
  let globalBudgetManager = null;
28
+ let globalModelRouter = null;
24
29
  /**
25
30
  * Create and configure a budget guard
26
31
  * @param options - Budget configuration options
@@ -51,22 +56,22 @@ function registerAdapter(adapter) {
51
56
  */
52
57
  function registerPricing(provider, model, pricing) {
53
58
  // Validate inputs
54
- if (!provider || typeof provider !== 'string') {
59
+ if (!provider || typeof provider !== 'string' || provider.trim() === '') {
55
60
  throw new Error('TokenFirewall: Provider must be a non-empty string');
56
61
  }
57
- if (!model || typeof model !== 'string') {
62
+ if (!model || typeof model !== 'string' || model.trim() === '') {
58
63
  throw new Error('TokenFirewall: Model must be a non-empty string');
59
64
  }
60
65
  if (!pricing || typeof pricing !== 'object') {
61
66
  throw new Error('TokenFirewall: Pricing must be an object');
62
67
  }
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');
68
+ if (typeof pricing.input !== 'number' || pricing.input < 0 || !isFinite(pricing.input) || isNaN(pricing.input)) {
69
+ throw new Error('TokenFirewall: Pricing input must be a non-negative finite number');
65
70
  }
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');
71
+ if (typeof pricing.output !== 'number' || pricing.output < 0 || !isFinite(pricing.output) || isNaN(pricing.output)) {
72
+ throw new Error('TokenFirewall: Pricing output must be a non-negative finite number');
68
73
  }
69
- pricingRegistry_1.pricingRegistry.register(provider, model, pricing);
74
+ pricingRegistry_1.pricingRegistry.register(provider.toLowerCase(), model, pricing);
70
75
  }
71
76
  /**
72
77
  * Register custom context limit for a provider and model
@@ -76,16 +81,47 @@ function registerPricing(provider, model, pricing) {
76
81
  */
77
82
  function registerContextLimit(provider, model, contextLimit) {
78
83
  // Validate inputs
79
- if (!provider || typeof provider !== 'string') {
84
+ if (!provider || typeof provider !== 'string' || provider.trim() === '') {
80
85
  throw new Error('TokenFirewall: Provider must be a non-empty string');
81
86
  }
82
- if (!model || typeof model !== 'string') {
87
+ if (!model || typeof model !== 'string' || model.trim() === '') {
83
88
  throw new Error('TokenFirewall: Model must be a non-empty string');
84
89
  }
85
- if (typeof contextLimit !== 'number' || contextLimit <= 0 || !isFinite(contextLimit)) {
86
- throw new Error('TokenFirewall: Context limit must be a positive number');
90
+ if (typeof contextLimit !== 'number' || contextLimit <= 0 || !isFinite(contextLimit) || isNaN(contextLimit)) {
91
+ throw new Error('TokenFirewall: Context limit must be a positive finite number');
92
+ }
93
+ contextRegistry_1.contextRegistry.register(provider.toLowerCase(), model, { tokens: contextLimit });
94
+ }
95
+ /**
96
+ * Register multiple models for a provider at once
97
+ * Useful for dynamic model discovery or custom provider setup
98
+ * @param provider - Provider name
99
+ * @param models - Array of model configurations
100
+ */
101
+ function registerModels(provider, models) {
102
+ // Validate inputs
103
+ if (!provider || typeof provider !== 'string' || provider.trim() === '') {
104
+ throw new Error('TokenFirewall: Provider must be a non-empty string');
105
+ }
106
+ if (!Array.isArray(models) || models.length === 0) {
107
+ throw new Error('TokenFirewall: Models must be a non-empty array');
108
+ }
109
+ // Normalize provider name to lowercase
110
+ const normalizedProvider = provider.toLowerCase();
111
+ // Register each model
112
+ for (const model of models) {
113
+ if (!model.name || typeof model.name !== 'string' || model.name.trim() === '') {
114
+ throw new Error('TokenFirewall: Each model must have a valid name');
115
+ }
116
+ // Register context limit if provided
117
+ if (model.contextLimit !== undefined) {
118
+ registerContextLimit(normalizedProvider, model.name, model.contextLimit);
119
+ }
120
+ // Register pricing if provided
121
+ if (model.pricing !== undefined) {
122
+ registerPricing(normalizedProvider, model.name, model.pricing);
123
+ }
87
124
  }
88
- contextRegistry_1.contextRegistry.register(provider, model, { tokens: contextLimit });
89
125
  }
90
126
  /**
91
127
  * Get current budget status
@@ -120,6 +156,24 @@ function importBudgetState(state) {
120
156
  }
121
157
  globalBudgetManager.importState(state);
122
158
  }
159
+ /**
160
+ * Create and configure a model router for automatic retries and fallbacks
161
+ * @param options - Router configuration options
162
+ * @returns Model router instance
163
+ */
164
+ function createModelRouter(options) {
165
+ const router = new modelRouter_1.ModelRouter(options);
166
+ globalModelRouter = router;
167
+ (0, fetchInterceptor_1.setModelRouter)(router);
168
+ return router;
169
+ }
170
+ /**
171
+ * Disable model router
172
+ */
173
+ function disableModelRouter() {
174
+ globalModelRouter = null;
175
+ (0, fetchInterceptor_1.setModelRouter)(null);
176
+ }
123
177
  /**
124
178
  * List available models for a provider with context limits and budget usage
125
179
  * @param options - Provider configuration and options
@@ -1,8 +1,13 @@
1
1
  import { BudgetManager } from "../core/budgetManager";
2
+ import { ModelRouter } from "../router/modelRouter";
2
3
  /**
3
4
  * Set the budget manager for fetch interception
4
5
  */
5
6
  export declare function setBudgetManager(manager: BudgetManager | null): void;
7
+ /**
8
+ * Set the model router for fetch interception
9
+ */
10
+ export declare function setModelRouter(router: ModelRouter | null): void;
6
11
  /**
7
12
  * Patch global fetch to intercept LLM API calls
8
13
  */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.setBudgetManager = setBudgetManager;
4
+ exports.setModelRouter = setModelRouter;
4
5
  exports.patchGlobalFetch = patchGlobalFetch;
5
6
  exports.unpatchGlobalFetch = unpatchGlobalFetch;
6
7
  const registry_1 = require("../registry");
@@ -8,6 +9,7 @@ const costEngine_1 = require("../core/costEngine");
8
9
  const logger_1 = require("../logger");
9
10
  let isPatched = false;
10
11
  let budgetManager = null;
12
+ let modelRouter = null;
11
13
  const originalFetch = globalThis.fetch;
12
14
  /**
13
15
  * Set the budget manager for fetch interception
@@ -15,6 +17,12 @@ const originalFetch = globalThis.fetch;
15
17
  function setBudgetManager(manager) {
16
18
  budgetManager = manager;
17
19
  }
20
+ /**
21
+ * Set the model router for fetch interception
22
+ */
23
+ function setModelRouter(router) {
24
+ modelRouter = router;
25
+ }
18
26
  /**
19
27
  * Patch global fetch to intercept LLM API calls
20
28
  */
@@ -23,44 +31,287 @@ function patchGlobalFetch() {
23
31
  return;
24
32
  }
25
33
  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;
34
+ // If router is enabled, wrap request with retry logic
35
+ if (modelRouter) {
36
+ return await fetchWithRetry(input, init);
37
+ }
38
+ // Otherwise, use standard fetch with tracking
39
+ return await standardFetch(input, init);
40
+ };
41
+ globalThis.fetch = interceptedFetch;
42
+ isPatched = true;
43
+ }
44
+ /**
45
+ * Standard fetch with cost tracking (no retry)
46
+ */
47
+ async function standardFetch(input, init) {
48
+ const response = await originalFetch(input, init);
49
+ // Try to clone response for tracking (may fail for some responses)
50
+ let clonedResponse;
51
+ try {
52
+ clonedResponse = response.clone();
53
+ }
54
+ catch (error) {
55
+ // If cloning fails, just return original response without tracking
56
+ console.warn('TokenFirewall: Failed to clone response for tracking');
57
+ return response;
58
+ }
59
+ // Process response and track budget BEFORE returning
60
+ try {
61
+ const responseData = await clonedResponse.json();
62
+ // Try to process with adapter registry
63
+ const normalizedUsage = registry_1.adapterRegistry.process(responseData);
64
+ if (normalizedUsage) {
65
+ // Calculate cost
66
+ const cost = (0, costEngine_1.calculateCost)(normalizedUsage);
67
+ // Track budget if manager exists - MUST await to enforce blocking
68
+ if (budgetManager) {
69
+ await budgetManager.track(cost.totalCost);
70
+ }
71
+ // Log usage
72
+ logger_1.logger.logUsage(normalizedUsage, cost);
73
+ }
74
+ }
75
+ catch (error) {
76
+ // If it's a budget error, re-throw it
77
+ if (error instanceof Error && error.message.includes('TokenFirewall: Budget exceeded')) {
78
+ throw error;
79
+ }
80
+ // Otherwise, not JSON or not an LLM response - ignore silently
81
+ }
82
+ return response;
83
+ }
84
+ /**
85
+ * Fetch with automatic retry and model switching
86
+ */
87
+ async function fetchWithRetry(input, init) {
88
+ // Check if streaming is enabled - router doesn't support streaming yet
89
+ if (init?.body) {
29
90
  try {
30
- clonedResponse = response.clone();
91
+ const body = JSON.parse(init.body);
92
+ if (body.stream === true) {
93
+ console.warn('TokenFirewall Router: Streaming requests are not supported by the router. ' +
94
+ 'Falling back to standard fetch without retry logic.');
95
+ return await standardFetch(input, init);
96
+ }
31
97
  }
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;
98
+ catch {
99
+ // Body is not JSON, continue normally
36
100
  }
37
- // Process response and track budget BEFORE returning
101
+ }
102
+ const attemptedModels = [];
103
+ let retryCount = 0;
104
+ let lastError;
105
+ let currentInit = init;
106
+ let currentInput = input;
107
+ // Extract original model and provider from request
108
+ const { originalModel, provider } = extractModelInfo(input, init);
109
+ if (originalModel) {
110
+ attemptedModels.push(originalModel);
111
+ }
112
+ while (retryCount <= (modelRouter?.getMaxRetries() || 0)) {
38
113
  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);
114
+ // Make the request
115
+ const response = await standardFetch(currentInput, currentInit);
116
+ // Check if response indicates an error
117
+ if (!response.ok) {
118
+ // Clone and read error response safely
119
+ let errorData = {};
120
+ try {
121
+ const clonedResponse = response.clone();
122
+ errorData = await clonedResponse.json();
123
+ }
124
+ catch {
125
+ // Response is not JSON or already consumed
48
126
  }
49
- // Log usage
50
- logger_1.logger.logUsage(normalizedUsage, cost);
127
+ // Throw proper Error instance with structured data
128
+ const errorObj = {
129
+ status: response.status,
130
+ response: { data: errorData }
131
+ };
132
+ throw new Error(JSON.stringify(errorObj));
51
133
  }
134
+ return response;
52
135
  }
53
136
  catch (error) {
54
- // If it's a budget error, re-throw it
55
- if (error instanceof Error && error.message.includes('TokenFirewall: Budget exceeded')) {
137
+ lastError = error;
138
+ // If no router or no model info, throw immediately
139
+ if (!modelRouter || !originalModel || !provider) {
140
+ throw error;
141
+ }
142
+ // Parse error if it's a JSON string
143
+ let parsedError = error;
144
+ if (error instanceof Error && error.message.startsWith('{')) {
145
+ try {
146
+ parsedError = JSON.parse(error.message);
147
+ }
148
+ catch {
149
+ // Keep original error
150
+ }
151
+ }
152
+ // Get routing decision
153
+ const decision = modelRouter.handleFailure({
154
+ error: parsedError,
155
+ originalModel,
156
+ requestBody: currentInit?.body ? (() => {
157
+ try {
158
+ return JSON.parse(currentInit.body);
159
+ }
160
+ catch {
161
+ return {};
162
+ }
163
+ })() : {},
164
+ provider,
165
+ retryCount,
166
+ attemptedModels
167
+ });
168
+ // If no retry, throw the error
169
+ if (!decision.retry || !decision.nextModel) {
56
170
  throw error;
57
171
  }
58
- // Otherwise, not JSON or not an LLM response - ignore silently
172
+ // Log the routing event
173
+ logger_1.logger.logRouterEvent({
174
+ originalModel,
175
+ nextModel: decision.nextModel,
176
+ reason: decision.reason,
177
+ attempt: retryCount + 1,
178
+ maxRetries: modelRouter.getMaxRetries()
179
+ });
180
+ // Update request with new model
181
+ attemptedModels.push(decision.nextModel);
182
+ const updated = updateRequestModel(currentInput, currentInit, decision.nextModel, provider);
183
+ currentInput = updated.input;
184
+ currentInit = updated.init;
185
+ retryCount++;
59
186
  }
60
- return response;
61
- };
62
- globalThis.fetch = interceptedFetch;
63
- isPatched = true;
187
+ }
188
+ // Max retries exceeded
189
+ throw new Error(`TokenFirewall: Max routing retries exceeded. Last error: ${lastError}`);
190
+ }
191
+ /**
192
+ * Extract model and provider information from request
193
+ */
194
+ function extractModelInfo(input, init) {
195
+ try {
196
+ // Parse URL to detect provider
197
+ const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : String(input));
198
+ let provider = null;
199
+ let model = null;
200
+ if (url.includes('api.openai.com')) {
201
+ provider = 'openai';
202
+ }
203
+ else if (url.includes('api.anthropic.com')) {
204
+ provider = 'anthropic';
205
+ }
206
+ else if (url.includes('generativelanguage.googleapis.com')) {
207
+ provider = 'gemini';
208
+ // Gemini model is in URL: /models/{model}:generateContent
209
+ const match = url.match(/\/models\/([^:]+):/);
210
+ if (match) {
211
+ model = match[1];
212
+ }
213
+ }
214
+ else if (url.includes('api.x.ai')) {
215
+ provider = 'grok';
216
+ }
217
+ else if (url.includes('api.moonshot.cn')) {
218
+ provider = 'kimi';
219
+ }
220
+ else {
221
+ // Unknown provider - try to extract from URL hostname
222
+ try {
223
+ const urlObj = new URL(url);
224
+ const hostname = urlObj.hostname;
225
+ // Use first part of hostname as provider (e.g., api.example.com -> example)
226
+ const parts = hostname.split('.');
227
+ if (parts.length >= 2) {
228
+ provider = parts[parts.length - 2]; // Get domain name
229
+ }
230
+ }
231
+ catch {
232
+ provider = 'unknown';
233
+ }
234
+ }
235
+ // Extract model from request body (for non-Gemini providers)
236
+ if (!model) {
237
+ let body = null;
238
+ // Get body from init or Request object
239
+ if (init?.body) {
240
+ body = typeof init.body === 'string' ? init.body : null;
241
+ }
242
+ else if (input instanceof Request && input.body) {
243
+ // Note: Request.body is a ReadableStream, we can't read it here without consuming it
244
+ // So we skip body parsing for Request objects without explicit init.body
245
+ body = null;
246
+ }
247
+ if (body) {
248
+ try {
249
+ const bodyObj = JSON.parse(body);
250
+ model = bodyObj.model || null;
251
+ }
252
+ catch {
253
+ // Body is not JSON or doesn't have model field
254
+ }
255
+ }
256
+ }
257
+ return { originalModel: model, provider };
258
+ }
259
+ catch {
260
+ return { originalModel: null, provider: null };
261
+ }
262
+ }
263
+ /**
264
+ * Update request with new model (handles both URL and body)
265
+ */
266
+ function updateRequestModel(input, init, newModel, provider) {
267
+ let newInput = input;
268
+ let newInit = init;
269
+ // Get the URL string
270
+ const url = typeof input === 'string' ? input : (input instanceof Request ? input.url : String(input));
271
+ // Update URL for Gemini (model is in URL path)
272
+ if (provider === 'gemini') {
273
+ const newUrl = url.replace(/\/models\/[^:]+:/, `/models/${newModel}:`);
274
+ // If input was a Request object, we need to create a new Request with updated URL
275
+ if (input instanceof Request) {
276
+ // Clone the request with new URL
277
+ newInput = new Request(newUrl, {
278
+ method: input.method,
279
+ headers: input.headers,
280
+ body: init?.body || null,
281
+ mode: input.mode,
282
+ credentials: input.credentials,
283
+ cache: input.cache,
284
+ redirect: input.redirect,
285
+ referrer: input.referrer,
286
+ integrity: input.integrity
287
+ });
288
+ }
289
+ else {
290
+ newInput = newUrl;
291
+ }
292
+ }
293
+ // Update body for other providers (model is in request body)
294
+ if (init?.body) {
295
+ try {
296
+ const body = JSON.parse(init.body);
297
+ body.model = newModel;
298
+ newInit = {
299
+ ...init,
300
+ body: JSON.stringify(body)
301
+ };
302
+ }
303
+ catch {
304
+ // Body is not JSON, log warning
305
+ console.warn(`TokenFirewall Router: Cannot update model in non-JSON request body. ` +
306
+ `Model switching may not work correctly.`);
307
+ }
308
+ }
309
+ else if (input instanceof Request && provider !== 'gemini') {
310
+ // Request object without explicit init.body - we can't modify it
311
+ console.warn(`TokenFirewall Router: Cannot update model in Request object without explicit body. ` +
312
+ `Model switching may not work correctly.`);
313
+ }
314
+ return { input: newInput, init: newInit };
64
315
  }
65
316
  /**
66
317
  * Restore original fetch
@@ -25,6 +25,11 @@ declare class ContextRegistry {
25
25
  * Check if provider is supported
26
26
  */
27
27
  isProviderSupported(provider: string): boolean;
28
+ /**
29
+ * Get all models for a provider
30
+ * Returns array of model names
31
+ */
32
+ getModelsForProvider(provider: string): string[];
28
33
  }
29
34
  export declare const contextRegistry: ContextRegistry;
30
35
  export {};