Spaces:
Building
Building
| ; | |
| /** | |
| * Multi-provider API key rotator for OpenClaw/HuggingClaw | |
| * -------------------------------------------------------- | |
| * Works exactly like nvidia-key-rotator.cjs but covers every | |
| * provider that HuggingClaw supports. | |
| * | |
| * For each provider you can supply a comma-separated pool: | |
| * ANTHROPIC_API_KEYS=key1,key2,key3 | |
| * Falls back to the singular env var, then to LLM_API_KEY. | |
| * | |
| * Keys are rotated round-robin per provider independently. | |
| * | |
| * Patches globalThis.fetch + node:http + node:https so that | |
| * virtually every caller is covered without code changes. | |
| */ | |
| const http = require('node:http'); | |
| const https = require('node:https'); | |
| // βββ Provider definitions ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // | |
| // hostname β regex tested against the request hostname (case-insensitive) | |
| // envPlural β env var that holds a comma-separated key pool (preferred) | |
| // envSingular β env var that holds a single key (fallback) | |
| // | |
| // LLM_API_KEY is the final fallback for every provider. | |
| // | |
| const PROVIDERS = [ | |
| { | |
| name: 'anthropic', | |
| hostname: /(?:^|\.)api\.anthropic\.com$/i, | |
| envPlural: 'ANTHROPIC_API_KEYS', | |
| envSingular:'ANTHROPIC_API_KEY', | |
| }, | |
| { | |
| name: 'openai', | |
| hostname: /(?:^|\.)api\.openai\.com$/i, | |
| envPlural: 'OPENAI_API_KEYS', | |
| envSingular:'OPENAI_API_KEY', | |
| }, | |
| { | |
| name: 'gemini', | |
| // Gemini uses generativelanguage API; also covers aiplatform | |
| hostname: /(?:^|\.)(?:generativelanguage\.googleapis\.com|aiplatform\.googleapis\.com)$/i, | |
| envPlural: 'GEMINI_API_KEYS', | |
| envSingular:'GEMINI_API_KEY', | |
| }, | |
| { | |
| name: 'deepseek', | |
| hostname: /(?:^|\.)api\.deepseek\.com$/i, | |
| envPlural: 'DEEPSEEK_API_KEYS', | |
| envSingular:'DEEPSEEK_API_KEY', | |
| }, | |
| { | |
| name: 'openrouter', | |
| hostname: /(?:^|\.)openrouter\.ai$/i, | |
| envPlural: 'OPENROUTER_API_KEYS', | |
| envSingular:'OPENROUTER_API_KEY', | |
| }, | |
| { | |
| name: 'kilocode', | |
| hostname: /(?:^|\.)kilocode\.ai$/i, | |
| envPlural: 'KILOCODE_API_KEYS', | |
| envSingular:'KILOCODE_API_KEY', | |
| }, | |
| { | |
| name: 'opencode', | |
| hostname: /(?:^|\.)opencode\.ai$/i, | |
| envPlural: 'OPENCODE_API_KEYS', | |
| envSingular:'OPENCODE_API_KEY', | |
| }, | |
| { | |
| name: 'zai', | |
| // Z.ai / GLM β both domains normalised to "zai" in OpenClaw | |
| hostname: /(?:^|\.)(?:z\.ai|open\.bigmodel\.cn)$/i, | |
| envPlural: 'ZAI_API_KEYS', | |
| envSingular:'ZAI_API_KEY', | |
| }, | |
| { | |
| name: 'moonshot', | |
| hostname: /(?:^|\.)api\.moonshot\.cn$/i, | |
| envPlural: 'MOONSHOT_API_KEYS', | |
| envSingular:'MOONSHOT_API_KEY', | |
| }, | |
| { | |
| name: 'minimax', | |
| hostname: /(?:^|\.)api\.minimax\.chat$/i, | |
| envPlural: 'MINIMAX_API_KEYS', | |
| envSingular:'MINIMAX_API_KEY', | |
| }, | |
| { | |
| name: 'xiaomi', | |
| // MiMo β update hostname if Xiaomi publishes an official domain | |
| hostname: /(?:^|\.)api\.xiaomi\.com$/i, | |
| envPlural: 'XIAOMI_API_KEYS', | |
| envSingular:'XIAOMI_API_KEY', | |
| }, | |
| { | |
| name: 'volcengine', | |
| // Volcengine / Doubao | |
| hostname: /(?:^|\.)(?:ark\.cn-beijing\.volces\.com|volcengineapi\.com)$/i, | |
| envPlural: 'VOLCANO_ENGINE_API_KEYS', | |
| envSingular:'VOLCANO_ENGINE_API_KEY', | |
| }, | |
| { | |
| name: 'byteplus', | |
| hostname: /(?:^|\.)maas-api\.ml-platform-cn-beijing\.byteplus\.com$/i, | |
| envPlural: 'BYTEPLUS_API_KEYS', | |
| envSingular:'BYTEPLUS_API_KEY', | |
| }, | |
| { | |
| name: 'mistral', | |
| hostname: /(?:^|\.)api\.mistral\.ai$/i, | |
| envPlural: 'MISTRAL_API_KEYS', | |
| envSingular:'MISTRAL_API_KEY', | |
| }, | |
| { | |
| name: 'xai', | |
| hostname: /(?:^|\.)api\.x\.ai$/i, | |
| envPlural: 'XAI_API_KEYS', | |
| envSingular:'XAI_API_KEY', | |
| }, | |
| { | |
| name: 'nvidia', | |
| hostname: /(?:^|\.)(?:integrate\.api\.nvidia\.com|api\.nvidia\.com)$/i, | |
| envPlural: 'NVIDIA_API_KEYS', | |
| envSingular:'NVIDIA_API_KEY', | |
| }, | |
| { | |
| name: 'groq', | |
| hostname: /(?:^|\.)api\.groq\.com$/i, | |
| envPlural: 'GROQ_API_KEYS', | |
| envSingular:'GROQ_API_KEY', | |
| }, | |
| { | |
| name: 'cohere', | |
| hostname: /(?:^|\.)api\.cohere\.(?:ai|com)$/i, | |
| envPlural: 'COHERE_API_KEYS', | |
| envSingular:'COHERE_API_KEY', | |
| }, | |
| { | |
| name: 'together', | |
| hostname: /(?:^|\.)api\.together\.(?:xyz|ai)$/i, | |
| envPlural: 'TOGETHER_API_KEYS', | |
| envSingular:'TOGETHER_API_KEY', | |
| }, | |
| { | |
| name: 'cerebras', | |
| hostname: /(?:^|\.)api\.cerebras\.ai$/i, | |
| envPlural: 'CEREBRAS_API_KEYS', | |
| envSingular:'CEREBRAS_API_KEY', | |
| }, | |
| { | |
| name: 'huggingface', | |
| hostname: /(?:^|\.)(?:api-inference\.huggingface\.co|router\.huggingface\.co|huggingface\.co)$/i, | |
| envPlural: 'HUGGINGFACE_HUB_TOKENS', // plural variant | |
| envSingular:'HUGGINGFACE_HUB_TOKEN', | |
| }, | |
| ]; | |
| // βββ Key loading βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function normalizeKeys(...inputs) { | |
| const seen = new Set(); | |
| const out = []; | |
| for (const input of inputs) { | |
| for (const k of String(input || '').split(',').map(s => s.trim()).filter(Boolean)) { | |
| if (!seen.has(k)) { seen.add(k); out.push(k); } | |
| } | |
| } | |
| return out; | |
| } | |
| // Build per-provider key pools + rotation indices | |
| const providerState = PROVIDERS.map(p => { | |
| const dedicatedKeys = normalizeKeys( | |
| process.env[p.envPlural] || '', | |
| process.env[p.envSingular] || '', | |
| ); | |
| const hasDedicated = dedicatedKeys.length > 0; | |
| const keys = hasDedicated | |
| ? dedicatedKeys | |
| : normalizeKeys(process.env.LLM_API_KEY || ''); | |
| if (hasDedicated) { | |
| console.log(`[key-rotator] ${p.name}: ${keys.length} key${keys.length === 1 ? '' : 's'}`); | |
| } else if (!keys.length) { | |
| console.warn(`[key-rotator] No keys for provider "${p.name}"`); | |
| } | |
| return { ...p, keys, idx: 0 }; | |
| }); | |
| // Summarise providers that fall back to LLM_API_KEY | |
| const fallbackCount = providerState.filter(p => { | |
| const dedicated = normalizeKeys( | |
| process.env[p.envPlural] || '', | |
| process.env[p.envSingular] || '', | |
| ); | |
| return dedicated.length === 0 && p.keys.length > 0; | |
| }).length; | |
| if (fallbackCount > 0) { | |
| console.log(`[key-rotator] ${fallbackCount} provider(s) using LLM_API_KEY fallback`); | |
| } | |
| // βββ Runtime helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function resolveHostname(urlLike) { | |
| try { | |
| const u = | |
| typeof urlLike === 'string' ? new URL(urlLike) | |
| : urlLike instanceof URL ? urlLike | |
| : urlLike && typeof urlLike.url === 'string' ? new URL(urlLike.url) | |
| : urlLike && typeof urlLike.href === 'string' ? new URL(urlLike.href) | |
| : urlLike && typeof urlLike.hostname === 'string' ? urlLike | |
| : null; | |
| return u ? u.hostname : null; | |
| } catch { | |
| return null; | |
| } | |
| } | |
| function matchProvider(hostname) { | |
| if (!hostname) return null; | |
| return providerState.find(p => p.hostname.test(hostname)) || null; | |
| } | |
| function nextKey(provider) { | |
| if (!provider || !provider.keys.length) return null; | |
| const key = provider.keys[provider.idx % provider.keys.length]; | |
| provider.idx = (provider.idx + 1) % provider.keys.length; | |
| return key; | |
| } | |
| function setAuthHeader(headers, key) { | |
| if (!key) return headers; | |
| const authValue = `Bearer ${key}`; | |
| if (typeof Headers !== 'undefined' && headers instanceof Headers) { | |
| headers.set('authorization', authValue); | |
| return headers; | |
| } | |
| if (Array.isArray(headers)) { | |
| const out = headers.filter(([k]) => String(k).toLowerCase() !== 'authorization'); | |
| out.push(['authorization', authValue]); | |
| return out; | |
| } | |
| if (headers && typeof headers === 'object') { | |
| return { ...headers, authorization: authValue }; | |
| } | |
| return { authorization: authValue }; | |
| } | |
| // βββ Patch globalThis.fetch βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function patchFetch() { | |
| if (typeof globalThis.fetch !== 'function') return; | |
| const originalFetch = globalThis.fetch.bind(globalThis); | |
| globalThis.fetch = async function patchedFetch(input, init = {}) { | |
| try { | |
| const urlLike = | |
| typeof input === 'string' || input instanceof URL | |
| ? input | |
| : input && typeof input.url === 'string' ? input.url : null; | |
| const hostname = resolveHostname(urlLike); | |
| const provider = matchProvider(hostname); | |
| if (provider) { | |
| const key = nextKey(provider); | |
| if (key) { | |
| const headers = init.headers || (input && input.headers) || undefined; | |
| const patchedHeaders = setAuthHeader(headers, key); | |
| init = { ...init, headers: patchedHeaders }; | |
| if (input && typeof input === 'object' && !(input instanceof URL) && input.headers) { | |
| try { input = new Request(input, { headers: patchedHeaders }); } catch { /* noop */ } | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('[key-rotator] fetch patch error:', err?.message || err); | |
| } | |
| return originalFetch(input, init); | |
| }; | |
| } | |
| // βββ Patch node:http / node:https ββββββββββββββββββββββββββββββββββββββββββββ | |
| function patchHttpModule(mod) { | |
| const originalRequest = mod.request; | |
| mod.request = function patchedRequest(...args) { | |
| try { | |
| const options = args[0]; | |
| const hostname = resolveHostname(options); | |
| const provider = matchProvider(hostname); | |
| if (provider) { | |
| const key = nextKey(provider); | |
| if (key) { | |
| if (typeof options === 'string' || options instanceof URL) { | |
| const u = new URL(String(options)); | |
| args[0] = { | |
| protocol: u.protocol, | |
| hostname: u.hostname, | |
| port: u.port, | |
| path: `${u.pathname}${u.search}`, | |
| headers: { authorization: `Bearer ${key}` }, | |
| }; | |
| } else if (options && typeof options === 'object') { | |
| args[0] = { ...options, headers: setAuthHeader(options.headers, key) }; | |
| } | |
| } | |
| } | |
| } catch (err) { | |
| console.warn('[key-rotator] http patch error:', err?.message || err); | |
| } | |
| return originalRequest.apply(mod, args); | |
| }; | |
| } | |
| // βββ Apply patches ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| patchFetch(); | |
| patchHttpModule(http); | |
| patchHttpModule(https); | |
| console.log('[key-rotator] loaded β all providers active'); | |