mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-27 09:52:25 +00:00
refactor: extract single-provider plugin entry helper
This commit is contained in:
186
src/plugin-sdk/provider-entry.test.ts
Normal file
186
src/plugin-sdk/provider-entry.test.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { capturePluginRegistration } from "../plugins/captured-registration.js";
|
||||
import type { ProviderCatalogContext } from "../plugins/types.js";
|
||||
import { defineSingleProviderPluginEntry } from "./provider-entry.js";
|
||||
|
||||
function createCatalogContext(
|
||||
config: ProviderCatalogContext["config"] = {},
|
||||
): ProviderCatalogContext {
|
||||
return {
|
||||
config,
|
||||
env: {},
|
||||
resolveProviderApiKey: () => ({ apiKey: "test-key" }),
|
||||
resolveProviderAuth: () => ({
|
||||
apiKey: "test-key",
|
||||
mode: "api_key",
|
||||
source: "env",
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("defineSingleProviderPluginEntry", () => {
|
||||
it("registers a single provider with default wizard metadata", async () => {
|
||||
const entry = defineSingleProviderPluginEntry({
|
||||
id: "demo",
|
||||
name: "Demo Provider",
|
||||
description: "Demo provider plugin",
|
||||
provider: {
|
||||
label: "Demo",
|
||||
docsPath: "/providers/demo",
|
||||
auth: [
|
||||
{
|
||||
methodId: "api-key",
|
||||
label: "Demo API key",
|
||||
hint: "Shared key",
|
||||
optionKey: "demoApiKey",
|
||||
flagName: "--demo-api-key",
|
||||
envVar: "DEMO_API_KEY",
|
||||
promptMessage: "Enter Demo API key",
|
||||
defaultModel: "demo/default",
|
||||
},
|
||||
],
|
||||
catalog: {
|
||||
buildProvider: () => ({
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://api.demo.test/v1",
|
||||
models: [{ id: "default", name: "Default" }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const captured = capturePluginRegistration(entry);
|
||||
expect(captured.providers).toHaveLength(1);
|
||||
const provider = captured.providers[0];
|
||||
expect(provider).toMatchObject({
|
||||
id: "demo",
|
||||
label: "Demo",
|
||||
docsPath: "/providers/demo",
|
||||
envVars: ["DEMO_API_KEY"],
|
||||
});
|
||||
expect(provider?.auth).toHaveLength(1);
|
||||
expect(provider?.auth[0]).toMatchObject({
|
||||
id: "api-key",
|
||||
label: "Demo API key",
|
||||
hint: "Shared key",
|
||||
});
|
||||
expect(provider?.auth[0]?.wizard).toMatchObject({
|
||||
choiceId: "demo-api-key",
|
||||
choiceLabel: "Demo API key",
|
||||
groupId: "demo",
|
||||
groupLabel: "Demo",
|
||||
groupHint: "Shared key",
|
||||
methodId: "api-key",
|
||||
});
|
||||
|
||||
const catalog = await provider?.catalog?.run(createCatalogContext());
|
||||
expect(catalog).toEqual({
|
||||
provider: {
|
||||
api: "openai-completions",
|
||||
apiKey: "test-key",
|
||||
baseUrl: "https://api.demo.test/v1",
|
||||
models: [{ id: "default", name: "Default" }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("supports provider overrides, explicit env vars, and extra registration", async () => {
|
||||
const entry = defineSingleProviderPluginEntry({
|
||||
id: "gateway-plugin",
|
||||
name: "Gateway Provider",
|
||||
description: "Gateway provider plugin",
|
||||
provider: {
|
||||
id: "gateway",
|
||||
label: "Gateway",
|
||||
aliases: ["gw"],
|
||||
docsPath: "/providers/gateway",
|
||||
envVars: ["GATEWAY_KEY", "SECONDARY_KEY"],
|
||||
auth: [
|
||||
{
|
||||
methodId: "api-key",
|
||||
label: "Gateway key",
|
||||
hint: "Primary key",
|
||||
optionKey: "gatewayKey",
|
||||
flagName: "--gateway-key",
|
||||
envVar: "GATEWAY_KEY",
|
||||
promptMessage: "Enter Gateway key",
|
||||
wizard: {
|
||||
groupId: "shared-gateway",
|
||||
groupLabel: "Shared Gateway",
|
||||
},
|
||||
},
|
||||
],
|
||||
catalog: {
|
||||
buildProvider: () => ({
|
||||
api: "openai-completions",
|
||||
baseUrl: "https://gateway.test/v1",
|
||||
models: [{ id: "router", name: "Router" }],
|
||||
}),
|
||||
allowExplicitBaseUrl: true,
|
||||
},
|
||||
capabilities: {
|
||||
transcriptToolCallIdMode: "strict9",
|
||||
},
|
||||
},
|
||||
register(api) {
|
||||
api.registerWebSearchProvider({
|
||||
id: "gateway-search",
|
||||
label: "Gateway Search",
|
||||
hint: "search",
|
||||
envVars: [],
|
||||
placeholder: "",
|
||||
signupUrl: "https://example.com",
|
||||
credentialPath: "tools.web.search.gateway.apiKey",
|
||||
getCredentialValue: () => undefined,
|
||||
setCredentialValue() {},
|
||||
createTool: () => ({
|
||||
description: "search",
|
||||
parameters: {},
|
||||
execute: async () => ({}),
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const captured = capturePluginRegistration(entry);
|
||||
expect(captured.providers).toHaveLength(1);
|
||||
expect(captured.webSearchProviders).toHaveLength(1);
|
||||
|
||||
const provider = captured.providers[0];
|
||||
expect(provider).toMatchObject({
|
||||
id: "gateway",
|
||||
label: "Gateway",
|
||||
aliases: ["gw"],
|
||||
envVars: ["GATEWAY_KEY", "SECONDARY_KEY"],
|
||||
capabilities: {
|
||||
transcriptToolCallIdMode: "strict9",
|
||||
},
|
||||
});
|
||||
expect(provider?.auth[0]?.wizard).toMatchObject({
|
||||
choiceId: "gateway-api-key",
|
||||
groupId: "shared-gateway",
|
||||
groupLabel: "Shared Gateway",
|
||||
groupHint: "Primary key",
|
||||
});
|
||||
|
||||
const catalog = await provider?.catalog?.run(
|
||||
createCatalogContext({
|
||||
models: {
|
||||
providers: {
|
||||
gateway: {
|
||||
baseUrl: "https://override.test/v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(catalog).toEqual({
|
||||
provider: {
|
||||
api: "openai-completions",
|
||||
apiKey: "test-key",
|
||||
baseUrl: "https://override.test/v1",
|
||||
models: [{ id: "router", name: "Router" }],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
142
src/plugin-sdk/provider-entry.ts
Normal file
142
src/plugin-sdk/provider-entry.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { ProviderPlugin, ProviderPluginWizardSetup } from "../plugins/types.js";
|
||||
import { definePluginEntry } from "./plugin-entry.js";
|
||||
import type {
|
||||
OpenClawPluginApi,
|
||||
OpenClawPluginConfigSchema,
|
||||
OpenClawPluginDefinition,
|
||||
} from "./plugin-entry.js";
|
||||
import { createProviderApiKeyAuthMethod } from "./provider-auth.js";
|
||||
import { buildSingleProviderApiKeyCatalog } from "./provider-catalog.js";
|
||||
|
||||
type ApiKeyAuthMethodOptions = Parameters<typeof createProviderApiKeyAuthMethod>[0];
|
||||
|
||||
export type SingleProviderPluginApiKeyAuthOptions = Omit<
|
||||
ApiKeyAuthMethodOptions,
|
||||
"providerId" | "expectedProviders" | "wizard"
|
||||
> & {
|
||||
expectedProviders?: string[];
|
||||
wizard?: false | ProviderPluginWizardSetup;
|
||||
};
|
||||
|
||||
export type SingleProviderPluginCatalogOptions = {
|
||||
buildProvider: Parameters<typeof buildSingleProviderApiKeyCatalog>[0]["buildProvider"];
|
||||
allowExplicitBaseUrl?: boolean;
|
||||
};
|
||||
|
||||
export type SingleProviderPluginOptions = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
kind?: OpenClawPluginDefinition["kind"];
|
||||
configSchema?: OpenClawPluginConfigSchema | (() => OpenClawPluginConfigSchema);
|
||||
provider?: {
|
||||
id?: string;
|
||||
label: string;
|
||||
docsPath: string;
|
||||
aliases?: string[];
|
||||
envVars?: string[];
|
||||
auth?: SingleProviderPluginApiKeyAuthOptions[];
|
||||
catalog: SingleProviderPluginCatalogOptions;
|
||||
} & Omit<
|
||||
ProviderPlugin,
|
||||
"id" | "label" | "docsPath" | "aliases" | "envVars" | "auth" | "catalog"
|
||||
>;
|
||||
register?: (api: OpenClawPluginApi) => void;
|
||||
};
|
||||
|
||||
function resolveWizardSetup(params: {
|
||||
providerId: string;
|
||||
providerLabel: string;
|
||||
auth: SingleProviderPluginApiKeyAuthOptions;
|
||||
}): ProviderPluginWizardSetup | undefined {
|
||||
if (params.auth.wizard === false) {
|
||||
return undefined;
|
||||
}
|
||||
const wizard = params.auth.wizard ?? {};
|
||||
const methodId = params.auth.methodId.trim();
|
||||
return {
|
||||
choiceId: wizard.choiceId ?? `${params.providerId}-${methodId}`,
|
||||
choiceLabel: wizard.choiceLabel ?? params.auth.label,
|
||||
...(wizard.choiceHint ? { choiceHint: wizard.choiceHint } : {}),
|
||||
groupId: wizard.groupId ?? params.providerId,
|
||||
groupLabel: wizard.groupLabel ?? params.providerLabel,
|
||||
...((wizard.groupHint ?? params.auth.hint)
|
||||
? { groupHint: wizard.groupHint ?? params.auth.hint }
|
||||
: {}),
|
||||
methodId,
|
||||
...(wizard.onboardingScopes ? { onboardingScopes: wizard.onboardingScopes } : {}),
|
||||
...(wizard.modelAllowlist ? { modelAllowlist: wizard.modelAllowlist } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resolveEnvVars(params: {
|
||||
envVars?: string[];
|
||||
auth?: SingleProviderPluginApiKeyAuthOptions[];
|
||||
}): string[] | undefined {
|
||||
const combined = [
|
||||
...(params.envVars ?? []),
|
||||
...(params.auth ?? []).map((entry) => entry.envVar).filter(Boolean),
|
||||
]
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
return combined.length > 0 ? [...new Set(combined)] : undefined;
|
||||
}
|
||||
|
||||
export function defineSingleProviderPluginEntry(options: SingleProviderPluginOptions) {
|
||||
return definePluginEntry({
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
description: options.description,
|
||||
...(options.kind ? { kind: options.kind } : {}),
|
||||
...(options.configSchema ? { configSchema: options.configSchema } : {}),
|
||||
register(api) {
|
||||
const provider = options.provider;
|
||||
if (provider) {
|
||||
const providerId = provider.id ?? options.id;
|
||||
const envVars = resolveEnvVars({
|
||||
envVars: provider.envVars,
|
||||
auth: provider.auth,
|
||||
});
|
||||
const auth = (provider.auth ?? []).map((entry) => {
|
||||
const { wizard: _wizard, ...authParams } = entry;
|
||||
const wizard = resolveWizardSetup({
|
||||
providerId,
|
||||
providerLabel: provider.label,
|
||||
auth: entry,
|
||||
});
|
||||
return createProviderApiKeyAuthMethod({
|
||||
...authParams,
|
||||
providerId,
|
||||
expectedProviders: entry.expectedProviders ?? [providerId],
|
||||
...(wizard ? { wizard } : {}),
|
||||
});
|
||||
});
|
||||
api.registerProvider({
|
||||
id: providerId,
|
||||
label: provider.label,
|
||||
docsPath: provider.docsPath,
|
||||
...(provider.aliases ? { aliases: provider.aliases } : {}),
|
||||
...(envVars ? { envVars } : {}),
|
||||
auth,
|
||||
catalog: {
|
||||
order: "simple",
|
||||
run: (ctx) =>
|
||||
buildSingleProviderApiKeyCatalog({
|
||||
ctx,
|
||||
providerId,
|
||||
buildProvider: provider.catalog.buildProvider,
|
||||
...(provider.catalog.allowExplicitBaseUrl ? { allowExplicitBaseUrl: true } : {}),
|
||||
}),
|
||||
},
|
||||
...Object.fromEntries(
|
||||
Object.entries(provider).filter(
|
||||
([key]) =>
|
||||
!["id", "label", "docsPath", "aliases", "envVars", "auth", "catalog"].includes(key),
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
options.register?.(api);
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user