call-ai
Version:
Lightweight library for making AI API calls with streaming support
192 lines • 8.13 kB
JavaScript
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