From 1c5a4d2a2b92c09944fdd2deabcb1e852cdc909a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 28 Mar 2026 10:46:46 +0000 Subject: [PATCH] fix: stabilize implicit provider discovery merges --- extensions/ollama/index.ts | 7 ++ extensions/ollama/src/setup.ts | 23 +++- src/agents/models-config.e2e-harness.ts | 3 + .../models-config.providers.implicit.ts | 101 +++++++++++++++--- 4 files changed, 116 insertions(+), 18 deletions(-) diff --git a/extensions/ollama/index.ts b/extensions/ollama/index.ts index c239c381b39..75b9525a4e9 100644 --- a/extensions/ollama/index.ts +++ b/extensions/ollama/index.ts @@ -20,6 +20,10 @@ import { const PROVIDER_ID = "ollama"; const DEFAULT_API_KEY = "ollama-local"; +function shouldSkipAmbientOllamaDiscovery(env: NodeJS.ProcessEnv): boolean { + return Boolean(env.VITEST) || env.NODE_ENV === "test"; +} + async function loadProviderSetup() { return await import("openclaw/plugin-sdk/provider-setup"); } @@ -95,6 +99,9 @@ export default definePluginEntry({ }, }; } + if (!ollamaKey && !explicit && shouldSkipAmbientOllamaDiscovery(ctx.env)) { + return null; + } const providerSetup = await loadProviderSetup(); const provider = await providerSetup.buildOllamaProvider(explicit?.baseUrl, { diff --git a/extensions/ollama/src/setup.ts b/extensions/ollama/src/setup.ts index db7f259470d..fd14128e962 100644 --- a/extensions/ollama/src/setup.ts +++ b/extensions/ollama/src/setup.ts @@ -14,6 +14,7 @@ import { const OLLAMA_SUGGESTED_MODELS_LOCAL = [OLLAMA_DEFAULT_MODEL]; const OLLAMA_SUGGESTED_MODELS_CLOUD = ["kimi-k2.5:cloud", "minimax-m2.5:cloud", "glm-5:cloud"]; +const OLLAMA_CONTEXT_ENRICH_LIMIT = 200; type OllamaMode = "remote" | "local"; type OllamaSetupOptions = { @@ -264,11 +265,17 @@ async function storeOllamaCredential(agentDir?: string): Promise { export async function buildOllamaProvider( configuredBaseUrl?: string, - _opts?: { quiet?: boolean }, + opts?: { quiet?: boolean }, ): Promise { const apiBase = resolveOllamaApiBase(configuredBaseUrl); - const { models } = await fetchOllamaModels(apiBase); - const discovered = await enrichOllamaModelsWithContext(apiBase, models.slice(0, 50)); + const { reachable, models } = await fetchOllamaModels(apiBase); + if (!reachable && !opts?.quiet) { + console.warn(`Ollama could not be reached at ${apiBase}.`); + } + const discovered = await enrichOllamaModelsWithContext( + apiBase, + models.slice(0, OLLAMA_CONTEXT_ENRICH_LIMIT), + ); return { baseUrl: apiBase, api: "ollama", @@ -308,7 +315,10 @@ export async function promptAndConfigureOllama(params: { throw new WizardCancelledError("Ollama not reachable"); } - const enrichedModels = await enrichOllamaModelsWithContext(baseUrl, models.slice(0, 50)); + const enrichedModels = await enrichOllamaModelsWithContext( + baseUrl, + models.slice(0, OLLAMA_CONTEXT_ENRICH_LIMIT), + ); const discoveredModelsByName = new Map(enrichedModels.map((model) => [model.name, model])); const modelNames = models.map((model) => model.name); const mode = (await params.prompter.select({ @@ -400,7 +410,10 @@ export async function configureOllamaNonInteractive(params: { await storeOllamaCredential(params.agentDir); - const enrichedModels = await enrichOllamaModelsWithContext(baseUrl, models.slice(0, 50)); + const enrichedModels = await enrichOllamaModelsWithContext( + baseUrl, + models.slice(0, OLLAMA_CONTEXT_ENRICH_LIMIT), + ); const discoveredModelsByName = new Map(enrichedModels.map((model) => [model.name, model])); const modelNames = models.map((model) => model.name); const orderedModelNames = [ diff --git a/src/agents/models-config.e2e-harness.ts b/src/agents/models-config.e2e-harness.ts index ff86e25c7a4..a54d06057e6 100644 --- a/src/agents/models-config.e2e-harness.ts +++ b/src/agents/models-config.e2e-harness.ts @@ -84,6 +84,8 @@ export async function withCopilotGithubToken( } export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [ + "VITEST", + "NODE_ENV", "AI_GATEWAY_API_KEY", "CLOUDFLARE_AI_GATEWAY_API_KEY", "COPILOT_GITHUB_TOKEN", @@ -114,6 +116,7 @@ export const MODELS_CONFIG_IMPLICIT_ENV_VARS = [ "GOOGLE_CLOUD_LOCATION", "GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT_ID", + "ANTHROPIC_VERTEX_USE_GCP_METADATA", "VENICE_API_KEY", "VLLM_API_KEY", "XIAOMI_API_KEY", diff --git a/src/agents/models-config.providers.implicit.ts b/src/agents/models-config.providers.implicit.ts index 0926b2c8d1a..76629f21b04 100644 --- a/src/agents/models-config.providers.implicit.ts +++ b/src/agents/models-config.providers.implicit.ts @@ -20,6 +20,7 @@ import { createProviderApiKeyResolver, createProviderAuthResolver, } from "./models-config.providers.secrets.js"; +import { findNormalizedProviderValue } from "./provider-id.js"; const log = createSubsystemLogger("agents/model-providers"); @@ -30,6 +31,7 @@ const PROVIDER_IMPLICIT_MERGERS: Partial< > > = { "anthropic-vertex": mergeImplicitAnthropicVertexProvider, + ollama: ({ implicit }) => implicit, }; const CORE_IMPLICIT_PROVIDER_RESOLVERS = [ @@ -102,6 +104,61 @@ function mergeImplicitProviderSet( } } +function mergeImplicitProviderConfig(params: { + providerId: string; + existing: ProviderConfig | undefined; + implicit: ProviderConfig; +}): ProviderConfig { + const { providerId, existing, implicit } = params; + if (!existing) { + return implicit; + } + const merge = PROVIDER_IMPLICIT_MERGERS[providerId]; + if (merge) { + return merge({ existing, implicit }); + } + return { + ...implicit, + ...existing, + models: + Array.isArray(existing.models) && existing.models.length > 0 + ? existing.models + : implicit.models, + }; +} + +function resolveConfiguredImplicitProvider(params: { + configuredProviders?: Record | null; + providerIds: readonly string[]; +}): ProviderConfig | undefined { + for (const providerId of params.providerIds) { + const configured = findNormalizedProviderValue( + params.configuredProviders ?? undefined, + providerId, + ); + if (configured) { + return configured; + } + } + return undefined; +} + +function resolveExistingImplicitProviderFromContext(params: { + ctx: ImplicitProviderContext; + providerIds: readonly string[]; +}): ProviderConfig | undefined { + return ( + resolveConfiguredImplicitProvider({ + configuredProviders: params.ctx.explicitProviders, + providerIds: params.providerIds, + }) ?? + resolveConfiguredImplicitProvider({ + configuredProviders: params.ctx.config?.models?.providers, + providerIds: params.providerIds, + }) + ); +} + async function resolvePluginImplicitProviders( ctx: ImplicitProviderContext, order: import("../plugins/types.js").ProviderDiscoveryOrder, @@ -132,13 +189,27 @@ async function resolvePluginImplicitProviders( if (!result) { continue; } - mergeImplicitProviderSet( - discovered, - normalizePluginDiscoveryResult({ - provider, - result, - }), - ); + const normalizedResult = normalizePluginDiscoveryResult({ + provider, + result, + }); + for (const [providerId, implicitProvider] of Object.entries(normalizedResult)) { + discovered[providerId] = mergeImplicitProviderConfig({ + providerId, + existing: + discovered[providerId] ?? + resolveExistingImplicitProviderFromContext({ + ctx, + providerIds: [ + providerId, + provider.id, + ...(provider.aliases ?? []), + ...(provider.hookAliases ?? []), + ], + }), + implicit: implicitProvider, + }); + } } return Object.keys(discovered).length > 0 ? discovered : undefined; } @@ -199,6 +270,7 @@ async function runProviderCatalogWithTimeout( async function mergeCoreImplicitProviders(params: { config?: OpenClawConfig; + explicitProviders?: Record | null; env: NodeJS.ProcessEnv; providers: Record; }): Promise { @@ -208,12 +280,14 @@ async function mergeCoreImplicitProviders(params: { continue; } const merge = PROVIDER_IMPLICIT_MERGERS[provider.id]; - if (!merge) { - params.providers[provider.id] = implicit; - continue; - } - params.providers[provider.id] = merge({ - existing: params.providers[provider.id], + params.providers[provider.id] = (merge ?? mergeImplicitProviderConfig)({ + providerId: provider.id, + existing: + params.providers[provider.id] ?? + resolveConfiguredImplicitProvider({ + configuredProviders: params.explicitProviders ?? params.config?.models?.providers, + providerIds: [provider.id], + }), implicit, }); } @@ -241,6 +315,7 @@ export async function resolveImplicitProviders( await mergeCoreImplicitProviders({ config: params.config, + explicitProviders: params.explicitProviders, env, providers, });