Files
openclaw/src/agents/live-auth-keys.ts
2026-04-08 19:40:57 +01:00

227 lines
5.9 KiB
TypeScript

import { getProviderEnvVars } from "../secrets/provider-env-vars.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { normalizeProviderId } from "./model-selection.js";
const KEY_SPLIT_RE = /[\s,;]+/g;
const GOOGLE_LIVE_SINGLE_KEY = "OPENCLAW_LIVE_GEMINI_KEY";
const PROVIDER_PREFIX_OVERRIDES: Record<string, string> = {
google: "GEMINI",
"google-vertex": "GEMINI",
};
type ProviderApiKeyConfig = {
liveSingle?: string;
listVar?: string;
primaryVar?: string;
prefixedVar?: string;
fallbackVars: string[];
};
type CollectProviderApiKeysOptions = {
env?: NodeJS.ProcessEnv;
providerEnvVars?: readonly string[];
};
const PROVIDER_API_KEY_CONFIG: Record<string, Omit<ProviderApiKeyConfig, "fallbackVars">> = {
anthropic: {
liveSingle: "OPENCLAW_LIVE_ANTHROPIC_KEY",
listVar: "OPENCLAW_LIVE_ANTHROPIC_KEYS",
primaryVar: "ANTHROPIC_API_KEY",
prefixedVar: "ANTHROPIC_API_KEY_",
},
google: {
liveSingle: GOOGLE_LIVE_SINGLE_KEY,
listVar: "GEMINI_API_KEYS",
primaryVar: "GEMINI_API_KEY",
prefixedVar: "GEMINI_API_KEY_",
},
"google-vertex": {
liveSingle: GOOGLE_LIVE_SINGLE_KEY,
listVar: "GEMINI_API_KEYS",
primaryVar: "GEMINI_API_KEY",
prefixedVar: "GEMINI_API_KEY_",
},
openai: {
liveSingle: "OPENCLAW_LIVE_OPENAI_KEY",
listVar: "OPENAI_API_KEYS",
primaryVar: "OPENAI_API_KEY",
prefixedVar: "OPENAI_API_KEY_",
},
};
function parseKeyList(raw?: string | null): string[] {
if (!raw) {
return [];
}
return raw
.split(KEY_SPLIT_RE)
.map((value) => value.trim())
.filter(Boolean);
}
function collectEnvPrefixedKeys(prefix: string, env: NodeJS.ProcessEnv): string[] {
const keys: string[] = [];
for (const [name, value] of Object.entries(env)) {
if (!name.startsWith(prefix)) {
continue;
}
const trimmed = normalizeOptionalString(value);
if (!trimmed) {
continue;
}
keys.push(trimmed);
}
return keys;
}
function resolveProviderApiKeyConfig(provider: string): ProviderApiKeyConfig {
const normalized = normalizeProviderId(provider);
const custom = PROVIDER_API_KEY_CONFIG[normalized];
const base = PROVIDER_PREFIX_OVERRIDES[normalized] ?? normalized.toUpperCase().replace(/-/g, "_");
const liveSingle = custom?.liveSingle ?? `OPENCLAW_LIVE_${base}_KEY`;
const listVar = custom?.listVar ?? `${base}_API_KEYS`;
const primaryVar = custom?.primaryVar ?? `${base}_API_KEY`;
const prefixedVar = custom?.prefixedVar ?? `${base}_API_KEY_`;
if (normalized === "google" || normalized === "google-vertex") {
return {
liveSingle,
listVar,
primaryVar,
prefixedVar,
fallbackVars: ["GOOGLE_API_KEY"],
};
}
return {
liveSingle,
listVar,
primaryVar,
prefixedVar,
fallbackVars: [],
};
}
export function collectProviderApiKeys(
provider: string,
options: CollectProviderApiKeysOptions = {},
): string[] {
const env = options.env ?? process.env;
const normalizedProvider = normalizeProviderId(provider);
const config = resolveProviderApiKeyConfig(normalizedProvider);
const forcedSingle = config.liveSingle
? normalizeOptionalString(env[config.liveSingle])
: undefined;
if (forcedSingle) {
return [forcedSingle];
}
const fromList = parseKeyList(config.listVar ? env[config.listVar] : undefined);
const primary = config.primaryVar ? normalizeOptionalString(env[config.primaryVar]) : undefined;
const fromPrefixed = config.prefixedVar ? collectEnvPrefixedKeys(config.prefixedVar, env) : [];
const fallback = config.fallbackVars
.map((envVar) => normalizeOptionalString(env[envVar]))
.filter(Boolean) as string[];
const manifestEnvVars = options.providerEnvVars ?? getProviderEnvVars(normalizedProvider);
const manifestFallback = manifestEnvVars
.map((envVar) => normalizeOptionalString(env[envVar]))
.filter(Boolean) as string[];
const seen = new Set<string>();
const add = (value?: string) => {
if (!value) {
return;
}
if (seen.has(value)) {
return;
}
seen.add(value);
};
for (const value of fromList) {
add(value);
}
add(primary);
for (const value of fromPrefixed) {
add(value);
}
for (const value of fallback) {
add(value);
}
for (const value of manifestFallback) {
add(value);
}
return Array.from(seen);
}
export function collectAnthropicApiKeys(): string[] {
return collectProviderApiKeys("anthropic");
}
export function collectGeminiApiKeys(): string[] {
return collectProviderApiKeys("google");
}
export function isApiKeyRateLimitError(message: string): boolean {
const lower = normalizeLowercaseStringOrEmpty(message);
if (lower.includes("rate_limit")) {
return true;
}
if (lower.includes("rate limit")) {
return true;
}
if (lower.includes("429")) {
return true;
}
if (lower.includes("quota exceeded") || lower.includes("quota_exceeded")) {
return true;
}
if (lower.includes("resource exhausted") || lower.includes("resource_exhausted")) {
return true;
}
if (lower.includes("too many requests")) {
return true;
}
return false;
}
export function isAnthropicRateLimitError(message: string): boolean {
return isApiKeyRateLimitError(message);
}
export function isAnthropicBillingError(message: string): boolean {
const lower = normalizeLowercaseStringOrEmpty(message);
if (lower.includes("credit balance")) {
return true;
}
if (lower.includes("insufficient credit")) {
return true;
}
if (lower.includes("insufficient credits")) {
return true;
}
if (lower.includes("payment required")) {
return true;
}
if (lower.includes("billing") && lower.includes("disabled")) {
return true;
}
if (
/["']?(?:status|code)["']?\s*[:=]\s*402\b|\bhttp\s*402\b|\berror(?:\s+code)?\s*[:=]?\s*402\b|\b(?:got|returned|received)\s+(?:a\s+)?402\b|^\s*402\spayment/i.test(
lower,
)
) {
return true;
}
return false;
}