From a3a5cad7d724aaede6ed0b2c8d764a970e2ba6ac Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 21 Mar 2026 07:42:32 -0700 Subject: [PATCH] fix(onboarding): hide image-only auth providers --- extensions/fal/index.ts | 1 + extensions/fal/openclaw.plugin.json | 1 + src/commands/auth-choice-options.test.ts | 46 ++++++++++++++++++++ src/commands/auth-choice-options.ts | 51 ++++++++++++++++------- src/plugins/manifest.ts | 12 ++++++ src/plugins/provider-auth-choices.test.ts | 2 + src/plugins/provider-auth-choices.ts | 2 + src/plugins/provider-validation.ts | 17 ++++++++ src/plugins/provider-wizard.test.ts | 35 ++++++++++++++++ src/plugins/provider-wizard.ts | 2 + src/plugins/types.ts | 5 +++ 11 files changed, 158 insertions(+), 16 deletions(-) diff --git a/extensions/fal/index.ts b/extensions/fal/index.ts index e1eaf0a9c36..8c1db68f391 100644 --- a/extensions/fal/index.ts +++ b/extensions/fal/index.ts @@ -35,6 +35,7 @@ export default definePluginEntry({ groupId: "fal", groupLabel: "fal", groupHint: "Image generation", + onboardingScopes: ["image-generation"], }, }), ], diff --git a/extensions/fal/openclaw.plugin.json b/extensions/fal/openclaw.plugin.json index 52128c23fac..d7f7e12f677 100644 --- a/extensions/fal/openclaw.plugin.json +++ b/extensions/fal/openclaw.plugin.json @@ -13,6 +13,7 @@ "groupId": "fal", "groupLabel": "fal", "groupHint": "Image generation", + "onboardingScopes": ["image-generation"], "optionKey": "falApiKey", "cliFlag": "--fal-api-key", "cliOption": "--fal-api-key ", diff --git a/src/commands/auth-choice-options.test.ts b/src/commands/auth-choice-options.test.ts index 933d998bc6e..7e97e7b890f 100644 --- a/src/commands/auth-choice-options.test.ts +++ b/src/commands/auth-choice-options.test.ts @@ -354,4 +354,50 @@ describe("buildAuthChoiceOptions", () => { expect(ollamaGroup).toBeDefined(); expect(ollamaGroup?.options.some((opt) => opt.value === "ollama")).toBe(true); }); + + it("hides image-generation-only providers from the interactive auth picker", () => { + resolveManifestProviderAuthChoices.mockReturnValue([ + { + pluginId: "fal", + providerId: "fal", + methodId: "api-key", + choiceId: "fal-api-key", + choiceLabel: "fal API key", + groupId: "fal", + groupLabel: "fal", + onboardingScopes: ["image-generation"], + }, + { + pluginId: "openai", + providerId: "openai", + methodId: "api-key", + choiceId: "openai-api-key", + choiceLabel: "OpenAI API key", + groupId: "openai", + groupLabel: "OpenAI", + }, + ]); + resolveProviderWizardOptions.mockReturnValue([ + { + value: "local-image-runtime", + label: "Local image runtime", + groupId: "local-image-runtime", + groupLabel: "Local image runtime", + onboardingScopes: ["image-generation"], + }, + { + value: "ollama", + label: "Ollama", + groupId: "ollama", + groupLabel: "Ollama", + }, + ]); + + const options = getOptions(); + + expect(options.some((option) => option.value === "openai-api-key")).toBe(true); + expect(options.some((option) => option.value === "ollama")).toBe(true); + expect(options.some((option) => option.value === "fal-api-key")).toBe(false); + expect(options.some((option) => option.value === "local-image-runtime")).toBe(false); + }); }); diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts index 03bfa86749a..ea7431bffcf 100644 --- a/src/commands/auth-choice-options.ts +++ b/src/commands/auth-choice-options.ts @@ -10,6 +10,17 @@ import { } from "./auth-choice-options.static.js"; import type { AuthChoice, AuthChoiceGroupId } from "./onboard-types.js"; +const DEFAULT_AUTH_CHOICE_ONBOARDING_SCOPE = "text-inference" as const; + +function includesOnboardingScope( + onboardingScopes: readonly ("text-inference" | "image-generation")[] | undefined, + scope: "text-inference" | "image-generation", +): boolean { + return onboardingScopes + ? onboardingScopes.includes(scope) + : scope === DEFAULT_AUTH_CHOICE_ONBOARDING_SCOPE; +} + function compareOptionLabels(a: AuthChoiceOption, b: AuthChoiceOption): number { return a.label.localeCompare(b.label); } @@ -23,14 +34,18 @@ function resolveManifestProviderChoiceOptions(params?: { workspaceDir?: string; env?: NodeJS.ProcessEnv; }): AuthChoiceOption[] { - return resolveManifestProviderAuthChoices(params ?? {}).map((choice) => ({ - value: choice.choiceId as AuthChoice, - label: choice.choiceLabel, - ...(choice.choiceHint ? { hint: choice.choiceHint } : {}), - ...(choice.groupId ? { groupId: choice.groupId as AuthChoiceGroupId } : {}), - ...(choice.groupLabel ? { groupLabel: choice.groupLabel } : {}), - ...(choice.groupHint ? { groupHint: choice.groupHint } : {}), - })); + return resolveManifestProviderAuthChoices(params ?? {}) + .filter((choice) => + includesOnboardingScope(choice.onboardingScopes, DEFAULT_AUTH_CHOICE_ONBOARDING_SCOPE), + ) + .map((choice) => ({ + value: choice.choiceId as AuthChoice, + label: choice.choiceLabel, + ...(choice.choiceHint ? { hint: choice.choiceHint } : {}), + ...(choice.groupId ? { groupId: choice.groupId as AuthChoiceGroupId } : {}), + ...(choice.groupLabel ? { groupLabel: choice.groupLabel } : {}), + ...(choice.groupHint ? { groupHint: choice.groupHint } : {}), + })); } function resolveRuntimeFallbackProviderChoiceOptions(params?: { @@ -38,14 +53,18 @@ function resolveRuntimeFallbackProviderChoiceOptions(params?: { workspaceDir?: string; env?: NodeJS.ProcessEnv; }): AuthChoiceOption[] { - return resolveProviderWizardOptions(params ?? {}).map((option) => ({ - value: option.value as AuthChoice, - label: option.label, - ...(option.hint ? { hint: option.hint } : {}), - groupId: option.groupId as AuthChoiceGroupId, - groupLabel: option.groupLabel, - ...(option.groupHint ? { groupHint: option.groupHint } : {}), - })); + return resolveProviderWizardOptions(params ?? {}) + .filter((option) => + includesOnboardingScope(option.onboardingScopes, DEFAULT_AUTH_CHOICE_ONBOARDING_SCOPE), + ) + .map((option) => ({ + value: option.value as AuthChoice, + label: option.label, + ...(option.hint ? { hint: option.hint } : {}), + groupId: option.groupId as AuthChoiceGroupId, + groupLabel: option.groupLabel, + ...(option.groupHint ? { groupHint: option.groupHint } : {}), + })); } export function formatAuthChoiceChoicesForCli(params?: { diff --git a/src/plugins/manifest.ts b/src/plugins/manifest.ts index a75a2a9b6ab..6498c8eb876 100644 --- a/src/plugins/manifest.ts +++ b/src/plugins/manifest.ts @@ -48,8 +48,15 @@ export type PluginManifestProviderAuthChoice = { cliFlag?: string; cliOption?: string; cliDescription?: string; + /** + * Interactive onboarding surfaces where this auth choice should appear. + * Defaults to `["text-inference"]` when omitted. + */ + onboardingScopes?: PluginManifestOnboardingScope[]; }; +export type PluginManifestOnboardingScope = "text-inference" | "image-generation"; + export type PluginManifestLoadResult = | { ok: true; manifest: PluginManifest; manifestPath: string } | { ok: false; error: string; manifestPath: string }; @@ -107,6 +114,10 @@ function normalizeProviderAuthChoices( const cliOption = typeof entry.cliOption === "string" ? entry.cliOption.trim() : ""; const cliDescription = typeof entry.cliDescription === "string" ? entry.cliDescription.trim() : ""; + const onboardingScopes = normalizeStringList(entry.onboardingScopes).filter( + (scope): scope is PluginManifestOnboardingScope => + scope === "text-inference" || scope === "image-generation", + ); normalized.push({ provider, method, @@ -120,6 +131,7 @@ function normalizeProviderAuthChoices( ...(cliFlag ? { cliFlag } : {}), ...(cliOption ? { cliOption } : {}), ...(cliDescription ? { cliDescription } : {}), + ...(onboardingScopes.length > 0 ? { onboardingScopes } : {}), }); } return normalized.length > 0 ? normalized : undefined; diff --git a/src/plugins/provider-auth-choices.test.ts b/src/plugins/provider-auth-choices.test.ts index 0b9c9ccb5d2..25b4a017a2b 100644 --- a/src/plugins/provider-auth-choices.test.ts +++ b/src/plugins/provider-auth-choices.test.ts @@ -24,6 +24,7 @@ describe("provider auth choice manifest helpers", () => { method: "api-key", choiceId: "openai-api-key", choiceLabel: "OpenAI API key", + onboardingScopes: ["text-inference"], optionKey: "openaiApiKey", cliFlag: "--openai-api-key", cliOption: "--openai-api-key ", @@ -40,6 +41,7 @@ describe("provider auth choice manifest helpers", () => { methodId: "api-key", choiceId: "openai-api-key", choiceLabel: "OpenAI API key", + onboardingScopes: ["text-inference"], optionKey: "openaiApiKey", cliFlag: "--openai-api-key", cliOption: "--openai-api-key ", diff --git a/src/plugins/provider-auth-choices.ts b/src/plugins/provider-auth-choices.ts index 86388d9b6e5..fd52c7e80a5 100644 --- a/src/plugins/provider-auth-choices.ts +++ b/src/plugins/provider-auth-choices.ts @@ -16,6 +16,7 @@ export type ProviderAuthChoiceMetadata = { cliFlag?: string; cliOption?: string; cliDescription?: string; + onboardingScopes?: ("text-inference" | "image-generation")[]; }; export type ProviderOnboardAuthFlag = { @@ -52,6 +53,7 @@ export function resolveManifestProviderAuthChoices(params?: { ...(choice.cliFlag ? { cliFlag: choice.cliFlag } : {}), ...(choice.cliOption ? { cliOption: choice.cliOption } : {}), ...(choice.cliDescription ? { cliDescription: choice.cliDescription } : {}), + ...(choice.onboardingScopes ? { onboardingScopes: choice.onboardingScopes } : {}), })), ); } diff --git a/src/plugins/provider-validation.ts b/src/plugins/provider-validation.ts index f53abc8bd6d..836b8782eff 100644 --- a/src/plugins/provider-validation.ts +++ b/src/plugins/provider-validation.ts @@ -27,6 +27,20 @@ function normalizeTextList(values: string[] | undefined): string[] | undefined { return normalized.length > 0 ? normalized : undefined; } +function normalizeOnboardingScopes( + values: Array<"text-inference" | "image-generation"> | undefined, +): Array<"text-inference" | "image-generation"> | undefined { + const normalized = Array.from( + new Set( + (values ?? []).filter( + (value): value is "text-inference" | "image-generation" => + value === "text-inference" || value === "image-generation", + ), + ), + ); + return normalized.length > 0 ? normalized : undefined; +} + function normalizeProviderWizardSetup(params: { providerId: string; pluginId: string; @@ -79,6 +93,9 @@ function normalizeProviderWizardSetup(params: { ? { groupHint: normalizeText(params.setup.groupHint) } : {}), ...(methodId && params.auth.some((method) => method.id === methodId) ? { methodId } : {}), + ...(normalizeOnboardingScopes(params.setup.onboardingScopes) + ? { onboardingScopes: normalizeOnboardingScopes(params.setup.onboardingScopes) } + : {}), ...(params.setup.modelAllowlist ? { modelAllowlist: { diff --git a/src/plugins/provider-wizard.test.ts b/src/plugins/provider-wizard.test.ts index d7b8348f9b2..c4d4fa55811 100644 --- a/src/plugins/provider-wizard.test.ts +++ b/src/plugins/provider-wizard.test.ts @@ -79,6 +79,7 @@ describe("provider wizard boundaries", () => { choiceLabel: "OpenAI API key", groupId: "openai", groupLabel: "OpenAI", + onboardingScopes: ["text-inference"], }, run: vi.fn(), }, @@ -92,6 +93,7 @@ describe("provider wizard boundaries", () => { label: "OpenAI API key", groupId: "openai", groupLabel: "OpenAI", + onboardingScopes: ["text-inference"], }, ]); expect( @@ -106,6 +108,39 @@ describe("provider wizard boundaries", () => { }); }); + it("preserves onboarding scopes on wizard options", () => { + const provider = makeProvider({ + id: "fal", + label: "fal", + auth: [ + { + id: "api-key", + label: "fal API key", + kind: "api_key", + wizard: { + choiceId: "fal-api-key", + choiceLabel: "fal API key", + groupId: "fal", + groupLabel: "fal", + onboardingScopes: ["image-generation"], + }, + run: vi.fn(), + }, + ], + }); + resolvePluginProviders.mockReturnValue([provider]); + + expect(resolveProviderWizardOptions({})).toEqual([ + { + value: "fal-api-key", + label: "fal API key", + groupId: "fal", + groupLabel: "fal", + onboardingScopes: ["image-generation"], + }, + ]); + }); + it("returns method wizard metadata for canonical choices", () => { const provider = makeProvider({ id: "anthropic", diff --git a/src/plugins/provider-wizard.ts b/src/plugins/provider-wizard.ts index 0b95a07f2b5..05bb364e9a8 100644 --- a/src/plugins/provider-wizard.ts +++ b/src/plugins/provider-wizard.ts @@ -20,6 +20,7 @@ export type ProviderWizardOption = { groupId: string; groupLabel: string; groupHint?: string; + onboardingScopes?: Array<"text-inference" | "image-generation">; }; export type ProviderModelPickerEntry = { @@ -88,6 +89,7 @@ function buildSetupOptionForMethod(params: { groupId: normalizedGroupId, groupLabel: params.wizard.groupLabel?.trim() || params.provider.label, groupHint: params.wizard.groupHint?.trim(), + ...(params.wizard.onboardingScopes ? { onboardingScopes: params.wizard.onboardingScopes } : {}), }; } diff --git a/src/plugins/types.ts b/src/plugins/types.ts index db27be43007..9b902944f0a 100644 --- a/src/plugins/types.ts +++ b/src/plugins/types.ts @@ -593,6 +593,11 @@ export type ProviderPluginWizardSetup = { groupLabel?: string; groupHint?: string; methodId?: string; + /** + * Interactive onboarding surfaces where this auth choice should appear. + * Defaults to `["text-inference"]` when omitted. + */ + onboardingScopes?: Array<"text-inference" | "image-generation">; /** * Optional model-allowlist prompt policy applied after this auth choice is * selected in configure/onboarding flows.