@mayank3238/keymux 1.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.
package/dist/index.js ADDED
@@ -0,0 +1,2567 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import os2 from 'os';
4
+ import http from 'http';
5
+
6
+ // keymux v1.0.0 — Smart API Key Multiplexer for LLM Providers
7
+
8
+ // src/types/index.ts
9
+ var CircuitState = /* @__PURE__ */ ((CircuitState2) => {
10
+ CircuitState2["HEALTHY"] = "healthy";
11
+ CircuitState2["DEGRADED"] = "degraded";
12
+ CircuitState2["OPEN"] = "open";
13
+ return CircuitState2;
14
+ })(CircuitState || {});
15
+ var KeyRouterError = class extends Error {
16
+ constructor(message, code, keyId) {
17
+ super(message);
18
+ this.code = code;
19
+ this.keyId = keyId;
20
+ this.name = "KeyRouterError";
21
+ }
22
+ code;
23
+ keyId;
24
+ };
25
+ var RateLimitError = class extends KeyRouterError {
26
+ constructor(keyId) {
27
+ super(`Rate limit exceeded for key ${keyId}`, "ALL_KEYS_EXHAUSTED", keyId);
28
+ this.name = "RateLimitError";
29
+ }
30
+ };
31
+ var PROVIDER_PRESETS = {
32
+ mistral: {
33
+ name: "Mistral AI",
34
+ baseURL: "https://api.mistral.ai/v1",
35
+ defaultRpmLimit: 30,
36
+ models: [
37
+ "codestral-2508",
38
+ "mistral-large-latest",
39
+ "open-mistral-nemo"
40
+ ]
41
+ },
42
+ nvidia: {
43
+ name: "NVIDIA",
44
+ baseURL: "https://integrate.api.nvidia.com/v1",
45
+ defaultRpmLimit: 40,
46
+ models: [
47
+ "nvidia/nemotron-3-ultra-550b-a55b",
48
+ "nvidia/nemotron-3-super-120b-a12b",
49
+ "stepfun-ai/step-3.7-flash"
50
+ ]
51
+ },
52
+ gemini: {
53
+ name: "Google Gemini",
54
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai",
55
+ defaultRpmLimit: 15,
56
+ models: [
57
+ "gemini-3.1-pro-preview",
58
+ "gemini-2.5-pro",
59
+ "gemini-2.5-flash",
60
+ "gemini-2.5-flash-thinking",
61
+ "gemini-3.5-flash"
62
+ ]
63
+ },
64
+ openrouter: {
65
+ name: "OpenRouter",
66
+ baseURL: "https://openrouter.ai/api/v1",
67
+ defaultRpmLimit: 20,
68
+ models: [
69
+ "nex-agi/nex-n2.5-mini:free",
70
+ "meta-llama/llama-3.3-70b-instruct:free",
71
+ "qwen/qwen3-235b-a22b:free"
72
+ ]
73
+ },
74
+ deepseek: {
75
+ name: "DeepSeek",
76
+ baseURL: "https://api.deepseek.com/v1",
77
+ defaultRpmLimit: 60,
78
+ models: ["deepseek-chat", "deepseek-reasoner"]
79
+ },
80
+ together: {
81
+ name: "Together AI",
82
+ baseURL: "https://api.together.xyz/v1",
83
+ defaultRpmLimit: 60,
84
+ models: [
85
+ "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo",
86
+ "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo"
87
+ ]
88
+ },
89
+ sambanova: {
90
+ name: "SambaNova",
91
+ baseURL: "https://api.sambanova.ai/v1",
92
+ defaultRpmLimit: 30,
93
+ models: ["Meta-Llama-3.1-405B-Instruct", "Meta-Llama-3.1-70B-Instruct"]
94
+ },
95
+ openai: {
96
+ name: "OpenAI",
97
+ baseURL: "https://api.openai.com/v1",
98
+ defaultRpmLimit: 500,
99
+ models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"]
100
+ },
101
+ groq: {
102
+ name: "Groq",
103
+ baseURL: "https://api.groq.com/openai/v1",
104
+ defaultRpmLimit: 30,
105
+ models: ["qwen/qwen3.8-27b", "openai/gpt-oss-120b", "groq/compound"]
106
+ }
107
+ };
108
+
109
+ // src/tracking/keyTracker.ts
110
+ var KeyTracker = class {
111
+ keys = /* @__PURE__ */ new Map();
112
+ options;
113
+ cleanupInterval;
114
+ constructor(options) {
115
+ this.options = options;
116
+ this.startCleanupLoop();
117
+ }
118
+ initialize(keyConfigs) {
119
+ const oldKeys = new Map(this.keys);
120
+ this.keys.clear();
121
+ for (const config2 of keyConfigs) {
122
+ const existing = oldKeys.get(config2.id);
123
+ if (existing) {
124
+ existing.config = {
125
+ ...config2,
126
+ rpmLimit: config2.rpmLimit ?? this.options.defaultRpmLimit,
127
+ weight: config2.weight ?? this.options.defaultWeight
128
+ };
129
+ this.keys.set(config2.id, existing);
130
+ } else {
131
+ const state = {
132
+ config: {
133
+ ...config2,
134
+ rpmLimit: config2.rpmLimit ?? this.options.defaultRpmLimit,
135
+ weight: config2.weight ?? this.options.defaultWeight
136
+ },
137
+ rpm: 0,
138
+ lastUsed: 0,
139
+ circuitState: "healthy" /* HEALTHY */,
140
+ failures: 0,
141
+ cooldownUntil: 0,
142
+ avgLatencyMs: 0,
143
+ latencyHistory: [],
144
+ totalRequests: 0,
145
+ totalErrors: 0,
146
+ requestTimestamps: []
147
+ };
148
+ this.keys.set(config2.id, state);
149
+ }
150
+ }
151
+ }
152
+ /**
153
+ * Get all key states - for debugging or stats
154
+ */
155
+ getAllStates() {
156
+ return Array.from(this.keys.values());
157
+ }
158
+ /**
159
+ * Get state of a specific key
160
+ */
161
+ getState(keyId) {
162
+ return this.keys.get(keyId);
163
+ }
164
+ /**
165
+ * Get only healthy and available keys
166
+ * Checks circuit breaker state and RPM limits
167
+ */
168
+ getAvailableKeys() {
169
+ const now = Date.now();
170
+ return Array.from(this.keys.values()).filter((state) => {
171
+ if (state.circuitState === "open" /* OPEN */) {
172
+ if (now >= state.cooldownUntil) {
173
+ this.transitionToHealthy(state);
174
+ return state.rpm < state.config.rpmLimit;
175
+ }
176
+ return false;
177
+ }
178
+ return state.rpm < state.config.rpmLimit;
179
+ });
180
+ }
181
+ /**
182
+ * Record a request attempt (reservation)
183
+ * Increments RPM count without touching failures or circuit state.
184
+ */
185
+ recordAttempt(keyId) {
186
+ const state = this.keys.get(keyId);
187
+ if (!state) return;
188
+ state.requestTimestamps.push(Date.now());
189
+ state.rpm = state.requestTimestamps.length;
190
+ state.lastUsed = Date.now();
191
+ state.totalRequests++;
192
+ }
193
+ /**
194
+ * Record a successful request completion
195
+ * Updates latency and resets failures (RPM already incremented by recordAttempt).
196
+ */
197
+ recordSuccess(keyId, latencyMs) {
198
+ const state = this.keys.get(keyId);
199
+ if (!state) return;
200
+ state.lastUsed = Date.now();
201
+ state.failures = 0;
202
+ if (this.options.trackLatency && latencyMs != null) {
203
+ state.latencyHistory.push(latencyMs);
204
+ if (state.latencyHistory.length > 25) {
205
+ state.latencyHistory.shift();
206
+ }
207
+ state.avgLatencyMs = Math.round(
208
+ state.latencyHistory.reduce((sum, v) => sum + v, 0) / state.latencyHistory.length
209
+ );
210
+ }
211
+ if (state.circuitState === "degraded" /* DEGRADED */ || state.circuitState === "open" /* OPEN */) {
212
+ this.transitionToHealthy(state);
213
+ }
214
+ this.emitDebug({
215
+ type: "key_selected",
216
+ keyId,
217
+ timestamp: Date.now(),
218
+ details: { rpm: state.rpm, latencyMs }
219
+ });
220
+ }
221
+ /**
222
+ * Record a failed request (rate limit, server error, etc.)
223
+ * Triggers circuit breaker logic (RPM already incremented by recordAttempt).
224
+ */
225
+ recordFailure(keyId, isRateLimit = false) {
226
+ const state = this.keys.get(keyId);
227
+ if (!state) return;
228
+ state.failures++;
229
+ state.totalErrors++;
230
+ state.lastUsed = Date.now();
231
+ this.emitDebug({
232
+ type: "key_failed",
233
+ keyId,
234
+ timestamp: Date.now(),
235
+ details: { failures: state.failures, isRateLimit }
236
+ });
237
+ if (state.failures >= this.options.failureThreshold) {
238
+ this.openCircuit(state);
239
+ } else if (state.failures >= Math.ceil(this.options.failureThreshold / 2)) {
240
+ if (state.circuitState === "healthy" /* HEALTHY */) {
241
+ state.circuitState = "degraded" /* DEGRADED */;
242
+ if (this.options.onStateChange) {
243
+ this.options.onStateChange(keyId, "degraded" /* DEGRADED */);
244
+ }
245
+ this.emitDebug({
246
+ type: "key_failed",
247
+ keyId,
248
+ timestamp: Date.now(),
249
+ details: { circuitState: "degraded" /* DEGRADED */ }
250
+ });
251
+ }
252
+ }
253
+ }
254
+ /**
255
+ * Manually report a key as recovered (for external health checks)
256
+ */
257
+ reportRecovery(keyId) {
258
+ const state = this.keys.get(keyId);
259
+ if (!state) return;
260
+ this.transitionToHealthy(state);
261
+ this.emitDebug({
262
+ type: "key_recovered",
263
+ keyId,
264
+ timestamp: Date.now()
265
+ });
266
+ }
267
+ /**
268
+ * Get statistics for all keys
269
+ */
270
+ getStats() {
271
+ return Array.from(this.keys.values()).map((state) => this.toStats(state));
272
+ }
273
+ /**
274
+ * Get overall router statistics
275
+ */
276
+ getOverallStats() {
277
+ const stats = this.getStats();
278
+ const totalRpm = stats.reduce((sum, s) => sum + s.rpm, 0);
279
+ const totalRpmLimit = stats.reduce((sum, s) => sum + s.rpmLimit, 0);
280
+ return {
281
+ totalKeys: stats.length,
282
+ healthyKeys: stats.filter((s) => s.isHealthy).length,
283
+ totalRpm,
284
+ totalRpmLimit,
285
+ overallUtilization: totalRpmLimit > 0 ? totalRpm / totalRpmLimit : 0,
286
+ keys: stats
287
+ };
288
+ }
289
+ /**
290
+ * Reset all tracking (useful for testing)
291
+ */
292
+ reset() {
293
+ for (const state of this.keys.values()) {
294
+ state.rpm = 0;
295
+ state.lastUsed = 0;
296
+ state.circuitState = "healthy" /* HEALTHY */;
297
+ state.failures = 0;
298
+ state.cooldownUntil = 0;
299
+ state.totalRequests = 0;
300
+ state.totalErrors = 0;
301
+ state.avgLatencyMs = 0;
302
+ state.latencyHistory = [];
303
+ state.requestTimestamps = [];
304
+ }
305
+ }
306
+ /**
307
+ * Shutdown cleanup
308
+ */
309
+ destroy() {
310
+ if (this.cleanupInterval) {
311
+ clearInterval(this.cleanupInterval);
312
+ this.cleanupInterval = void 0;
313
+ }
314
+ }
315
+ // ==================== Private Methods ====================
316
+ transitionToHealthy(state) {
317
+ const wasUnhealthy = state.circuitState !== "healthy" /* HEALTHY */;
318
+ state.circuitState = "healthy" /* HEALTHY */;
319
+ state.failures = 0;
320
+ state.cooldownUntil = 0;
321
+ if (wasUnhealthy) {
322
+ if (this.options.onStateChange) {
323
+ this.options.onStateChange(state.config.id, "healthy" /* HEALTHY */);
324
+ }
325
+ this.emitDebug({
326
+ type: "key_recovered",
327
+ keyId: state.config.id,
328
+ timestamp: Date.now()
329
+ });
330
+ }
331
+ }
332
+ openCircuit(state) {
333
+ if (state.circuitState === "open" /* OPEN */) return;
334
+ state.circuitState = "open" /* OPEN */;
335
+ state.cooldownUntil = Date.now() + this.options.cooldownMs;
336
+ if (this.options.onStateChange) {
337
+ this.options.onStateChange(state.config.id, "open" /* OPEN */);
338
+ }
339
+ this.emitDebug({
340
+ type: "circuit_opened",
341
+ keyId: state.config.id,
342
+ timestamp: Date.now(),
343
+ details: { cooldownUntil: state.cooldownUntil, failures: state.failures }
344
+ });
345
+ }
346
+ toStats(state) {
347
+ const rpmLimit = state.config.rpmLimit ?? this.options.defaultRpmLimit;
348
+ const now = Date.now();
349
+ const cooldownRemainingMs = state.cooldownUntil > now ? state.cooldownUntil - now : 0;
350
+ return {
351
+ id: state.config.id,
352
+ key: this.maskKey(state.config.key),
353
+ rpm: state.rpm,
354
+ rpmLimit,
355
+ utilization: rpmLimit > 0 ? state.rpm / rpmLimit : 0,
356
+ circuitState: state.circuitState,
357
+ failures: state.failures,
358
+ avgLatencyMs: state.avgLatencyMs,
359
+ totalRequests: state.totalRequests,
360
+ totalErrors: state.totalErrors,
361
+ isHealthy: (state.circuitState !== "open" /* OPEN */ || cooldownRemainingMs === 0) && state.rpm < rpmLimit,
362
+ cooldownRemainingMs
363
+ };
364
+ }
365
+ maskKey(key) {
366
+ if (key.length <= 8) return "****";
367
+ return key.slice(0, 4) + "****" + key.slice(-4);
368
+ }
369
+ emitDebug(event) {
370
+ if (this.options.onDebug) {
371
+ this.options.onDebug(event);
372
+ }
373
+ }
374
+ startCleanupLoop() {
375
+ this.cleanupInterval = setInterval(() => {
376
+ this.cleanupExpiredWindows();
377
+ this.checkCircuitRecovery();
378
+ }, Math.min(this.options.windowMs / 10, 1e4));
379
+ }
380
+ cleanupExpiredWindows() {
381
+ const now = Date.now();
382
+ const windowStart = now - this.options.windowMs;
383
+ for (const state of this.keys.values()) {
384
+ state.requestTimestamps = state.requestTimestamps.filter((ts) => ts >= windowStart);
385
+ state.rpm = state.requestTimestamps.length;
386
+ }
387
+ }
388
+ checkCircuitRecovery() {
389
+ const now = Date.now();
390
+ for (const state of this.keys.values()) {
391
+ if (state.circuitState === "open" /* OPEN */ && now >= state.cooldownUntil) {
392
+ this.transitionToHealthy(state);
393
+ }
394
+ }
395
+ }
396
+ };
397
+
398
+ // src/strategies/selectionStrategies.ts
399
+ var WeightedLeastUtilizationStrategy = class {
400
+ name = "weighted-least-utilization";
401
+ select(keys, options) {
402
+ if (keys.length === 0) return null;
403
+ let candidates = keys;
404
+ if (options?.maxUtilization != null) {
405
+ candidates = keys.filter((k) => {
406
+ const limit = k.config.rpmLimit ?? 40;
407
+ return k.rpm / limit <= options.maxUtilization;
408
+ });
409
+ if (candidates.length === 0) return null;
410
+ }
411
+ candidates.sort((a, b) => {
412
+ const limitA = a.config.rpmLimit ?? 40;
413
+ const limitB = b.config.rpmLimit ?? 40;
414
+ const weightA = a.config.weight ?? 1;
415
+ const weightB = b.config.weight ?? 1;
416
+ const utilA = a.rpm / limitA / weightA;
417
+ const utilB = b.rpm / limitB / weightB;
418
+ return utilA - utilB;
419
+ });
420
+ return candidates[0] ?? null;
421
+ }
422
+ };
423
+ var LeastRequestsStrategy = class {
424
+ name = "least-requests";
425
+ select(keys, options) {
426
+ if (keys.length === 0) return null;
427
+ let candidates = keys;
428
+ if (options?.maxUtilization != null) {
429
+ candidates = keys.filter((k) => {
430
+ const limit = k.config.rpmLimit ?? 40;
431
+ return k.rpm / limit <= options.maxUtilization;
432
+ });
433
+ if (candidates.length === 0) return null;
434
+ }
435
+ candidates.sort((a, b) => a.rpm - b.rpm);
436
+ return candidates[0] ?? null;
437
+ }
438
+ };
439
+ var SmartRoutingStrategy = class {
440
+ name = "smart";
441
+ select(keys, options) {
442
+ if (!keys || keys.length === 0) return null;
443
+ let candidates = keys;
444
+ if (options?.maxUtilization != null) {
445
+ candidates = keys.filter((k) => {
446
+ const limit = k.config.rpmLimit ?? 40;
447
+ return k.rpm / limit <= options.maxUtilization;
448
+ });
449
+ if (candidates.length === 0) return null;
450
+ }
451
+ const untestedKeys = candidates.filter((k) => (k.latencyHistory?.length ?? 0) === 0);
452
+ if (untestedKeys.length > 0) {
453
+ untestedKeys.sort((a, b) => (a.totalRequests ?? 0) - (b.totalRequests ?? 0));
454
+ return untestedKeys[0] ?? null;
455
+ }
456
+ const keysWithLatency = candidates.filter(
457
+ (k) => typeof k.avgLatencyMs === "number" && !isNaN(k.avgLatencyMs) && k.avgLatencyMs > 0
458
+ );
459
+ let pool = candidates;
460
+ if (keysWithLatency.length > 0) {
461
+ const fastest = Math.min(...keysWithLatency.map((k) => k.avgLatencyMs));
462
+ pool = keysWithLatency.filter((k) => k.avgLatencyMs <= fastest + 100);
463
+ }
464
+ if (pool.length === 0) {
465
+ pool = candidates;
466
+ }
467
+ pool.sort((a, b) => {
468
+ if (a.avgLatencyMs !== b.avgLatencyMs) return a.avgLatencyMs - b.avgLatencyMs;
469
+ const limitA = a.config.rpmLimit ?? 40;
470
+ const limitB = b.config.rpmLimit ?? 40;
471
+ return a.rpm / limitA - b.rpm / limitB;
472
+ });
473
+ return pool[0] ?? null;
474
+ }
475
+ };
476
+ var LeastLatencyStrategy = class {
477
+ name = "least-latency";
478
+ select(keys, options) {
479
+ if (keys.length === 0) return null;
480
+ let candidates = keys;
481
+ if (options?.maxUtilization != null) {
482
+ candidates = keys.filter((k) => {
483
+ const limit = k.config.rpmLimit ?? 40;
484
+ return k.rpm / limit <= options.maxUtilization;
485
+ });
486
+ if (candidates.length === 0) return null;
487
+ }
488
+ candidates = candidates.filter((k) => k.avgLatencyMs > 0);
489
+ if (candidates.length === 0) {
490
+ return new LeastRequestsStrategy().select(keys, options);
491
+ }
492
+ candidates.sort((a, b) => a.avgLatencyMs - b.avgLatencyMs);
493
+ return candidates[0] ?? null;
494
+ }
495
+ };
496
+ var PreferredKeysStrategy = class {
497
+ constructor(fallbackStrategy) {
498
+ this.fallbackStrategy = fallbackStrategy;
499
+ }
500
+ fallbackStrategy;
501
+ name = "preferred-keys";
502
+ select(keys, options) {
503
+ if (!options?.preferredKeys?.length) {
504
+ return this.fallbackStrategy.select(keys, options);
505
+ }
506
+ const preferredKeysSet = new Set(options.preferredKeys);
507
+ const preferredCandidates = keys.filter((k) => preferredKeysSet.has(k.config.id));
508
+ if (preferredCandidates.length > 0) {
509
+ const selected = this.fallbackStrategy.select(preferredCandidates, options);
510
+ if (selected) return selected;
511
+ }
512
+ return this.fallbackStrategy.select(keys, options);
513
+ }
514
+ };
515
+ var RandomStrategy = class {
516
+ name = "random";
517
+ select(keys, options) {
518
+ if (keys.length === 0) return null;
519
+ let candidates = keys;
520
+ if (options?.maxUtilization != null) {
521
+ candidates = keys.filter((k) => {
522
+ const limit = k.config.rpmLimit ?? 40;
523
+ return k.rpm / limit <= options.maxUtilization;
524
+ });
525
+ if (candidates.length === 0) return null;
526
+ }
527
+ const index = Math.floor(Math.random() * candidates.length);
528
+ return candidates[index] ?? null;
529
+ }
530
+ };
531
+ var RoundRobinStrategy = class {
532
+ name = "round-robin";
533
+ index = 0;
534
+ select(keys, options) {
535
+ if (keys.length === 0) return null;
536
+ let candidates = keys;
537
+ if (options?.maxUtilization != null) {
538
+ candidates = keys.filter((k) => {
539
+ const limit = k.config.rpmLimit ?? 40;
540
+ return limit > 0 ? k.rpm / limit <= options.maxUtilization : true;
541
+ });
542
+ if (candidates.length === 0) return null;
543
+ }
544
+ const selected = candidates[this.index % candidates.length];
545
+ this.index++;
546
+ return selected || null;
547
+ }
548
+ };
549
+ function createStrategy(name, fallback) {
550
+ switch (name) {
551
+ case "weighted-least-utilization":
552
+ return new WeightedLeastUtilizationStrategy();
553
+ case "least-requests":
554
+ return new LeastRequestsStrategy();
555
+ case "round-robin":
556
+ return new RoundRobinStrategy();
557
+ case "smart":
558
+ return new SmartRoutingStrategy();
559
+ case "least-latency":
560
+ return new LeastLatencyStrategy();
561
+ case "random":
562
+ return new RandomStrategy();
563
+ case "preferred-keys":
564
+ return new PreferredKeysStrategy(fallback ?? new WeightedLeastUtilizationStrategy());
565
+ default:
566
+ return new WeightedLeastUtilizationStrategy();
567
+ }
568
+ }
569
+ var defaultStrategy = new WeightedLeastUtilizationStrategy();
570
+
571
+ // src/core/keyRouter.ts
572
+ var KeyRouter = class _KeyRouter {
573
+ tracker;
574
+ strategy;
575
+ config;
576
+ initialized = false;
577
+ constructor(config2) {
578
+ this.config = {
579
+ keys: config2.keys ? config2.keys.map((k) => ({ ...k })) : [],
580
+ defaultRpmLimit: config2.defaultRpmLimit ?? 40,
581
+ defaultWeight: config2.defaultWeight ?? 1,
582
+ failureThreshold: config2.failureThreshold ?? 3,
583
+ cooldownMs: config2.cooldownMs ?? 3e4,
584
+ windowMs: config2.windowMs ?? 6e4,
585
+ trackLatency: config2.trackLatency ?? true,
586
+ onStateChange: config2.onStateChange ?? (() => {
587
+ }),
588
+ onDebug: config2.onDebug ?? (() => {
589
+ })
590
+ };
591
+ if (this.config.defaultRpmLimit <= 0) throw new KeyRouterError("Invalid config: defaultRpmLimit must be > 0", "INVALID_CONFIG");
592
+ if (this.config.defaultWeight <= 0) throw new KeyRouterError("Invalid config: defaultWeight must be > 0", "INVALID_CONFIG");
593
+ if (this.config.failureThreshold <= 0) throw new KeyRouterError("Invalid config: failureThreshold must be > 0", "INVALID_CONFIG");
594
+ if (this.config.cooldownMs <= 0) throw new KeyRouterError("Invalid config: cooldownMs must be > 0", "INVALID_CONFIG");
595
+ if (this.config.windowMs <= 0) throw new KeyRouterError("Invalid config: windowMs must be > 0", "INVALID_CONFIG");
596
+ for (const key of this.config.keys) {
597
+ if (key.rpmLimit !== void 0 && key.rpmLimit <= 0) {
598
+ throw new KeyRouterError("Invalid config: rpmLimit must be > 0", "INVALID_CONFIG");
599
+ }
600
+ if (key.weight !== void 0 && key.weight <= 0) {
601
+ throw new KeyRouterError("Invalid config: weight must be > 0", "INVALID_CONFIG");
602
+ }
603
+ }
604
+ this.tracker = new KeyTracker({
605
+ defaultRpmLimit: this.config.defaultRpmLimit,
606
+ defaultWeight: this.config.defaultWeight,
607
+ failureThreshold: this.config.failureThreshold,
608
+ cooldownMs: this.config.cooldownMs,
609
+ windowMs: this.config.windowMs,
610
+ trackLatency: this.config.trackLatency,
611
+ onStateChange: this.config.onStateChange,
612
+ onDebug: this.config.onDebug
613
+ });
614
+ this.strategy = new WeightedLeastUtilizationStrategy();
615
+ }
616
+ /**
617
+ * Initialize the router with keys
618
+ * Must be called before using getKey()
619
+ */
620
+ initialize() {
621
+ if (this.initialized) {
622
+ return;
623
+ }
624
+ if (!this.config.keys.length) {
625
+ throw new KeyRouterError(
626
+ "No keys configured. At least one key must be provided.",
627
+ "NO_KEYS_CONFIGURED"
628
+ );
629
+ }
630
+ for (const keyConfig of this.config.keys) {
631
+ if (!keyConfig.key || !keyConfig.id) {
632
+ throw new KeyRouterError(
633
+ `Invalid key config: each key must have 'id' and 'key' properties`,
634
+ "NO_KEYS_CONFIGURED"
635
+ );
636
+ }
637
+ }
638
+ this.tracker.initialize(this.config.keys);
639
+ this.initialized = true;
640
+ this.config.onDebug?.({
641
+ type: "key_selected",
642
+ timestamp: Date.now(),
643
+ details: { initialized: true, keyCount: this.config.keys.length }
644
+ });
645
+ }
646
+ /**
647
+ * Get the next available API key
648
+ * Returns key string ready to use with OpenAI/LangChain SDKs
649
+ */
650
+ async getKey(options) {
651
+ this.ensureInitialized();
652
+ const availableKeys = this.tracker.getAvailableKeys();
653
+ if (availableKeys.length === 0) {
654
+ this.config.onDebug?.({
655
+ type: "all_exhausted",
656
+ timestamp: Date.now(),
657
+ details: { totalKeys: this.config.keys.length }
658
+ });
659
+ throw new RateLimitError("all");
660
+ }
661
+ let strategy = this.strategy;
662
+ if (options?.preferredKeys?.length) {
663
+ strategy = createStrategy("preferred-keys", this.strategy);
664
+ } else if (options?.strategy) {
665
+ strategy = createStrategy(options.strategy, this.strategy);
666
+ }
667
+ const selected = strategy.select(availableKeys, options);
668
+ if (!selected) {
669
+ throw new RateLimitError("all");
670
+ }
671
+ this.tracker.recordAttempt(selected.config.id);
672
+ return selected.config.key;
673
+ }
674
+ /**
675
+ * Get key with full metadata (for debugging/logging)
676
+ */
677
+ async getKeyWithMeta(options) {
678
+ this.ensureInitialized();
679
+ const availableKeys = this.tracker.getAvailableKeys();
680
+ if (availableKeys.length === 0) {
681
+ throw new RateLimitError("all");
682
+ }
683
+ const strategy = options?.strategy ? createStrategy(options.strategy, this.strategy) : this.strategy;
684
+ const selected = strategy.select(availableKeys, options);
685
+ if (!selected) {
686
+ throw new RateLimitError("all");
687
+ }
688
+ const limit = selected.config.rpmLimit ?? this.config.defaultRpmLimit;
689
+ return {
690
+ key: selected.config.key,
691
+ config: selected.config,
692
+ keyId: selected.config.id,
693
+ utilization: limit > 0 ? selected.rpm / limit : 0
694
+ };
695
+ }
696
+ /**
697
+ * Report a failed request for a specific key or key ID
698
+ * Call this when you catch a 429/5xx error from the API
699
+ */
700
+ reportFailure(keyOrId, isRateLimit = false) {
701
+ const state = this.findKeyOrId(keyOrId);
702
+ if (state) {
703
+ this.tracker.recordFailure(state.config.id, isRateLimit);
704
+ }
705
+ }
706
+ /**
707
+ * Report a successful request for a specific key or key ID
708
+ * Optional: call this if you want to track latency manually
709
+ */
710
+ reportSuccess(keyOrId, latencyMs) {
711
+ const state = this.findKeyOrId(keyOrId);
712
+ if (state) {
713
+ this.tracker.recordSuccess(state.config.id, latencyMs);
714
+ }
715
+ }
716
+ /**
717
+ * Manually mark a key as recovered
718
+ */
719
+ reportRecovery(keyOrId) {
720
+ const state = this.findKeyOrId(keyOrId);
721
+ if (state) {
722
+ this.tracker.reportRecovery(state.config.id);
723
+ }
724
+ }
725
+ /**
726
+ * Get statistics for all keys
727
+ */
728
+ getStats() {
729
+ return this.tracker.getStats();
730
+ }
731
+ /**
732
+ * Get overall router statistics
733
+ */
734
+ getOverallStats() {
735
+ return this.tracker.getOverallStats();
736
+ }
737
+ /**
738
+ * Get a specific key's stats by ID
739
+ */
740
+ getKeyStats(keyId) {
741
+ const state = this.tracker.getState(keyId);
742
+ if (!state) return void 0;
743
+ const stats = this.tracker.getStats();
744
+ return stats.find((s) => s.id === keyId);
745
+ }
746
+ /**
747
+ * Check if a specific key is healthy
748
+ */
749
+ isKeyHealthy(keyId) {
750
+ const state = this.tracker.getState(keyId);
751
+ if (!state) return false;
752
+ const limit = state.config.rpmLimit ?? this.config.defaultRpmLimit;
753
+ if (state.circuitState === "open" /* OPEN */) {
754
+ if (Date.now() >= state.cooldownUntil) {
755
+ return state.rpm < limit;
756
+ }
757
+ return false;
758
+ }
759
+ return state.rpm < limit;
760
+ }
761
+ /**
762
+ * Get all available (healthy + under limit) key IDs
763
+ */
764
+ getAvailableKeyIds() {
765
+ return this.tracker.getAvailableKeys().map((s) => s.config.id);
766
+ }
767
+ /**
768
+ * Set custom selection strategy
769
+ */
770
+ setStrategy(strategy) {
771
+ if (typeof strategy === "string") {
772
+ this.strategy = createStrategy(strategy, this.strategy);
773
+ } else {
774
+ this.strategy = strategy;
775
+ }
776
+ }
777
+ /**
778
+ * Add a new key at runtime
779
+ */
780
+ addKey(keyConfig) {
781
+ this.ensureInitialized();
782
+ this.tracker.initialize([...this.config.keys, keyConfig]);
783
+ this.config.keys.push(keyConfig);
784
+ }
785
+ /**
786
+ * Remove a key at runtime
787
+ */
788
+ removeKey(keyId) {
789
+ const index = this.config.keys.findIndex((k) => k.id === keyId);
790
+ if (index === -1) return false;
791
+ this.config.keys.splice(index, 1);
792
+ this.tracker.initialize(this.config.keys);
793
+ return true;
794
+ }
795
+ /**
796
+ * Update key configuration at runtime
797
+ */
798
+ updateKey(keyId, updates) {
799
+ const key = this.config.keys.find((k) => k.id === keyId);
800
+ if (!key) return false;
801
+ Object.assign(key, updates);
802
+ this.tracker.initialize(this.config.keys);
803
+ return true;
804
+ }
805
+ /**
806
+ * Reset all tracking (useful for testing)
807
+ */
808
+ reset() {
809
+ this.tracker.reset();
810
+ }
811
+ /**
812
+ * Shutdown and cleanup
813
+ */
814
+ destroy() {
815
+ this.tracker.destroy();
816
+ this.initialized = false;
817
+ }
818
+ // ==================== Provider Preset Helpers ====================
819
+ /**
820
+ * Create a router from a provider preset
821
+ */
822
+ static fromProvider(provider, keys, options) {
823
+ const preset = PROVIDER_PRESETS[provider];
824
+ if (!preset) {
825
+ throw new KeyRouterError(
826
+ `Unknown provider: ${provider}. Available: ${Object.keys(PROVIDER_PRESETS).join(", ")}`,
827
+ "NO_KEYS_CONFIGURED"
828
+ );
829
+ }
830
+ const keyConfigs = keys.map((key, index) => ({
831
+ id: `${provider}-${index + 1}`,
832
+ key,
833
+ rpmLimit: preset.defaultRpmLimit,
834
+ baseURL: preset.baseURL
835
+ }));
836
+ return new _KeyRouter({
837
+ keys: keyConfigs,
838
+ ...options
839
+ });
840
+ }
841
+ /**
842
+ * Create a router for NVIDIA specifically (most common use case)
843
+ */
844
+ static forNvidia(keys, options) {
845
+ return _KeyRouter.fromProvider("nvidia", keys, options);
846
+ }
847
+ /**
848
+ * Create router from environment variable (comma-separated keys)
849
+ */
850
+ static fromEnv(envVar, options) {
851
+ const keys = process.env[envVar]?.split(",").map((k) => k.trim()).filter(Boolean) ?? [];
852
+ if (!keys.length) {
853
+ throw new KeyRouterError(
854
+ `Environment variable ${envVar} not set or empty`,
855
+ "NO_KEYS_CONFIGURED"
856
+ );
857
+ }
858
+ return new _KeyRouter({
859
+ keys: keys.map((key, i) => ({ id: `env-${i + 1}`, key })),
860
+ ...options
861
+ });
862
+ }
863
+ // ==================== Private Helpers ====================
864
+ ensureInitialized() {
865
+ if (!this.initialized) {
866
+ this.initialize();
867
+ }
868
+ }
869
+ findKeyOrId(keyOrId) {
870
+ for (const state of this.tracker.getAllStates()) {
871
+ if (state.config.id === keyOrId || state.config.key === keyOrId) {
872
+ return state;
873
+ }
874
+ }
875
+ return void 0;
876
+ }
877
+ };
878
+
879
+ // src/utils/helpers.ts
880
+ function isRateLimitError(error, seen = /* @__PURE__ */ new WeakSet()) {
881
+ if (!error) return false;
882
+ if (error instanceof Error || typeof error === "object" && error !== null) {
883
+ if (seen.has(error)) return false;
884
+ seen.add(error);
885
+ if (error instanceof Error) {
886
+ const message = error.message.toLowerCase();
887
+ if (message.includes("429") || message.includes("rate limit") || message.includes("too many requests")) {
888
+ return true;
889
+ }
890
+ }
891
+ const errWithStatus = error;
892
+ if (errWithStatus.status === 429 || errWithStatus.statusCode === 429 || errWithStatus.code === "429") {
893
+ return true;
894
+ }
895
+ if (errWithStatus.cause) {
896
+ return isRateLimitError(errWithStatus.cause, seen);
897
+ }
898
+ }
899
+ return false;
900
+ }
901
+ function isServerError(error, seen = /* @__PURE__ */ new WeakSet()) {
902
+ if (!error) return false;
903
+ if (error instanceof Error || typeof error === "object" && error !== null) {
904
+ if (seen.has(error)) return false;
905
+ seen.add(error);
906
+ const errWithStatus = error;
907
+ const status = errWithStatus.status ?? errWithStatus.statusCode;
908
+ if (status && status >= 500 && status < 600) {
909
+ return true;
910
+ }
911
+ if (errWithStatus.cause) {
912
+ return isServerError(errWithStatus.cause, seen);
913
+ }
914
+ }
915
+ return false;
916
+ }
917
+ function isRetryableError(error) {
918
+ return isRateLimitError(error) || isServerError(error);
919
+ }
920
+ function calculateBackoff(attempt, baseMs = 1e3, maxMs = 3e4) {
921
+ const delay = Math.min(baseMs * Math.pow(2, attempt), maxMs);
922
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
923
+ return Math.min(Math.max(0, Math.round(delay + jitter)), maxMs);
924
+ }
925
+ function maskKey(key) {
926
+ if (!key || key.length <= 8) return "****";
927
+ return key.slice(0, 4) + "****" + key.slice(-4);
928
+ }
929
+ function parseKeys(keysString) {
930
+ return keysString.split(",").map((k) => k.trim()).filter(Boolean);
931
+ }
932
+ function createKeyGetter(router2) {
933
+ return async () => {
934
+ return router2.getKey();
935
+ };
936
+ }
937
+ function createFailoverKeyGetter(router2, maxRetries = 3) {
938
+ return async () => {
939
+ let lastError;
940
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
941
+ try {
942
+ const key = await router2.getKey();
943
+ return key;
944
+ } catch (error) {
945
+ lastError = error instanceof Error ? error : new Error(String(error));
946
+ if (error instanceof Error && error.name === "RateLimitError") {
947
+ throw error;
948
+ }
949
+ if (attempt < maxRetries - 1) {
950
+ await new Promise((resolve) => setTimeout(resolve, calculateBackoff(attempt)));
951
+ }
952
+ }
953
+ }
954
+ throw lastError;
955
+ };
956
+ }
957
+ function sleep(ms) {
958
+ return new Promise((resolve) => setTimeout(resolve, ms));
959
+ }
960
+ function formatStats(stats) {
961
+ const { totalKeys, healthyKeys, totalRpm, totalRpmLimit, overallUtilization, keys } = stats;
962
+ const lines = [
963
+ `\u{1F4CA} KeyRouter Stats: ${healthyKeys}/${totalKeys} healthy | ${totalRpm}/${totalRpmLimit} RPM (${(overallUtilization * 100).toFixed(1)}%)`,
964
+ ""
965
+ ];
966
+ for (const key of keys) {
967
+ const status = key.isHealthy ? "\u2705" : "\u274C";
968
+ const circuit = key.circuitState === "open" ? " \u{1F534}" : key.circuitState === "degraded" ? " \u{1F7E1}" : "";
969
+ lines.push(
970
+ ` ${status} ${key.id}: ${key.rpm}/${key.rpmLimit} RPM (${(key.utilization * 100).toFixed(1)}%)${circuit} | Latency: ${key.avgLatencyMs}ms | Req: ${key.totalRequests} | Err: ${key.totalErrors}`
971
+ );
972
+ }
973
+ return lines.join("\n");
974
+ }
975
+ function createStatsLogger(router2, intervalMs = 6e4) {
976
+ return setInterval(() => {
977
+ console.log(formatStats(router2.getOverallStats()));
978
+ }, intervalMs);
979
+ }
980
+
981
+ // src/core/multiProviderRouter.ts
982
+ var MultiProviderRouter = class {
983
+ tracker;
984
+ strategy;
985
+ endpointMap = /* @__PURE__ */ new Map();
986
+ providerGroups = /* @__PURE__ */ new Map();
987
+ lastRoute = null;
988
+ keyConfigs = [];
989
+ providers;
990
+ config;
991
+ constructor(providers, config2) {
992
+ if (!providers.length) {
993
+ throw new KeyRouterError(
994
+ "At least one provider must be configured.",
995
+ "NO_KEYS_CONFIGURED"
996
+ );
997
+ }
998
+ this.providers = providers.map((p) => ({
999
+ ...p,
1000
+ keys: [...p.keys],
1001
+ models: p.models ? [...p.models] : void 0
1002
+ }));
1003
+ this.config = {
1004
+ failureThreshold: config2?.failureThreshold ?? 3,
1005
+ cooldownMs: config2?.cooldownMs ?? 3e4,
1006
+ windowMs: config2?.windowMs ?? 6e4,
1007
+ trackLatency: config2?.trackLatency ?? true,
1008
+ strategy: config2?.strategy ?? "weighted-least-utilization",
1009
+ onStateChange: config2?.onStateChange ?? (() => {
1010
+ }),
1011
+ onDebug: config2?.onDebug ?? (() => {
1012
+ })
1013
+ };
1014
+ if (this.config.failureThreshold <= 0) throw new KeyRouterError("Invalid config: failureThreshold must be > 0", "INVALID_CONFIG");
1015
+ if (this.config.cooldownMs <= 0) throw new KeyRouterError("Invalid config: cooldownMs must be > 0", "INVALID_CONFIG");
1016
+ if (this.config.windowMs <= 0) throw new KeyRouterError("Invalid config: windowMs must be > 0", "INVALID_CONFIG");
1017
+ for (const provider of this.providers) {
1018
+ if (provider.rpmLimit !== void 0 && provider.rpmLimit <= 0) {
1019
+ throw new KeyRouterError("Invalid config: rpmLimit must be > 0", "INVALID_CONFIG");
1020
+ }
1021
+ if (provider.weight !== void 0 && provider.weight <= 0) {
1022
+ throw new KeyRouterError("Invalid config: weight must be > 0", "INVALID_CONFIG");
1023
+ }
1024
+ }
1025
+ this.tracker = new KeyTracker({
1026
+ defaultRpmLimit: 40,
1027
+ defaultWeight: 1,
1028
+ failureThreshold: this.config.failureThreshold,
1029
+ cooldownMs: this.config.cooldownMs,
1030
+ windowMs: this.config.windowMs,
1031
+ trackLatency: this.config.trackLatency,
1032
+ onStateChange: this.config.onStateChange,
1033
+ onDebug: this.config.onDebug
1034
+ });
1035
+ this.strategy = createStrategy(this.config.strategy);
1036
+ this.buildPool(providers);
1037
+ }
1038
+ // ==================== Core Methods ====================
1039
+ /**
1040
+ * Get the next best endpoint for making an API call
1041
+ * Returns key + baseURL + model + provider — everything needed for a request
1042
+ *
1043
+ * Supports filtering:
1044
+ * - preferProviders: try these first
1045
+ * - excludeProviders: skip these entirely
1046
+ * - model: pick a provider that has this model
1047
+ * - maxUtilization: skip overloaded endpoints
1048
+ */
1049
+ async getEndpoint(options) {
1050
+ let availableKeys = this.tracker.getAvailableKeys();
1051
+ if (options?.excludeProviders?.length) {
1052
+ availableKeys = availableKeys.filter((k) => {
1053
+ const meta2 = this.endpointMap.get(k.config.id);
1054
+ return meta2 ? !options.excludeProviders.includes(meta2.provider) : true;
1055
+ });
1056
+ }
1057
+ if (options?.model) {
1058
+ availableKeys = availableKeys.filter((k) => {
1059
+ const meta2 = this.endpointMap.get(k.config.id);
1060
+ return meta2 ? meta2.model === options.model : false;
1061
+ });
1062
+ }
1063
+ if (options?.maxUtilization != null) {
1064
+ availableKeys = availableKeys.filter((k) => {
1065
+ const limit2 = k.config.rpmLimit ?? 40;
1066
+ return k.rpm / limit2 <= options.maxUtilization;
1067
+ });
1068
+ }
1069
+ if (availableKeys.length === 0) {
1070
+ this.config.onDebug({
1071
+ type: "all_exhausted",
1072
+ timestamp: Date.now(),
1073
+ details: { totalEndpoints: this.endpointMap.size }
1074
+ });
1075
+ throw new RateLimitError("all");
1076
+ }
1077
+ const preferredKeys = options?.preferProviders?.length ? options.preferProviders.flatMap(
1078
+ (p) => this.providerGroups.get(p) ?? []
1079
+ ) : void 0;
1080
+ let activeStrategy = options?.strategy ? createStrategy(options.strategy, this.strategy) : this.strategy;
1081
+ if (preferredKeys?.length) {
1082
+ activeStrategy = new PreferredKeysStrategy(activeStrategy);
1083
+ }
1084
+ const selected = activeStrategy.select(availableKeys, { preferredKeys });
1085
+ if (!selected) {
1086
+ throw new RateLimitError("all");
1087
+ }
1088
+ this.tracker.recordAttempt(selected.config.id);
1089
+ const m = this.endpointMap.get(selected.config.id);
1090
+ if (m) {
1091
+ this.lastRoute = {
1092
+ provider: m.provider,
1093
+ model: m.model,
1094
+ key: selected.config.key,
1095
+ time: Date.now()
1096
+ };
1097
+ }
1098
+ const meta = this.endpointMap.get(selected.config.id);
1099
+ if (!meta) {
1100
+ throw new KeyRouterError(
1101
+ `Endpoint metadata not found for ${selected.config.id}`,
1102
+ "KEY_NOT_FOUND",
1103
+ selected.config.id
1104
+ );
1105
+ }
1106
+ const limit = selected.config.rpmLimit ?? 40;
1107
+ return {
1108
+ key: selected.config.key,
1109
+ baseURL: meta.baseURL,
1110
+ model: meta.model,
1111
+ provider: meta.provider,
1112
+ endpointId: selected.config.id,
1113
+ utilization: limit > 0 ? selected.rpm / limit : 0
1114
+ };
1115
+ }
1116
+ /**
1117
+ * Report a successful request for an endpoint
1118
+ * Resets failure count, updates latency (EMA), recovers degraded state
1119
+ */
1120
+ reportSuccess(endpointId, latencyMs) {
1121
+ this.tracker.recordSuccess(endpointId, latencyMs);
1122
+ }
1123
+ /**
1124
+ * Report a failed request for an endpoint
1125
+ * Increments failure count, may open circuit breaker
1126
+ * Next getEndpoint() call will automatically route to a different provider
1127
+ */
1128
+ reportFailure(endpointId, isRateLimit = false) {
1129
+ this.tracker.recordFailure(endpointId, isRateLimit);
1130
+ }
1131
+ /**
1132
+ * Manually recover an endpoint (e.g. after external health check)
1133
+ */
1134
+ reportRecovery(endpointId) {
1135
+ this.tracker.reportRecovery(endpointId);
1136
+ }
1137
+ // ==================== Stats & Monitoring ====================
1138
+ /**
1139
+ * Get overall stats across all providers (same familiar format as KeyRouter)
1140
+ */
1141
+ getOverallStats() {
1142
+ return this.tracker.getOverallStats();
1143
+ }
1144
+ /**
1145
+ * Get stats for all endpoints
1146
+ */
1147
+ getStats() {
1148
+ return this.tracker.getStats();
1149
+ }
1150
+ /**
1151
+ * Get stats for a specific provider
1152
+ */
1153
+ getProviderStats(provider) {
1154
+ const keyIds = this.providerGroups.get(provider) ?? [];
1155
+ const allStats = this.tracker.getStats();
1156
+ const providerKeyStats = allStats.filter((s) => keyIds.includes(s.id));
1157
+ const totalRpm = providerKeyStats.reduce((sum, s) => sum + s.rpm, 0);
1158
+ const totalRpmLimit = providerKeyStats.reduce(
1159
+ (sum, s) => sum + s.rpmLimit,
1160
+ 0
1161
+ );
1162
+ return {
1163
+ provider,
1164
+ totalKeys: providerKeyStats.length,
1165
+ healthyKeys: providerKeyStats.filter((s) => s.isHealthy).length,
1166
+ totalRpm,
1167
+ totalRpmLimit,
1168
+ utilization: totalRpmLimit > 0 ? totalRpm / totalRpmLimit : 0,
1169
+ keys: providerKeyStats
1170
+ };
1171
+ }
1172
+ /**
1173
+ * Get list of all configured provider names
1174
+ */
1175
+ getProviderNames() {
1176
+ return Array.from(this.providerGroups.keys());
1177
+ }
1178
+ /**
1179
+ * Get all available (healthy + under limit) endpoint IDs
1180
+ */
1181
+ getLastRoute() {
1182
+ return this.lastRoute;
1183
+ }
1184
+ getAvailableEndpointIds() {
1185
+ return this.tracker.getAvailableKeys().map((s) => s.config.id);
1186
+ }
1187
+ /**
1188
+ * Check if a specific endpoint is healthy
1189
+ */
1190
+ isEndpointHealthy(endpointId) {
1191
+ const state = this.tracker.getState(endpointId);
1192
+ if (!state) return false;
1193
+ const limit = state.config.rpmLimit ?? 40;
1194
+ if (state.circuitState === "open" /* OPEN */) {
1195
+ if (Date.now() >= state.cooldownUntil) {
1196
+ return state.rpm < limit;
1197
+ }
1198
+ return false;
1199
+ }
1200
+ return state.rpm < limit;
1201
+ }
1202
+ /**
1203
+ * Check if a provider has any healthy endpoints
1204
+ */
1205
+ isProviderHealthy(provider) {
1206
+ const keyIds = this.providerGroups.get(provider) ?? [];
1207
+ return keyIds.some((id) => this.isEndpointHealthy(id));
1208
+ }
1209
+ // ==================== Dynamic Management ====================
1210
+ /**
1211
+ * Add a new provider at runtime
1212
+ */
1213
+ addProvider(entry) {
1214
+ if (!entry.keys.length) {
1215
+ throw new KeyRouterError(
1216
+ `Provider '${entry.provider}' must have at least one key.`,
1217
+ "NO_KEYS_CONFIGURED"
1218
+ );
1219
+ }
1220
+ const clonedEntry = JSON.parse(JSON.stringify(entry));
1221
+ this.providers.push(clonedEntry);
1222
+ this.rebuildPool();
1223
+ }
1224
+ /**
1225
+ * Add a key to an existing provider at runtime
1226
+ */
1227
+ addKeyToProvider(provider, key) {
1228
+ const entry = this.providers.find((p) => p.provider === provider);
1229
+ if (!entry) {
1230
+ throw new KeyRouterError(
1231
+ `Provider '${provider}' not found. Use addProvider() to add a new provider.`,
1232
+ "KEY_NOT_FOUND"
1233
+ );
1234
+ }
1235
+ const clonedKey = JSON.parse(JSON.stringify(key));
1236
+ entry.keys.push(clonedKey);
1237
+ this.rebuildPool();
1238
+ }
1239
+ /**
1240
+ * Remove a provider entirely at runtime
1241
+ */
1242
+ removeProvider(provider) {
1243
+ const index = this.providers.findIndex((p) => p.provider === provider);
1244
+ if (index === -1) return false;
1245
+ this.providers.splice(index, 1);
1246
+ this.rebuildPool();
1247
+ return true;
1248
+ }
1249
+ /**
1250
+ * Remove a specific key from a provider at runtime
1251
+ */
1252
+ removeKey(provider, key) {
1253
+ const entry = this.providers.find((p) => p.provider === provider);
1254
+ if (!entry) return false;
1255
+ const keyIndex = entry.keys.indexOf(key);
1256
+ if (keyIndex === -1) return false;
1257
+ entry.keys.splice(keyIndex, 1);
1258
+ if (entry.keys.length === 0) {
1259
+ return this.removeProvider(provider);
1260
+ }
1261
+ this.rebuildPool();
1262
+ return true;
1263
+ }
1264
+ // ==================== Strategy ====================
1265
+ /**
1266
+ * Set the selection strategy
1267
+ */
1268
+ setStrategy(strategy) {
1269
+ if (typeof strategy === "string") {
1270
+ this.strategy = createStrategy(strategy, this.strategy);
1271
+ } else {
1272
+ this.strategy = strategy;
1273
+ }
1274
+ }
1275
+ // ==================== Lifecycle ====================
1276
+ /**
1277
+ * Reset all tracking data (useful for testing)
1278
+ */
1279
+ reset() {
1280
+ this.tracker.reset();
1281
+ }
1282
+ /**
1283
+ * Shutdown and cleanup (stops background cleanup timer)
1284
+ */
1285
+ destroy() {
1286
+ this.tracker.destroy();
1287
+ }
1288
+ // ==================== Private Helpers ====================
1289
+ /**
1290
+ * Flatten all providers into a single key pool
1291
+ * Each key gets a unique ID: `${provider}-${keyIndex}`
1292
+ * Provider metadata (baseURL, model) is stored in endpointMap
1293
+ */
1294
+ buildPool(providers) {
1295
+ this.keyConfigs = [];
1296
+ this.endpointMap.clear();
1297
+ this.providerGroups.clear();
1298
+ for (const entry of providers) {
1299
+ const preset = PROVIDER_PRESETS[entry.provider];
1300
+ const baseURL = entry.baseURL ?? preset?.baseURL ?? "";
1301
+ const rpmLimit = entry.rpmLimit ?? preset?.defaultRpmLimit ?? 40;
1302
+ const models = entry.models ?? preset?.models ?? [];
1303
+ const defaultModel = models[0] ?? "";
1304
+ const weight = entry.weight ?? 1;
1305
+ if (!baseURL) {
1306
+ throw new KeyRouterError(
1307
+ `Provider '${entry.provider}' has no baseURL. Provide one or use a known provider preset.`,
1308
+ "NO_KEYS_CONFIGURED"
1309
+ );
1310
+ }
1311
+ const groupIds = [];
1312
+ for (let i = 0; i < entry.keys.length; i++) {
1313
+ const key = entry.keys[i];
1314
+ const id = `${entry.provider}-${i}`;
1315
+ this.keyConfigs.push({
1316
+ id,
1317
+ key,
1318
+ rpmLimit,
1319
+ weight,
1320
+ baseURL,
1321
+ metadata: {
1322
+ provider: entry.provider,
1323
+ model: defaultModel,
1324
+ ...entry.metadata
1325
+ }
1326
+ });
1327
+ this.endpointMap.set(id, {
1328
+ baseURL,
1329
+ model: defaultModel,
1330
+ provider: entry.provider
1331
+ });
1332
+ groupIds.push(id);
1333
+ }
1334
+ this.providerGroups.set(entry.provider, groupIds);
1335
+ }
1336
+ this.tracker.initialize(this.keyConfigs);
1337
+ }
1338
+ /**
1339
+ * Rebuild the pool from current providers (used after dynamic changes)
1340
+ */
1341
+ rebuildPool() {
1342
+ this.buildPool(this.providers);
1343
+ }
1344
+ };
1345
+
1346
+ // src/core/fetcher.ts
1347
+ var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1348
+ async function fetchWithFailover(urlBuilder, fetchOptionsBuilder, config2) {
1349
+ const router2 = config2.router;
1350
+ const maxRetries = config2.maxRetries ?? 3;
1351
+ let attempts = 0;
1352
+ let lastResponse = null;
1353
+ let lastError = null;
1354
+ const dynamicExcludeProviders = [...config2.excludeProviders || []];
1355
+ while (attempts < maxRetries) {
1356
+ attempts++;
1357
+ let ep;
1358
+ try {
1359
+ ep = await router2.getEndpoint({
1360
+ preferProviders: config2.preferProviders,
1361
+ excludeProviders: dynamicExcludeProviders,
1362
+ model: config2.model
1363
+ });
1364
+ } catch (error) {
1365
+ if (attempts >= maxRetries) continue;
1366
+ if (lastResponse) {
1367
+ return lastResponse;
1368
+ }
1369
+ throw error;
1370
+ }
1371
+ const url = urlBuilder(ep);
1372
+ const options = fetchOptionsBuilder(ep);
1373
+ try {
1374
+ const controller = new AbortController();
1375
+ const timeoutId = setTimeout(() => controller.abort(), 3e5);
1376
+ const response = await fetch(url, { ...options, signal: controller.signal });
1377
+ clearTimeout(timeoutId);
1378
+ lastResponse = response;
1379
+ if (!response.ok) {
1380
+ let errorText = "";
1381
+ try {
1382
+ const clone = response.clone();
1383
+ if (clone.body) {
1384
+ const reader = clone.body.getReader();
1385
+ const { value } = await reader.read();
1386
+ if (value) {
1387
+ errorText = new TextDecoder().decode(value).slice(0, 2048);
1388
+ }
1389
+ reader.cancel().catch(() => {
1390
+ });
1391
+ } else {
1392
+ errorText = (await clone.text()).slice(0, 2048);
1393
+ }
1394
+ } catch (e) {
1395
+ }
1396
+ const lower = errorText.toLowerCase();
1397
+ const isContextError = lower.includes("context length exceeded") || lower.includes("maximum context length");
1398
+ const isModelError = lower.includes("decommissioned") || lower.includes("not a valid model") || lower.includes("model not found") || lower.includes("does not exist") || lower.includes("not supported");
1399
+ const isAuthError = response.status === 401 || response.status === 403;
1400
+ const isRateLimit = response.status === 429;
1401
+ router2.reportFailure(ep.endpointId, isRateLimit);
1402
+ if (isContextError || isModelError || isAuthError || response.status === 402 || response.status === 404) {
1403
+ if (!dynamicExcludeProviders.includes(ep.provider)) {
1404
+ dynamicExcludeProviders.push(ep.provider);
1405
+ }
1406
+ }
1407
+ if (isRateLimit || response.status >= 500) {
1408
+ await sleep2(Math.min(1e3 * Math.pow(2, attempts - 1), 1e4));
1409
+ }
1410
+ continue;
1411
+ }
1412
+ return response;
1413
+ } catch (error) {
1414
+ lastError = error;
1415
+ router2.reportFailure(ep.endpointId, false);
1416
+ }
1417
+ }
1418
+ if (lastResponse) {
1419
+ return lastResponse;
1420
+ }
1421
+ if (lastError && lastError.name === "AbortError") {
1422
+ throw new Error("timeout");
1423
+ }
1424
+ throw new Error(`fetchWithFailover exhausted all retries. Last error: ${lastError?.message}`);
1425
+ }
1426
+ var UsageTracker = class {
1427
+ data;
1428
+ filePath;
1429
+ saveTimeout = null;
1430
+ sessionProviderUsage = {};
1431
+ initialized = false;
1432
+ constructor() {
1433
+ const dir = path.join(os2.homedir(), ".keymux");
1434
+ this.filePath = path.join(dir, "usage.json");
1435
+ this.data = {
1436
+ totalRequests: 0,
1437
+ totalInputTokens: 0,
1438
+ totalOutputTokens: 0,
1439
+ totalCacheTokens: 0,
1440
+ providerUsage: {},
1441
+ daily: {},
1442
+ sessions: 0,
1443
+ firstUsed: Date.now()
1444
+ };
1445
+ }
1446
+ ensureInitialized() {
1447
+ if (this.initialized) return;
1448
+ this.initialized = true;
1449
+ const dir = path.dirname(this.filePath);
1450
+ if (!fs.existsSync(dir)) {
1451
+ fs.mkdirSync(dir, { recursive: true });
1452
+ }
1453
+ this.data = this.loadData();
1454
+ this.data.sessions += 1;
1455
+ this.saveData();
1456
+ }
1457
+ loadData() {
1458
+ const defaultData = {
1459
+ totalRequests: 0,
1460
+ totalInputTokens: 0,
1461
+ totalOutputTokens: 0,
1462
+ totalCacheTokens: 0,
1463
+ providerUsage: {},
1464
+ daily: {},
1465
+ sessions: 0,
1466
+ firstUsed: Date.now()
1467
+ };
1468
+ if (fs.existsSync(this.filePath)) {
1469
+ try {
1470
+ const content = fs.readFileSync(this.filePath, "utf-8");
1471
+ const parsed = JSON.parse(content);
1472
+ if (parsed && typeof parsed === "object") {
1473
+ const providerUsage = {};
1474
+ if (parsed.providerUsage) {
1475
+ for (const [k, v] of Object.entries(parsed.providerUsage)) {
1476
+ if (typeof v === "number") {
1477
+ providerUsage[k] = { requests: v, tokens: 0 };
1478
+ } else {
1479
+ providerUsage[k] = v;
1480
+ }
1481
+ }
1482
+ }
1483
+ return { ...defaultData, ...parsed, providerUsage, daily: parsed.daily || {} };
1484
+ }
1485
+ } catch (e) {
1486
+ }
1487
+ }
1488
+ return defaultData;
1489
+ }
1490
+ saveData() {
1491
+ try {
1492
+ if (fs.existsSync(this.filePath)) {
1493
+ try {
1494
+ const diskContent = fs.readFileSync(this.filePath, "utf-8");
1495
+ const diskData = JSON.parse(diskContent);
1496
+ if (diskData.totalRequests > this.data.totalRequests) {
1497
+ this.data.totalRequests = diskData.totalRequests;
1498
+ }
1499
+ } catch (e) {
1500
+ }
1501
+ }
1502
+ const tempPath = this.filePath + ".tmp";
1503
+ fs.writeFileSync(tempPath, JSON.stringify(this.data, null, 2));
1504
+ fs.renameSync(tempPath, this.filePath);
1505
+ } catch (e) {
1506
+ }
1507
+ }
1508
+ scheduleSave() {
1509
+ if (!this.saveTimeout) {
1510
+ this.saveTimeout = setTimeout(() => {
1511
+ this.saveData();
1512
+ this.saveTimeout = null;
1513
+ }, 5e3);
1514
+ }
1515
+ }
1516
+ recordRequest(provider, inputTokens, outputTokens, cacheTokens = 0) {
1517
+ this.ensureInitialized();
1518
+ const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1519
+ this.data.totalRequests += 1;
1520
+ this.data.totalInputTokens += inputTokens;
1521
+ this.data.totalOutputTokens += outputTokens;
1522
+ this.data.totalCacheTokens += cacheTokens;
1523
+ if (!this.data.providerUsage[provider]) {
1524
+ this.data.providerUsage[provider] = { requests: 0, tokens: 0 };
1525
+ } else if (typeof this.data.providerUsage[provider] === "number") {
1526
+ this.data.providerUsage[provider] = { requests: this.data.providerUsage[provider], tokens: 0 };
1527
+ }
1528
+ const provUsage = this.data.providerUsage[provider];
1529
+ provUsage.requests += 1;
1530
+ provUsage.tokens += inputTokens + outputTokens;
1531
+ if (!this.data.daily[dateStr]) {
1532
+ this.data.daily[dateStr] = {
1533
+ date: dateStr,
1534
+ inputTokens: 0,
1535
+ outputTokens: 0,
1536
+ cacheTokens: 0,
1537
+ requests: 0
1538
+ };
1539
+ }
1540
+ this.data.daily[dateStr].inputTokens += inputTokens;
1541
+ this.data.daily[dateStr].outputTokens += outputTokens;
1542
+ this.data.daily[dateStr].cacheTokens += cacheTokens;
1543
+ this.data.daily[dateStr].requests += 1;
1544
+ if (!this.sessionProviderUsage[provider]) {
1545
+ this.sessionProviderUsage[provider] = { requests: 0, tokens: 0 };
1546
+ }
1547
+ this.sessionProviderUsage[provider].requests += 1;
1548
+ this.sessionProviderUsage[provider].tokens += inputTokens + outputTokens;
1549
+ if (!this.data.daily[dateStr].providerUsage) {
1550
+ this.data.daily[dateStr].providerUsage = {};
1551
+ }
1552
+ if (!this.data.daily[dateStr].providerUsage[provider]) {
1553
+ this.data.daily[dateStr].providerUsage[provider] = { requests: 0, tokens: 0 };
1554
+ }
1555
+ this.data.daily[dateStr].providerUsage[provider].requests += 1;
1556
+ this.data.daily[dateStr].providerUsage[provider].tokens += inputTokens + outputTokens;
1557
+ this.scheduleSave();
1558
+ }
1559
+ getSessionUsage() {
1560
+ this.ensureInitialized();
1561
+ return this.sessionProviderUsage;
1562
+ }
1563
+ getTodayUsage() {
1564
+ this.ensureInitialized();
1565
+ const todayDateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1566
+ return this.data.daily[todayDateStr]?.providerUsage || {};
1567
+ }
1568
+ getData() {
1569
+ this.ensureInitialized();
1570
+ return this.data;
1571
+ }
1572
+ };
1573
+ var _globalTracker = null;
1574
+ var globalUsageTracker = new Proxy({}, {
1575
+ get: (target, prop) => {
1576
+ if (!_globalTracker) _globalTracker = new UsageTracker();
1577
+ return _globalTracker[prop];
1578
+ }
1579
+ });
1580
+
1581
+ // src/proxy/translators/requestTranslator.ts
1582
+ function translateMessages(anthropicMessages, systemPrompt) {
1583
+ const openaiMessages = [];
1584
+ if (systemPrompt) {
1585
+ let sysContent = systemPrompt;
1586
+ if (Array.isArray(systemPrompt)) {
1587
+ sysContent = systemPrompt.map((b) => b.text || "").join("\n");
1588
+ }
1589
+ openaiMessages.push({ role: "system", content: sysContent });
1590
+ }
1591
+ for (const msg of anthropicMessages || []) {
1592
+ const role = msg.role;
1593
+ if (typeof msg.content === "string") {
1594
+ openaiMessages.push({ role, content: msg.content });
1595
+ } else if (Array.isArray(msg.content)) {
1596
+ const contentParts = [];
1597
+ const calls = [];
1598
+ for (const block of msg.content) {
1599
+ if (block.type === "text") {
1600
+ contentParts.push({ type: "text", text: block.text });
1601
+ } else if (block.type === "thinking") {
1602
+ contentParts.push({ type: "text", text: `<thinking>${block.thinking}</thinking>` });
1603
+ } else if (block.type === "image") {
1604
+ contentParts.push({
1605
+ type: "image_url",
1606
+ image_url: { url: `data:${block.source.media_type};base64,${block.source.data}` }
1607
+ });
1608
+ } else if (block.type === "tool_use") {
1609
+ const inputCopy = typeof block.input === "object" && block.input !== null ? { ...block.input } : {};
1610
+ calls.push({
1611
+ id: block.id,
1612
+ type: "function",
1613
+ function: {
1614
+ name: block.name,
1615
+ arguments: JSON.stringify(inputCopy)
1616
+ }
1617
+ });
1618
+ } else if (block.type === "document") {
1619
+ let title = block.title || "document";
1620
+ if (typeof title === "string" && (title.includes("../") || title.includes("..\\") || title.includes("/etc/") || title.startsWith("/"))) {
1621
+ title = "sanitized_document";
1622
+ }
1623
+ if (block.source && block.source.type === "base64" && block.source.data) {
1624
+ let docText = `[Attached File: ${title}]
1625
+ `;
1626
+ if (block.source.media_type === "application/pdf") {
1627
+ docText += "[PDF extraction via proxy is limited. Try pasting text if it fails]";
1628
+ } else if (block.source.media_type && !block.source.media_type.startsWith("text/")) {
1629
+ docText += "[Binary content unsupported by translation layer]";
1630
+ } else {
1631
+ try {
1632
+ docText += Buffer.from(block.source.data, "base64").toString("utf8");
1633
+ } catch (e) {
1634
+ docText += "[Binary file cannot be read]";
1635
+ }
1636
+ }
1637
+ contentParts.push({ type: "text", text: docText });
1638
+ } else if (block.source && block.source.type === "text") {
1639
+ contentParts.push({ type: "text", text: `[Attached File: ${title}]
1640
+ ${block.source.data}` });
1641
+ }
1642
+ } else if (block.type === "tool_result") {
1643
+ let toolText = "";
1644
+ if (typeof block.content === "string") {
1645
+ toolText = block.content;
1646
+ } else if (Array.isArray(block.content)) {
1647
+ toolText = block.content.map((c) => {
1648
+ if (c.type === "text") return c.text;
1649
+ if (c.type === "image") return "[Tool output included an image]";
1650
+ return typeof c === "string" ? c : JSON.stringify(c);
1651
+ }).join("\n");
1652
+ }
1653
+ if (block.is_error) {
1654
+ toolText = `[Error executing tool]
1655
+ ${toolText}`;
1656
+ }
1657
+ openaiMessages.push({
1658
+ role: "tool",
1659
+ tool_call_id: block.tool_use_id,
1660
+ content: toolText || "Executed successfully"
1661
+ });
1662
+ }
1663
+ }
1664
+ if (contentParts.length > 0 || calls.length > 0) {
1665
+ const messageObj = { role };
1666
+ if (contentParts.length > 0) {
1667
+ messageObj.content = contentParts;
1668
+ }
1669
+ if (calls.length > 0) {
1670
+ messageObj.tool_calls = calls;
1671
+ }
1672
+ openaiMessages.push(messageObj);
1673
+ }
1674
+ }
1675
+ }
1676
+ return openaiMessages;
1677
+ }
1678
+ function translateTools(anthropicTools) {
1679
+ if (!anthropicTools) return void 0;
1680
+ return anthropicTools.map((t) => {
1681
+ const parameters = JSON.parse(JSON.stringify(t.input_schema || {}));
1682
+ if (parameters && parameters.properties && parameters.properties.thought_signature) {
1683
+ delete parameters.properties.thought_signature;
1684
+ if (Array.isArray(parameters.required)) {
1685
+ parameters.required = parameters.required.filter((r) => r !== "thought_signature");
1686
+ }
1687
+ }
1688
+ return {
1689
+ type: "function",
1690
+ function: {
1691
+ name: t.name,
1692
+ description: t.description,
1693
+ parameters
1694
+ }
1695
+ };
1696
+ });
1697
+ }
1698
+ function translateAnthropicToOpenAI(anthropicReq) {
1699
+ if (anthropicReq.model) {
1700
+ if (anthropicReq.model === "claude-3-5-sonnet-minimax-m3") {
1701
+ anthropicReq.model = "minimax/minimax-m3:free";
1702
+ } else if (anthropicReq.model === "claude-3-5-sonnet-minimax-m2.7") {
1703
+ anthropicReq.model = "minimax/minimax-m2.7:free";
1704
+ } else if (anthropicReq.model === "claude-3-5-sonnet-glm-5.2") {
1705
+ anthropicReq.model = "z-ai/glm-5.2:free";
1706
+ } else if (anthropicReq.model === "claude-3-5-sonnet-nemotron-3-super-120b-a12b") {
1707
+ anthropicReq.model = "nvidia/nemotron-3-super-120b-a12b";
1708
+ } else if (anthropicReq.model === "claude-3-5-sonnet-nemotron-3-ultra-550b-a55b") {
1709
+ anthropicReq.model = "nvidia/nemotron-3-ultra-550b-a55b";
1710
+ }
1711
+ }
1712
+ const openaiReq = {
1713
+ messages: translateMessages(anthropicReq.messages, anthropicReq.system),
1714
+ stream: anthropicReq.stream !== void 0 ? anthropicReq.stream : false
1715
+ };
1716
+ if (openaiReq.stream) {
1717
+ openaiReq.stream_options = { include_usage: true };
1718
+ }
1719
+ const tools = translateTools(anthropicReq.tools);
1720
+ if (tools) openaiReq.tools = tools;
1721
+ if (anthropicReq.temperature !== void 0) {
1722
+ openaiReq.temperature = anthropicReq.temperature;
1723
+ }
1724
+ if (anthropicReq.top_k !== void 0) {
1725
+ openaiReq.top_k = anthropicReq.top_k;
1726
+ }
1727
+ if (anthropicReq.top_p !== void 0) {
1728
+ openaiReq.top_p = anthropicReq.top_p;
1729
+ }
1730
+ if (anthropicReq.max_tokens !== void 0) {
1731
+ openaiReq.max_tokens = anthropicReq.max_tokens;
1732
+ }
1733
+ if (anthropicReq.stop_sequences) {
1734
+ openaiReq.stop = anthropicReq.stop_sequences;
1735
+ }
1736
+ if (anthropicReq.tool_choice) {
1737
+ if (anthropicReq.tool_choice.type === "tool") {
1738
+ openaiReq.tool_choice = { type: "function", function: { name: anthropicReq.tool_choice.name } };
1739
+ } else if (anthropicReq.tool_choice.type === "auto") {
1740
+ openaiReq.tool_choice = "auto";
1741
+ }
1742
+ }
1743
+ return openaiReq;
1744
+ }
1745
+
1746
+ // src/proxy/translators/streamTranslator.ts
1747
+ var OpenRouterStreamTranslator = class {
1748
+ res;
1749
+ requestedModel;
1750
+ currentBlockType;
1751
+ currentBlockIndex;
1752
+ toolIndexMap;
1753
+ state;
1754
+ buffer;
1755
+ _thinkTag;
1756
+ hasSentMessageStart;
1757
+ constructor(res, requestedModel) {
1758
+ this.res = res;
1759
+ this.requestedModel = requestedModel;
1760
+ this.currentBlockType = null;
1761
+ this.currentBlockIndex = 0;
1762
+ this.toolIndexMap = {};
1763
+ this.state = "NORMAL";
1764
+ this.buffer = "";
1765
+ this._thinkTag = "<thinking>";
1766
+ this.hasSentMessageStart = false;
1767
+ }
1768
+ writeEvent(type, dataObj) {
1769
+ this.res.write(`event: ${type}
1770
+ data: ${JSON.stringify(dataObj)}
1771
+
1772
+ `);
1773
+ }
1774
+ startMessage() {
1775
+ if (this.hasSentMessageStart) return;
1776
+ this.writeEvent("message_start", {
1777
+ type: "message_start",
1778
+ message: {
1779
+ id: "msg_" + Math.random().toString(36).slice(2, 11),
1780
+ type: "message",
1781
+ role: "assistant",
1782
+ content: [],
1783
+ model: this.requestedModel,
1784
+ stop_reason: null,
1785
+ stop_sequence: null,
1786
+ usage: { input_tokens: 0, output_tokens: 0 }
1787
+ }
1788
+ });
1789
+ this.hasSentMessageStart = true;
1790
+ }
1791
+ stopCurrentBlock() {
1792
+ if (this.currentBlockType !== null) {
1793
+ this.writeEvent("content_block_stop", {
1794
+ type: "content_block_stop",
1795
+ index: this.currentBlockIndex
1796
+ });
1797
+ this.currentBlockIndex++;
1798
+ this.currentBlockType = null;
1799
+ }
1800
+ }
1801
+ startBlock(type, extra = {}) {
1802
+ this.stopCurrentBlock();
1803
+ const content_block = { type, ...extra };
1804
+ if (type === "thinking") {
1805
+ content_block.thinking = "";
1806
+ content_block.signature = "dummy_sig";
1807
+ } else if (type === "text") {
1808
+ content_block.text = "";
1809
+ }
1810
+ this.writeEvent("content_block_start", {
1811
+ type: "content_block_start",
1812
+ index: this.currentBlockIndex,
1813
+ content_block
1814
+ });
1815
+ this.currentBlockType = type;
1816
+ }
1817
+ handleChunk(chunkObj) {
1818
+ this.startMessage();
1819
+ const choice = chunkObj.choices && chunkObj.choices[0];
1820
+ if (!choice) return;
1821
+ const delta = choice.delta;
1822
+ if (!delta) return;
1823
+ const reasoning = delta.reasoning || delta.reasoning_content;
1824
+ if (reasoning) {
1825
+ if (this.currentBlockType !== "thinking") {
1826
+ this.startBlock("thinking");
1827
+ }
1828
+ this.writeEvent("content_block_delta", {
1829
+ type: "content_block_delta",
1830
+ index: this.currentBlockIndex,
1831
+ delta: {
1832
+ type: "thinking_delta",
1833
+ thinking: reasoning
1834
+ }
1835
+ });
1836
+ }
1837
+ if (typeof delta.content === "string" && delta.content.length > 0) {
1838
+ let pendingText = "";
1839
+ let pendingThinking = "";
1840
+ const flushText = () => {
1841
+ if (pendingText.length > 0) {
1842
+ if (this.currentBlockType !== "text") this.startBlock("text");
1843
+ this.writeEvent("content_block_delta", {
1844
+ type: "content_block_delta",
1845
+ index: this.currentBlockIndex,
1846
+ delta: { type: "text_delta", text: pendingText }
1847
+ });
1848
+ pendingText = "";
1849
+ }
1850
+ };
1851
+ const flushThinking = () => {
1852
+ if (pendingThinking.length > 0) {
1853
+ if (this.currentBlockType !== "thinking") this.startBlock("thinking");
1854
+ this.writeEvent("content_block_delta", {
1855
+ type: "content_block_delta",
1856
+ index: this.currentBlockIndex,
1857
+ delta: { type: "thinking_delta", thinking: pendingThinking }
1858
+ });
1859
+ pendingThinking = "";
1860
+ }
1861
+ };
1862
+ for (const char of delta.content) {
1863
+ if (this.state === "NORMAL") {
1864
+ if (char === "<") {
1865
+ this.state = "POTENTIAL_START";
1866
+ this.buffer = char;
1867
+ } else {
1868
+ pendingText += char;
1869
+ }
1870
+ } else if (this.state === "POTENTIAL_START") {
1871
+ this.buffer += char;
1872
+ const targets = ["<thinking>", "<think>"];
1873
+ const matchedTarget = targets.find((t) => t === this.buffer);
1874
+ const partialMatch = targets.some((t) => t.startsWith(this.buffer));
1875
+ if (matchedTarget) {
1876
+ flushText();
1877
+ this.state = "IN_THINKING";
1878
+ this._thinkTag = matchedTarget;
1879
+ this.buffer = "";
1880
+ this.startBlock("thinking");
1881
+ } else if (!partialMatch || this.buffer.length > 20) {
1882
+ pendingText += this.buffer;
1883
+ this.state = "NORMAL";
1884
+ this.buffer = "";
1885
+ }
1886
+ } else if (this.state === "IN_THINKING") {
1887
+ if (char === "<") {
1888
+ this.state = "POTENTIAL_END";
1889
+ this.buffer = char;
1890
+ } else {
1891
+ pendingThinking += char;
1892
+ }
1893
+ } else if (this.state === "POTENTIAL_END") {
1894
+ this.buffer += char;
1895
+ const closeTag = this._thinkTag === "<think>" ? "</think>" : "</thinking>";
1896
+ if (this.buffer === closeTag) {
1897
+ flushThinking();
1898
+ this.state = "NORMAL";
1899
+ this.buffer = "";
1900
+ this.startBlock("text");
1901
+ } else if (!closeTag.startsWith(this.buffer) || this.buffer.length > 20) {
1902
+ pendingThinking += this.buffer;
1903
+ this.state = "IN_THINKING";
1904
+ this.buffer = "";
1905
+ }
1906
+ }
1907
+ }
1908
+ flushText();
1909
+ flushThinking();
1910
+ }
1911
+ if (delta.tool_calls) {
1912
+ for (const tc of delta.tool_calls) {
1913
+ if (tc.id) {
1914
+ let rawName = tc.function.name || "";
1915
+ let safeName = rawName.split("<")[0].replace(/[^a-zA-Z0-9_-]/g, "");
1916
+ this.startBlock("tool_use", { id: tc.id, name: safeName, input: {} });
1917
+ this.toolIndexMap[tc.index] = this.currentBlockIndex;
1918
+ }
1919
+ if (tc.function && tc.function.arguments) {
1920
+ const targetIndex = this.toolIndexMap[tc.index] !== void 0 ? this.toolIndexMap[tc.index] : this.currentBlockIndex;
1921
+ this.writeEvent("content_block_delta", {
1922
+ type: "content_block_delta",
1923
+ index: targetIndex,
1924
+ delta: {
1925
+ type: "input_json_delta",
1926
+ partial_json: tc.function.arguments
1927
+ }
1928
+ });
1929
+ }
1930
+ }
1931
+ }
1932
+ }
1933
+ finish(streamUsage, finishReason) {
1934
+ this.startMessage();
1935
+ if (this.buffer && this.buffer.length > 0) {
1936
+ if (this.state === "POTENTIAL_START") {
1937
+ if (this.currentBlockType !== "text") this.startBlock("text");
1938
+ this.writeEvent("content_block_delta", {
1939
+ type: "content_block_delta",
1940
+ index: this.currentBlockIndex,
1941
+ delta: { type: "text_delta", text: this.buffer }
1942
+ });
1943
+ } else if (this.state === "POTENTIAL_END") {
1944
+ if (this.currentBlockType !== "thinking") this.startBlock("thinking");
1945
+ this.writeEvent("content_block_delta", {
1946
+ type: "content_block_delta",
1947
+ index: this.currentBlockIndex,
1948
+ delta: { type: "thinking_delta", thinking: this.buffer }
1949
+ });
1950
+ }
1951
+ this.buffer = "";
1952
+ }
1953
+ this.stopCurrentBlock();
1954
+ const stop_reason = finishReason === "stop" || finishReason === null ? "end_turn" : finishReason === "tool_calls" || finishReason === "function_call" ? "tool_use" : finishReason === "length" ? "max_tokens" : finishReason === "content_filter" ? "end_turn" : finishReason || "end_turn";
1955
+ const inputTokens = streamUsage?.prompt_tokens || 0;
1956
+ const outputTokens = streamUsage?.completion_tokens || 0;
1957
+ this.writeEvent("message_delta", {
1958
+ type: "message_delta",
1959
+ delta: { stop_reason, stop_sequence: null },
1960
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens }
1961
+ });
1962
+ this.writeEvent("message_stop", { type: "message_stop" });
1963
+ }
1964
+ };
1965
+ var NvidiaStreamTranslator = class extends OpenRouterStreamTranslator {
1966
+ };
1967
+
1968
+ // src/proxy/translators/responseTranslator.ts
1969
+ function translateOpenAIToAnthropic(openaiRes, requestedModel) {
1970
+ const choice = openaiRes.choices && openaiRes.choices[0];
1971
+ const message = choice && choice.message;
1972
+ let stop_reason = choice?.finish_reason || "end_turn";
1973
+ if (stop_reason === "stop") stop_reason = "end_turn";
1974
+ if (stop_reason === "tool_calls" || stop_reason === "function_call") stop_reason = "tool_use";
1975
+ if (stop_reason === "length") stop_reason = "max_tokens";
1976
+ if (stop_reason === "content_filter") stop_reason = "end_turn";
1977
+ const anthropicRes = {
1978
+ id: "msg_" + Math.random().toString(36).slice(2, 11),
1979
+ type: "message",
1980
+ role: "assistant",
1981
+ content: [],
1982
+ model: requestedModel,
1983
+ stop_reason,
1984
+ stop_sequence: null,
1985
+ usage: {
1986
+ input_tokens: openaiRes.usage?.prompt_tokens || 0,
1987
+ output_tokens: openaiRes.usage?.completion_tokens || 0
1988
+ }
1989
+ };
1990
+ if (message) {
1991
+ if (message.content) {
1992
+ anthropicRes.content.push({
1993
+ type: "text",
1994
+ text: message.content
1995
+ });
1996
+ }
1997
+ if (message.tool_calls) {
1998
+ for (const tc of message.tool_calls) {
1999
+ let parsedInput;
2000
+ if (typeof tc.function.arguments === "string") {
2001
+ try {
2002
+ parsedInput = JSON.parse(tc.function.arguments);
2003
+ } catch (e) {
2004
+ parsedInput = { _error: "Malformed JSON from model", _raw: tc.function.arguments };
2005
+ }
2006
+ } else {
2007
+ parsedInput = tc.function.arguments;
2008
+ }
2009
+ anthropicRes.content.push({
2010
+ type: "tool_use",
2011
+ id: tc.id,
2012
+ name: tc.function.name,
2013
+ input: parsedInput
2014
+ });
2015
+ }
2016
+ }
2017
+ }
2018
+ return anthropicRes;
2019
+ }
2020
+
2021
+ // src/proxy/server.ts
2022
+ var DEFAULT_PORT = 3002;
2023
+ var MODEL_NAME = process.env["ANTHROPIC_MODEL"] || "nvidia/nemotron-3-ultra-550b-a55b";
2024
+ var serverInstance = null;
2025
+ var router;
2026
+ var config;
2027
+ var globalUsageTracker2 = new UsageTracker();
2028
+ function parseKeyList(raw) {
2029
+ return String(raw || "").split(",").map((part) => part.trim()).filter(Boolean);
2030
+ }
2031
+ function loadConfig(configPath) {
2032
+ try {
2033
+ if (fs.existsSync(configPath)) {
2034
+ return JSON.parse(fs.readFileSync(configPath, "utf8"));
2035
+ }
2036
+ } catch (e) {
2037
+ console.error("Failed to read config:", e);
2038
+ }
2039
+ return { defaultProvider: null, defaultModel: "", keys: { openrouter: [], nvidia: [] } };
2040
+ }
2041
+ function createRouterInstance(config2) {
2042
+ const keysObj = config2?.keys || {};
2043
+ let nvidiaKeys = Array.isArray(keysObj.nvidia) ? keysObj.nvidia : [];
2044
+ let openRouterKeys = Array.isArray(keysObj.openrouter) ? keysObj.openrouter : [];
2045
+ let mistralKeys = Array.isArray(keysObj.mistral) ? keysObj.mistral : [];
2046
+ let geminiKeys = Array.isArray(keysObj.gemini) ? keysObj.gemini : [];
2047
+ let groqKeys = Array.isArray(keysObj.groq) ? keysObj.groq : [];
2048
+ if (nvidiaKeys.length === 0) {
2049
+ nvidiaKeys = parseKeyList(process.env["NVIDIA_KEYS"]);
2050
+ }
2051
+ if (openRouterKeys.length === 0) {
2052
+ openRouterKeys = parseKeyList(process.env["OPENROUTER_KEYS"]);
2053
+ }
2054
+ if (mistralKeys.length === 0) {
2055
+ mistralKeys = parseKeyList(process.env["MISTRAL_KEYS"]);
2056
+ }
2057
+ if (geminiKeys.length === 0) {
2058
+ geminiKeys = parseKeyList(process.env["GEMINI_KEYS"]);
2059
+ }
2060
+ if (groqKeys.length === 0) {
2061
+ groqKeys = parseKeyList(process.env["GROQ_KEYS"]);
2062
+ }
2063
+ return new MultiProviderRouter([
2064
+ {
2065
+ provider: "openrouter",
2066
+ keys: openRouterKeys,
2067
+ models: config2.openrouterModels || ["minimax/minimax-m3:free", "minimax/minimax-m2.7:free", "z-ai/glm-5.2:free"]
2068
+ },
2069
+ {
2070
+ provider: "nvidia",
2071
+ keys: nvidiaKeys
2072
+ },
2073
+ {
2074
+ provider: "mistral",
2075
+ keys: mistralKeys
2076
+ },
2077
+ {
2078
+ provider: "gemini",
2079
+ keys: geminiKeys
2080
+ },
2081
+ {
2082
+ provider: "groq",
2083
+ keys: groqKeys,
2084
+ models: config2.groqModels || ["qwen/qwen3.8-27b"]
2085
+ }
2086
+ ], {
2087
+ strategy: config2.strategy || "smart",
2088
+ trackLatency: config2.trackLatency !== void 0 ? config2.trackLatency : true,
2089
+ failureThreshold: config2.failureThreshold || 2,
2090
+ cooldownMs: config2.cooldownMs || 6e4,
2091
+ windowMs: config2.windowMs || 6e4,
2092
+ onDebug: (event) => {
2093
+ if (event.type === "all_exhausted" || event.type === "circuit_opened" || event.type === "key_recovered") {
2094
+ console.log(`[keymux] ${event.type}`, event.details || {});
2095
+ }
2096
+ }
2097
+ });
2098
+ }
2099
+ function logUsage(provider, model, key, attempts, failoverReason, anthropicReq) {
2100
+ if (process.env["KEYMUX_ENABLE_LOGGING"] !== "true") return;
2101
+ try {
2102
+ const maskedKeyStr = key ? maskKey(key) : "none";
2103
+ let statusStr = attempts > 1 ? `[FAILOVER #${attempts} | Prev Reason: ${failoverReason}]` : `[PRIMARY TRY #1]`;
2104
+ const logLine = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${statusStr.padEnd(50)} | Provider: ${provider.padEnd(10)} | Key: ${maskedKeyStr.padEnd(15)} | Model: ${model.padEnd(30)} | Query: <redacted for privacy>
2105
+ `;
2106
+ const logDir = path.join(os2.homedir(), ".claude");
2107
+ if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
2108
+ const logPath = path.join(logDir, "queries.log");
2109
+ if (fs.existsSync(logPath) && fs.statSync(logPath).size > 5 * 1024 * 1024) {
2110
+ fs.renameSync(logPath, logPath.replace("queries.log", "queries.old.log"));
2111
+ }
2112
+ fs.appendFileSync(logPath, logLine);
2113
+ } catch (e) {
2114
+ console.error("Failed to log usage", e);
2115
+ }
2116
+ }
2117
+ function sendJson(res, statusCode, payload) {
2118
+ res.writeHead(statusCode, {
2119
+ "Content-Type": "application/json",
2120
+ "Access-Control-Allow-Origin": "*",
2121
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
2122
+ "Access-Control-Allow-Headers": "*"
2123
+ });
2124
+ res.end(JSON.stringify(payload));
2125
+ }
2126
+ function startProxyServer(options) {
2127
+ const port = options?.port || Number(process.env["NVIDIA_PROXY_PORT"]) || DEFAULT_PORT;
2128
+ const configPath = options?.configPath || path.join(os2.homedir(), ".keymux", "config.json");
2129
+ config = loadConfig(configPath);
2130
+ router = createRouterInstance(config);
2131
+ console.log("[keymux] Multi-Provider proxy initialized");
2132
+ global.isConfigReloading = false;
2133
+ serverInstance = http.createServer(async (req, res) => {
2134
+ if (global.isConfigReloading) {
2135
+ await new Promise((r) => setTimeout(r, 100));
2136
+ }
2137
+ if (!req.url?.startsWith("/v1/keymux/")) {
2138
+ res.setHeader("Access-Control-Allow-Origin", "*");
2139
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
2140
+ res.setHeader("Access-Control-Allow-Headers", "*");
2141
+ }
2142
+ if (req.method === "OPTIONS") {
2143
+ res.writeHead(204);
2144
+ res.end();
2145
+ return;
2146
+ }
2147
+ if (req.url === "/api/hello") {
2148
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
2149
+ res.end(JSON.stringify({ message: "hello" }));
2150
+ return;
2151
+ }
2152
+ if (req.method === "GET" && (req.url === "/" || req.url === "/api" || req.url === "/api/")) {
2153
+ return sendJson(res, 200, { status: "ok", service: "keymux-proxy" });
2154
+ }
2155
+ if (req.url?.startsWith("/v1/keymux/")) {
2156
+ const adminToken = process.env["KEYMUX_ADMIN_TOKEN"];
2157
+ if (adminToken) {
2158
+ const authHeader = req.headers.authorization || "";
2159
+ if (authHeader !== `Bearer ${adminToken}`) {
2160
+ return sendJson(res, 401, { error: "Unauthorized" });
2161
+ }
2162
+ }
2163
+ if (req.method === "POST" && req.url === "/v1/keymux/reload") {
2164
+ try {
2165
+ const newConfig = loadConfig(configPath);
2166
+ const newRouter = createRouterInstance(newConfig);
2167
+ global.isConfigReloading = true;
2168
+ config = newConfig;
2169
+ router = newRouter;
2170
+ console.log("[keymux] Proxy settings reloaded");
2171
+ return sendJson(res, 200, { status: "reloaded" });
2172
+ } catch (err) {
2173
+ console.error("[keymux] Proxy reload failed:", err);
2174
+ return sendJson(res, 500, { error: "reload failed", details: err.message });
2175
+ } finally {
2176
+ global.isConfigReloading = false;
2177
+ }
2178
+ }
2179
+ if (req.method === "GET" && req.url === "/v1/keymux/lastRoute") {
2180
+ res.writeHead(200, { "Content-Type": "application/json" });
2181
+ const lastRoute = typeof router.getLastRoute === "function" ? router.getLastRoute() : null;
2182
+ if (lastRoute && global.lastActualTargetModel) {
2183
+ lastRoute.model = global.lastActualTargetModel;
2184
+ }
2185
+ res.end(JSON.stringify(lastRoute || {}));
2186
+ return;
2187
+ }
2188
+ if (req.method === "GET" && req.url === "/v1/keymux/activeModel") {
2189
+ res.writeHead(200, { "Content-Type": "application/json" });
2190
+ res.end(JSON.stringify({
2191
+ mode: config.strictMode ? "STRICT" : "AUTO",
2192
+ model: config.defaultModel || "auto",
2193
+ provider: config.defaultProvider || "auto"
2194
+ }));
2195
+ return;
2196
+ }
2197
+ if (req.method === "GET" && req.url === "/v1/keymux/stats") {
2198
+ res.writeHead(200, { "Content-Type": "application/json" });
2199
+ res.end(JSON.stringify(router.getStats()));
2200
+ return;
2201
+ }
2202
+ if (req.method === "GET" && req.url === "/v1/keymux/usage") {
2203
+ res.writeHead(200, { "Content-Type": "application/json" });
2204
+ res.end(JSON.stringify({
2205
+ session: globalUsageTracker2.getSessionUsage(),
2206
+ today: globalUsageTracker2.getTodayUsage()
2207
+ }));
2208
+ return;
2209
+ }
2210
+ }
2211
+ if (req.method === "GET" && (req.url?.includes("/v1/models") || req.url?.includes("/models"))) {
2212
+ return sendJson(res, 200, {
2213
+ data: [
2214
+ {
2215
+ id: "claude-3-5-sonnet-minimax/minimax-m3",
2216
+ type: "model",
2217
+ created: 1715e6,
2218
+ display_name: "MiniMax M3",
2219
+ anthropic_family_tier: "sonnet"
2220
+ },
2221
+ {
2222
+ id: "claude-3-5-sonnet-minimax/minimax-m2.7",
2223
+ type: "model",
2224
+ created: 1715e6,
2225
+ display_name: "MiniMax M2.7",
2226
+ anthropic_family_tier: "sonnet"
2227
+ },
2228
+ {
2229
+ id: "claude-3-5-sonnet-nvidia/nemotron-3-super-120b-a12b",
2230
+ type: "model",
2231
+ created: 1715e6,
2232
+ display_name: "Nemotron Super",
2233
+ anthropic_family_tier: "sonnet"
2234
+ },
2235
+ {
2236
+ id: "claude-3-5-sonnet-nvidia/nemotron-3-ultra-550b-a55b",
2237
+ type: "model",
2238
+ created: 1715e6,
2239
+ display_name: "Nemotron Ultra",
2240
+ anthropic_family_tier: "sonnet"
2241
+ },
2242
+ {
2243
+ id: "claude-3-5-sonnet-codestral-2508",
2244
+ type: "model",
2245
+ created: 1715e6,
2246
+ display_name: "Codestral",
2247
+ anthropic_family_tier: "sonnet"
2248
+ },
2249
+ {
2250
+ id: "claude-3-5-sonnet-gemini-1.5-flash",
2251
+ type: "model",
2252
+ created: 1715e6,
2253
+ display_name: "Gemini 1.5 Flash",
2254
+ anthropic_family_tier: "sonnet"
2255
+ }
2256
+ ]
2257
+ });
2258
+ }
2259
+ if (req.method === "POST" && req.url?.includes("/count_tokens")) {
2260
+ return sendJson(res, 200, { input_tokens: 10 });
2261
+ }
2262
+ if (req.method === "POST" && req.url?.includes("/v1/messages")) {
2263
+ let bodyStr = "";
2264
+ const MAX_BODY_SIZE = 25 * 1024 * 1024;
2265
+ req.on("data", (chunk) => {
2266
+ bodyStr += chunk;
2267
+ if (bodyStr.length > MAX_BODY_SIZE) {
2268
+ req.destroy();
2269
+ res.writeHead(413, { "Content-Type": "application/json" });
2270
+ res.end(JSON.stringify({ error: "Payload Too Large" }));
2271
+ }
2272
+ });
2273
+ req.on("end", async () => {
2274
+ try {
2275
+ const anthropicReq = JSON.parse(bodyStr);
2276
+ const isProbe = anthropicReq.messages && anthropicReq.messages.length === 1 && (anthropicReq.messages[0].content === "." || anthropicReq.messages[0].content === "ping" || anthropicReq.max_tokens <= 1);
2277
+ if (isProbe) {
2278
+ return sendJson(res, 200, {
2279
+ id: "msg_probe_" + Math.random().toString(36).slice(2, 9),
2280
+ type: "message",
2281
+ role: "assistant",
2282
+ content: [{ type: "text", text: "OK" }],
2283
+ model: anthropicReq.model || MODEL_NAME,
2284
+ stop_reason: "end_turn",
2285
+ stop_sequence: null,
2286
+ usage: { input_tokens: 1, output_tokens: 1 }
2287
+ });
2288
+ }
2289
+ const openaiReq = translateAnthropicToOpenAI(anthropicReq);
2290
+ openaiReq.model = anthropicReq.model || MODEL_NAME;
2291
+ if (openaiReq.stream) {
2292
+ openaiReq.stream_options = { include_usage: true };
2293
+ }
2294
+ const REQUEST_TIMEOUT_MS = Number(process.env["NVIDIA_PROXY_TIMEOUT"]) || 6e4;
2295
+ let attempts = 0;
2296
+ let lastErrorReason = "";
2297
+ let finalEp = null;
2298
+ let startedAt = 0;
2299
+ const reqString = JSON.stringify(openaiReq);
2300
+ const needsVision = reqString.includes('"type":"image_url"');
2301
+ const needsComputerUse = reqString.includes('"name":"computer_20241022"');
2302
+ const isComplexRequest = needsVision || needsComputerUse;
2303
+ let preferProviders = config.defaultProvider ? [config.defaultProvider] : void 0;
2304
+ let excludeProviders = config.strictMode && config.defaultProvider ? router.getProviderNames().filter((p) => p !== config.defaultProvider) : [];
2305
+ if (isComplexRequest && !config.strictMode) {
2306
+ console.log(`[PROXY] Detected ${needsVision ? "Vision" : "Computer Use"} request. Activating Smart Router...`);
2307
+ preferProviders = ["openrouter", "gemini"];
2308
+ const blindProviders = ["groq", "mistral", "nvidia"];
2309
+ for (const p of blindProviders) {
2310
+ if (!excludeProviders.includes(p)) excludeProviders.push(p);
2311
+ }
2312
+ }
2313
+ try {
2314
+ const fetchRes = await fetchWithFailover(
2315
+ (ep2) => {
2316
+ const url = new URL(ep2.baseURL + "/chat/completions");
2317
+ return url.toString();
2318
+ },
2319
+ (ep2) => {
2320
+ attempts++;
2321
+ startedAt = Date.now();
2322
+ console.log(`[PROXY] Attempt ${attempts}: Routing to ${ep2.provider} (${ep2.model})`);
2323
+ logUsage(ep2.provider, ep2.model, ep2.key, attempts, lastErrorReason, anthropicReq);
2324
+ let targetModel = config.defaultModel || anthropicReq.model || ep2.model || MODEL_NAME;
2325
+ if (targetModel.startsWith("claude-3-5-sonnet-") && targetModel !== "claude-3-5-sonnet-20240620") {
2326
+ targetModel = targetModel.replace("claude-3-5-sonnet-", "");
2327
+ }
2328
+ if (!config.strictMode) {
2329
+ if (isComplexRequest && ep2.provider === "openrouter") {
2330
+ targetModel = "inclusionai/ling-3.0-flash-vl:free";
2331
+ } else if (isComplexRequest && ep2.provider === "gemini") {
2332
+ targetModel = "gemini-1.5-flash";
2333
+ } else if (ep2.provider === "nvidia" && !targetModel.startsWith("nvidia/") && !targetModel.startsWith("deepseek")) {
2334
+ targetModel = "nvidia/nemotron-3-super-120b-a12b";
2335
+ } else if (ep2.provider === "mistral" && !targetModel.startsWith("mistral") && !targetModel.startsWith("codestral") && !targetModel.startsWith("devstral")) {
2336
+ targetModel = "codestral-2508";
2337
+ } else if (ep2.provider === "gemini" && !targetModel.startsWith("gemini")) {
2338
+ targetModel = "gemini-3.5-flash-lite";
2339
+ } else if (ep2.provider === "groq" && !targetModel.startsWith("qwen/") && !targetModel.startsWith("groq/") && !targetModel.startsWith("openai/")) {
2340
+ targetModel = "qwen/qwen3.8-27b";
2341
+ } else if (ep2.provider === "openrouter") {
2342
+ if (targetModel.startsWith("nvidia/")) {
2343
+ targetModel = "qwen/qwen3.8-27b";
2344
+ } else if (targetModel === "codestral-2508") {
2345
+ targetModel = "mistralai/codestral-2501";
2346
+ } else if (!targetModel.includes("/")) {
2347
+ targetModel = "qwen/qwen3.8-27b";
2348
+ }
2349
+ }
2350
+ }
2351
+ openaiReq.model = targetModel;
2352
+ global.lastActualTargetModel = targetModel;
2353
+ if (ep2.provider === "openrouter" || targetModel.includes("deepseek")) {
2354
+ openaiReq.include_reasoning = true;
2355
+ } else {
2356
+ delete openaiReq.include_reasoning;
2357
+ }
2358
+ const postData = JSON.stringify(openaiReq);
2359
+ finalEp = ep2;
2360
+ return {
2361
+ method: "POST",
2362
+ headers: {
2363
+ "Authorization": `Bearer ${ep2.key}`,
2364
+ "Content-Type": "application/json"
2365
+ },
2366
+ body: postData,
2367
+ signal: AbortSignal.timeout ? AbortSignal.timeout(REQUEST_TIMEOUT_MS) : void 0
2368
+ };
2369
+ },
2370
+ {
2371
+ router,
2372
+ maxRetries: 10,
2373
+ preferProviders,
2374
+ excludeProviders
2375
+ }
2376
+ );
2377
+ const ep = finalEp;
2378
+ if (!fetchRes.ok) {
2379
+ const errorData = await fetchRes.text();
2380
+ if (globalUsageTracker2) {
2381
+ globalUsageTracker2.recordRequest(ep.provider, 0, 0, 0);
2382
+ }
2383
+ lastErrorReason = `${ep.provider} HTTP ${fetchRes.status}`;
2384
+ console.error(`[PROXY] Provider ${ep.provider} failed with ${fetchRes.status}: ${errorData}`);
2385
+ let customMessage = `[Keymux Gateway] Provider error (${fetchRes.status}): ${errorData.substring(0, 150)}`;
2386
+ const lowerErr = errorData.toLowerCase();
2387
+ if (lowerErr.includes("tokens limit") || lowerErr.includes("context") || lowerErr.includes("too large")) {
2388
+ customMessage = `[Keymux Gateway] Context window exceeded! Your prompt is too large for the current model. Please clear some history or switch to a larger model via 'keymux -d'. (Details: ${errorData.substring(0, 150)})`;
2389
+ } else if (fetchRes.status === 402 || fetchRes.status === 429) {
2390
+ customMessage = `[Keymux Gateway] All available APIs for this model are currently exhausted (rate-limited or out of credits). Please try again in a few minutes, or change your Provider/Model via 'keymux -d'.`;
2391
+ }
2392
+ res.writeHead(fetchRes.status, { "Content-Type": "application/json" });
2393
+ res.end(JSON.stringify({
2394
+ type: "error",
2395
+ error: {
2396
+ type: "api_error",
2397
+ message: customMessage
2398
+ }
2399
+ }));
2400
+ return;
2401
+ }
2402
+ if (!openaiReq.stream) {
2403
+ const responseData = await fetchRes.text();
2404
+ try {
2405
+ const openaiRes = JSON.parse(responseData);
2406
+ const inputTokens = openaiRes.usage?.prompt_tokens || 0;
2407
+ const outputTokens = openaiRes.usage?.completion_tokens || 0;
2408
+ if (globalUsageTracker2 && openaiRes.usage) globalUsageTracker2.recordRequest(ep.provider, inputTokens, outputTokens, 0);
2409
+ const anthropicRes = translateOpenAIToAnthropic(openaiRes, anthropicReq.model || MODEL_NAME);
2410
+ router.reportSuccess(ep.endpointId, Date.now() - startedAt);
2411
+ sendJson(res, 200, anthropicRes);
2412
+ } catch (e) {
2413
+ router.reportFailure(ep.endpointId, false);
2414
+ sendJson(res, 500, { error: e.message });
2415
+ }
2416
+ return;
2417
+ }
2418
+ res.writeHead(fetchRes.status, {
2419
+ "Content-Type": "text/event-stream",
2420
+ "Cache-Control": "no-cache",
2421
+ Connection: "keep-alive"
2422
+ });
2423
+ const TranslatorClass = ep.provider === "nvidia" ? NvidiaStreamTranslator : OpenRouterStreamTranslator;
2424
+ const translator = new TranslatorClass(res, anthropicReq.model || MODEL_NAME);
2425
+ let buffer = "";
2426
+ let streamUsage = null;
2427
+ let generatedText = "";
2428
+ if (!fetchRes.body) {
2429
+ res.end();
2430
+ return;
2431
+ }
2432
+ const reader = fetchRes.body.getReader();
2433
+ const decoder = new TextDecoder();
2434
+ let ttftRecorded = false;
2435
+ let ttftMs = Date.now() - startedAt;
2436
+ let streamFailed = false;
2437
+ try {
2438
+ while (true) {
2439
+ const { done, value } = await reader.read();
2440
+ if (done) break;
2441
+ if (!ttftRecorded) {
2442
+ ttftRecorded = true;
2443
+ ttftMs = Date.now() - startedAt;
2444
+ }
2445
+ buffer += decoder.decode(value, { stream: true });
2446
+ const lines = buffer.split("\n");
2447
+ buffer = lines.pop() || "";
2448
+ for (const line of lines) {
2449
+ const trimmed = line.trim();
2450
+ if (!trimmed) continue;
2451
+ if (trimmed === "data: [DONE]") continue;
2452
+ if (trimmed.startsWith("data: ")) {
2453
+ const jsonStr = trimmed.slice(6);
2454
+ try {
2455
+ const chunkObj = JSON.parse(jsonStr);
2456
+ if (chunkObj.usage) streamUsage = chunkObj.usage;
2457
+ if (chunkObj.choices?.[0]?.delta?.content) {
2458
+ generatedText += chunkObj.choices[0].delta.content;
2459
+ }
2460
+ translator.handleChunk(chunkObj);
2461
+ } catch (e) {
2462
+ }
2463
+ }
2464
+ }
2465
+ }
2466
+ } catch (e) {
2467
+ console.error("[PROXY] Stream read error:", e);
2468
+ router.reportFailure(ep.endpointId, false);
2469
+ streamFailed = true;
2470
+ }
2471
+ if (!streamFailed) {
2472
+ router.reportSuccess(ep.endpointId, ttftMs);
2473
+ let inputTokens = streamUsage?.prompt_tokens || 0;
2474
+ let outputTokens = streamUsage?.completion_tokens || 0;
2475
+ if (inputTokens === 0 && outputTokens === 0) {
2476
+ inputTokens = Math.ceil(JSON.stringify(anthropicReq.messages || []).length / 4);
2477
+ outputTokens = Math.ceil(generatedText.length / 4);
2478
+ }
2479
+ if (globalUsageTracker2) {
2480
+ globalUsageTracker2.recordRequest(ep.provider, inputTokens, outputTokens, 0);
2481
+ }
2482
+ translator.finish(streamUsage, streamUsage ? "stop" : null);
2483
+ }
2484
+ res.end();
2485
+ } catch (error) {
2486
+ console.error("[PROXY] fetchWithFailover Error:", error);
2487
+ let customMessage = "[Keymux Gateway] API Error occurred.";
2488
+ const targetProvider = config.defaultProvider;
2489
+ const hasKeysForTarget = targetProvider && config.keys && config.keys[targetProvider] && config.keys[targetProvider].length > 0;
2490
+ if (config.strictMode && !hasKeysForTarget) {
2491
+ customMessage = `[Keymux Gateway] \u{1F6A8} No API keys found for '${targetProvider}'. Please open another terminal, run 'keymux -d', go to Settings and add your API key.`;
2492
+ } else if (error.message?.includes("timeout") || error.name === "AbortError") {
2493
+ customMessage = "[Keymux Gateway] The model provider is not responding (Timeout > 60s). The server might be down or overloaded. Please try changing your Default Model or Provider via 'keymux -d'.";
2494
+ } else if (error.message?.includes("exhausted") || error.name === "RateLimitError") {
2495
+ customMessage = "[Keymux Gateway] All available APIs for this model are currently exhausted (rate-limited or down). Please try again in a few minutes, or change your Provider/Model via 'keymux -d'.";
2496
+ } else {
2497
+ customMessage = `[Keymux Gateway] ${error.message}`;
2498
+ }
2499
+ res.writeHead(500, { "Content-Type": "application/json" });
2500
+ res.end(JSON.stringify({
2501
+ type: "error",
2502
+ error: {
2503
+ type: "api_error",
2504
+ message: customMessage
2505
+ }
2506
+ }));
2507
+ }
2508
+ } catch (err) {
2509
+ sendJson(res, 400, { error: "Invalid JSON" });
2510
+ }
2511
+ });
2512
+ } else {
2513
+ res.writeHead(404, { "Content-Type": "text/plain" });
2514
+ res.end("Not Found");
2515
+ }
2516
+ });
2517
+ serverInstance.listen(port, "127.0.0.1", () => {
2518
+ console.log(`[keymux] HTTP Proxy Server listening on 127.0.0.1:${port}`);
2519
+ });
2520
+ return serverInstance;
2521
+ }
2522
+ function stopProxyServer() {
2523
+ if (serverInstance) {
2524
+ serverInstance.close();
2525
+ serverInstance = null;
2526
+ console.log("[keymux] HTTP Proxy Server stopped");
2527
+ }
2528
+ }
2529
+
2530
+ // src/index.ts
2531
+ function createNvidiaRouter(keys, options) {
2532
+ return KeyRouter.forNvidia(keys, options);
2533
+ }
2534
+ function createRouter(provider, keys, options) {
2535
+ return KeyRouter.fromProvider(provider, keys, options);
2536
+ }
2537
+ function createRouterFromEnv(envVar, options) {
2538
+ return KeyRouter.fromEnv(envVar, options);
2539
+ }
2540
+ function createMultiProviderRouter(providers, config2) {
2541
+ return new MultiProviderRouter(providers, config2);
2542
+ }
2543
+ function createMultiProviderRouterFromEnv(envMap, overrides, config2) {
2544
+ const providers = [];
2545
+ for (const [provider, envVar] of Object.entries(envMap)) {
2546
+ const keys = process.env[envVar]?.split(",").map((k) => k.trim()).filter(Boolean) ?? [];
2547
+ if (keys.length === 0) {
2548
+ continue;
2549
+ }
2550
+ providers.push({
2551
+ provider,
2552
+ keys,
2553
+ ...overrides?.[provider]
2554
+ });
2555
+ }
2556
+ if (providers.length === 0) {
2557
+ throw new KeyRouterError(
2558
+ "No provider keys found in environment variables.",
2559
+ "NO_KEYS_CONFIGURED"
2560
+ );
2561
+ }
2562
+ return new MultiProviderRouter(providers, config2);
2563
+ }
2564
+
2565
+ export { CircuitState, KeyRouter, KeyRouterError, KeyTracker, LeastLatencyStrategy, LeastRequestsStrategy, MultiProviderRouter, NvidiaStreamTranslator, OpenRouterStreamTranslator, PROVIDER_PRESETS, PreferredKeysStrategy, RandomStrategy, RateLimitError, SmartRoutingStrategy, UsageTracker, WeightedLeastUtilizationStrategy, calculateBackoff, createFailoverKeyGetter, createKeyGetter, createMultiProviderRouter, createMultiProviderRouterFromEnv, createNvidiaRouter, createRouter, createRouterFromEnv, createStatsLogger, createStrategy, defaultStrategy, fetchWithFailover, formatStats, globalUsageTracker, isRateLimitError, isRetryableError, isServerError, maskKey, parseKeys, sleep, startProxyServer, stopProxyServer, translateAnthropicToOpenAI, translateOpenAIToAnthropic };
2566
+ //# sourceMappingURL=index.js.map
2567
+ //# sourceMappingURL=index.js.map