fix: stabilize implicit provider discovery merges

This commit is contained in:
Peter Steinberger
2026-03-28 10:46:46 +00:00
parent e34a770b8a
commit 1c5a4d2a2b
4 changed files with 116 additions and 18 deletions

View File

@@ -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, {

View File

@@ -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<void> {
export async function buildOllamaProvider(
configuredBaseUrl?: string,
_opts?: { quiet?: boolean },
opts?: { quiet?: boolean },
): Promise<ProviderConfig> {
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 = [

View File

@@ -84,6 +84,8 @@ export async function withCopilotGithubToken<T>(
}
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",

View File

@@ -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<string, ProviderConfig> | 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<string, ProviderConfig> | null;
env: NodeJS.ProcessEnv;
providers: Record<string, ProviderConfig>;
}): Promise<void> {
@@ -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,
});