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