Files
openclaw/src/agents/models-config.plan.ts
brokemac79 20c7a98fb8 fix(plugins): keep provider discovery metadata-only
Fix startup and per-turn provider registry hot paths by keeping primary-model startup discovery on metadata-only provider entries and by keeping capability provider fallback loads scoped to manifest-derived owners, including explicit empty scopes when no bundled owner exists.

Evidence:
- Reproduces the reported code paths from #73729, #73835, and #73793: startup prewarm was able to enter provider/model discovery that loaded plugin runtime, and capability lookups could bypass active registry reuse or broaden fallback registry loads.
- Fix threads providerDiscoveryEntriesOnly through models-config planning into plugin discovery.
- Fix reuses active non-memory/non-speech capability providers even with explicit plugins.entries.
- Fix keeps fallback registry loads scoped with onlyPluginIds, including [] for no-owner media capability checks.
- Local targeted tests passed for gateway startup, models config, provider discovery, capability providers, and web provider runtimes.
- Testbox pnpm check:changed passed.
- Testbox pnpm build passed.
- GitHub CI required checks passed on e5e6fe1d52.

Fixes #73729.
Fixes #73835.
Fixes #73793.
Supersedes #73794.
2026-04-29 07:52:32 +01:00

195 lines
6.4 KiB
TypeScript

import type { OpenClawConfig } from "../config/types.openclaw.js";
import type { PluginMetadataSnapshot } from "../plugins/plugin-metadata-snapshot.js";
import { isRecord } from "../utils.js";
import {
mergeProviders,
mergeWithExistingProviderSecrets,
type ExistingProviderConfig,
} from "./models-config.merge.js";
import {
applyNativeStreamingUsageCompat,
enforceSourceManagedProviderSecrets,
normalizeProviders,
resolveImplicitProviders,
type ProviderConfig,
} from "./models-config.providers.js";
type ModelsConfig = NonNullable<OpenClawConfig["models"]>;
export type ResolveImplicitProvidersForModelsJson = (params: {
agentDir: string;
config: OpenClawConfig;
env: NodeJS.ProcessEnv;
workspaceDir?: string;
explicitProviders: Record<string, ProviderConfig>;
pluginMetadataSnapshot?: Pick<PluginMetadataSnapshot, "index" | "manifestRegistry" | "owners">;
providerDiscoveryProviderIds?: readonly string[];
providerDiscoveryTimeoutMs?: number;
providerDiscoveryEntriesOnly?: boolean;
}) => Promise<Record<string, ProviderConfig>>;
export type ModelsJsonPlan =
| {
action: "skip";
}
| {
action: "noop";
}
| {
action: "write";
contents: string;
};
export async function resolveProvidersForModelsJsonWithDeps(
params: {
cfg: OpenClawConfig;
agentDir: string;
env: NodeJS.ProcessEnv;
workspaceDir?: string;
pluginMetadataSnapshot?: Pick<PluginMetadataSnapshot, "index" | "manifestRegistry" | "owners">;
providerDiscoveryProviderIds?: readonly string[];
providerDiscoveryTimeoutMs?: number;
providerDiscoveryEntriesOnly?: boolean;
},
deps?: {
resolveImplicitProviders?: ResolveImplicitProvidersForModelsJson;
},
): Promise<Record<string, ProviderConfig>> {
const { cfg, agentDir, env } = params;
const explicitProviders = cfg.models?.providers ?? {};
const resolveImplicitProvidersImpl = deps?.resolveImplicitProviders ?? resolveImplicitProviders;
const implicitProviders = await resolveImplicitProvidersImpl({
agentDir,
config: cfg,
env,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
explicitProviders,
...(params.pluginMetadataSnapshot
? { pluginMetadataSnapshot: params.pluginMetadataSnapshot }
: {}),
...(params.providerDiscoveryProviderIds
? { providerDiscoveryProviderIds: params.providerDiscoveryProviderIds }
: {}),
...(params.providerDiscoveryTimeoutMs !== undefined
? { providerDiscoveryTimeoutMs: params.providerDiscoveryTimeoutMs }
: {}),
...(params.providerDiscoveryEntriesOnly === true ? { providerDiscoveryEntriesOnly: true } : {}),
});
return mergeProviders({
implicit: implicitProviders,
explicit: explicitProviders,
});
}
function resolveProvidersForMode(params: {
mode: NonNullable<ModelsConfig["mode"]>;
existingParsed: unknown;
providers: Record<string, ProviderConfig>;
secretRefManagedProviders: ReadonlySet<string>;
}): Record<string, ProviderConfig> {
if (params.mode !== "merge") {
return params.providers;
}
const existing = params.existingParsed;
if (!isRecord(existing) || !isRecord(existing.providers)) {
return params.providers;
}
const existingProviders = existing.providers as Record<
string,
NonNullable<ModelsConfig["providers"]>[string]
>;
return mergeWithExistingProviderSecrets({
nextProviders: params.providers,
existingProviders: existingProviders as Record<string, ExistingProviderConfig>,
secretRefManagedProviders: params.secretRefManagedProviders,
});
}
export async function planOpenClawModelsJsonWithDeps(
params: {
cfg: OpenClawConfig;
sourceConfigForSecrets?: OpenClawConfig;
agentDir: string;
env: NodeJS.ProcessEnv;
workspaceDir?: string;
existingRaw: string;
existingParsed: unknown;
pluginMetadataSnapshot?: Pick<PluginMetadataSnapshot, "index" | "manifestRegistry" | "owners">;
providerDiscoveryProviderIds?: readonly string[];
providerDiscoveryTimeoutMs?: number;
providerDiscoveryEntriesOnly?: boolean;
},
deps?: {
resolveImplicitProviders?: ResolveImplicitProvidersForModelsJson;
},
): Promise<ModelsJsonPlan> {
const { cfg, agentDir, env } = params;
const providers = await resolveProvidersForModelsJsonWithDeps(
{
cfg,
agentDir,
env,
...(params.workspaceDir ? { workspaceDir: params.workspaceDir } : {}),
...(params.pluginMetadataSnapshot
? { pluginMetadataSnapshot: params.pluginMetadataSnapshot }
: {}),
...(params.providerDiscoveryProviderIds
? { providerDiscoveryProviderIds: params.providerDiscoveryProviderIds }
: {}),
...(params.providerDiscoveryTimeoutMs !== undefined
? { providerDiscoveryTimeoutMs: params.providerDiscoveryTimeoutMs }
: {}),
...(params.providerDiscoveryEntriesOnly === true
? { providerDiscoveryEntriesOnly: true }
: {}),
},
deps,
);
if (Object.keys(providers).length === 0) {
return { action: "skip" };
}
const mode = cfg.models?.mode ?? "merge";
const secretRefManagedProviders = new Set<string>();
const normalizedProviders =
normalizeProviders({
providers,
agentDir,
env,
secretDefaults: cfg.secrets?.defaults,
sourceProviders: params.sourceConfigForSecrets?.models?.providers,
sourceSecretDefaults: params.sourceConfigForSecrets?.secrets?.defaults,
secretRefManagedProviders,
}) ?? providers;
const mergedProviders = resolveProvidersForMode({
mode,
existingParsed: params.existingParsed,
providers: normalizedProviders,
secretRefManagedProviders,
});
const secretEnforcedProviders =
enforceSourceManagedProviderSecrets({
providers: mergedProviders,
sourceProviders: params.sourceConfigForSecrets?.models?.providers,
sourceSecretDefaults: params.sourceConfigForSecrets?.secrets?.defaults,
secretRefManagedProviders,
}) ?? mergedProviders;
const finalProviders = applyNativeStreamingUsageCompat(secretEnforcedProviders);
const nextContents = `${JSON.stringify({ providers: finalProviders }, null, 2)}\n`;
if (params.existingRaw === nextContents) {
return { action: "noop" };
}
return {
action: "write",
contents: nextContents,
};
}
export async function planOpenClawModelsJson(
params: Parameters<typeof planOpenClawModelsJsonWithDeps>[0],
): Promise<ModelsJsonPlan> {
return planOpenClawModelsJsonWithDeps(params);
}