From f66bd105a4731cb79ad5b03765c76d90ddbd52f4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 8 Mar 2026 16:58:01 +0000 Subject: [PATCH] refactor: decompose implicit provider resolution --- ...s-config.providers.auth-provenance.test.ts | 6 +- src/agents/models-config.providers.ts | 432 ++++++++++-------- 2 files changed, 247 insertions(+), 191 deletions(-) diff --git a/src/agents/models-config.providers.auth-provenance.test.ts b/src/agents/models-config.providers.auth-provenance.test.ts index 0a606762d66..c06653973f3 100644 --- a/src/agents/models-config.providers.auth-provenance.test.ts +++ b/src/agents/models-config.providers.auth-provenance.test.ts @@ -41,7 +41,7 @@ describe("models-config provider auth provenance", () => { "utf8", ); try { - const providers = await resolveImplicitProviders({ agentDir }); + const providers = await resolveImplicitProviders({ agentDir, env: {} }); expect(providers?.volcengine?.apiKey).toBe("VOLCANO_ENGINE_API_KEY"); expect(providers?.["volcengine-plan"]?.apiKey).toBe("VOLCANO_ENGINE_API_KEY"); expect(providers?.together?.apiKey).toBe("TOGETHER_API_KEY"); @@ -78,7 +78,7 @@ describe("models-config provider auth provenance", () => { "utf8", ); - const providers = await resolveImplicitProviders({ agentDir }); + const providers = await resolveImplicitProviders({ agentDir, env: {} }); expect(providers?.byteplus?.apiKey).toBe(NON_ENV_SECRETREF_MARKER); expect(providers?.["byteplus-plan"]?.apiKey).toBe(NON_ENV_SECRETREF_MARKER); expect(providers?.together?.apiKey).toBe(NON_ENV_SECRETREF_MARKER); @@ -114,7 +114,7 @@ describe("models-config provider auth provenance", () => { "utf8", ); - const providers = await resolveImplicitProviders({ agentDir }); + const providers = await resolveImplicitProviders({ agentDir, env: {} }); expect(providers?.["minimax-portal"]?.apiKey).toBe(MINIMAX_OAUTH_MARKER); expect(providers?.["qwen-portal"]?.apiKey).toBe(QWEN_OAUTH_MARKER); }); diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 8aca2c26d0e..9f6316ecc02 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -1126,18 +1126,244 @@ async function buildKilocodeProviderWithDiscovery(): Promise { }; } -export async function resolveImplicitProviders(params: { +type ImplicitProviderParams = { agentDir: string; config?: OpenClawConfig; env?: NodeJS.ProcessEnv; explicitProviders?: Record | null; -}): Promise { +}; + +type ProviderApiKeyResolver = (provider: string) => { + apiKey: string | undefined; + discoveryApiKey?: string; +}; + +type ImplicitProviderContext = ImplicitProviderParams & { + authStore: ReturnType; + env: NodeJS.ProcessEnv; + resolveProviderApiKey: ProviderApiKeyResolver; +}; + +type ImplicitProviderLoader = ( + ctx: ImplicitProviderContext, +) => Promise | undefined>; + +function withApiKey( + providerKey: string, + build: (params: { + apiKey: string; + discoveryApiKey?: string; + }) => ProviderConfig | Promise, +): ImplicitProviderLoader { + return async (ctx) => { + const { apiKey, discoveryApiKey } = ctx.resolveProviderApiKey(providerKey); + if (!apiKey) { + return undefined; + } + return { + [providerKey]: await build({ apiKey, discoveryApiKey }), + }; + }; +} + +function withProfilePresence( + providerKey: string, + build: () => ProviderConfig | Promise, +): ImplicitProviderLoader { + return async (ctx) => { + if (listProfilesForProvider(ctx.authStore, providerKey).length === 0) { + return undefined; + } + return { + [providerKey]: await build(), + }; + }; +} + +function mergeImplicitProviderSet( + target: Record, + additions: Record | undefined, +): void { + if (!additions) { + return; + } + for (const [key, value] of Object.entries(additions)) { + target[key] = value; + } +} + +const SIMPLE_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [ + withApiKey("minimax", async ({ apiKey }) => ({ ...buildMinimaxProvider(), apiKey })), + withApiKey("moonshot", async ({ apiKey }) => ({ ...buildMoonshotProvider(), apiKey })), + withApiKey("kimi-coding", async ({ apiKey }) => ({ ...buildKimiCodingProvider(), apiKey })), + withApiKey("synthetic", async ({ apiKey }) => ({ ...buildSyntheticProvider(), apiKey })), + withApiKey("venice", async ({ apiKey }) => ({ ...(await buildVeniceProvider()), apiKey })), + withApiKey("xiaomi", async ({ apiKey }) => ({ ...buildXiaomiProvider(), apiKey })), + withApiKey("vercel-ai-gateway", async ({ apiKey }) => ({ + ...(await buildVercelAiGatewayProvider()), + apiKey, + })), + withApiKey("together", async ({ apiKey }) => ({ ...buildTogetherProvider(), apiKey })), + withApiKey("huggingface", async ({ apiKey, discoveryApiKey }) => ({ + ...(await buildHuggingfaceProvider(discoveryApiKey)), + apiKey, + })), + withApiKey("qianfan", async ({ apiKey }) => ({ ...buildQianfanProvider(), apiKey })), + withApiKey("openrouter", async ({ apiKey }) => ({ ...buildOpenrouterProvider(), apiKey })), + withApiKey("nvidia", async ({ apiKey }) => ({ ...buildNvidiaProvider(), apiKey })), + withApiKey("kilocode", async ({ apiKey }) => ({ + ...(await buildKilocodeProviderWithDiscovery()), + apiKey, + })), +]; + +const PROFILE_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [ + async (ctx) => { + const envKey = resolveEnvApiKeyVarName("minimax-portal", ctx.env); + const hasProfiles = listProfilesForProvider(ctx.authStore, "minimax-portal").length > 0; + if (!envKey && !hasProfiles) { + return undefined; + } + return { + "minimax-portal": { + ...buildMinimaxPortalProvider(), + apiKey: MINIMAX_OAUTH_MARKER, + }, + }; + }, + withProfilePresence("qwen-portal", async () => ({ + ...buildQwenPortalProvider(), + apiKey: QWEN_OAUTH_MARKER, + })), + withProfilePresence("openai-codex", async () => buildOpenAICodexProvider()), +]; + +const PAIRED_IMPLICIT_PROVIDER_LOADERS: ImplicitProviderLoader[] = [ + async (ctx) => { + const volcengineKey = ctx.resolveProviderApiKey("volcengine").apiKey; + if (!volcengineKey) { + return undefined; + } + return { + volcengine: { ...buildDoubaoProvider(), apiKey: volcengineKey }, + "volcengine-plan": { + ...buildDoubaoCodingProvider(), + apiKey: volcengineKey, + }, + }; + }, + async (ctx) => { + const byteplusKey = ctx.resolveProviderApiKey("byteplus").apiKey; + if (!byteplusKey) { + return undefined; + } + return { + byteplus: { ...buildBytePlusProvider(), apiKey: byteplusKey }, + "byteplus-plan": { + ...buildBytePlusCodingProvider(), + apiKey: byteplusKey, + }, + }; + }, +]; + +async function resolveCloudflareAiGatewayImplicitProvider( + ctx: ImplicitProviderContext, +): Promise | undefined> { + const cloudflareProfiles = listProfilesForProvider(ctx.authStore, "cloudflare-ai-gateway"); + for (const profileId of cloudflareProfiles) { + const cred = ctx.authStore.profiles[profileId]; + if (cred?.type !== "api_key") { + continue; + } + const accountId = cred.metadata?.accountId?.trim(); + const gatewayId = cred.metadata?.gatewayId?.trim(); + if (!accountId || !gatewayId) { + continue; + } + const baseUrl = resolveCloudflareAiGatewayBaseUrl({ accountId, gatewayId }); + if (!baseUrl) { + continue; + } + const envVarApiKey = resolveEnvApiKeyVarName("cloudflare-ai-gateway", ctx.env); + const profileApiKey = resolveApiKeyFromCredential(cred, ctx.env)?.apiKey; + const apiKey = envVarApiKey ?? profileApiKey ?? ""; + if (!apiKey) { + continue; + } + return { + "cloudflare-ai-gateway": { + baseUrl, + api: "anthropic-messages", + apiKey, + models: [buildCloudflareAiGatewayModelDefinition()], + }, + }; + } + return undefined; +} + +async function resolveOllamaImplicitProvider( + ctx: ImplicitProviderContext, +): Promise | undefined> { + const ollamaKey = ctx.resolveProviderApiKey("ollama").apiKey; + const explicitOllama = ctx.explicitProviders?.ollama; + const hasExplicitModels = + Array.isArray(explicitOllama?.models) && explicitOllama.models.length > 0; + if (hasExplicitModels && explicitOllama) { + return { + ollama: { + ...explicitOllama, + baseUrl: resolveOllamaApiBase(explicitOllama.baseUrl), + api: explicitOllama.api ?? "ollama", + apiKey: ollamaKey ?? explicitOllama.apiKey ?? OLLAMA_LOCAL_AUTH_MARKER, + }, + }; + } + + const ollamaBaseUrl = explicitOllama?.baseUrl; + const hasExplicitOllamaConfig = Boolean(explicitOllama); + const ollamaProvider = await buildOllamaProvider(ollamaBaseUrl, { + quiet: !ollamaKey && !hasExplicitOllamaConfig, + }); + if (ollamaProvider.models.length === 0 && !ollamaKey && !explicitOllama?.apiKey) { + return undefined; + } + return { + ollama: { + ...ollamaProvider, + apiKey: ollamaKey ?? explicitOllama?.apiKey ?? OLLAMA_LOCAL_AUTH_MARKER, + }, + }; +} + +async function resolveVllmImplicitProvider( + ctx: ImplicitProviderContext, +): Promise | undefined> { + if (ctx.explicitProviders?.vllm) { + return undefined; + } + const { apiKey: vllmKey, discoveryApiKey } = ctx.resolveProviderApiKey("vllm"); + if (!vllmKey) { + return undefined; + } + return { + vllm: { + ...(await buildVllmProvider({ apiKey: discoveryApiKey })), + apiKey: vllmKey, + }, + }; +} + +export async function resolveImplicitProviders( + params: ImplicitProviderParams, +): Promise { const providers: Record = {}; const env = params.env ?? process.env; const authStore = ensureAuthProfileStore(params.agentDir, { allowKeychainPrompt: false, }); - const resolveProviderApiKey = ( + const resolveProviderApiKey: ProviderApiKeyResolver = ( provider: string, ): { apiKey: string | undefined; discoveryApiKey?: string } => { const envVar = resolveEnvApiKeyVarName(provider, env); @@ -1153,195 +1379,25 @@ export async function resolveImplicitProviders(params: { discoveryApiKey: fromProfiles?.discoveryApiKey, }; }; + const context: ImplicitProviderContext = { + ...params, + authStore, + env, + resolveProviderApiKey, + }; - const minimaxKey = resolveProviderApiKey("minimax").apiKey; - if (minimaxKey) { - providers.minimax = { ...buildMinimaxProvider(), apiKey: minimaxKey }; + for (const loader of SIMPLE_IMPLICIT_PROVIDER_LOADERS) { + mergeImplicitProviderSet(providers, await loader(context)); } - - const minimaxPortalEnvKey = resolveEnvApiKeyVarName("minimax-portal", env); - const minimaxOauthProfile = listProfilesForProvider(authStore, "minimax-portal"); - if (minimaxPortalEnvKey || minimaxOauthProfile.length > 0) { - providers["minimax-portal"] = { - ...buildMinimaxPortalProvider(), - apiKey: MINIMAX_OAUTH_MARKER, - }; + for (const loader of PROFILE_IMPLICIT_PROVIDER_LOADERS) { + mergeImplicitProviderSet(providers, await loader(context)); } - - const moonshotKey = resolveProviderApiKey("moonshot").apiKey; - if (moonshotKey) { - providers.moonshot = { ...buildMoonshotProvider(), apiKey: moonshotKey }; - } - - const kimiCodingKey = resolveProviderApiKey("kimi-coding").apiKey; - if (kimiCodingKey) { - providers["kimi-coding"] = { ...buildKimiCodingProvider(), apiKey: kimiCodingKey }; - } - - const syntheticKey = resolveProviderApiKey("synthetic").apiKey; - if (syntheticKey) { - providers.synthetic = { ...buildSyntheticProvider(), apiKey: syntheticKey }; - } - - const veniceKey = resolveProviderApiKey("venice").apiKey; - if (veniceKey) { - providers.venice = { ...(await buildVeniceProvider()), apiKey: veniceKey }; - } - - const qwenProfiles = listProfilesForProvider(authStore, "qwen-portal"); - if (qwenProfiles.length > 0) { - providers["qwen-portal"] = { - ...buildQwenPortalProvider(), - apiKey: QWEN_OAUTH_MARKER, - }; - } - - const volcengineKey = resolveProviderApiKey("volcengine").apiKey; - if (volcengineKey) { - providers.volcengine = { ...buildDoubaoProvider(), apiKey: volcengineKey }; - providers["volcengine-plan"] = { - ...buildDoubaoCodingProvider(), - apiKey: volcengineKey, - }; - } - - const byteplusKey = resolveProviderApiKey("byteplus").apiKey; - if (byteplusKey) { - providers.byteplus = { ...buildBytePlusProvider(), apiKey: byteplusKey }; - providers["byteplus-plan"] = { - ...buildBytePlusCodingProvider(), - apiKey: byteplusKey, - }; - } - - const xiaomiKey = resolveProviderApiKey("xiaomi").apiKey; - if (xiaomiKey) { - providers.xiaomi = { ...buildXiaomiProvider(), apiKey: xiaomiKey }; - } - - const cloudflareProfiles = listProfilesForProvider(authStore, "cloudflare-ai-gateway"); - for (const profileId of cloudflareProfiles) { - const cred = authStore.profiles[profileId]; - if (cred?.type !== "api_key") { - continue; - } - const accountId = cred.metadata?.accountId?.trim(); - const gatewayId = cred.metadata?.gatewayId?.trim(); - if (!accountId || !gatewayId) { - continue; - } - const baseUrl = resolveCloudflareAiGatewayBaseUrl({ accountId, gatewayId }); - if (!baseUrl) { - continue; - } - const envVarApiKey = resolveEnvApiKeyVarName("cloudflare-ai-gateway", env); - const profileApiKey = resolveApiKeyFromCredential(cred, env)?.apiKey; - const apiKey = envVarApiKey ?? profileApiKey ?? ""; - if (!apiKey) { - continue; - } - providers["cloudflare-ai-gateway"] = { - baseUrl, - api: "anthropic-messages", - apiKey, - models: [buildCloudflareAiGatewayModelDefinition()], - }; - break; - } - - const vercelAiGatewayKey = resolveProviderApiKey("vercel-ai-gateway").apiKey; - if (vercelAiGatewayKey) { - providers["vercel-ai-gateway"] = { - ...(await buildVercelAiGatewayProvider()), - apiKey: vercelAiGatewayKey, - }; - } - - // Ollama provider - auto-discover if running locally, or add if explicitly configured. - // Use the user's configured baseUrl (from explicit providers) for model - // discovery so that remote / non-default Ollama instances are reachable. - // Skip discovery when explicit models are already defined. - const ollamaKey = resolveProviderApiKey("ollama").apiKey; - const explicitOllama = params.explicitProviders?.ollama; - const hasExplicitModels = - Array.isArray(explicitOllama?.models) && explicitOllama.models.length > 0; - if (hasExplicitModels && explicitOllama) { - providers.ollama = { - ...explicitOllama, - baseUrl: resolveOllamaApiBase(explicitOllama.baseUrl), - api: explicitOllama.api ?? "ollama", - apiKey: ollamaKey ?? explicitOllama.apiKey ?? OLLAMA_LOCAL_AUTH_MARKER, - }; - } else { - const ollamaBaseUrl = explicitOllama?.baseUrl; - const hasExplicitOllamaConfig = Boolean(explicitOllama); - // Only suppress warnings for implicit local probing when user has not - // explicitly configured Ollama. - const ollamaProvider = await buildOllamaProvider(ollamaBaseUrl, { - quiet: !ollamaKey && !hasExplicitOllamaConfig, - }); - if (ollamaProvider.models.length > 0 || ollamaKey || explicitOllama?.apiKey) { - providers.ollama = { - ...ollamaProvider, - apiKey: ollamaKey ?? explicitOllama?.apiKey ?? OLLAMA_LOCAL_AUTH_MARKER, - }; - } - } - - // vLLM provider - OpenAI-compatible local server (opt-in via env/profile). - // If explicitly configured, keep user-defined models/settings as-is. - if (!params.explicitProviders?.vllm) { - const { apiKey: vllmKey, discoveryApiKey } = resolveProviderApiKey("vllm"); - if (vllmKey) { - providers.vllm = { - ...(await buildVllmProvider({ apiKey: discoveryApiKey })), - apiKey: vllmKey, - }; - } - } - - const togetherKey = resolveProviderApiKey("together").apiKey; - if (togetherKey) { - providers.together = { - ...buildTogetherProvider(), - apiKey: togetherKey, - }; - } - - const { apiKey: huggingfaceKey, discoveryApiKey: huggingfaceDiscoveryApiKey } = - resolveProviderApiKey("huggingface"); - if (huggingfaceKey) { - const hfProvider = await buildHuggingfaceProvider(huggingfaceDiscoveryApiKey); - providers.huggingface = { - ...hfProvider, - apiKey: huggingfaceKey, - }; - } - - const qianfanKey = resolveProviderApiKey("qianfan").apiKey; - if (qianfanKey) { - providers.qianfan = { ...buildQianfanProvider(), apiKey: qianfanKey }; - } - - const openrouterKey = resolveProviderApiKey("openrouter").apiKey; - if (openrouterKey) { - providers.openrouter = { ...buildOpenrouterProvider(), apiKey: openrouterKey }; - } - - const openaiCodexProfiles = listProfilesForProvider(authStore, "openai-codex"); - if (openaiCodexProfiles.length > 0) { - providers["openai-codex"] = buildOpenAICodexProvider(); - } - - const nvidiaKey = resolveProviderApiKey("nvidia").apiKey; - if (nvidiaKey) { - providers.nvidia = { ...buildNvidiaProvider(), apiKey: nvidiaKey }; - } - - const kilocodeKey = resolveProviderApiKey("kilocode").apiKey; - if (kilocodeKey) { - providers.kilocode = { ...(await buildKilocodeProviderWithDiscovery()), apiKey: kilocodeKey }; + for (const loader of PAIRED_IMPLICIT_PROVIDER_LOADERS) { + mergeImplicitProviderSet(providers, await loader(context)); } + mergeImplicitProviderSet(providers, await resolveCloudflareAiGatewayImplicitProvider(context)); + mergeImplicitProviderSet(providers, await resolveOllamaImplicitProvider(context)); + mergeImplicitProviderSet(providers, await resolveVllmImplicitProvider(context)); if (!providers["github-copilot"]) { const implicitCopilot = await resolveImplicitCopilotProvider({