mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 02:45:38 -06:00
fix(onboard): activate media-only provider authentication (#126711)
* fix(onboard): activate media-only provider authentication * refactor(onboard): keep media model defaults plugin-private
This commit is contained in:
committed by
GitHub
parent
8c6c7a30cf
commit
e50f73d331
@@ -4,7 +4,7 @@ import {
|
||||
resolveAgentModelPrimaryValue,
|
||||
} from "openclaw/plugin-sdk/provider-onboard";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyFalConfig, FAL_DEFAULT_IMAGE_MODEL_REF } from "./onboard.js";
|
||||
import { applyFalConfig } from "./onboard.js";
|
||||
|
||||
const emptyCfg: OpenClawConfig = {};
|
||||
|
||||
@@ -13,7 +13,7 @@ describe("applyFalConfig", () => {
|
||||
const result = applyFalConfig(emptyCfg);
|
||||
|
||||
expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.mediaModels?.image)).toBe(
|
||||
FAL_DEFAULT_IMAGE_MODEL_REF,
|
||||
"fal/fal-ai/flux/dev",
|
||||
);
|
||||
// The retired key must stay untouched: nothing in the runtime reads it.
|
||||
expect(result.agents?.defaults).not.toHaveProperty("imageGenerationModel");
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Fal setup module handles plugin onboarding behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
|
||||
export const FAL_DEFAULT_IMAGE_MODEL_REF = "fal/fal-ai/flux/dev";
|
||||
const FAL_DEFAULT_IMAGE_MODEL_REF = "fal/fal-ai/flux/dev";
|
||||
|
||||
export function applyFalConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
if (cfg.agents?.defaults?.mediaModels?.image) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
|
||||
const PROVIDER_ID = "fal";
|
||||
const FAL_DEFAULT_IMAGE_MODEL_REF = "fal/fal-ai/flux/dev";
|
||||
|
||||
export function createFalProvider(): ProviderPlugin {
|
||||
return {
|
||||
@@ -16,7 +15,7 @@ export function createFalProvider(): ProviderPlugin {
|
||||
kind: "api_key",
|
||||
label: "fal API key",
|
||||
hint: "Image, video, and music generation API key",
|
||||
run: async () => ({ profiles: [], defaultModel: FAL_DEFAULT_IMAGE_MODEL_REF }),
|
||||
run: async () => ({ profiles: [] }),
|
||||
wizard: {
|
||||
choiceId: "fal-api-key",
|
||||
choiceLabel: "fal API key",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Fal provider module implements model/runtime integration.
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { applyFalConfig, FAL_DEFAULT_IMAGE_MODEL_REF } from "./onboard.js";
|
||||
import { applyFalConfig } from "./onboard.js";
|
||||
|
||||
const PROVIDER_ID = "fal";
|
||||
|
||||
@@ -21,7 +21,6 @@ export function createFalProvider(): ProviderPlugin {
|
||||
flagName: "--fal-api-key",
|
||||
envVar: "FAL_KEY",
|
||||
promptMessage: "Enter fal API key",
|
||||
defaultModel: FAL_DEFAULT_IMAGE_MODEL_REF,
|
||||
expectedProviders: ["fal"],
|
||||
applyConfig: (cfg) => applyFalConfig(cfg),
|
||||
wizard: {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
||||
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
|
||||
import { buildVydraImageGenerationProvider } from "./image-generation-provider.js";
|
||||
import { applyVydraConfig, VYDRA_DEFAULT_IMAGE_MODEL_REF } from "./onboard.js";
|
||||
import { applyVydraConfig } from "./onboard.js";
|
||||
import { buildVydraSpeechProvider } from "./speech-provider.js";
|
||||
import { buildVydraVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
|
||||
@@ -28,7 +28,6 @@ export default definePluginEntry({
|
||||
flagName: "--vydra-api-key",
|
||||
envVar: "VYDRA_API_KEY",
|
||||
promptMessage: "Enter Vydra API key",
|
||||
defaultModel: VYDRA_DEFAULT_IMAGE_MODEL_REF,
|
||||
expectedProviders: [PROVIDER_ID],
|
||||
applyConfig: (cfg) => applyVydraConfig(cfg),
|
||||
wizard: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Vydra setup module handles plugin onboarding behavior.
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard";
|
||||
|
||||
export const VYDRA_DEFAULT_IMAGE_MODEL_REF = "vydra/grok-imagine";
|
||||
const VYDRA_DEFAULT_IMAGE_MODEL_REF = "vydra/grok-imagine";
|
||||
|
||||
export function applyVydraConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||
if (cfg.agents?.defaults?.mediaModels?.image) {
|
||||
|
||||
@@ -27,14 +27,14 @@ vi.mock("./auth-choice-legacy.js", () => ({
|
||||
|
||||
function includesOnboardingScope(
|
||||
scopes: readonly ("text-inference" | "image-generation" | "music-generation")[] | undefined,
|
||||
scope: "text-inference" | "image-generation" | "music-generation",
|
||||
scope: "text-inference" | "image-generation" | "music-generation" | "all",
|
||||
): boolean {
|
||||
return scopes ? scopes.includes(scope) : scope === "text-inference";
|
||||
return scope === "all" || (scopes ? scopes.includes(scope) : scope === "text-inference");
|
||||
}
|
||||
|
||||
vi.mock("../flows/provider-flow.js", () => ({
|
||||
resolveProviderSetupFlowContributions: vi.fn(
|
||||
(params?: { scope?: "text-inference" | "image-generation" | "music-generation" }) => {
|
||||
(params?: { scope?: "text-inference" | "image-generation" | "music-generation" | "all" }) => {
|
||||
const scope = params?.scope ?? "text-inference";
|
||||
return [
|
||||
...resolveManifestProviderAuthChoices()
|
||||
@@ -717,7 +717,7 @@ describe("buildAuthChoiceOptions", () => {
|
||||
expect(openCodeValues).toContain("opencode-go");
|
||||
});
|
||||
|
||||
it("hides media-generation-only providers from the interactive auth picker", () => {
|
||||
it("keeps media-generation auth choices available to the CLI but out of the interactive picker", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "fal",
|
||||
@@ -727,6 +727,16 @@ describe("buildAuthChoiceOptions", () => {
|
||||
choiceLabel: "fal API key",
|
||||
groupId: "fal",
|
||||
groupLabel: "fal",
|
||||
onboardingScopes: ["image-generation", "music-generation"],
|
||||
},
|
||||
{
|
||||
pluginId: "vydra",
|
||||
providerId: "vydra",
|
||||
methodId: "api-key",
|
||||
choiceId: "vydra-api-key",
|
||||
choiceLabel: "Vydra API key",
|
||||
groupId: "vydra",
|
||||
groupLabel: "Vydra",
|
||||
onboardingScopes: ["image-generation"],
|
||||
},
|
||||
{
|
||||
@@ -774,12 +784,26 @@ describe("buildAuthChoiceOptions", () => {
|
||||
|
||||
const options = getOptions();
|
||||
const optionValues = options.map((option) => option.value);
|
||||
const cliChoiceValues = formatAuthChoiceChoicesForCli({
|
||||
includeLegacyAliases: false,
|
||||
includeSkip: true,
|
||||
}).split("|");
|
||||
|
||||
expect(optionValues).toContain("openai-api-key");
|
||||
expect(optionValues).toContain("ollama");
|
||||
expect(optionValues).not.toContain("fal-api-key");
|
||||
expect(optionValues).not.toContain("vydra-api-key");
|
||||
expect(optionValues).not.toContain("openrouter-api-key");
|
||||
expect(optionValues).not.toContain("local-image-runtime");
|
||||
expect(optionValues).not.toContain("local-music-runtime");
|
||||
expect(cliChoiceValues).toEqual(
|
||||
expect.arrayContaining([
|
||||
"openai-api-key",
|
||||
"fal-api-key",
|
||||
"vydra-api-key",
|
||||
"openrouter-api-key",
|
||||
]),
|
||||
);
|
||||
expect(cliChoiceValues.filter((choice) => choice === "fal-api-key")).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,10 +82,9 @@ export function formatAuthChoiceChoicesForCli(params?: {
|
||||
}): string {
|
||||
const values = [
|
||||
...formatStaticAuthChoiceChoicesForCli(params).split("|"),
|
||||
...resolveProviderSetupFlowContributions({
|
||||
...params,
|
||||
scope: "text-inference",
|
||||
}).map((contribution) => contribution.option.value),
|
||||
...resolveProviderSetupFlowContributions({ ...params, scope: "all" }).map(
|
||||
(contribution) => contribution.option.value,
|
||||
),
|
||||
];
|
||||
|
||||
return uniqueStrings(values).join("|");
|
||||
|
||||
@@ -287,6 +287,52 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
expect(result).toEqual({ plugins: { allow: ["vllm"] } });
|
||||
});
|
||||
|
||||
it("loads a media setup provider without treating it as a text model provider", async () => {
|
||||
const runtime = createRuntime();
|
||||
const provider = { id: "pixverse", pluginId: "pixverse", label: "PixVerse" };
|
||||
const initialConfig: OpenClawConfig = {
|
||||
agents: { defaults: { model: { primary: "openai/gpt-5.6" } } },
|
||||
};
|
||||
const runNonInteractive = vi.fn(async ({ config }: { config: OpenClawConfig }) => ({
|
||||
...config,
|
||||
agents: {
|
||||
...config.agents,
|
||||
defaults: {
|
||||
...config.agents?.defaults,
|
||||
mediaModels: { video: { primary: "pixverse/pixverse-v5.6" } },
|
||||
},
|
||||
},
|
||||
}));
|
||||
resolvePreferredProviderForAuthChoice.mockResolvedValue("pixverse" as never);
|
||||
resolvePluginProvidersCore.mockImplementation((...args: unknown[]) => {
|
||||
const input = args[0] as { providerRefs?: string[] } | undefined;
|
||||
return (input?.providerRefs?.includes("pixverse") ? [provider] : []) as never;
|
||||
});
|
||||
resolveProviderPluginChoice.mockImplementation((...args: unknown[]) => {
|
||||
const input = args[0] as { providers?: unknown[] } | undefined;
|
||||
return input?.providers?.includes(provider)
|
||||
? { provider, method: { runNonInteractive } }
|
||||
: undefined;
|
||||
});
|
||||
|
||||
const result = await applyNonInteractivePluginProviderChoice({
|
||||
nextConfig: initialConfig,
|
||||
authChoice: "pixverse-api-key",
|
||||
opts: { pixverseApiKey: "pixverse-test-key" } as never,
|
||||
runtime: runtime as never,
|
||||
baseConfig: initialConfig,
|
||||
target,
|
||||
resolveApiKey: vi.fn(),
|
||||
toApiKeyCredential: vi.fn(),
|
||||
});
|
||||
|
||||
expect(runNonInteractive).toHaveBeenCalledOnce();
|
||||
expect(result?.agents?.defaults?.model).toEqual({ primary: "openai/gpt-5.6" });
|
||||
expect(result?.agents?.defaults?.mediaModels?.video).toEqual({
|
||||
primary: "pixverse/pixverse-v5.6",
|
||||
});
|
||||
});
|
||||
|
||||
it("installs an official catalog provider before applying a cold auth choice", async () => {
|
||||
const runtime = createRuntime();
|
||||
const runNonInteractive = vi.fn(async ({ config }: { config: OpenClawConfig }) => ({
|
||||
@@ -300,6 +346,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
const provider = { id: "groq", pluginId: "groq", label: "Groq" };
|
||||
resolveProviderInstallCatalogEntry.mockReturnValue({
|
||||
pluginId: "groq",
|
||||
providerId: "groq",
|
||||
label: "Groq",
|
||||
origin: "bundled",
|
||||
install: {
|
||||
@@ -358,6 +405,7 @@ describe("applyNonInteractivePluginProviderChoice", () => {
|
||||
}),
|
||||
);
|
||||
expect(resolvePluginProvidersCore).toHaveBeenCalledTimes(2);
|
||||
expect(mockArg(resolvePluginProvidersCore, 1).providerRefs).toEqual(["groq"]);
|
||||
expect(runNonInteractive).toHaveBeenCalledOnce();
|
||||
expect(result).toMatchObject({
|
||||
agents: {
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
onlyPluginIds: owningPluginIds,
|
||||
...(preferredProviderId ? { providerRefs: [preferredProviderId] } : {}),
|
||||
mode: "setup",
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
}),
|
||||
@@ -189,6 +190,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
||||
config: nextConfig,
|
||||
workspaceDir,
|
||||
onlyPluginIds: [installCatalogEntry.pluginId],
|
||||
providerRefs: [installCatalogEntry.providerId],
|
||||
mode: "setup",
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
}),
|
||||
|
||||
@@ -128,6 +128,47 @@ describe("provider flow install catalog contributions", () => {
|
||||
expect(resolvePluginProvidersCore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("resolves text and media setup choices in one metadata-only pass", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "fal",
|
||||
providerId: "fal",
|
||||
methodId: "api-key",
|
||||
choiceId: "fal-api-key",
|
||||
choiceLabel: "fal API key",
|
||||
onboardingScopes: ["image-generation", "music-generation"],
|
||||
},
|
||||
{
|
||||
pluginId: "openai",
|
||||
providerId: "openai",
|
||||
methodId: "api-key",
|
||||
choiceId: "openai-api-key",
|
||||
choiceLabel: "OpenAI API key",
|
||||
},
|
||||
]);
|
||||
resolveProviderInstallCatalogEntries.mockReturnValue([
|
||||
{
|
||||
pluginId: "vydra",
|
||||
providerId: "vydra",
|
||||
methodId: "api-key",
|
||||
choiceId: "vydra-api-key",
|
||||
choiceLabel: "Vydra API key",
|
||||
onboardingScopes: ["image-generation"],
|
||||
label: "Vydra",
|
||||
origin: "bundled",
|
||||
install: { npmSpec: "@openclaw/vydra-provider" },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(
|
||||
resolveProviderSetupFlowContributions({ scope: "all" }).map(({ option }) => option.value),
|
||||
).toEqual(expect.arrayContaining(["fal-api-key", "openai-api-key", "vydra-api-key"]));
|
||||
expect(resolveManifestProviderAuthChoices).toHaveBeenCalledOnce();
|
||||
expect(resolveProviderInstallCatalogEntries).toHaveBeenCalledOnce();
|
||||
expect(resolveProviderWizardOptions).not.toHaveBeenCalled();
|
||||
expect(resolvePluginProvidersCore).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("prefers manifest setup contributions over duplicate install-catalog entries", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
|
||||
+20
-20
@@ -11,6 +11,13 @@ type ProviderFlowScope = "text-inference" | "image-generation" | "music-generati
|
||||
|
||||
const DEFAULT_PROVIDER_FLOW_SCOPE: ProviderFlowScope = "text-inference";
|
||||
|
||||
type ProviderSetupFlowParams = {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope | "all";
|
||||
};
|
||||
|
||||
type ProviderSetupFlowOption = FlowOption & {
|
||||
onboardingScopes?: ProviderFlowScope[];
|
||||
onboardingFeatured?: boolean;
|
||||
@@ -28,18 +35,17 @@ type ProviderSetupFlowContribution = FlowContribution & {
|
||||
|
||||
function includesProviderFlowScope(
|
||||
scopes: readonly ProviderFlowScope[] | undefined,
|
||||
scope: ProviderFlowScope,
|
||||
scope: ProviderFlowScope | "all",
|
||||
): boolean {
|
||||
// Missing scope means the historic text-inference onboarding surface only.
|
||||
return scopes ? scopes.includes(scope) : scope === DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
return (
|
||||
scope === "all" || (scopes ? scopes.includes(scope) : scope === DEFAULT_PROVIDER_FLOW_SCOPE)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveInstallCatalogProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
function resolveInstallCatalogProviderSetupFlowContributions(
|
||||
params?: ProviderSetupFlowParams,
|
||||
): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
const normalizedPluginsConfig = normalizePluginsConfig(params?.config?.plugins);
|
||||
return providerInstallCatalog
|
||||
@@ -91,12 +97,9 @@ function resolveInstallCatalogProviderSetupFlowContributions(params?: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveManifestProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
function resolveManifestProviderSetupFlowContributions(
|
||||
params?: ProviderSetupFlowParams,
|
||||
): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
return providerAuthChoices
|
||||
.resolveManifestProviderAuthChoices({
|
||||
@@ -138,12 +141,9 @@ function resolveManifestProviderSetupFlowContributions(params?: {
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
export function resolveProviderSetupFlowContributions(
|
||||
params?: ProviderSetupFlowParams,
|
||||
): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
const manifestContributions = resolveManifestProviderSetupFlowContributions({
|
||||
...params,
|
||||
|
||||
Reference in New Issue
Block a user