@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/README.md +79 -0
- package/dist/cli.cjs +870 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +840 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +2615 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +859 -0
- package/dist/index.d.ts +859 -0
- package/dist/index.js +2567 -0
- package/dist/index.js.map +1 -0
- package/dist/proxy/index.cjs +2138 -0
- package/dist/proxy/index.cjs.map +1 -0
- package/dist/proxy/index.d.cts +38 -0
- package/dist/proxy/index.d.ts +38 -0
- package/dist/proxy/index.js +2122 -0
- package/dist/proxy/index.js.map +1 -0
- package/package.json +77 -0
|
@@ -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
|