import path from "node:path"; import { type Api, getEnvApiKey, type Model } from "@mariozechner/pi-ai"; import type { ClawdbotConfig } from "../config/config.js"; import type { ModelProviderAuthMode, ModelProviderConfig } from "../config/types.js"; import { getShellEnvAppliedKeys } from "../infra/shell-env.js"; import { formatCliCommand } from "../cli/command-format.js"; import { type AuthProfileStore, ensureAuthProfileStore, listProfilesForProvider, resolveApiKeyForProfile, resolveAuthProfileOrder, resolveAuthStorePathForDisplay, } from "./auth-profiles.js"; import { normalizeProviderId } from "./model-selection.js"; export { ensureAuthProfileStore, resolveAuthProfileOrder } from "./auth-profiles.js"; const AWS_BEARER_ENV = "AWS_BEARER_TOKEN_BEDROCK"; const AWS_ACCESS_KEY_ENV = "AWS_ACCESS_KEY_ID"; const AWS_SECRET_KEY_ENV = "AWS_SECRET_ACCESS_KEY"; const AWS_PROFILE_ENV = "AWS_PROFILE"; function resolveProviderConfig( cfg: ClawdbotConfig | undefined, provider: string, ): ModelProviderConfig | undefined { const providers = cfg?.models?.providers ?? {}; const direct = providers[provider] as ModelProviderConfig | undefined; if (direct) return direct; const normalized = normalizeProviderId(provider); if (normalized === provider) { const matched = Object.entries(providers).find( ([key]) => normalizeProviderId(key) === normalized, ); return matched?.[1] as ModelProviderConfig | undefined; } return ( (providers[normalized] as ModelProviderConfig | undefined) ?? (Object.entries(providers).find(([key]) => normalizeProviderId(key) === normalized)?.[1] as | ModelProviderConfig | undefined) ); } export function getCustomProviderApiKey( cfg: ClawdbotConfig | undefined, provider: string, ): string | undefined { const entry = resolveProviderConfig(cfg, provider); const key = entry?.apiKey?.trim(); return key || undefined; } function resolveProviderAuthOverride( cfg: ClawdbotConfig | undefined, provider: string, ): ModelProviderAuthMode | undefined { const entry = resolveProviderConfig(cfg, provider); const auth = entry?.auth; if (auth === "api-key" || auth === "aws-sdk" || auth === "oauth" || auth === "token") { return auth; } return undefined; } function resolveEnvSourceLabel(params: { applied: Set; envVars: string[]; label: string; }): string { const shellApplied = params.envVars.some((envVar) => params.applied.has(envVar)); const prefix = shellApplied ? "shell env: " : "env: "; return `${prefix}${params.label}`; } export function resolveAwsSdkEnvVarName(): string | undefined { if (process.env[AWS_BEARER_ENV]?.trim()) return AWS_BEARER_ENV; if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) { return AWS_ACCESS_KEY_ENV; } if (process.env[AWS_PROFILE_ENV]?.trim()) return AWS_PROFILE_ENV; return undefined; } function resolveAwsSdkAuthInfo(): { mode: "aws-sdk"; source: string } { const applied = new Set(getShellEnvAppliedKeys()); if (process.env[AWS_BEARER_ENV]?.trim()) { return { mode: "aws-sdk", source: resolveEnvSourceLabel({ applied, envVars: [AWS_BEARER_ENV], label: AWS_BEARER_ENV, }), }; } if (process.env[AWS_ACCESS_KEY_ENV]?.trim() && process.env[AWS_SECRET_KEY_ENV]?.trim()) { return { mode: "aws-sdk", source: resolveEnvSourceLabel({ applied, envVars: [AWS_ACCESS_KEY_ENV, AWS_SECRET_KEY_ENV], label: `${AWS_ACCESS_KEY_ENV} + ${AWS_SECRET_KEY_ENV}`, }), }; } if (process.env[AWS_PROFILE_ENV]?.trim()) { return { mode: "aws-sdk", source: resolveEnvSourceLabel({ applied, envVars: [AWS_PROFILE_ENV], label: AWS_PROFILE_ENV, }), }; } return { mode: "aws-sdk", source: "aws-sdk default chain" }; } export type ResolvedProviderAuth = { apiKey?: string; profileId?: string; source: string; mode: "api-key" | "oauth" | "token" | "aws-sdk"; }; export async function resolveApiKeyForProvider(params: { provider: string; cfg?: ClawdbotConfig; profileId?: string; preferredProfile?: string; store?: AuthProfileStore; agentDir?: string; }): Promise { const { provider, cfg, profileId, preferredProfile } = params; const store = params.store ?? ensureAuthProfileStore(params.agentDir); if (profileId) { const resolved = await resolveApiKeyForProfile({ cfg, store, profileId, agentDir: params.agentDir, }); if (!resolved) { throw new Error(`No credentials found for profile "${profileId}".`); } const mode = store.profiles[profileId]?.type; return { apiKey: resolved.apiKey, profileId, source: `profile:${profileId}`, mode: mode === "oauth" ? "oauth" : mode === "token" ? "token" : "api-key", }; } const authOverride = resolveProviderAuthOverride(cfg, provider); if (authOverride === "aws-sdk") { return resolveAwsSdkAuthInfo(); } const order = resolveAuthProfileOrder({ cfg, store, provider, preferredProfile, }); for (const candidate of order) { try { const resolved = await resolveApiKeyForProfile({ cfg, store, profileId: candidate, agentDir: params.agentDir, }); if (resolved) { const mode = store.profiles[candidate]?.type; return { apiKey: resolved.apiKey, profileId: candidate, source: `profile:${candidate}`, mode: mode === "oauth" ? "oauth" : mode === "token" ? "token" : "api-key", }; } } catch {} } const envResolved = resolveEnvApiKey(provider); if (envResolved) { return { apiKey: envResolved.apiKey, source: envResolved.source, mode: envResolved.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key", }; } const customKey = getCustomProviderApiKey(cfg, provider); if (customKey) { return { apiKey: customKey, source: "models.json", mode: "api-key" }; } const normalized = normalizeProviderId(provider); if (authOverride === undefined && normalized === "amazon-bedrock") { return resolveAwsSdkAuthInfo(); } if (provider === "openai") { const hasCodex = listProfilesForProvider(store, "openai-codex").length > 0; if (hasCodex) { throw new Error( 'No API key found for provider "openai". You are authenticated with OpenAI Codex OAuth. Use openai-codex/gpt-5.2 (ChatGPT OAuth) or set OPENAI_API_KEY for openai/gpt-5.2.', ); } } const authStorePath = resolveAuthStorePathForDisplay(params.agentDir); const resolvedAgentDir = path.dirname(authStorePath); throw new Error( [ `No API key found for provider "${provider}".`, `Auth store: ${authStorePath} (agentDir: ${resolvedAgentDir}).`, `Configure auth for this agent (${formatCliCommand("clawdbot agents add ")}) or copy auth-profiles.json from the main agentDir.`, ].join(" "), ); } export type EnvApiKeyResult = { apiKey: string; source: string }; export type ModelAuthMode = "api-key" | "oauth" | "token" | "mixed" | "aws-sdk" | "unknown"; export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { const normalized = normalizeProviderId(provider); const applied = new Set(getShellEnvAppliedKeys()); const pick = (envVar: string): EnvApiKeyResult | null => { const value = process.env[envVar]?.trim(); if (!value) return null; const source = applied.has(envVar) ? `shell env: ${envVar}` : `env: ${envVar}`; return { apiKey: value, source }; }; if (normalized === "github-copilot") { return pick("COPILOT_GITHUB_TOKEN") ?? pick("GH_TOKEN") ?? pick("GITHUB_TOKEN"); } if (normalized === "anthropic") { return pick("ANTHROPIC_OAUTH_TOKEN") ?? pick("ANTHROPIC_API_KEY"); } if (normalized === "chutes") { return pick("CHUTES_OAUTH_TOKEN") ?? pick("CHUTES_API_KEY"); } if (normalized === "zai") { return pick("ZAI_API_KEY") ?? pick("Z_AI_API_KEY"); } if (normalized === "google-vertex") { const envKey = getEnvApiKey(normalized); if (!envKey) return null; return { apiKey: envKey, source: "gcloud adc" }; } if (normalized === "opencode") { return pick("OPENCODE_API_KEY") ?? pick("OPENCODE_ZEN_API_KEY"); } if (normalized === "qwen-portal") { return pick("QWEN_OAUTH_TOKEN") ?? pick("QWEN_PORTAL_API_KEY"); } const envMap: Record = { openai: "OPENAI_API_KEY", google: "GEMINI_API_KEY", groq: "GROQ_API_KEY", deepgram: "DEEPGRAM_API_KEY", cerebras: "CEREBRAS_API_KEY", xai: "XAI_API_KEY", openrouter: "OPENROUTER_API_KEY", "vercel-ai-gateway": "AI_GATEWAY_API_KEY", moonshot: "MOONSHOT_API_KEY", "kimi-code": "KIMICODE_API_KEY", minimax: "MINIMAX_API_KEY", synthetic: "SYNTHETIC_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; return pick(envVar); } export function resolveModelAuthMode( provider?: string, cfg?: ClawdbotConfig, store?: AuthProfileStore, ): ModelAuthMode | undefined { const resolved = provider?.trim(); if (!resolved) return undefined; const authOverride = resolveProviderAuthOverride(cfg, resolved); if (authOverride === "aws-sdk") return "aws-sdk"; const authStore = store ?? ensureAuthProfileStore(); const profiles = listProfilesForProvider(authStore, resolved); if (profiles.length > 0) { const modes = new Set( profiles .map((id) => authStore.profiles[id]?.type) .filter((mode): mode is "api_key" | "oauth" | "token" => Boolean(mode)), ); const distinct = ["oauth", "token", "api_key"].filter((k) => modes.has(k as "oauth" | "token" | "api_key"), ); if (distinct.length >= 2) return "mixed"; if (modes.has("oauth")) return "oauth"; if (modes.has("token")) return "token"; if (modes.has("api_key")) return "api-key"; } if (authOverride === undefined && normalizeProviderId(resolved) === "amazon-bedrock") { return "aws-sdk"; } const envKey = resolveEnvApiKey(resolved); if (envKey?.apiKey) { return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key"; } if (getCustomProviderApiKey(cfg, resolved)) return "api-key"; return "unknown"; } export async function getApiKeyForModel(params: { model: Model; cfg?: ClawdbotConfig; profileId?: string; preferredProfile?: string; store?: AuthProfileStore; agentDir?: string; }): Promise { return resolveApiKeyForProvider({ provider: params.model.provider, cfg: params.cfg, profileId: params.profileId, preferredProfile: params.preferredProfile, store: params.store, agentDir: params.agentDir, }); } export function requireApiKey(auth: ResolvedProviderAuth, provider: string): string { const key = auth.apiKey?.trim(); if (key) return key; throw new Error(`No API key resolved for provider "${provider}" (auth mode: ${auth.mode}).`); }