diff --git a/docs/plugins/architecture-internals.md b/docs/plugins/architecture-internals.md index 89b3fd9458e..75e831ad0fa 100644 --- a/docs/plugins/architecture-internals.md +++ b/docs/plugins/architecture-internals.md @@ -72,8 +72,8 @@ or fallback behavior without changing runtime loading semantics. Setup discovery now prefers descriptor-owned ids such as `setup.providers` and `setup.cliBackends` to narrow candidate plugins before it falls back to `setup-api` for plugins that still need setup-time runtime hooks. Provider -setup flow uses manifest `providerAuthChoices` first, then falls back to -runtime wizard choices and install-catalog choices for compatibility. Explicit +setup lists use manifest `providerAuthChoices`, descriptor-derived setup +choices, and install-catalog metadata without loading provider runtime. Explicit `setup.requiresRuntime: false` is a descriptor-only cutoff; omitted `requiresRuntime` keeps the legacy setup-api fallback for compatibility. If more than one discovered plugin claims the same normalized setup provider or CLI diff --git a/src/commands/model-picker.runtime.ts b/src/commands/model-picker.runtime.ts index c77260956ee..11918f689cb 100644 --- a/src/commands/model-picker.runtime.ts +++ b/src/commands/model-picker.runtime.ts @@ -1,7 +1,7 @@ import { resolveProviderModelPickerFlowContributions, resolveProviderModelPickerFlowEntries, -} from "../flows/provider-flow.js"; +} from "../flows/provider-flow.runtime.js"; import { runProviderPluginAuthMethod } from "../plugins/provider-auth-choice.js"; import { resolveProviderPluginChoice, diff --git a/src/flows/provider-flow.runtime.ts b/src/flows/provider-flow.runtime.ts new file mode 100644 index 00000000000..1fdba8c7fd4 --- /dev/null +++ b/src/flows/provider-flow.runtime.ts @@ -0,0 +1,79 @@ +import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { + resolveProviderModelPickerEntries, + type ProviderModelPickerEntry, +} from "../plugins/provider-wizard.js"; +import { resolvePluginProviders } from "../plugins/providers.runtime.js"; +import type { ProviderPlugin } from "../plugins/types.js"; +import { normalizeOptionalString } from "../shared/string-coerce.js"; +import type { FlowContribution } from "./types.js"; +import { sortFlowContributionsByLabel } from "./types.js"; + +export type ProviderModelPickerFlowEntry = ProviderModelPickerEntry; + +export type ProviderModelPickerFlowContribution = FlowContribution & { + kind: "provider"; + surface: "model-picker"; + providerId: string; + option: ProviderModelPickerFlowEntry; + source: "runtime"; +}; + +function resolveProviderDocsById(params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): Map { + return new Map( + resolvePluginProviders({ + config: params?.config, + workspaceDir: params?.workspaceDir, + env: params?.env, + mode: "setup", + }) + .filter((provider): provider is ProviderPlugin & { docsPath: string } => + Boolean(normalizeOptionalString(provider.docsPath)), + ) + .map((provider) => [provider.id, normalizeOptionalString(provider.docsPath)!]), + ); +} + +export function resolveProviderModelPickerFlowEntries(params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): ProviderModelPickerFlowEntry[] { + return resolveProviderModelPickerFlowContributions(params).map( + (contribution) => contribution.option, + ); +} + +export function resolveProviderModelPickerFlowContributions(params?: { + config?: OpenClawConfig; + workspaceDir?: string; + env?: NodeJS.ProcessEnv; +}): ProviderModelPickerFlowContribution[] { + const docsByProvider = resolveProviderDocsById(params ?? {}); + return sortFlowContributionsByLabel( + resolveProviderModelPickerEntries(params ?? {}).map((entry) => { + const providerId = entry.value.startsWith("provider-plugin:") + ? entry.value.slice("provider-plugin:".length).split(":")[0] + : entry.value; + return { + id: `provider:model-picker:${entry.value}`, + kind: "provider" as const, + surface: "model-picker" as const, + providerId, + option: { + value: entry.value, + label: entry.label, + ...(entry.hint ? { hint: entry.hint } : {}), + ...(docsByProvider.get(providerId) + ? { docs: { path: docsByProvider.get(providerId)! } } + : {}), + }, + source: "runtime" as const, + }; + }), + ); +} diff --git a/src/flows/provider-flow.test.ts b/src/flows/provider-flow.test.ts index 72957e8615f..9302677f6d5 100644 --- a/src/flows/provider-flow.test.ts +++ b/src/flows/provider-flow.test.ts @@ -41,10 +41,8 @@ vi.mock("../plugins/providers.runtime.js", () => ({ resolvePluginProviders, })); -import { - resolveProviderModelPickerFlowContributions, - resolveProviderSetupFlowContributions, -} from "./provider-flow.js"; +import { resolveProviderSetupFlowContributions } from "./provider-flow.js"; +import { resolveProviderModelPickerFlowContributions } from "./provider-flow.runtime.js"; describe("provider flow install catalog contributions", () => { beforeEach(() => { @@ -106,9 +104,11 @@ describe("provider flow install catalog contributions", () => { includeUntrustedWorkspacePlugins: false, }), ); + expect(resolveProviderWizardOptions).not.toHaveBeenCalled(); + expect(resolvePluginProviders).not.toHaveBeenCalled(); }); - it("prefers manifest setup contributions over duplicate runtime and install-catalog entries", () => { + it("prefers manifest setup contributions over duplicate install-catalog entries", () => { resolveManifestProviderAuthChoices.mockReturnValue([ { pluginId: "openai", @@ -118,14 +118,6 @@ describe("provider flow install catalog contributions", () => { choiceLabel: "OpenAI API key", }, ]); - resolveProviderWizardOptions.mockReturnValue([ - { - value: "openai-api-key", - label: "Runtime OpenAI API key", - groupId: "openai", - groupLabel: "OpenAI", - }, - ]); resolveProviderInstallCatalogEntries.mockReturnValue([ { pluginId: "openai", @@ -159,6 +151,7 @@ describe("provider flow install catalog contributions", () => { source: "manifest", }, ]); + expect(resolveProviderWizardOptions).not.toHaveBeenCalled(); }); it("surfaces install-catalog provider choices when runtime setup options are absent", () => { @@ -299,7 +292,7 @@ describe("provider flow install catalog contributions", () => { ).toEqual([]); }); - it("prefers runtime setup contributions over duplicate install-catalog entries", () => { + it("keeps setup contributions on cold metadata instead of runtime wizard options", () => { resolveProviderWizardOptions.mockReturnValue([ { value: "openai-api-key", @@ -331,6 +324,7 @@ describe("provider flow install catalog contributions", () => { kind: "provider", surface: "setup", providerId: "openai", + pluginId: "openai", option: { value: "openai-api-key", label: "OpenAI API key", @@ -339,9 +333,11 @@ describe("provider flow install catalog contributions", () => { label: "OpenAI", }, }, - source: "runtime", + source: "install-catalog", }, ]); + expect(resolveProviderWizardOptions).not.toHaveBeenCalled(); + expect(resolvePluginProviders).not.toHaveBeenCalled(); }); it("keeps docs attached to runtime model-picker contributions", () => { diff --git a/src/flows/provider-flow.ts b/src/flows/provider-flow.ts index ad38e5fa45f..97f45fc287f 100644 --- a/src/flows/provider-flow.ts +++ b/src/flows/provider-flow.ts @@ -2,13 +2,6 @@ import type { OpenClawConfig } from "../config/types.openclaw.js"; import { normalizePluginsConfig, resolveEffectiveEnableState } from "../plugins/config-state.js"; import { resolveManifestProviderAuthChoices } from "../plugins/provider-auth-choices.js"; import { resolveProviderInstallCatalogEntries } from "../plugins/provider-install-catalog.js"; -import { - resolveProviderModelPickerEntries, - resolveProviderWizardOptions, -} from "../plugins/provider-wizard.js"; -import { resolvePluginProviders } from "../plugins/providers.runtime.js"; -import type { ProviderPlugin } from "../plugins/types.js"; -import { normalizeOptionalString } from "../shared/string-coerce.js"; import type { FlowContribution, FlowOption } from "./types.js"; import { sortFlowContributionsByLabel } from "./types.js"; @@ -29,15 +22,7 @@ export type ProviderSetupFlowContribution = FlowContribution & { pluginId?: string; option: ProviderSetupFlowOption; onboardingScopes?: ProviderFlowScope[]; - source: "manifest" | "runtime" | "install-catalog"; -}; - -export type ProviderModelPickerFlowContribution = FlowContribution & { - kind: "provider"; - surface: "model-picker"; - providerId: string; - option: ProviderModelPickerFlowEntry; - source: "runtime"; + source: "manifest" | "install-catalog"; }; function includesProviderFlowScope( @@ -47,25 +32,6 @@ function includesProviderFlowScope( return scopes ? scopes.includes(scope) : scope === DEFAULT_PROVIDER_FLOW_SCOPE; } -function resolveProviderDocsById(params?: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; -}): Map { - return new Map( - resolvePluginProviders({ - config: params?.config, - workspaceDir: params?.workspaceDir, - env: params?.env, - mode: "setup", - }) - .filter((provider): provider is ProviderPlugin & { docsPath: string } => - Boolean(normalizeOptionalString(provider.docsPath)), - ) - .map((provider) => [provider.id, normalizeOptionalString(provider.docsPath)!]), - ); -} - function resolveInstallCatalogProviderSetupFlowContributions(params?: { config?: OpenClawConfig; workspaceDir?: string; @@ -174,7 +140,6 @@ export function resolveProviderSetupFlowContributions(params?: { scope?: ProviderFlowScope; }): ProviderSetupFlowContribution[] { const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE; - const docsByProvider = resolveProviderDocsById(params ?? {}); const manifestContributions = resolveManifestProviderSetupFlowContributions({ ...params, scope, @@ -182,92 +147,11 @@ export function resolveProviderSetupFlowContributions(params?: { const seenOptionValues = new Set( manifestContributions.map((contribution) => contribution.option.value), ); - const runtimeContributions = resolveProviderWizardOptions(params ?? {}) - .filter((option) => includesProviderFlowScope(option.onboardingScopes, scope)) - .filter((option) => !seenOptionValues.has(option.value)) - .map((option) => - Object.assign( - { - id: `provider:setup:${option.value}`, - kind: `provider` as const, - surface: `setup` as const, - providerId: option.groupId, - option: { - value: option.value, - label: option.label, - ...(option.hint ? { hint: option.hint } : {}), - ...(option.assistantPriority !== undefined - ? { assistantPriority: option.assistantPriority } - : {}), - ...(option.assistantVisibility - ? { assistantVisibility: option.assistantVisibility } - : {}), - group: { - id: option.groupId, - label: option.groupLabel, - ...(option.groupHint ? { hint: option.groupHint } : {}), - }, - ...(docsByProvider.get(option.groupId) - ? { docs: { path: docsByProvider.get(option.groupId)! } } - : {}), - }, - }, - option.onboardingScopes ? { onboardingScopes: [...option.onboardingScopes] } : {}, - { source: `runtime` as const }, - ), - ); - for (const contribution of runtimeContributions) { - seenOptionValues.add(contribution.option.value); - } const installCatalogContributions = resolveInstallCatalogProviderSetupFlowContributions({ ...params, scope, }).filter((contribution) => !seenOptionValues.has(contribution.option.value)); - return sortFlowContributionsByLabel([ - ...manifestContributions, - ...runtimeContributions, - ...installCatalogContributions, - ]); -} - -export function resolveProviderModelPickerFlowEntries(params?: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; -}): ProviderModelPickerFlowEntry[] { - return resolveProviderModelPickerFlowContributions(params).map( - (contribution) => contribution.option, - ); -} - -export function resolveProviderModelPickerFlowContributions(params?: { - config?: OpenClawConfig; - workspaceDir?: string; - env?: NodeJS.ProcessEnv; -}): ProviderModelPickerFlowContribution[] { - const docsByProvider = resolveProviderDocsById(params ?? {}); - return sortFlowContributionsByLabel( - resolveProviderModelPickerEntries(params ?? {}).map((entry) => { - const providerId = entry.value.startsWith("provider-plugin:") - ? entry.value.slice("provider-plugin:".length).split(":")[0] - : entry.value; - return { - id: `provider:model-picker:${entry.value}`, - kind: "provider" as const, - surface: "model-picker" as const, - providerId, - option: { - value: entry.value, - label: entry.label, - ...(entry.hint ? { hint: entry.hint } : {}), - ...(docsByProvider.get(providerId) - ? { docs: { path: docsByProvider.get(providerId)! } } - : {}), - }, - source: "runtime" as const, - }; - }), - ); + return sortFlowContributionsByLabel([...manifestContributions, ...installCatalogContributions]); } export { includesProviderFlowScope };