UNPKG

call-ai

Version:

Lightweight library for making AI API calls with streaming support

192 lines 8.13 kB
import { callAiFetch, entriesHeaders } from "./utils.js"; import { callAiEnv } from "./env.js"; const _keyStore = { current: undefined, refreshEndpoint: "https://vibecode.garden", refreshToken: "use-vibes", isRefreshing: false, lastRefreshAttempt: 0, metadata: {}, }; export function keyStore() { return _keyStore; } let globalDebug = false; function initKeyStore(env = callAiEnv) { const store = keyStore(); store.current = typeof env.CALLAI_API_KEY === "string" ? env.CALLAI_API_KEY : undefined; store.refreshEndpoint = typeof env.CALLAI_REFRESH_ENDPOINT === "string" ? env.CALLAI_REFRESH_ENDPOINT : "https://vibecode.garden"; store.refreshToken = typeof env.CALL_AI_REFRESH_TOKEN === "string" ? env.CALL_AI_REFRESH_TOKEN : "use-vibes"; globalDebug = !!env.CALLAI_DEBUG; } function isNewKeyError(ierror, debug = false) { const error = ierror; let status = error?.status || error?.statusCode || error?.response?.status || 450; const errorMessage = String(error || "").toLowerCase(); if (!status && errorMessage.includes("status:")) { const statusMatch = errorMessage.match(/status:\\s*(\\d+)/i); if (statusMatch && statusMatch[1]) { status = parseInt(statusMatch[1], 10); } } const is4xx = status >= 400 && status < 500; const isAuthError = status === 401 || status === 403 || errorMessage.includes("unauthorized") || errorMessage.includes("forbidden") || errorMessage.includes("authentication") || errorMessage.includes("api key") || errorMessage.includes("apikey") || errorMessage.includes("auth"); const isInvalidKeyError = errorMessage.includes("invalid api key") || errorMessage.includes("invalid key") || errorMessage.includes("incorrect api key") || errorMessage.includes("incorrect key") || errorMessage.includes("authentication failed") || errorMessage.includes("not authorized"); const isOpenAIKeyError = errorMessage.includes("openai") && (errorMessage.includes("api key") || errorMessage.includes("authentication")); const isRateLimitError = status === 429 || errorMessage.includes("rate limit") || errorMessage.includes("too many requests") || errorMessage.includes("quota") || errorMessage.includes("exceed"); const isBillingError = errorMessage.includes("billing") || errorMessage.includes("payment") || errorMessage.includes("subscription") || errorMessage.includes("account"); const needsNewKey = is4xx && (isAuthError || isInvalidKeyError || isOpenAIKeyError || isRateLimitError || isBillingError); if (debug && needsNewKey) { console.log(`[callAi:key-refresh] Detected error requiring key refresh: ${errorMessage}`); } return needsNewKey; } async function refreshApiKey(options, currentKey, endpoint, refreshToken, debug = globalDebug) { if (!endpoint) { throw new Error("No API key refresh endpoint specified"); } if (!refreshToken) { throw new Error("No API key refresh token specified"); } if (keyStore().isRefreshing) { if (debug) { console.log("API key refresh already in progress, waiting..."); } return new Promise((resolve) => { const checkInterval = setInterval(() => { if (!keyStore().isRefreshing && keyStore().current) { clearInterval(checkInterval); const ck = keyStore().current; if (!ck) { throw new Error("API key refresh failed"); } resolve({ apiKey: ck, topup: false }); } }, 100); }); } const now = Date.now(); const timeSinceLastRefresh = now - keyStore().lastRefreshAttempt; const minRefreshInterval = 2000; if (timeSinceLastRefresh < minRefreshInterval) { if (debug) { console.log(`Rate limiting key refresh, last attempt was ${timeSinceLastRefresh}ms ago`); } await new Promise((resolve) => setTimeout(resolve, minRefreshInterval - timeSinceLastRefresh)); } keyStore().isRefreshing = true; keyStore().lastRefreshAttempt = Date.now(); const apiPath = "/api/keys"; const baseUrl = endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint; const url = `${baseUrl}${apiPath}`; if (debug) { console.log(`Refreshing API key from: ${url}`); } try { const requestPayload = { key: currentKey, hash: currentKey ? getHashFromKey(currentKey) : null, name: "call-ai-client", }; if (debug) { console.log(`[callAi:key-refresh] Request URL: ${url}`); console.log(`[callAi:key-refresh] Request headers:`, { "Content-Type": "application/json", Authorization: `Bearer ${refreshToken}`, }); console.log(`[callAi:key-refresh] Request payload:`, requestPayload); } const response = await callAiFetch(options)(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${refreshToken}`, }, body: JSON.stringify(requestPayload), }); if (debug) { console.log(`[callAi:key-refresh] Response status: ${response.status} ${response.statusText}`); console.log(`[callAi:key-refresh] Response headers:`, Object.fromEntries([...entriesHeaders(response.headers)])); } if (!response.ok) { const errorText = await response.text(); if (debug) { console.log(`[callAi:key-refresh] Error response body: ${errorText}`); } throw new Error(`API key refresh failed: ${response.status} ${response.statusText}${errorText ? ` - ${errorText}` : ""}`); } const data = await response.json(); if (debug) { console.log(`[callAi:key-refresh] Full response structure:`, JSON.stringify(data, null, 2)); } let newKey; if (data.key && typeof data.key === "object" && data.key.key) { newKey = data.key.key; } // Check for old format where data.key is the string key directly else if (data.key && typeof data.key === "string") { newKey = data.key; } // Handle error case else { throw new Error("Invalid response from key refresh endpoint: missing or malformed key"); } if (debug) { console.log(`API key refreshed successfully: ${newKey.substring(0, 10)}...`); } if (data.metadata || (data.key && typeof data.key === "object" && data.key.metadata)) { const metadata = data.metadata || data.key.metadata; storeKeyMetadata(metadata); } keyStore().current = newKey; const hashValue = data.hash || (data.key && typeof data.key === "object" && data.key.hash); const isTopup = currentKey && hashValue && hashValue === getHashFromKey(currentKey); keyStore().isRefreshing = false; return { apiKey: newKey, // Return the string key, not the object topup: isTopup, }; } catch (error) { keyStore().isRefreshing = false; throw error; } } function getHashFromKey(key) { if (!key) return null; const metaKey = Object.keys(keyStore().metadata).find((k) => k === key); return metaKey ? keyStore().metadata[metaKey].hash || null : null; } function storeKeyMetadata(data) { if (!data || !data.key) return; keyStore().metadata[data.key] = { hash: data.hash, created: data.created || Date.now(), expires: data.expires, remaining: data.remaining, limit: data.limit, }; } export { globalDebug, initKeyStore, isNewKeyError, refreshApiKey, getHashFromKey, storeKeyMetadata }; //# sourceMappingURL=key-management.js.map