import type { ClawdbotConfig } from "../config/config.js"; import { DEFAULT_COPILOT_API_BASE_URL, resolveCopilotApiToken, } from "../providers/github-copilot-token.js"; import { ensureAuthProfileStore, listProfilesForProvider } from "./auth-profiles.js"; import { resolveAwsSdkEnvVarName, resolveEnvApiKey } from "./model-auth.js"; import { buildSyntheticModelDefinition, SYNTHETIC_BASE_URL, SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; const MINIMAX_API_BASE_URL = "https://api.minimax.io/anthropic"; const MINIMAX_DEFAULT_MODEL_ID = "MiniMax-M2.1"; const MINIMAX_DEFAULT_VISION_MODEL_ID = "MiniMax-VL-01"; const MINIMAX_DEFAULT_CONTEXT_WINDOW = 200000; const MINIMAX_DEFAULT_MAX_TOKENS = 8192; // Pricing: MiniMax doesn't publish public rates. Override in models.json for accurate costs. const MINIMAX_API_COST = { input: 15, output: 60, cacheRead: 2, cacheWrite: 10, }; const MOONSHOT_BASE_URL = "https://api.moonshot.ai/v1"; const MOONSHOT_DEFAULT_MODEL_ID = "kimi-k2-0905-preview"; const MOONSHOT_DEFAULT_CONTEXT_WINDOW = 256000; const MOONSHOT_DEFAULT_MAX_TOKENS = 8192; const MOONSHOT_DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, }; const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1"; const KIMI_CODE_MODEL_ID = "kimi-for-coding"; const KIMI_CODE_CONTEXT_WINDOW = 262144; const KIMI_CODE_MAX_TOKENS = 32768; const KIMI_CODE_HEADERS = { "User-Agent": "KimiCLI/0.77" } as const; const KIMI_CODE_COMPAT = { supportsDeveloperRole: false } as const; const KIMI_CODE_DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, }; const QWEN_PORTAL_BASE_URL = "https://portal.qwen.ai/v1"; const QWEN_PORTAL_OAUTH_PLACEHOLDER = "qwen-oauth"; const QWEN_PORTAL_DEFAULT_CONTEXT_WINDOW = 128000; const QWEN_PORTAL_DEFAULT_MAX_TOKENS = 8192; const QWEN_PORTAL_DEFAULT_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, }; function normalizeApiKeyConfig(value: string): string { const trimmed = value.trim(); const match = /^\$\{([A-Z0-9_]+)\}$/.exec(trimmed); return match?.[1] ?? trimmed; } function resolveEnvApiKeyVarName(provider: string): string | undefined { const resolved = resolveEnvApiKey(provider); if (!resolved) return undefined; const match = /^(?:env: |shell env: )([A-Z0-9_]+)$/.exec(resolved.source); return match ? match[1] : undefined; } function resolveAwsSdkApiKeyVarName(): string { return resolveAwsSdkEnvVarName() ?? "AWS_PROFILE"; } function resolveApiKeyFromProfiles(params: { provider: string; store: ReturnType; }): string | undefined { const ids = listProfilesForProvider(params.store, params.provider); for (const id of ids) { const cred = params.store.profiles[id]; if (!cred) continue; if (cred.type === "api_key") return cred.key; if (cred.type === "token") return cred.token; } return undefined; } export function normalizeGoogleModelId(id: string): string { if (id === "gemini-3-pro") return "gemini-3-pro-preview"; if (id === "gemini-3-flash") return "gemini-3-flash-preview"; return id; } function normalizeGoogleProvider(provider: ProviderConfig): ProviderConfig { let mutated = false; const models = provider.models.map((model) => { const nextId = normalizeGoogleModelId(model.id); if (nextId === model.id) return model; mutated = true; return { ...model, id: nextId }; }); return mutated ? { ...provider, models } : provider; } export function normalizeProviders(params: { providers: ModelsConfig["providers"]; agentDir: string; }): ModelsConfig["providers"] { const { providers } = params; if (!providers) return providers; const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }); let mutated = false; const next: Record = {}; for (const [key, provider] of Object.entries(providers)) { const normalizedKey = key.trim(); let normalizedProvider = provider; // Fix common misconfig: apiKey set to "${ENV_VAR}" instead of "ENV_VAR". if ( normalizedProvider.apiKey && normalizeApiKeyConfig(normalizedProvider.apiKey) !== normalizedProvider.apiKey ) { mutated = true; normalizedProvider = { ...normalizedProvider, apiKey: normalizeApiKeyConfig(normalizedProvider.apiKey), }; } // If a provider defines models, pi's ModelRegistry requires apiKey to be set. // Fill it from the environment or auth profiles when possible. const hasModels = Array.isArray(normalizedProvider.models) && normalizedProvider.models.length > 0; if (hasModels && !normalizedProvider.apiKey?.trim()) { const authMode = normalizedProvider.auth ?? (normalizedKey === "amazon-bedrock" ? "aws-sdk" : undefined); if (authMode === "aws-sdk") { const apiKey = resolveAwsSdkApiKeyVarName(); mutated = true; normalizedProvider = { ...normalizedProvider, apiKey }; } else { const fromEnv = resolveEnvApiKeyVarName(normalizedKey); const fromProfiles = resolveApiKeyFromProfiles({ provider: normalizedKey, store: authStore, }); const apiKey = fromEnv ?? fromProfiles; if (apiKey?.trim()) { mutated = true; normalizedProvider = { ...normalizedProvider, apiKey }; } } } if (normalizedKey === "google") { const googleNormalized = normalizeGoogleProvider(normalizedProvider); if (googleNormalized !== normalizedProvider) mutated = true; normalizedProvider = googleNormalized; } next[key] = normalizedProvider; } return mutated ? next : providers; } function buildMinimaxProvider(): ProviderConfig { return { baseUrl: MINIMAX_API_BASE_URL, api: "anthropic-messages", models: [ { id: MINIMAX_DEFAULT_MODEL_ID, name: "MiniMax M2.1", reasoning: false, input: ["text"], cost: MINIMAX_API_COST, contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW, maxTokens: MINIMAX_DEFAULT_MAX_TOKENS, }, { id: MINIMAX_DEFAULT_VISION_MODEL_ID, name: "MiniMax VL 01", reasoning: false, input: ["text", "image"], cost: MINIMAX_API_COST, contextWindow: MINIMAX_DEFAULT_CONTEXT_WINDOW, maxTokens: MINIMAX_DEFAULT_MAX_TOKENS, }, ], }; } function buildMoonshotProvider(): ProviderConfig { return { baseUrl: MOONSHOT_BASE_URL, api: "openai-completions", models: [ { id: MOONSHOT_DEFAULT_MODEL_ID, name: "Kimi K2 0905 Preview", reasoning: false, input: ["text"], cost: MOONSHOT_DEFAULT_COST, contextWindow: MOONSHOT_DEFAULT_CONTEXT_WINDOW, maxTokens: MOONSHOT_DEFAULT_MAX_TOKENS, }, ], }; } function buildKimiCodeProvider(): ProviderConfig { return { baseUrl: KIMI_CODE_BASE_URL, api: "openai-completions", models: [ { id: KIMI_CODE_MODEL_ID, name: "Kimi For Coding", reasoning: true, input: ["text"], cost: KIMI_CODE_DEFAULT_COST, contextWindow: KIMI_CODE_CONTEXT_WINDOW, maxTokens: KIMI_CODE_MAX_TOKENS, headers: KIMI_CODE_HEADERS, compat: KIMI_CODE_COMPAT, }, ], }; } function buildQwenPortalProvider(): ProviderConfig { return { baseUrl: QWEN_PORTAL_BASE_URL, api: "openai-completions", models: [ { id: "coder-model", name: "Qwen Coder", reasoning: false, input: ["text"], cost: QWEN_PORTAL_DEFAULT_COST, contextWindow: QWEN_PORTAL_DEFAULT_CONTEXT_WINDOW, maxTokens: QWEN_PORTAL_DEFAULT_MAX_TOKENS, }, { id: "vision-model", name: "Qwen Vision", reasoning: false, input: ["text", "image"], cost: QWEN_PORTAL_DEFAULT_COST, contextWindow: QWEN_PORTAL_DEFAULT_CONTEXT_WINDOW, maxTokens: QWEN_PORTAL_DEFAULT_MAX_TOKENS, }, ], }; } function buildSyntheticProvider(): ProviderConfig { return { baseUrl: SYNTHETIC_BASE_URL, api: "anthropic-messages", models: SYNTHETIC_MODEL_CATALOG.map(buildSyntheticModelDefinition), }; } export function resolveImplicitProviders(params: { agentDir: string }): ModelsConfig["providers"] { const providers: Record = {}; const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }); const minimaxKey = resolveEnvApiKeyVarName("minimax") ?? resolveApiKeyFromProfiles({ provider: "minimax", store: authStore }); if (minimaxKey) { providers.minimax = { ...buildMinimaxProvider(), apiKey: minimaxKey }; } const moonshotKey = resolveEnvApiKeyVarName("moonshot") ?? resolveApiKeyFromProfiles({ provider: "moonshot", store: authStore }); if (moonshotKey) { providers.moonshot = { ...buildMoonshotProvider(), apiKey: moonshotKey }; } const kimiCodeKey = resolveEnvApiKeyVarName("kimi-code") ?? resolveApiKeyFromProfiles({ provider: "kimi-code", store: authStore }); if (kimiCodeKey) { providers["kimi-code"] = { ...buildKimiCodeProvider(), apiKey: kimiCodeKey }; } const syntheticKey = resolveEnvApiKeyVarName("synthetic") ?? resolveApiKeyFromProfiles({ provider: "synthetic", store: authStore }); if (syntheticKey) { providers.synthetic = { ...buildSyntheticProvider(), apiKey: syntheticKey }; } const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); if (qwenProfiles.length > 0) { providers["qwen-portal"] = { ...buildQwenPortalProvider(), apiKey: QWEN_PORTAL_OAUTH_PLACEHOLDER, }; } return providers; } export async function resolveImplicitCopilotProvider(params: { agentDir: string; env?: NodeJS.ProcessEnv; }): Promise { const env = params.env ?? process.env; const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false }); const hasProfile = listProfilesForProvider(authStore, "github-copilot").length > 0; const envToken = env.COPILOT_GITHUB_TOKEN ?? env.GH_TOKEN ?? env.GITHUB_TOKEN; const githubToken = (envToken ?? "").trim(); if (!hasProfile && !githubToken) return null; let selectedGithubToken = githubToken; if (!selectedGithubToken && hasProfile) { // Use the first available profile as a default for discovery (it will be // re-resolved per-run by the embedded runner). const profileId = listProfilesForProvider(authStore, "github-copilot")[0]; const profile = profileId ? authStore.profiles[profileId] : undefined; if (profile && profile.type === "token") { selectedGithubToken = profile.token; } } let baseUrl = DEFAULT_COPILOT_API_BASE_URL; if (selectedGithubToken) { try { const token = await resolveCopilotApiToken({ githubToken: selectedGithubToken, env, }); baseUrl = token.baseUrl; } catch { baseUrl = DEFAULT_COPILOT_API_BASE_URL; } } // pi-coding-agent's ModelRegistry marks a model "available" only if its // `AuthStorage` has auth configured for that provider (via auth.json/env/etc). // Our Copilot auth lives in Clawdbot's auth-profiles store instead, so we also // write a runtime-only auth.json entry for pi-coding-agent to pick up. // // This is safe because it's (1) within Clawdbot's agent dir, (2) contains the // GitHub token (not the exchanged Copilot token), and (3) matches existing // patterns for OAuth-like providers in pi-coding-agent. // Note: we deliberately do not write pi-coding-agent's `auth.json` here. // Clawdbot uses its own auth store and exchanges tokens at runtime. // `models list` uses Clawdbot's auth heuristics for availability. // We intentionally do NOT define custom models for Copilot in models.json. // pi-coding-agent treats providers with models as replacements requiring apiKey. // We only override baseUrl; the model list comes from pi-ai built-ins. return { baseUrl, models: [], } satisfies ProviderConfig; }