anurag008w commited on
Commit
e6ee6f1
Β·
1 Parent(s): 332c458

update rotator and startup files

Browse files
Dockerfile CHANGED
@@ -72,8 +72,8 @@ COPY --chown=1000:1000 wa-guardian.js /home/node/app/wa-guardian.js
72
  COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
73
  COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
74
  RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py
75
- COPY --chown=1000:1000 nvidia-key-rotator.cjs /home/node/app/nvidia-key-rotator.cjs
76
- RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py /home/node/app/nvidia-key-rotator.cjs
77
 
78
  USER node
79
 
 
72
  COPY --chown=1000:1000 cloudflare-keepalive-setup.py /home/node/app/cloudflare-keepalive-setup.py
73
  COPY --chown=1000:1000 openclaw-sync.py /home/node/app/openclaw-sync.py
74
  RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py
75
+ COPY --chown=1000:1000 multi-provider-key-rotator.cjs /home/node/app/multi-provider-key-rotator.cjs
76
+ RUN chmod +x /home/node/app/start.sh /home/node/app/cloudflare-proxy-setup.py /home/node/app/cloudflare-keepalive-setup.py /home/node/app/openclaw-sync.py /home/node/app/multi-provider-key-rotator.cjs
77
 
78
  USER node
79
 
multi-provider-key-rotator.cjs ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use strict';
2
+
3
+ /**
4
+ * Multi-provider API key rotator for OpenClaw/HuggingClaw
5
+ * --------------------------------------------------------
6
+ * Works exactly like nvidia-key-rotator.cjs but covers every
7
+ * provider that HuggingClaw supports.
8
+ *
9
+ * For each provider you can supply a comma-separated pool:
10
+ * ANTHROPIC_API_KEYS=key1,key2,key3
11
+ * Falls back to the singular env var, then to LLM_API_KEY.
12
+ *
13
+ * Keys are rotated round-robin per provider independently.
14
+ *
15
+ * Patches globalThis.fetch + node:http + node:https so that
16
+ * virtually every caller is covered without code changes.
17
+ */
18
+
19
+ const http = require('node:http');
20
+ const https = require('node:https');
21
+
22
+ // ─── Provider definitions ────────────────────────────────────────────────────
23
+ //
24
+ // hostname – regex tested against the request hostname (case-insensitive)
25
+ // envPlural – env var that holds a comma-separated key pool (preferred)
26
+ // envSingular – env var that holds a single key (fallback)
27
+ //
28
+ // LLM_API_KEY is the final fallback for every provider.
29
+ //
30
+ const PROVIDERS = [
31
+ {
32
+ name: 'anthropic',
33
+ hostname: /(?:^|\.)api\.anthropic\.com$/i,
34
+ envPlural: 'ANTHROPIC_API_KEYS',
35
+ envSingular:'ANTHROPIC_API_KEY',
36
+ },
37
+ {
38
+ name: 'openai',
39
+ hostname: /(?:^|\.)api\.openai\.com$/i,
40
+ envPlural: 'OPENAI_API_KEYS',
41
+ envSingular:'OPENAI_API_KEY',
42
+ },
43
+ {
44
+ name: 'gemini',
45
+ // Gemini uses generativelanguage API; also covers aiplatform
46
+ hostname: /(?:^|\.)(?:generativelanguage\.googleapis\.com|aiplatform\.googleapis\.com)$/i,
47
+ envPlural: 'GEMINI_API_KEYS',
48
+ envSingular:'GEMINI_API_KEY',
49
+ },
50
+ {
51
+ name: 'deepseek',
52
+ hostname: /(?:^|\.)api\.deepseek\.com$/i,
53
+ envPlural: 'DEEPSEEK_API_KEYS',
54
+ envSingular:'DEEPSEEK_API_KEY',
55
+ },
56
+ {
57
+ name: 'openrouter',
58
+ hostname: /(?:^|\.)openrouter\.ai$/i,
59
+ envPlural: 'OPENROUTER_API_KEYS',
60
+ envSingular:'OPENROUTER_API_KEY',
61
+ },
62
+ {
63
+ name: 'kilocode',
64
+ hostname: /(?:^|\.)kilocode\.ai$/i,
65
+ envPlural: 'KILOCODE_API_KEYS',
66
+ envSingular:'KILOCODE_API_KEY',
67
+ },
68
+ {
69
+ name: 'opencode',
70
+ hostname: /(?:^|\.)opencode\.ai$/i,
71
+ envPlural: 'OPENCODE_API_KEYS',
72
+ envSingular:'OPENCODE_API_KEY',
73
+ },
74
+ {
75
+ name: 'zai',
76
+ // Z.ai / GLM β€” both domains normalised to "zai" in OpenClaw
77
+ hostname: /(?:^|\.)(?:z\.ai|open\.bigmodel\.cn)$/i,
78
+ envPlural: 'ZAI_API_KEYS',
79
+ envSingular:'ZAI_API_KEY',
80
+ },
81
+ {
82
+ name: 'moonshot',
83
+ hostname: /(?:^|\.)api\.moonshot\.cn$/i,
84
+ envPlural: 'MOONSHOT_API_KEYS',
85
+ envSingular:'MOONSHOT_API_KEY',
86
+ },
87
+ {
88
+ name: 'minimax',
89
+ hostname: /(?:^|\.)api\.minimax\.chat$/i,
90
+ envPlural: 'MINIMAX_API_KEYS',
91
+ envSingular:'MINIMAX_API_KEY',
92
+ },
93
+ {
94
+ name: 'xiaomi',
95
+ // MiMo β€” update hostname if Xiaomi publishes an official domain
96
+ hostname: /(?:^|\.)api\.xiaomi\.com$/i,
97
+ envPlural: 'XIAOMI_API_KEYS',
98
+ envSingular:'XIAOMI_API_KEY',
99
+ },
100
+ {
101
+ name: 'volcengine',
102
+ // Volcengine / Doubao
103
+ hostname: /(?:^|\.)(?:ark\.cn-beijing\.volces\.com|volcengineapi\.com)$/i,
104
+ envPlural: 'VOLCANO_ENGINE_API_KEYS',
105
+ envSingular:'VOLCANO_ENGINE_API_KEY',
106
+ },
107
+ {
108
+ name: 'byteplus',
109
+ hostname: /(?:^|\.)maas-api\.ml-platform-cn-beijing\.byteplus\.com$/i,
110
+ envPlural: 'BYTEPLUS_API_KEYS',
111
+ envSingular:'BYTEPLUS_API_KEY',
112
+ },
113
+ {
114
+ name: 'mistral',
115
+ hostname: /(?:^|\.)api\.mistral\.ai$/i,
116
+ envPlural: 'MISTRAL_API_KEYS',
117
+ envSingular:'MISTRAL_API_KEY',
118
+ },
119
+ {
120
+ name: 'xai',
121
+ hostname: /(?:^|\.)api\.x\.ai$/i,
122
+ envPlural: 'XAI_API_KEYS',
123
+ envSingular:'XAI_API_KEY',
124
+ },
125
+ {
126
+ name: 'nvidia',
127
+ hostname: /(?:^|\.)(?:integrate\.api\.nvidia\.com|api\.nvidia\.com)$/i,
128
+ envPlural: 'NVIDIA_API_KEYS',
129
+ envSingular:'NVIDIA_API_KEY',
130
+ },
131
+ {
132
+ name: 'groq',
133
+ hostname: /(?:^|\.)api\.groq\.com$/i,
134
+ envPlural: 'GROQ_API_KEYS',
135
+ envSingular:'GROQ_API_KEY',
136
+ },
137
+ {
138
+ name: 'cohere',
139
+ hostname: /(?:^|\.)api\.cohere\.(?:ai|com)$/i,
140
+ envPlural: 'COHERE_API_KEYS',
141
+ envSingular:'COHERE_API_KEY',
142
+ },
143
+ {
144
+ name: 'together',
145
+ hostname: /(?:^|\.)api\.together\.(?:xyz|ai)$/i,
146
+ envPlural: 'TOGETHER_API_KEYS',
147
+ envSingular:'TOGETHER_API_KEY',
148
+ },
149
+ {
150
+ name: 'cerebras',
151
+ hostname: /(?:^|\.)api\.cerebras\.ai$/i,
152
+ envPlural: 'CEREBRAS_API_KEYS',
153
+ envSingular:'CEREBRAS_API_KEY',
154
+ },
155
+ {
156
+ name: 'huggingface',
157
+ hostname: /(?:^|\.)(?:api-inference\.huggingface\.co|router\.huggingface\.co|huggingface\.co)$/i,
158
+ envPlural: 'HUGGINGFACE_HUB_TOKENS', // plural variant
159
+ envSingular:'HUGGINGFACE_HUB_TOKEN',
160
+ },
161
+ ];
162
+
163
+ // ─── Key loading ───────��─────────────────────────────────────────────────────
164
+
165
+ function normalizeKeys(...inputs) {
166
+ const seen = new Set();
167
+ const out = [];
168
+ for (const input of inputs) {
169
+ for (const k of String(input || '').split(',').map(s => s.trim()).filter(Boolean)) {
170
+ if (!seen.has(k)) { seen.add(k); out.push(k); }
171
+ }
172
+ }
173
+ return out;
174
+ }
175
+
176
+ // Build per-provider key pools + rotation indices
177
+ const providerState = PROVIDERS.map(p => {
178
+ const keys = normalizeKeys(
179
+ process.env[p.envPlural] || '',
180
+ process.env[p.envSingular] || '',
181
+ process.env.LLM_API_KEY || '',
182
+ );
183
+ if (!keys.length) {
184
+ console.warn(`[key-rotator] No keys for provider "${p.name}"`);
185
+ } else {
186
+ console.log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`);
187
+ }
188
+ return { ...p, keys, idx: 0 };
189
+ });
190
+
191
+ // ─── Runtime helpers ─────────────────────────────────────────────────────────
192
+
193
+ function resolveHostname(urlLike) {
194
+ try {
195
+ const u =
196
+ typeof urlLike === 'string' ? new URL(urlLike)
197
+ : urlLike instanceof URL ? urlLike
198
+ : urlLike && typeof urlLike.url === 'string' ? new URL(urlLike.url)
199
+ : urlLike && typeof urlLike.href === 'string' ? new URL(urlLike.href)
200
+ : urlLike && typeof urlLike.hostname === 'string' ? urlLike
201
+ : null;
202
+ return u ? u.hostname : null;
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ function matchProvider(hostname) {
209
+ if (!hostname) return null;
210
+ return providerState.find(p => p.hostname.test(hostname)) || null;
211
+ }
212
+
213
+ function nextKey(provider) {
214
+ if (!provider || !provider.keys.length) return null;
215
+ const key = provider.keys[provider.idx % provider.keys.length];
216
+ provider.idx = (provider.idx + 1) % provider.keys.length;
217
+ return key;
218
+ }
219
+
220
+ function setAuthHeader(headers, key) {
221
+ if (!key) return headers;
222
+ const authValue = `Bearer ${key}`;
223
+
224
+ if (typeof Headers !== 'undefined' && headers instanceof Headers) {
225
+ headers.set('authorization', authValue);
226
+ return headers;
227
+ }
228
+ if (Array.isArray(headers)) {
229
+ const out = headers.filter(([k]) => String(k).toLowerCase() !== 'authorization');
230
+ out.push(['authorization', authValue]);
231
+ return out;
232
+ }
233
+ if (headers && typeof headers === 'object') {
234
+ return { ...headers, authorization: authValue };
235
+ }
236
+ return { authorization: authValue };
237
+ }
238
+
239
+ // ─── Patch globalThis.fetch ───────────────────────────────────────────────────
240
+
241
+ function patchFetch() {
242
+ if (typeof globalThis.fetch !== 'function') return;
243
+
244
+ const originalFetch = globalThis.fetch.bind(globalThis);
245
+
246
+ globalThis.fetch = async function patchedFetch(input, init = {}) {
247
+ try {
248
+ const urlLike =
249
+ typeof input === 'string' || input instanceof URL
250
+ ? input
251
+ : input && typeof input.url === 'string' ? input.url : null;
252
+
253
+ const hostname = resolveHostname(urlLike);
254
+ const provider = matchProvider(hostname);
255
+
256
+ if (provider) {
257
+ const key = nextKey(provider);
258
+ if (key) {
259
+ const headers = init.headers || (input && input.headers) || undefined;
260
+ const patchedHeaders = setAuthHeader(headers, key);
261
+ init = { ...init, headers: patchedHeaders };
262
+
263
+ if (input && typeof input === 'object' && !(input instanceof URL) && input.headers) {
264
+ try { input = new Request(input, { headers: patchedHeaders }); } catch { /* noop */ }
265
+ }
266
+ }
267
+ }
268
+ } catch (err) {
269
+ console.warn('[key-rotator] fetch patch error:', err?.message || err);
270
+ }
271
+
272
+ return originalFetch(input, init);
273
+ };
274
+ }
275
+
276
+ // ─── Patch node:http / node:https ────────────────────────────────────────────
277
+
278
+ function patchHttpModule(mod) {
279
+ const originalRequest = mod.request;
280
+
281
+ mod.request = function patchedRequest(...args) {
282
+ try {
283
+ const options = args[0];
284
+ const hostname = resolveHostname(options);
285
+ const provider = matchProvider(hostname);
286
+
287
+ if (provider) {
288
+ const key = nextKey(provider);
289
+ if (key) {
290
+ if (typeof options === 'string' || options instanceof URL) {
291
+ const u = new URL(String(options));
292
+ args[0] = {
293
+ protocol: u.protocol,
294
+ hostname: u.hostname,
295
+ port: u.port,
296
+ path: `${u.pathname}${u.search}`,
297
+ headers: { authorization: `Bearer ${key}` },
298
+ };
299
+ } else if (options && typeof options === 'object') {
300
+ args[0] = { ...options, headers: setAuthHeader(options.headers, key) };
301
+ }
302
+ }
303
+ }
304
+ } catch (err) {
305
+ console.warn('[key-rotator] http patch error:', err?.message || err);
306
+ }
307
+
308
+ return originalRequest.apply(mod, args);
309
+ };
310
+ }
311
+
312
+ // ─── Apply patches ────────────────────────────────────────────────────────────
313
+
314
+ patchFetch();
315
+ patchHttpModule(http);
316
+ patchHttpModule(https);
317
+
318
+ console.log('[key-rotator] loaded β€” all providers active');
nvidia-key-rotator.cjs DELETED
@@ -1,185 +0,0 @@
1
- 'use strict';
2
-
3
- /**
4
- * NVIDIA API key rotator for OpenClaw/HuggingClaw
5
- * ------------------------------------------------
6
- * - Supports comma-separated keys in NVIDIA_API_KEYS
7
- * - Falls back to NVIDIA_API_KEY, then LLM_API_KEY
8
- * - Rotates keys on every NVIDIA request
9
- * - Patches fetch + http/https request so most callers are covered
10
- */
11
-
12
- const http = require('node:http');
13
- const https = require('node:https');
14
-
15
- const NVIDIA_HOST_RE = /(^|\.)((integrate\.api\.nvidia\.com)|(api\.nvidia\.com))$/i;
16
-
17
- function normalizeKeys(input) {
18
- return String(input || '')
19
- .split(',')
20
- .map((s) => s.trim())
21
- .filter(Boolean);
22
- }
23
-
24
- const keys = Array.from(
25
- new Set(
26
- normalizeKeys(process.env.NVIDIA_API_KEYS)
27
- .concat(normalizeKeys(process.env.NVIDIA_API_KEY))
28
- .concat(normalizeKeys(process.env.LLM_API_KEY))
29
- )
30
- );
31
-
32
- if (!keys.length) {
33
- console.warn('[nvidia-key-rotator] No NVIDIA keys found');
34
- }
35
-
36
- let idx = 0;
37
-
38
- function nextKey() {
39
- if (!keys.length) return null;
40
- const key = keys[idx % keys.length];
41
- idx = (idx + 1) % keys.length;
42
- return key;
43
- }
44
-
45
- function isNvidiaUrl(urlLike) {
46
- try {
47
- const u = typeof urlLike === 'string'
48
- ? new URL(urlLike)
49
- : urlLike instanceof URL
50
- ? urlLike
51
- : urlLike && typeof urlLike.url === 'string'
52
- ? new URL(urlLike.url)
53
- : null;
54
-
55
- if (!u) return false;
56
- return NVIDIA_HOST_RE.test(u.hostname);
57
- } catch {
58
- return false;
59
- }
60
- }
61
-
62
- function setAuthHeader(headers, key) {
63
- if (!key) return headers;
64
-
65
- const authValue = `Bearer ${key}`;
66
-
67
- // Headers instance
68
- if (typeof Headers !== 'undefined' && headers instanceof Headers) {
69
- headers.set('authorization', authValue);
70
- return headers;
71
- }
72
-
73
- // Array of tuples
74
- if (Array.isArray(headers)) {
75
- const out = headers.filter(([k]) => String(k).toLowerCase() !== 'authorization');
76
- out.push(['authorization', authValue]);
77
- return out;
78
- }
79
-
80
- // Plain object
81
- if (headers && typeof headers === 'object') {
82
- return {
83
- ...headers,
84
- authorization: authValue,
85
- };
86
- }
87
-
88
- return { authorization: authValue };
89
- }
90
-
91
- function patchFetch() {
92
- if (typeof globalThis.fetch !== 'function') return;
93
-
94
- const originalFetch = globalThis.fetch.bind(globalThis);
95
-
96
- globalThis.fetch = async function patchedFetch(input, init = {}) {
97
- try {
98
- const urlLike =
99
- typeof input === 'string' || input instanceof URL
100
- ? input
101
- : input && typeof input.url === 'string'
102
- ? input.url
103
- : null;
104
-
105
- if (urlLike && isNvidiaUrl(urlLike)) {
106
- const key = nextKey();
107
- if (key) {
108
- const headers = init.headers || (input && input.headers) || undefined;
109
- const patchedHeaders = setAuthHeader(headers, key);
110
-
111
- if (init && typeof init === 'object') {
112
- init = { ...init, headers: patchedHeaders };
113
- } else {
114
- init = { headers: patchedHeaders };
115
- }
116
-
117
- if (input && typeof input === 'object' && !(input instanceof URL) && input.headers) {
118
- try {
119
- input = new Request(input, { headers: patchedHeaders });
120
- } catch {
121
- // ignore and let fetch handle original input
122
- }
123
- }
124
- }
125
- }
126
- } catch (err) {
127
- console.warn('[nvidia-key-rotator] fetch patch error:', err?.message || err);
128
- }
129
-
130
- return originalFetch(input, init);
131
- };
132
- }
133
-
134
- function patchHttpModule(mod) {
135
- const originalRequest = mod.request;
136
-
137
- mod.request = function patchedRequest(...args) {
138
- try {
139
- let options = args[0];
140
-
141
- const urlLike =
142
- typeof options === 'string' || options instanceof URL
143
- ? options
144
- : options && typeof options === 'object' && typeof options.href === 'string'
145
- ? options.href
146
- : null;
147
-
148
- if (urlLike && isNvidiaUrl(urlLike)) {
149
- const key = nextKey();
150
- if (key) {
151
- if (typeof options === 'string' || options instanceof URL) {
152
- const u = new URL(String(options));
153
- u.username = '';
154
- u.password = '';
155
- args[0] = {
156
- protocol: u.protocol,
157
- hostname: u.hostname,
158
- port: u.port,
159
- path: `${u.pathname}${u.search}`,
160
- headers: { authorization: `Bearer ${key}` },
161
- };
162
- } else if (options && typeof options === 'object') {
163
- const headers = setAuthHeader(options.headers, key);
164
- args[0] = {
165
- ...options,
166
- headers,
167
- };
168
- }
169
- }
170
- }
171
- } catch (err) {
172
- console.warn('[nvidia-key-rotator] http patch error:', err?.message || err);
173
- }
174
-
175
- return originalRequest.apply(mod, args);
176
- };
177
- }
178
-
179
- patchFetch();
180
- patchHttpModule(http);
181
- patchHttpModule(https);
182
-
183
- console.log(
184
- `[nvidia-key-rotator] loaded (${keys.length} key${keys.length === 1 ? '' : 's'})`
185
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
start.sh CHANGED
@@ -426,7 +426,7 @@ chmod 600 /home/node/.openclaw/openclaw.json
426
 
427
  # ── Enable Gateway Preload Fixes ──
428
  # This preload script keeps iframe embedding working on HF Spaces.
429
- export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs --require /home/node/app/nvidia-key-rotator.cjs"
430
 
431
  # ── Startup Summary ──
432
  echo ""
 
426
 
427
  # ── Enable Gateway Preload Fixes ──
428
  # This preload script keeps iframe embedding working on HF Spaces.
429
+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require /home/node/app/iframe-fix.cjs --require /home/node/app/multi-provider-key-rotator.cjs"
430
 
431
  # ── Startup Summary ──
432
  echo ""
test-rotator.mjs ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { createRequire } from 'module';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const require = createRequire(import.meta.url);
5
+ const G = s => `\x1b[32m${s}\x1b[0m`;
6
+ const R = s => `\x1b[31m${s}\x1b[0m`;
7
+ const Y = s => `\x1b[33m${s}\x1b[0m`;
8
+ const B = s => `\x1b[1m${s}\x1b[0m`;
9
+
10
+ Object.assign(process.env, {
11
+ ANTHROPIC_API_KEYS:'ant-k1,ant-k2,ant-k3', OPENAI_API_KEYS:'oai-k1,oai-k2,oai-k3',
12
+ GEMINI_API_KEYS:'gem-k1,gem-k2,gem-k3', DEEPSEEK_API_KEYS:'dsk-k1,dsk-k2,dsk-k3',
13
+ OPENROUTER_API_KEYS:'ort-k1,ort-k2,ort-k3', KILOCODE_API_KEYS:'kil-k1,kil-k2,kil-k3',
14
+ OPENCODE_API_KEYS:'ocd-k1,ocd-k2,ocd-k3', ZAI_API_KEYS:'zai-k1,zai-k2,zai-k3',
15
+ MOONSHOT_API_KEYS:'msn-k1,msn-k2,msn-k3', MINIMAX_API_KEYS:'mmx-k1,mmx-k2,mmx-k3',
16
+ XIAOMI_API_KEYS:'xmi-k1,xmi-k2,xmi-k3', VOLCANO_ENGINE_API_KEYS:'vlc-k1,vlc-k2,vlc-k3',
17
+ BYTEPLUS_API_KEYS:'btp-k1,btp-k2,btp-k3', MISTRAL_API_KEYS:'mst-k1,mst-k2,mst-k3',
18
+ XAI_API_KEYS:'xai-k1,xai-k2,xai-k3', NVIDIA_API_KEYS:'nv-k1,nv-k2,nv-k3',
19
+ GROQ_API_KEYS:'grq-k1,grq-k2,grq-k3', COHERE_API_KEYS:'coh-k1,coh-k2,coh-k3',
20
+ TOGETHER_API_KEYS:'tgt-k1,tgt-k2,tgt-k3', CEREBRAS_API_KEYS:'crb-k1,crb-k2,crb-k3',
21
+ HUGGINGFACE_HUB_TOKENS:'hf-k1,hf-k2,hf-k3',
22
+ });
23
+
24
+ // Mock fetch BEFORE loading rotator (rotator captures this as "originalFetch")
25
+ const fetchLog = [];
26
+ globalThis.fetch = async function mockFetch(input, init = {}) {
27
+ const url = typeof input === 'string' ? input : input?.url ?? '?';
28
+ const h = init?.headers ?? {};
29
+ const auth = typeof h.get === 'function' ? h.get('authorization') : (h.authorization ?? null);
30
+ fetchLog.push({ url, auth });
31
+ return new Response('{}', { status: 200 });
32
+ };
33
+
34
+ // Mock http/https BEFORE loading rotator
35
+ const http = require('http');
36
+ const https = require('https');
37
+ const httpLog = [];
38
+ for (const mod of [http, https]) {
39
+ mod.request = function mockReq(...args) {
40
+ const opts = args[0];
41
+ const hostname = typeof opts === 'string' ? new URL(opts).hostname : opts?.hostname ?? '?';
42
+ const auth = opts?.headers?.authorization ?? null;
43
+ httpLog.push({ hostname, auth });
44
+ return { on() { return this; }, end() {}, write() {} };
45
+ };
46
+ }
47
+
48
+ // Load rotator
49
+ console.log(B('\n── Loading rotator ─────────────────────────────────────\n'));
50
+ const origLog = console.log; const ll = [];
51
+ console.log = (...a) => ll.push(a.join(' '));
52
+ require('./multi-provider-key-rotator.cjs');
53
+ console.log = origLog;
54
+ ll.forEach(l => console.log(' ' + l));
55
+
56
+ const TESTS = [
57
+ ['api.anthropic.com','ant-','Anthropic'],
58
+ ['api.openai.com','oai-','OpenAI'],
59
+ ['generativelanguage.googleapis.com','gem-','Gemini'],
60
+ ['api.deepseek.com','dsk-','DeepSeek'],
61
+ ['openrouter.ai','ort-','OpenRouter'],
62
+ ['kilocode.ai','kil-','KiloCode'],
63
+ ['opencode.ai','ocd-','OpenCode'],
64
+ ['open.bigmodel.cn','zai-','Z.ai / GLM'],
65
+ ['api.moonshot.cn','msn-','Moonshot'],
66
+ ['api.minimax.chat','mmx-','MiniMax'],
67
+ ['api.xiaomi.com','xmi-','Xiaomi'],
68
+ ['ark.cn-beijing.volces.com','vlc-','Volcengine'],
69
+ ['maas-api.ml-platform-cn-beijing.byteplus.com','btp-','BytePlus'],
70
+ ['api.mistral.ai','mst-','Mistral'],
71
+ ['api.x.ai','xai-','xAI / Grok'],
72
+ ['integrate.api.nvidia.com','nv-','NVIDIA (integrate)'],
73
+ ['api.nvidia.com','nv-','NVIDIA (api)'],
74
+ ['api.groq.com','grq-','Groq'],
75
+ ['api.cohere.com','coh-','Cohere'],
76
+ ['api.cohere.ai','coh-','Cohere (alt)'],
77
+ ['api.together.xyz','tgt-','Together'],
78
+ ['api.cerebras.ai','crb-','Cerebras'],
79
+ ['api-inference.huggingface.co','hf-','HuggingFace'],
80
+ ];
81
+
82
+ console.log(B('\n── fetch() β€” 3 requests each (rotation check) ─────────\n'));
83
+ let passed = 0, failed = 0;
84
+
85
+ for (const [hostname, prefix, label] of TESTS) {
86
+ fetchLog.length = 0;
87
+ for (let i = 0; i < 3; i++) await globalThis.fetch(`https://${hostname}/v1/messages`, {});
88
+ const keys = fetchLog.map(f => f.auth?.replace('Bearer ', '') ?? null);
89
+ const ok = keys.every(k => k?.startsWith(prefix)) && keys[0] !== keys[1] && keys[1] !== keys[2];
90
+ if (ok) { passed++; console.log(` ${G('βœ“')} ${label.padEnd(24)} ${keys.join(' β†’ ')}`); }
91
+ else { failed++; console.log(` ${R('βœ—')} ${label.padEnd(24)} got: ${JSON.stringify(keys)}`); }
92
+ }
93
+
94
+ // Unknown host β€” no header
95
+ fetchLog.length = 0;
96
+ await globalThis.fetch('https://example.com/test', {});
97
+ const unk = fetchLog[0]?.auth ?? null;
98
+ if (!unk) { passed++; console.log(`\n ${G('βœ“')} ${'Unknown host'.padEnd(24)} correctly skipped`); }
99
+ else { failed++; console.log(`\n ${R('βœ—')} ${'Unknown host'.padEnd(24)} wrongly injected: ${unk}`); }
100
+
101
+ console.log(B('\n── http.request() β€” direct options object ──────────────\n'));
102
+ const HTTP_TESTS = [
103
+ ['api.anthropic.com','ant-','Anthropic'],
104
+ ['api.openai.com','oai-','OpenAI'],
105
+ ['api.mistral.ai','mst-','Mistral'],
106
+ ['integrate.api.nvidia.com','nv-','NVIDIA'],
107
+ ['api.groq.com','grq-','Groq'],
108
+ ['api.x.ai','xai-','xAI'],
109
+ ];
110
+ for (const [hostname, prefix, label] of HTTP_TESTS) {
111
+ httpLog.length = 0;
112
+ http.request({ hostname, path: '/v1', headers: {} });
113
+ http.request({ hostname, path: '/v1', headers: {} });
114
+ const keys = httpLog.map(e => e.auth?.replace('Bearer ', '') ?? null);
115
+ const ok = keys.every(k => k?.startsWith(prefix)) && keys[0] !== keys[1];
116
+ if (ok) { passed++; console.log(` ${G('βœ“')} ${label.padEnd(24)} ${keys.join(' β†’ ')}`); }
117
+ else { failed++; console.log(` ${R('βœ—')} ${label.padEnd(24)} got: ${JSON.stringify(keys)}`); }
118
+ }
119
+
120
+ console.log(B('\n────────────────────────────────────────────────────────'));
121
+ const total = passed + failed;
122
+ console.log(` ${G(passed+'/'+total+' passed')} ${failed ? R(failed+' failed') : ''}`);
123
+ if (failed === 0) console.log(` ${G('βœ“ Rotator sahi kaam kar raha hai β€” deploy karo!')}\n`);
124
+ else console.log(` ${R('βœ— Issues hain β€” upar dekho.')}\n`);