test: use provider contract artifacts

This commit is contained in:
Peter Steinberger
2026-04-18 00:29:50 +01:00
parent ac39cef969
commit 4143da0ffa
11 changed files with 487 additions and 35 deletions

View File

@@ -0,0 +1,59 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
const noopAuth = async () => ({ profiles: [] });
export function createAnthropicProvider(): ProviderPlugin {
return {
id: "anthropic",
label: "Anthropic",
docsPath: "/providers/models",
hookAliases: ["claude-cli"],
envVars: ["ANTHROPIC_OAUTH_TOKEN", "ANTHROPIC_API_KEY"],
auth: [
{
id: "cli",
kind: "custom",
label: "Claude CLI",
hint: "Reuse a local Claude CLI login and switch model selection to claude-cli/*",
run: noopAuth,
wizard: {
choiceId: "anthropic-cli",
choiceLabel: "Anthropic Claude CLI",
choiceHint: "Reuse a local Claude CLI login on this host",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "Claude CLI + API key",
},
},
{
id: "setup-token",
kind: "token",
label: "Anthropic setup-token",
hint: "Manual bearer token path",
run: noopAuth,
wizard: {
choiceId: "setup-token",
choiceLabel: "Anthropic setup-token",
choiceHint: "Manual token path",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "Claude CLI + API key + token",
},
},
{
id: "api-key",
kind: "api_key",
label: "Anthropic API key",
hint: "Direct Anthropic API key",
run: noopAuth,
wizard: {
choiceId: "apiKey",
choiceLabel: "Anthropic API key",
groupId: "anthropic",
groupLabel: "Anthropic",
groupHint: "Claude CLI + API key",
},
},
],
};
}

View File

@@ -1,7 +1,6 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { buildFalImageGenerationProvider } from "./image-generation-provider.js";
import { applyFalConfig, FAL_DEFAULT_IMAGE_MODEL_REF } from "./onboard.js";
import { createFalProvider } from "./provider-registration.js";
import { buildFalVideoGenerationProvider } from "./video-generation-provider.js";
const PROVIDER_ID = "fal";
@@ -11,36 +10,7 @@ export default definePluginEntry({
name: "fal Provider",
description: "Bundled fal image and video generation provider",
register(api) {
api.registerProvider({
id: PROVIDER_ID,
label: "fal",
docsPath: "/providers/models",
envVars: ["FAL_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "fal API key",
hint: "Image and video generation API key",
optionKey: "falApiKey",
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: {
choiceId: "fal-api-key",
choiceLabel: "fal API key",
choiceHint: "Image and video generation API key",
groupId: "fal",
groupLabel: "fal",
groupHint: "Image and video generation",
onboardingScopes: ["image-generation"],
},
}),
],
});
api.registerProvider(createFalProvider());
api.registerImageGenerationProvider(buildFalImageGenerationProvider());
api.registerVideoGenerationProvider(buildFalVideoGenerationProvider());
},

View File

@@ -0,0 +1,31 @@
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 {
id: PROVIDER_ID,
label: "fal",
docsPath: "/providers/models",
envVars: ["FAL_KEY"],
auth: [
{
id: "api-key",
kind: "api_key",
label: "fal API key",
hint: "Image and video generation API key",
run: async () => ({ profiles: [], defaultModel: FAL_DEFAULT_IMAGE_MODEL_REF }),
wizard: {
choiceId: "fal-api-key",
choiceLabel: "fal API key",
choiceHint: "Image and video generation API key",
groupId: "fal",
groupLabel: "fal",
groupHint: "Image and video generation",
onboardingScopes: ["image-generation"],
},
},
],
};
}

View File

@@ -0,0 +1,38 @@
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";
const PROVIDER_ID = "fal";
export function createFalProvider(): ProviderPlugin {
return {
id: PROVIDER_ID,
label: "fal",
docsPath: "/providers/models",
envVars: ["FAL_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
methodId: "api-key",
label: "fal API key",
hint: "Image and video generation API key",
optionKey: "falApiKey",
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: {
choiceId: "fal-api-key",
choiceLabel: "fal API key",
choiceHint: "Image and video generation API key",
groupId: "fal",
groupLabel: "fal",
groupHint: "Image and video generation",
onboardingScopes: ["image-generation"],
},
}),
],
};
}

View File

@@ -0,0 +1,61 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
const noopAuth = async () => ({ profiles: [] });
export function createGoogleProvider(): ProviderPlugin {
return {
id: "google",
label: "Google AI Studio",
docsPath: "/providers/models",
hookAliases: ["google-antigravity", "google-vertex"],
envVars: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
auth: [
{
id: "api-key",
kind: "api_key",
label: "Google Gemini API key",
hint: "AI Studio / Gemini API key",
run: noopAuth,
wizard: {
choiceId: "gemini-api-key",
choiceLabel: "Google Gemini API key",
groupId: "google",
groupLabel: "Google",
groupHint: "Gemini API key + OAuth",
},
},
],
};
}
export function createGoogleGeminiCliProvider(): ProviderPlugin {
return {
id: "google-gemini-cli",
label: "Gemini CLI OAuth",
docsPath: "/providers/models",
aliases: ["gemini-cli"],
envVars: [
"OPENCLAW_GEMINI_OAUTH_CLIENT_ID",
"OPENCLAW_GEMINI_OAUTH_CLIENT_SECRET",
"GEMINI_CLI_OAUTH_CLIENT_ID",
"GEMINI_CLI_OAUTH_CLIENT_SECRET",
],
auth: [
{
id: "oauth",
kind: "oauth",
label: "Google OAuth",
hint: "PKCE + localhost callback",
run: noopAuth,
},
],
wizard: {
setup: {
choiceId: "google-gemini-cli",
choiceLabel: "Gemini CLI OAuth",
choiceHint: "Google OAuth with project-aware token payload",
methodId: "oauth",
},
},
};
}

View File

@@ -0,0 +1,84 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
const noopAuth = async () => ({ profiles: [] });
const wizardGroup = {
groupId: "minimax",
groupLabel: "MiniMax",
groupHint: "M2.7 (recommended)",
} as const;
export function createMinimaxProvider(): ProviderPlugin {
return {
id: "minimax",
label: "MiniMax",
hookAliases: ["minimax-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_API_KEY"],
auth: [
{
id: "api-global",
kind: "api_key",
label: "MiniMax API key (Global)",
hint: "Global endpoint - api.minimax.io",
run: noopAuth,
wizard: {
choiceId: "minimax-global-api",
choiceLabel: "MiniMax API key (Global)",
choiceHint: "Global endpoint - api.minimax.io",
...wizardGroup,
},
},
{
id: "api-cn",
kind: "api_key",
label: "MiniMax API key (CN)",
hint: "CN endpoint - api.minimaxi.com",
run: noopAuth,
wizard: {
choiceId: "minimax-cn-api",
choiceLabel: "MiniMax API key (CN)",
choiceHint: "CN endpoint - api.minimaxi.com",
...wizardGroup,
},
},
],
};
}
export function createMinimaxPortalProvider(): ProviderPlugin {
return {
id: "minimax-portal",
label: "MiniMax",
hookAliases: ["minimax-portal-cn"],
docsPath: "/providers/minimax",
envVars: ["MINIMAX_OAUTH_TOKEN", "MINIMAX_API_KEY"],
auth: [
{
id: "oauth",
kind: "device_code",
label: "MiniMax OAuth (Global)",
hint: "Global endpoint - api.minimax.io",
run: noopAuth,
wizard: {
choiceId: "minimax-global-oauth",
choiceLabel: "MiniMax OAuth (Global)",
choiceHint: "Global endpoint - api.minimax.io",
...wizardGroup,
},
},
{
id: "oauth-cn",
kind: "device_code",
label: "MiniMax OAuth (CN)",
hint: "CN endpoint - api.minimaxi.com",
run: noopAuth,
wizard: {
choiceId: "minimax-cn-oauth",
choiceLabel: "MiniMax OAuth (CN)",
choiceHint: "CN endpoint - api.minimaxi.com",
...wizardGroup,
},
},
],
};
}

View File

@@ -0,0 +1,33 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
const noopAuth = async () => ({ profiles: [] });
export function createMoonshotProvider(): ProviderPlugin {
return {
id: "moonshot",
label: "Moonshot",
docsPath: "/providers/moonshot",
auth: [
{
id: "api-key",
kind: "api_key",
label: "Kimi API key (.ai)",
hint: "Kimi K2.5 + Kimi",
run: noopAuth,
wizard: {
groupLabel: "Moonshot AI (Kimi K2.5)",
},
},
{
id: "api-key-cn",
kind: "api_key",
label: "Kimi API key (.cn)",
hint: "Kimi K2.5 + Kimi",
run: noopAuth,
wizard: {
groupLabel: "Moonshot AI (Kimi K2.5)",
},
},
],
};
}

View File

@@ -0,0 +1,54 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
const noopAuth = async () => ({ profiles: [] });
export function createOpenAICodexProvider(): ProviderPlugin {
return {
id: "openai-codex",
label: "OpenAI Codex",
docsPath: "/providers/models",
auth: [
{
id: "oauth",
kind: "oauth",
label: "ChatGPT OAuth",
hint: "Browser sign-in",
run: noopAuth,
},
],
wizard: {
setup: {
choiceId: "openai-codex",
choiceLabel: "OpenAI Codex (ChatGPT OAuth)",
choiceHint: "Browser sign-in",
methodId: "oauth",
},
},
};
}
export function createOpenAIProvider(): ProviderPlugin {
return {
id: "openai",
label: "OpenAI",
hookAliases: ["azure-openai", "azure-openai-responses"],
docsPath: "/providers/models",
envVars: ["OPENAI_API_KEY"],
auth: [
{
id: "api-key",
kind: "api_key",
label: "OpenAI API key",
hint: "Direct OpenAI API key",
run: noopAuth,
wizard: {
choiceId: "openai-api-key",
choiceLabel: "OpenAI API key",
groupId: "openai",
groupLabel: "OpenAI",
groupHint: "Codex OAuth + API key",
},
},
],
};
}

View File

@@ -0,0 +1,26 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
export function createOpenrouterProvider(): ProviderPlugin {
return {
id: "openrouter",
label: "OpenRouter",
docsPath: "/providers/models",
envVars: ["OPENROUTER_API_KEY"],
auth: [
{
id: "api-key",
kind: "api_key",
label: "OpenRouter API key",
hint: "API key",
run: async () => ({ profiles: [] }),
wizard: {
choiceId: "openrouter-api-key",
choiceLabel: "OpenRouter API key",
groupId: "openrouter",
groupLabel: "OpenRouter",
groupHint: "API key",
},
},
],
};
}

View File

@@ -0,0 +1,22 @@
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
export function createXaiProvider(): ProviderPlugin {
return {
id: "xai",
label: "xAI",
aliases: ["x-ai"],
docsPath: "/providers/xai",
auth: [
{
id: "api-key",
kind: "api_key",
label: "xAI API key",
hint: "API key",
run: async () => ({ profiles: [] }),
wizard: {
groupLabel: "xAI (Grok)",
},
},
],
};
}

View File

@@ -2,19 +2,87 @@ import { describe, expect, it } from "vitest";
import {
pluginRegistrationContractRegistry,
providerContractLoadError,
requireProviderContractProvider,
resolveProviderContractProvidersForPluginIds,
} from "../../../src/plugins/contracts/registry.js";
import { loadBundledPluginPublicArtifactModuleSync } from "../../../src/plugins/public-surface-loader.js";
import type { ProviderPlugin } from "../../../src/plugins/types.js";
import { installProviderPluginContractSuite } from "./provider-contract-suites.js";
type ProviderContractEntry = {
pluginId: string;
provider: ProviderPlugin;
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function isProviderPlugin(value: unknown): value is ProviderPlugin {
return (
isRecord(value) &&
typeof value.id === "string" &&
typeof value.label === "string" &&
Array.isArray(value.auth)
);
}
function resolveProviderContractProvidersFromPublicArtifact(
pluginId: string,
): ProviderContractEntry[] | null {
let mod: Record<string, unknown>;
try {
mod = loadBundledPluginPublicArtifactModuleSync<Record<string, unknown>>({
dirName: pluginId,
artifactBasename: "provider-contract-api.js",
});
} catch (error) {
if (
error instanceof Error &&
error.message.startsWith("Unable to resolve bundled plugin public surface ")
) {
return null;
}
throw error;
}
const providers: ProviderContractEntry[] = [];
for (const [name, exported] of Object.entries(mod).toSorted(([left], [right]) =>
left.localeCompare(right),
)) {
if (
typeof exported !== "function" ||
exported.length !== 0 ||
!name.startsWith("create") ||
!name.endsWith("Provider")
) {
continue;
}
const provider = exported();
if (isProviderPlugin(provider)) {
providers.push({ pluginId, provider });
}
}
return providers.length > 0 ? providers : null;
}
export function describeProviderContracts(pluginId: string) {
const providerIds =
pluginRegistrationContractRegistry.find((entry) => entry.pluginId === pluginId)?.providerIds ??
[];
const resolveProviderEntries = (): ProviderContractEntry[] => {
const publicArtifactProviders = resolveProviderContractProvidersFromPublicArtifact(pluginId);
if (publicArtifactProviders) {
return publicArtifactProviders;
}
return resolveProviderContractProvidersForPluginIds([pluginId]).map((provider) => ({
pluginId,
provider,
}));
};
describe(`${pluginId} provider contract registry load`, () => {
it("loads bundled providers without import-time registry failure", () => {
const providers = resolveProviderContractProvidersForPluginIds([pluginId]);
const providers = resolveProviderEntries();
expect(providerContractLoadError).toBeUndefined();
expect(providers.length).toBeGreaterThan(0);
});
@@ -25,7 +93,13 @@ export function describeProviderContracts(pluginId: string) {
// Resolve provider entries lazily so the non-isolated extension runner
// does not race provider contract collection against other file imports.
installProviderPluginContractSuite({
provider: () => requireProviderContractProvider(providerId),
provider: () => {
const entry = resolveProviderEntries().find((entry) => entry.provider.id === providerId);
if (!entry) {
throw new Error(`provider contract entry missing for ${pluginId}:${providerId}`);
}
return entry.provider;
},
});
});
}