fix: start configured generation providers

This commit is contained in:
Peter Steinberger
2026-05-05 05:03:34 +01:00
parent 68a500c465
commit 0eb06caae3
3 changed files with 194 additions and 0 deletions

View File

@@ -64,6 +64,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- WhatsApp/onboarding: canonicalize setup and pairing allowlist entries to WhatsApp's digit-only phone ids while still accepting E.164, JID, and `whatsapp:` inputs, so personal-phone allowlists match WhatsApp Web sender ids after setup. Thanks @vincentkoc.
- Gateway/startup: load provider plugins that own explicitly configured image, video, or music generation defaults so generation tools become live after gateway restart instead of remaining catalog-only. Fixes #77244. Thanks @buyuangtampan, @Nikoxx99, and @vincentkoc.
- Slack/subagents: keep resumed parent `message.send` calls in the originating Slack thread when ambient session thread context is present, and suppress successful silent child completion rows from follow-up findings. Thanks @bek91.
- Infra/Windows: skip the POSIX `/tmp/openclaw` preferred path on Windows in `resolvePreferredOpenClawTmpDir` so log files, TTS temp files, and other writes land in `%TEMP%\openclaw-<uid>` instead of `C:\tmp\openclaw`. Fixes #60713. Thanks @juan-flores077.
- Gateway/diagnostics: make stuck-session recovery outcome-driven and generation-guarded, add `diagnostics.stuckSessionAbortMs`, and emit structured recovery requested/completed events so stale or skipped recovery no longer looks like a successful abort.

View File

@@ -164,6 +164,10 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
enabledByDefault: true,
providers: ["openai", "openai-codex"],
cliBackends: ["codex-cli"],
contracts: {
imageGenerationProviders: ["openai"],
videoGenerationProviders: ["openai"],
},
},
{
id: "google",
@@ -172,6 +176,11 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
enabledByDefault: true,
providers: ["google", "google-gemini-cli"],
cliBackends: ["google-gemini-cli"],
contracts: {
imageGenerationProviders: ["google"],
videoGenerationProviders: ["google"],
musicGenerationProviders: ["google"],
},
},
{
id: "codex",
@@ -754,6 +763,53 @@ describe("resolveGatewayStartupPluginIds", () => {
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"includes bundled generation providers configured by media defaults at startup",
{
channels: {},
agents: {
defaults: {
imageGenerationModel: {
primary: "openai/gpt-image-2",
fallbacks: ["google/gemini-3-pro-image-preview"],
},
videoGenerationModel: {
primary: "google/veo-3.1-fast-generate-preview",
},
musicGenerationModel: {
primary: "google/lyria-3-clip-preview",
},
},
},
} as OpenClawConfig,
["browser", "openai", "google", "memory-core"],
],
[
"honors explicit plugin disablement for configured generation providers",
{
channels: {},
agents: {
defaults: {
imageGenerationModel: { primary: "google/gemini-3-pro-image-preview" },
},
},
plugins: { entries: { google: { enabled: false } } },
} as OpenClawConfig,
["browser", "memory-core"],
],
[
"keeps configured generation providers behind restrictive allowlists",
{
channels: {},
agents: {
defaults: {
imageGenerationModel: { primary: "google/gemini-3-pro-image-preview" },
},
},
plugins: { allow: ["browser"] },
} as OpenClawConfig,
["browser"],
],
[
"includes explicitly enabled non-channel sidecars in startup scope",
createStartupConfig({

View File

@@ -39,6 +39,11 @@ export type GatewayStartupPluginPlan = {
};
type NormalizedPluginsConfig = ReturnType<typeof normalizePluginsConfigWithRegistry>;
type GenerationProviderContractKey =
| "imageGenerationProviders"
| "videoGenerationProviders"
| "musicGenerationProviders";
type ConfiguredGenerationProviderIds = Record<GenerationProviderContractKey, ReadonlySet<string>>;
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -209,6 +214,123 @@ function manifestOwnsConfiguredSpeechProvider(params: {
});
}
function listModelProviderRefs(value: unknown): string[] {
if (typeof value === "string") {
return [value];
}
if (!isRecord(value)) {
return [];
}
const refs: string[] = [];
if (typeof value.primary === "string") {
refs.push(value.primary);
}
if (Array.isArray(value.fallbacks)) {
for (const fallback of value.fallbacks) {
if (typeof fallback === "string") {
refs.push(fallback);
}
}
}
return refs;
}
function collectModelProviderIds(value: unknown): ReadonlySet<string> {
return new Set(
listModelProviderRefs(value)
.map((ref) => {
const slashIndex = ref.indexOf("/");
return slashIndex > 0 ? normalizeOptionalLowercaseString(ref.slice(0, slashIndex)) : "";
})
.filter((providerId): providerId is string => Boolean(providerId)),
);
}
function collectConfiguredGenerationProviderIds(
config: OpenClawConfig,
): ConfiguredGenerationProviderIds {
const defaults = config.agents?.defaults;
return {
imageGenerationProviders: collectModelProviderIds(defaults?.imageGenerationModel),
videoGenerationProviders: collectModelProviderIds(defaults?.videoGenerationModel),
musicGenerationProviders: collectModelProviderIds(defaults?.musicGenerationModel),
};
}
function manifestOwnsConfiguredGenerationProvider(params: {
manifest: PluginManifestRecord | undefined;
configuredGenerationProviderIds: ConfiguredGenerationProviderIds;
}): boolean {
for (const contractKey of [
"imageGenerationProviders",
"videoGenerationProviders",
"musicGenerationProviders",
] as const) {
const configuredProviderIds = params.configuredGenerationProviderIds[contractKey];
if (configuredProviderIds.size === 0) {
continue;
}
if (
(params.manifest?.contracts?.[contractKey] ?? []).some((providerId) => {
const normalized = normalizeOptionalLowercaseString(providerId);
return normalized ? configuredProviderIds.has(normalized) : false;
})
) {
return true;
}
}
return false;
}
function canStartConfiguredGenerationProviderPlugin(params: {
plugin: InstalledPluginIndexRecord;
manifest: PluginManifestRecord | undefined;
config: OpenClawConfig;
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
activationSource: {
plugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
rootConfig?: OpenClawConfig;
};
configuredGenerationProviderIds: ConfiguredGenerationProviderIds;
platform?: NodeJS.Platform;
}): boolean {
if (
!manifestOwnsConfiguredGenerationProvider({
manifest: params.manifest,
configuredGenerationProviderIds: params.configuredGenerationProviderIds,
})
) {
return false;
}
if (!params.pluginsConfig.enabled || !params.activationSource.plugins.enabled) {
return false;
}
if (
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
params.activationSource.plugins.deny.includes(params.plugin.pluginId)
) {
return false;
}
if (
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
params.activationSource.plugins.entries[params.plugin.pluginId]?.enabled === false
) {
return false;
}
const activationState = resolveEffectivePluginActivationState({
id: params.plugin.pluginId,
origin: params.plugin.origin,
config: params.pluginsConfig,
rootConfig: params.config,
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin, params.platform),
activationSource: params.activationSource,
});
return (
activationState.enabled &&
(params.plugin.origin === "bundled" || activationState.explicitlyEnabled)
);
}
function canStartConfiguredSpeechProviderPlugin(params: {
plugin: InstalledPluginIndexRecord;
manifest: PluginManifestRecord | undefined;
@@ -512,6 +634,8 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
const manifestLookup = createManifestRegistryLookup(params.manifestRegistry);
const configuredSpeechProviderIds = collectConfiguredSpeechProviderIds(activationSourceConfig);
const configuredGenerationProviderIds =
collectConfiguredGenerationProviderIds(activationSourceConfig);
const normalizePluginId = createPluginRegistryIdNormalizer(params.index, {
manifestRegistry: params.manifestRegistry,
});
@@ -581,6 +705,19 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
) {
return true;
}
if (
canStartConfiguredGenerationProviderPlugin({
plugin,
manifest,
config: params.config,
pluginsConfig,
activationSource,
configuredGenerationProviderIds,
platform: params.platform,
})
) {
return true;
}
if (
canStartExplicitHookPlugin({
plugin,