fix(onboarding): hide image-only auth providers

This commit is contained in:
Vincent Koc
2026-03-21 07:42:32 -07:00
parent f10d054745
commit a3a5cad7d7
11 changed files with 158 additions and 16 deletions

View File

@@ -35,6 +35,7 @@ export default definePluginEntry({
groupId: "fal",
groupLabel: "fal",
groupHint: "Image generation",
onboardingScopes: ["image-generation"],
},
}),
],

View File

@@ -13,6 +13,7 @@
"groupId": "fal",
"groupLabel": "fal",
"groupHint": "Image generation",
"onboardingScopes": ["image-generation"],
"optionKey": "falApiKey",
"cliFlag": "--fal-api-key",
"cliOption": "--fal-api-key <key>",

View File

@@ -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);
});
});

View File

@@ -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?: {

View File

@@ -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;

View File

@@ -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 <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 <key>",

View File

@@ -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 } : {}),
})),
);
}

View File

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

View File

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

View File

@@ -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 } : {}),
};
}

View File

@@ -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.