From b810e94a1756d96bad2fe619fbd4d2e4db359128 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 15 Mar 2026 14:37:41 -0700 Subject: [PATCH] Commands: lazy-load non-interactive plugin provider runtime (#47593) * Commands: lazy-load non-interactive plugin provider runtime * Tests: cover non-interactive plugin provider ordering * Update src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.runtime.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- .../auth-choice.plugin-providers.runtime.ts | 4 ++ .../auth-choice.plugin-providers.test.ts | 54 +++++++++++++++++++ .../local/auth-choice.plugin-providers.ts | 12 +++-- .../local/auth-choice.test.ts | 53 ++++++++++++++++++ .../local/auth-choice.ts | 36 ++++++------- 5 files changed, 136 insertions(+), 23 deletions(-) create mode 100644 src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.runtime.ts create mode 100644 src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts create mode 100644 src/commands/onboard-non-interactive/local/auth-choice.test.ts diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.runtime.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.runtime.ts new file mode 100644 index 00000000000..fd4a36d4a9f --- /dev/null +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.runtime.ts @@ -0,0 +1,4 @@ +export { + resolveProviderPluginChoice, +} from "../../../plugins/provider-wizard.js"; +export { resolvePluginProviders } from "../../../plugins/providers.js"; diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts new file mode 100644 index 00000000000..4e0f37e2882 --- /dev/null +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.test.ts @@ -0,0 +1,54 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../config/config.js"; +import { applyNonInteractivePluginProviderChoice } from "./auth-choice.plugin-providers.js"; + +const resolvePreferredProviderForAuthChoice = vi.hoisted(() => vi.fn(async () => undefined)); +vi.mock("../../auth-choice.preferred-provider.js", () => ({ + resolvePreferredProviderForAuthChoice, +})); + +const resolveProviderPluginChoice = vi.hoisted(() => vi.fn()); +const resolvePluginProviders = vi.hoisted(() => vi.fn(() => [])); +vi.mock("./auth-choice.plugin-providers.runtime.js", () => ({ + resolveProviderPluginChoice, + resolvePluginProviders, + PROVIDER_PLUGIN_CHOICE_PREFIX: "provider-plugin:", +})); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +function createRuntime() { + return { + error: vi.fn(), + exit: vi.fn(), + }; +} + +describe("applyNonInteractivePluginProviderChoice", () => { + it("loads plugin providers for provider-plugin auth choices", async () => { + const runtime = createRuntime(); + const runNonInteractive = vi.fn(async () => ({ plugins: { allow: ["vllm"] } })); + resolvePluginProviders.mockReturnValue([{ id: "vllm", pluginId: "vllm" }] as never); + resolveProviderPluginChoice.mockReturnValue({ + provider: { id: "vllm", pluginId: "vllm", label: "vLLM" }, + method: { runNonInteractive }, + }); + + const result = await applyNonInteractivePluginProviderChoice({ + nextConfig: { agents: { defaults: {} } } as OpenClawConfig, + authChoice: "provider-plugin:vllm:custom", + opts: {} as never, + runtime: runtime as never, + baseConfig: { agents: { defaults: {} } } as OpenClawConfig, + resolveApiKey: vi.fn(), + toApiKeyCredential: vi.fn(), + }); + + expect(resolvePluginProviders).toHaveBeenCalledOnce(); + expect(resolveProviderPluginChoice).toHaveBeenCalledOnce(); + expect(runNonInteractive).toHaveBeenCalledOnce(); + expect(result).toEqual({ plugins: { allow: ["vllm"] } }); + }); +}); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts index d6e1440eb20..e5c8dedb12f 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.plugin-providers.ts @@ -3,11 +3,6 @@ import type { ApiKeyCredential } from "../../../agents/auth-profiles/types.js"; import { resolveDefaultAgentWorkspaceDir } from "../../../agents/workspace.js"; import type { OpenClawConfig } from "../../../config/config.js"; import { enablePluginInConfig } from "../../../plugins/enable.js"; -import { - PROVIDER_PLUGIN_CHOICE_PREFIX, - resolveProviderPluginChoice, -} from "../../../plugins/provider-wizard.js"; -import { resolvePluginProviders } from "../../../plugins/providers.js"; import type { ProviderNonInteractiveApiKeyCredentialParams, ProviderResolveNonInteractiveApiKeyParams, @@ -16,6 +11,12 @@ import type { RuntimeEnv } from "../../../runtime.js"; import { resolvePreferredProviderForAuthChoice } from "../../auth-choice.preferred-provider.js"; import type { OnboardOptions } from "../../onboard-types.js"; +const PROVIDER_PLUGIN_CHOICE_PREFIX = "provider-plugin:"; + +async function loadPluginProviderRuntime() { + return import("./auth-choice.plugin-providers.runtime.js"); +} + function buildIsolatedProviderResolutionConfig( cfg: OpenClawConfig, providerId: string | undefined, @@ -73,6 +74,7 @@ export async function applyNonInteractivePluginProviderChoice(params: { params.nextConfig, preferredProviderId, ); + const { resolveProviderPluginChoice, resolvePluginProviders } = await loadPluginProviderRuntime(); const providerChoice = resolveProviderPluginChoice({ providers: resolvePluginProviders({ config: resolutionConfig, diff --git a/src/commands/onboard-non-interactive/local/auth-choice.test.ts b/src/commands/onboard-non-interactive/local/auth-choice.test.ts new file mode 100644 index 00000000000..9fe7a34cda9 --- /dev/null +++ b/src/commands/onboard-non-interactive/local/auth-choice.test.ts @@ -0,0 +1,53 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { OpenClawConfig } from "../../../config/config.js"; +import { applyNonInteractiveAuthChoice } from "./auth-choice.js"; + +const applySimpleNonInteractiveApiKeyChoice = vi.hoisted(() => + vi.fn<() => Promise>(async () => undefined), +); +vi.mock("./auth-choice.api-key-providers.js", () => ({ + applySimpleNonInteractiveApiKeyChoice, +})); + +const applyNonInteractivePluginProviderChoice = vi.hoisted(() => vi.fn(async () => undefined)); +vi.mock("./auth-choice.plugin-providers.js", () => ({ + applyNonInteractivePluginProviderChoice, +})); + +const resolveNonInteractiveApiKey = vi.hoisted(() => vi.fn()); +vi.mock("../api-keys.js", () => ({ + resolveNonInteractiveApiKey, +})); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +function createRuntime() { + return { + error: vi.fn(), + exit: vi.fn(), + log: vi.fn(), + }; +} + +describe("applyNonInteractiveAuthChoice", () => { + it("resolves builtin API key auth before plugin provider resolution", async () => { + const runtime = createRuntime(); + const nextConfig = { agents: { defaults: {} } } as OpenClawConfig; + const resolvedConfig = { auth: { profiles: { "openai:default": { mode: "api_key" } } } }; + applySimpleNonInteractiveApiKeyChoice.mockResolvedValueOnce(resolvedConfig as never); + + const result = await applyNonInteractiveAuthChoice({ + nextConfig, + authChoice: "openai-api-key", + opts: {} as never, + runtime: runtime as never, + baseConfig: nextConfig, + }); + + expect(result).toBe(resolvedConfig); + expect(applySimpleNonInteractiveApiKeyChoice).toHaveBeenCalledOnce(); + expect(applyNonInteractivePluginProviderChoice).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/onboard-non-interactive/local/auth-choice.ts b/src/commands/onboard-non-interactive/local/auth-choice.ts index 500e19ee574..6d360487ee9 100644 --- a/src/commands/onboard-non-interactive/local/auth-choice.ts +++ b/src/commands/onboard-non-interactive/local/auth-choice.ts @@ -161,24 +161,6 @@ export async function applyNonInteractiveAuthChoice(params: { return null; } - const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({ - nextConfig, - authChoice, - opts, - runtime, - baseConfig, - resolveApiKey: (input) => - resolveApiKey({ - ...input, - cfg: baseConfig, - runtime, - }), - toApiKeyCredential, - }); - if (pluginProviderChoice !== undefined) { - return pluginProviderChoice; - } - if (authChoice === "token") { const providerRaw = opts.tokenProvider?.trim(); if (!providerRaw) { @@ -484,6 +466,24 @@ export async function applyNonInteractiveAuthChoice(params: { } } + const pluginProviderChoice = await applyNonInteractivePluginProviderChoice({ + nextConfig, + authChoice, + opts, + runtime, + baseConfig, + resolveApiKey: (input) => + resolveApiKey({ + ...input, + cfg: baseConfig, + runtime, + }), + toApiKeyCredential, + }); + if (pluginProviderChoice !== undefined) { + return pluginProviderChoice; + } + if ( authChoice === "oauth" || authChoice === "chutes" ||