mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
test: use provider contract artifacts
This commit is contained in:
59
extensions/anthropic/provider-contract-api.ts
Normal file
59
extensions/anthropic/provider-contract-api.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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());
|
||||
},
|
||||
|
||||
31
extensions/fal/provider-contract-api.ts
Normal file
31
extensions/fal/provider-contract-api.ts
Normal 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"],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
38
extensions/fal/provider-registration.ts
Normal file
38
extensions/fal/provider-registration.ts
Normal 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"],
|
||||
},
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
61
extensions/google/provider-contract-api.ts
Normal file
61
extensions/google/provider-contract-api.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
84
extensions/minimax/provider-contract-api.ts
Normal file
84
extensions/minimax/provider-contract-api.ts
Normal 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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
33
extensions/moonshot/provider-contract-api.ts
Normal file
33
extensions/moonshot/provider-contract-api.ts
Normal 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)",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
54
extensions/openai/provider-contract-api.ts
Normal file
54
extensions/openai/provider-contract-api.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
26
extensions/openrouter/provider-contract-api.ts
Normal file
26
extensions/openrouter/provider-contract-api.ts
Normal 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",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
22
extensions/xai/provider-contract-api.ts
Normal file
22
extensions/xai/provider-contract-api.ts
Normal 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)",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user