mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
feat(plugins): surface manifest provider setup choices (#71240)
This commit is contained in:
@@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/setup: report descriptor/runtime drift when setup-api registrations disagree with `setup.providers` or `setup.cliBackends`, without rejecting legacy setup plugins. Thanks @vincentkoc.
|
||||
- Plugin hooks: expose first-class run, message, sender, session, and trace correlation fields on message hook contexts and run lifecycle events. Thanks @vincentkoc.
|
||||
- Plugins/setup: include `setup.providers[].envVars` in generic provider auth/env lookups and warn non-bundled plugins that still rely on deprecated `providerAuthEnvVars` compatibility metadata. Thanks @vincentkoc.
|
||||
- Plugins/setup: surface manifest provider auth choices directly in provider setup flow before falling back to setup runtime or install-catalog choices. Thanks @vincentkoc.
|
||||
- TUI/dependencies: remove direct `cli-highlight` usage from the OpenClaw TUI code-block renderer, keeping themed code coloring without the extra root dependency. Thanks @vincentkoc.
|
||||
- Diagnostics/OTEL: export run, model-call, and tool-execution diagnostic lifecycle events as OTEL spans without retaining live span state. Thanks @vincentkoc.
|
||||
- Providers/Anthropic Vertex: move the Vertex SDK runtime behind the bundled provider plugin so core no longer owns that provider-specific dependency. Thanks @vincentkoc.
|
||||
|
||||
@@ -71,7 +71,9 @@ 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. Explicit
|
||||
`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.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
|
||||
|
||||
@@ -168,6 +168,8 @@ or npm install metadata. Those belong in your plugin code and `package.json`.
|
||||
|
||||
Each `providerAuthChoices` entry describes one onboarding or auth choice.
|
||||
OpenClaw reads this before provider runtime loads.
|
||||
Provider setup flow prefers these manifest choices, then falls back to runtime
|
||||
wizard metadata and install-catalog choices for compatibility.
|
||||
|
||||
| Field | Required | Type | What it means |
|
||||
| --------------------- | -------- | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
|
||||
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type ResolveProviderInstallCatalogEntries =
|
||||
typeof import("../plugins/provider-install-catalog.js").resolveProviderInstallCatalogEntries;
|
||||
type ResolveManifestProviderAuthChoices =
|
||||
typeof import("../plugins/provider-auth-choices.js").resolveManifestProviderAuthChoices;
|
||||
type ResolveProviderWizardOptions =
|
||||
typeof import("../plugins/provider-wizard.js").resolveProviderWizardOptions;
|
||||
type ResolveProviderModelPickerEntries =
|
||||
@@ -16,6 +18,13 @@ vi.mock("../plugins/provider-install-catalog.js", () => ({
|
||||
resolveProviderInstallCatalogEntries,
|
||||
}));
|
||||
|
||||
const resolveManifestProviderAuthChoices = vi.hoisted(() =>
|
||||
vi.fn<ResolveManifestProviderAuthChoices>(() => []),
|
||||
);
|
||||
vi.mock("../plugins/provider-auth-choices.js", () => ({
|
||||
resolveManifestProviderAuthChoices,
|
||||
}));
|
||||
|
||||
const resolveProviderWizardOptions = vi.hoisted(() =>
|
||||
vi.fn<ResolveProviderWizardOptions>(() => []),
|
||||
);
|
||||
@@ -39,7 +48,117 @@ import {
|
||||
|
||||
describe("provider flow install catalog contributions", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
resolveManifestProviderAuthChoices.mockReset();
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([]);
|
||||
resolveProviderInstallCatalogEntries.mockReset();
|
||||
resolveProviderInstallCatalogEntries.mockReturnValue([]);
|
||||
resolveProviderWizardOptions.mockReset();
|
||||
resolveProviderWizardOptions.mockReturnValue([]);
|
||||
resolveProviderModelPickerEntries.mockReset();
|
||||
resolveProviderModelPickerEntries.mockReturnValue([]);
|
||||
resolvePluginProviders.mockReset();
|
||||
resolvePluginProviders.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it("surfaces manifest provider auth choices before setup runtime loads", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "openai-compatible",
|
||||
providerId: "openai-compatible",
|
||||
methodId: "api-key",
|
||||
choiceId: "openai-compatible-api-key",
|
||||
choiceLabel: "OpenAI-compatible API key",
|
||||
choiceHint: "Use a compatible endpoint",
|
||||
assistantPriority: -5,
|
||||
assistantVisibility: "visible",
|
||||
groupId: "openai-compatible",
|
||||
groupLabel: "OpenAI-compatible",
|
||||
groupHint: "Self-hosted and compatible providers",
|
||||
onboardingScopes: ["text-inference"],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resolveProviderSetupFlowContributions()).toEqual([
|
||||
{
|
||||
id: "provider:setup:openai-compatible-api-key",
|
||||
kind: "provider",
|
||||
surface: "setup",
|
||||
providerId: "openai-compatible",
|
||||
pluginId: "openai-compatible",
|
||||
option: {
|
||||
value: "openai-compatible-api-key",
|
||||
label: "OpenAI-compatible API key",
|
||||
hint: "Use a compatible endpoint",
|
||||
assistantPriority: -5,
|
||||
assistantVisibility: "visible",
|
||||
group: {
|
||||
id: "openai-compatible",
|
||||
label: "OpenAI-compatible",
|
||||
hint: "Self-hosted and compatible providers",
|
||||
},
|
||||
},
|
||||
onboardingScopes: ["text-inference"],
|
||||
source: "manifest",
|
||||
},
|
||||
]);
|
||||
expect(resolveManifestProviderAuthChoices).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("prefers manifest setup contributions over duplicate runtime and install-catalog entries", () => {
|
||||
resolveManifestProviderAuthChoices.mockReturnValue([
|
||||
{
|
||||
pluginId: "openai",
|
||||
providerId: "openai",
|
||||
methodId: "api-key",
|
||||
choiceId: "openai-api-key",
|
||||
choiceLabel: "OpenAI API key",
|
||||
},
|
||||
]);
|
||||
resolveProviderWizardOptions.mockReturnValue([
|
||||
{
|
||||
value: "openai-api-key",
|
||||
label: "Runtime OpenAI API key",
|
||||
groupId: "openai",
|
||||
groupLabel: "OpenAI",
|
||||
},
|
||||
]);
|
||||
resolveProviderInstallCatalogEntries.mockReturnValue([
|
||||
{
|
||||
pluginId: "openai",
|
||||
providerId: "openai",
|
||||
methodId: "api-key",
|
||||
choiceId: "openai-api-key",
|
||||
choiceLabel: "Catalog OpenAI API key",
|
||||
label: "OpenAI",
|
||||
origin: "bundled",
|
||||
install: {
|
||||
npmSpec: "@openclaw/openai",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
expect(resolveProviderSetupFlowContributions()).toEqual([
|
||||
{
|
||||
id: "provider:setup:openai-api-key",
|
||||
kind: "provider",
|
||||
surface: "setup",
|
||||
providerId: "openai",
|
||||
pluginId: "openai",
|
||||
option: {
|
||||
value: "openai-api-key",
|
||||
label: "OpenAI API key",
|
||||
group: {
|
||||
id: "openai",
|
||||
label: "OpenAI API key",
|
||||
},
|
||||
},
|
||||
source: "manifest",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("surfaces install-catalog provider choices when runtime setup options are absent", () => {
|
||||
|
||||
@@ -1,5 +1,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,
|
||||
@@ -28,7 +29,7 @@ export type ProviderSetupFlowContribution = FlowContribution & {
|
||||
pluginId?: string;
|
||||
option: ProviderSetupFlowOption;
|
||||
onboardingScopes?: ProviderFlowScope[];
|
||||
source: "runtime" | "install-catalog";
|
||||
source: "manifest" | "runtime" | "install-catalog";
|
||||
};
|
||||
|
||||
export type ProviderModelPickerFlowContribution = FlowContribution & {
|
||||
@@ -121,6 +122,51 @@ function resolveInstallCatalogProviderSetupFlowContributions(params?: {
|
||||
});
|
||||
}
|
||||
|
||||
function resolveManifestProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
scope?: ProviderFlowScope;
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
return resolveManifestProviderAuthChoices({
|
||||
...params,
|
||||
includeUntrustedWorkspacePlugins: false,
|
||||
})
|
||||
.filter((choice) => includesProviderFlowScope(choice.onboardingScopes, scope))
|
||||
.map((choice) => {
|
||||
const groupId = choice.groupId ?? choice.providerId;
|
||||
const groupLabel = choice.groupLabel ?? choice.choiceLabel;
|
||||
return Object.assign(
|
||||
{
|
||||
id: `provider:setup:${choice.choiceId}`,
|
||||
kind: `provider` as const,
|
||||
surface: `setup` as const,
|
||||
providerId: choice.providerId,
|
||||
pluginId: choice.pluginId,
|
||||
option: {
|
||||
value: choice.choiceId,
|
||||
label: choice.choiceLabel,
|
||||
...(choice.choiceHint ? { hint: choice.choiceHint } : {}),
|
||||
...(choice.assistantPriority !== undefined
|
||||
? { assistantPriority: choice.assistantPriority }
|
||||
: {}),
|
||||
...(choice.assistantVisibility
|
||||
? { assistantVisibility: choice.assistantVisibility }
|
||||
: {}),
|
||||
group: {
|
||||
id: groupId,
|
||||
label: groupLabel,
|
||||
...(choice.groupHint ? { hint: choice.groupHint } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
choice.onboardingScopes ? { onboardingScopes: [...choice.onboardingScopes] } : {},
|
||||
{ source: `manifest` as const },
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveProviderSetupFlowContributions(params?: {
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
@@ -129,8 +175,16 @@ export function resolveProviderSetupFlowContributions(params?: {
|
||||
}): ProviderSetupFlowContribution[] {
|
||||
const scope = params?.scope ?? DEFAULT_PROVIDER_FLOW_SCOPE;
|
||||
const docsByProvider = resolveProviderDocsById(params ?? {});
|
||||
const manifestContributions = resolveManifestProviderSetupFlowContributions({
|
||||
...params,
|
||||
scope,
|
||||
});
|
||||
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(
|
||||
{
|
||||
@@ -162,14 +216,18 @@ export function resolveProviderSetupFlowContributions(params?: {
|
||||
{ source: `runtime` as const },
|
||||
),
|
||||
);
|
||||
const seenOptionValues = new Set(
|
||||
runtimeContributions.map((contribution) => contribution.option.value),
|
||||
);
|
||||
for (const contribution of runtimeContributions) {
|
||||
seenOptionValues.add(contribution.option.value);
|
||||
}
|
||||
const installCatalogContributions = resolveInstallCatalogProviderSetupFlowContributions({
|
||||
...params,
|
||||
scope,
|
||||
}).filter((contribution) => !seenOptionValues.has(contribution.option.value));
|
||||
return sortFlowContributionsByLabel([...runtimeContributions, ...installCatalogContributions]);
|
||||
return sortFlowContributionsByLabel([
|
||||
...manifestContributions,
|
||||
...runtimeContributions,
|
||||
...installCatalogContributions,
|
||||
]);
|
||||
}
|
||||
|
||||
export function resolveProviderModelPickerFlowEntries(params?: {
|
||||
|
||||
Reference in New Issue
Block a user