refactor(providers): derive simple providers from manifests (#114331)

This commit is contained in:
Peter Steinberger
2026-07-27 01:29:53 -04:00
committed by GitHub
parent ccd6845e4d
commit dc8fdb1756
29 changed files with 409 additions and 412 deletions

View File

@@ -3,12 +3,9 @@ import { buildOpenAICompatibleLiveModelProviderConfig } from "openclaw/plugin-sd
import type { ProviderCatalogContext } from "openclaw/plugin-sdk/provider-catalog-shared";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import {
BASETEN_DEFAULT_MODEL_REF,
projectBasetenLiveModels,
resolveBasetenDynamicModel,
} from "./models.js";
import { projectBasetenLiveModels, resolveBasetenDynamicModel } from "./models.js";
import { applyBasetenConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildStaticBasetenProvider } from "./provider-catalog.js";
import { createBasetenThinkingWrapper } from "./stream.js";
import { resolveBasetenThinkingProfile } from "./thinking.js";
@@ -19,31 +16,18 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Baseten Provider",
description: "Official Baseten Model APIs provider plugin",
manifest,
provider: {
label: "Baseten",
docsPath: "/providers/baseten",
auth: [
{
methodId: "api-key",
label: "Baseten API key",
hint: "Hosted Model APIs, including Inkling",
optionKey: "basetenApiKey",
flagName: "--baseten-api-key",
envVar: "BASETEN_API_KEY",
promptMessage: "Enter Baseten API key",
defaultModel: BASETEN_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyBasetenConfig(cfg),
noteTitle: "Baseten",
noteMessage: [
"Baseten hosts Thinking Machines Lab's Inkling and other frontier models behind one OpenAI-compatible API.",
"Get your API key at: https://app.baseten.co/settings/api_keys",
].join("\n"),
wizard: {
groupLabel: "Baseten",
groupHint: "Hosted Model APIs, including Inkling",
},
},
],
manifestAuth: {
applyConfig: applyBasetenConfig,
noteTitle: "Baseten",
noteMessage: [
"Baseten hosts Thinking Machines Lab's Inkling and other frontier models behind one OpenAI-compatible API.",
"Get your API key at: https://app.baseten.co/settings/api_keys",
].join("\n"),
},
catalog: {
order: "simple",
run: async (ctx: ProviderCatalogContext) => {

View File

@@ -2,8 +2,8 @@
* Cerebras provider plugin entrypoint.
*/
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyCerebrasConfig, CEREBRAS_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildCerebrasProvider } from "./provider-catalog.js";
import { applyCerebrasConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const PROVIDER_ID = "cerebras";
@@ -11,36 +11,19 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Cerebras Provider",
description: "Bundled Cerebras provider plugin",
manifest,
provider: {
label: "Cerebras",
docsPath: "/providers/cerebras",
auth: [
{
methodId: "api-key",
label: "Cerebras API key",
hint: "Fast OpenAI-compatible inference",
optionKey: "cerebrasApiKey",
flagName: "--cerebras-api-key",
envVar: "CEREBRAS_API_KEY",
promptMessage: "Enter Cerebras API key",
defaultModel: CEREBRAS_DEFAULT_MODEL_REF,
preserveExistingPrimary: true,
applyConfig: (cfg) => applyCerebrasConfig(cfg),
noteMessage: [
"Cerebras provides high-speed OpenAI-compatible inference for GPT OSS and GLM models.",
"Get your API key at: https://cloud.cerebras.ai",
].join("\n"),
noteTitle: "Cerebras",
wizard: {
groupLabel: "Cerebras",
groupHint: "Fast OpenAI-compatible inference",
},
},
],
catalog: {
buildProvider: buildCerebrasProvider,
buildStaticProvider: buildCerebrasProvider,
liveModelDiscovery: true,
manifestAuth: {
preserveExistingPrimary: true,
applyConfig: applyCerebrasConfig,
noteMessage: [
"Cerebras provides high-speed OpenAI-compatible inference for GPT OSS and GLM models.",
"Get your API key at: https://cloud.cerebras.ai",
].join("\n"),
noteTitle: "Cerebras",
},
catalog: { liveModelDiscovery: true },
},
});

View File

@@ -1,11 +1,12 @@
import { readFileSync } from "node:fs";
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { Context, Model } from "openclaw/plugin-sdk/llm";
import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime";
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime";
import { describe, expect, it } from "vitest";
import plugin from "./index.js";
import { buildCohereProvider, COHERE_LIVE_MODEL_DISCOVERY } from "./provider-catalog.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { COHERE_LIVE_MODEL_DISCOVERY } from "./provider-catalog.js";
import { createCohereCompletionsWrapper } from "./stream.js";
const COHERE_COMMAND_A_PLUS_MODEL_ID = "command-a-plus-05-2026";
@@ -13,11 +14,11 @@ const COHERE_COMMAND_A_REASONING_MODEL_ID = "command-a-reasoning-08-2025";
const COHERE_COMMAND_A_VISION_MODEL_ID = "command-a-vision-07-2025";
const COHERE_NORTH_MINI_CODE_MODEL_ID = "north-mini-code-1-0";
function readManifest() {
return JSON.parse(readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8")) as {
providerAuthChoices?: Array<{ choiceId?: string; optionKey?: string; cliFlag?: string }>;
setup?: { providers?: Array<{ id?: string; envVars?: string[] }> };
};
function buildCohereProvider() {
return buildManifestModelProviderConfig({
providerId: "cohere",
catalog: manifest.modelCatalog.providers.cohere,
});
}
function requireCohereModel(modelId = COHERE_COMMAND_A_PLUS_MODEL_ID): Model<"openai-completions"> {
@@ -78,16 +79,14 @@ describe("Cohere provider plugin", () => {
kind: "api_key",
wizard: { choiceId: "cohere-api-key" },
});
expect(readManifest().providerAuthChoices).toEqual([
expect(manifest.providerAuthChoices).toEqual([
expect.objectContaining({
choiceId: "cohere-api-key",
optionKey: "cohereApiKey",
cliFlag: "--cohere-api-key",
}),
]);
expect(readManifest().setup?.providers).toEqual([
{ id: "cohere", envVars: ["COHERE_API_KEY"] },
]);
expect(manifest.setup.providers).toEqual([{ id: "cohere", envVars: ["COHERE_API_KEY"] }]);
});
it("exposes the static Cohere catalog", () => {

View File

@@ -1,36 +1,20 @@
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { isModernCohereModelId } from "./models.js";
import { applyCohereConfig, COHERE_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildCohereProvider, COHERE_LIVE_MODEL_DISCOVERY } from "./provider-catalog.js";
import { applyCohereConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { COHERE_LIVE_MODEL_DISCOVERY } from "./provider-catalog.js";
import { createCohereCompletionsWrapper } from "./stream.js";
export default defineSingleProviderPluginEntry({
id: "cohere",
name: "Cohere Provider",
description: "Cohere provider plugin",
manifest,
provider: {
label: "Cohere",
docsPath: "/providers/cohere",
auth: [
{
methodId: "api-key",
label: "Cohere API key",
hint: "OpenAI-compatible inference",
optionKey: "cohereApiKey",
flagName: "--cohere-api-key",
envVar: "COHERE_API_KEY",
promptMessage: "Enter Cohere API key",
defaultModel: COHERE_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyCohereConfig(cfg),
wizard: {
groupLabel: "Cohere",
groupHint: "OpenAI-compatible inference",
},
},
],
manifestAuth: { applyConfig: applyCohereConfig },
catalog: {
buildProvider: buildCohereProvider,
buildStaticProvider: buildCohereProvider,
liveModelDiscovery: COHERE_LIVE_MODEL_DISCOVERY,
},
wrapStreamFn: (ctx) => createCohereCompletionsWrapper(ctx.streamFn),

View File

@@ -2,9 +2,10 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard";
import { describe, expect, it } from "vitest";
import { buildCohereCatalogModels, COHERE_BASE_URL } from "./models.js";
import { applyCohereConfig, COHERE_DEFAULT_MODEL_REF } from "./onboard.js";
import { applyCohereConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const COHERE_DEFAULT_MODEL_REF = `cohere/${manifest.modelCatalog.providers.cohere.defaultModel}`;
const COHERE_DEFAULT_MODEL_ID = "command-a-plus-05-2026";
const COHERE_COMMAND_A_REASONING_MODEL_ID = "command-a-reasoning-08-2025";
const COHERE_COMMAND_A_VISION_MODEL_ID = "command-a-vision-07-2025";

View File

@@ -6,7 +6,7 @@ import {
import { buildCohereCatalogModels, COHERE_BASE_URL } from "./models.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
export const COHERE_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(manifest, "cohere")!;
const COHERE_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(manifest, "cohere")!;
const coherePresetAppliers = createModelCatalogPresetAppliers({
primaryModelRef: COHERE_DEFAULT_MODEL_REF,

View File

@@ -1,6 +1,5 @@
import type { OpenAICompatibleModelDiscoveryOptions } from "openclaw/plugin-sdk/provider-catalog-live-runtime";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import { buildCohereCatalogModels, COHERE_BASE_URL } from "./models.js";
import { COHERE_BASE_URL } from "./models.js";
export const COHERE_LIVE_MODEL_DISCOVERY: OpenAICompatibleModelDiscoveryOptions = {
endpointUrl: {
@@ -25,11 +24,3 @@ export const COHERE_LIVE_MODEL_DISCOVERY: OpenAICompatibleModelDiscoveryOptions
});
},
};
export function buildCohereProvider(): ModelProviderConfig {
return {
baseUrl: COHERE_BASE_URL,
api: "openai-completions",
models: buildCohereCatalogModels(),
};
}

View File

@@ -4,7 +4,8 @@ import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-en
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { fetchDeepSeekUsage } from "openclaw/plugin-sdk/provider-usage";
import { applyDeepSeekConfig, DEEPSEEK_DEFAULT_MODEL_REF } from "./onboard.js";
import { applyDeepSeekConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildDeepSeekProvider } from "./provider-catalog.js";
import { createDeepSeekV4ThinkingWrapper } from "./stream.js";
import { resolveDeepSeekV4ThinkingProfile } from "./thinking.js";
@@ -15,29 +16,11 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "DeepSeek Provider",
description: "Bundled DeepSeek provider plugin",
manifest,
provider: {
label: "DeepSeek",
docsPath: "/providers/deepseek",
auth: [
{
methodId: "api-key",
label: "DeepSeek API key",
hint: "API key",
optionKey: "deepseekApiKey",
flagName: "--deepseek-api-key",
envVar: "DEEPSEEK_API_KEY",
promptMessage: "Enter DeepSeek API key",
defaultModel: DEEPSEEK_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyDeepSeekConfig(cfg),
wizard: {
choiceId: "deepseek-api-key",
choiceLabel: "DeepSeek API key",
groupId: "deepseek",
groupLabel: "DeepSeek",
groupHint: "API key",
},
},
],
manifestAuth: { applyConfig: applyDeepSeekConfig },
catalog: {
buildProvider: buildDeepSeekProvider,
buildStaticProvider: buildDeepSeekProvider,

View File

@@ -1,8 +1,10 @@
import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard";
import { describe, expect, it } from "vitest";
import { applyDeepSeekConfig, DEEPSEEK_DEFAULT_MODEL_REF } from "./onboard.js";
import { applyDeepSeekConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const DEEPSEEK_DEFAULT_MODEL_REF = `deepseek/${manifest.modelCatalog.providers.deepseek.defaultModel}`;
describe("DeepSeek onboarding", () => {
it("applies the manifest catalog, default, and alias", () => {
const config = applyDeepSeekConfig({});

View File

@@ -6,10 +6,7 @@ import {
import { DEEPSEEK_BASE_URL, DEEPSEEK_MODEL_CATALOG } from "./models.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
export const DEEPSEEK_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(
manifest,
"deepseek",
)!;
const DEEPSEEK_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(manifest, "deepseek")!;
const deepSeekPresetAppliers = createModelCatalogPresetAppliers({
primaryModelRef: DEEPSEEK_DEFAULT_MODEL_REF,

View File

@@ -12,8 +12,8 @@ import {
} from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { applyFeatherlessConfig, FEATHERLESS_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import {
buildFeatherlessProvider,
FEATHERLESS_BASE_URL,
FEATHERLESS_DEFAULT_MODEL_ID,
FEATHERLESS_DYNAMIC_COMPAT,
@@ -75,31 +75,20 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Featherless AI Provider",
description: "Featherless AI provider plugin",
manifest,
provider: {
label: "Featherless AI",
docsPath: "/providers/featherless",
envVars: ["FEATHERLESS_API_KEY"],
auth: [
{
methodId: "api-key",
label: "Featherless AI API key",
hint: "OpenAI-compatible access to open models",
optionKey: "featherlessApiKey",
flagName: "--featherless-api-key",
envVar: "FEATHERLESS_API_KEY",
promptMessage: "Enter Featherless AI API key",
defaultModel: FEATHERLESS_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyFeatherlessConfig(cfg),
noteTitle: "Featherless AI",
noteMessage: [
"Featherless AI serves open models through an OpenAI-compatible API.",
"Create an API key at: https://featherless.ai/account/api-keys",
].join("\n"),
},
],
manifestAuth: {
defaultModel: FEATHERLESS_DEFAULT_MODEL_REF,
applyConfig: applyFeatherlessConfig,
noteTitle: "Featherless AI",
noteMessage: [
"Featherless AI serves open models through an OpenAI-compatible API.",
"Create an API key at: https://featherless.ai/account/api-keys",
].join("\n"),
},
catalog: {
buildProvider: buildFeatherlessProvider,
buildStaticProvider: buildFeatherlessProvider,
allowExplicitBaseUrl: true,
liveModelDiscovery: {
endpointPath: "models?capabilities=chat",

View File

@@ -8,13 +8,12 @@ import {
} from "openclaw/plugin-sdk/provider-model-shared";
import { isFireworksKimiModelId } from "./model-id.js";
import { applyFireworksConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import {
buildFireworksProvider,
FIREWORKS_BASE_URL,
FIREWORKS_DEFAULT_CONTEXT_WINDOW,
FIREWORKS_DEFAULT_MAX_TOKENS,
FIREWORKS_DEFAULT_MODEL_ID,
FIREWORKS_DEFAULT_MODEL_REF,
isFireworksCatalogModelId,
} from "./provider-catalog.js";
import { wrapFireworksProviderStream } from "./stream.js";
@@ -76,26 +75,13 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Fireworks Provider",
description: "Bundled Fireworks AI provider plugin",
manifest,
provider: {
label: "Fireworks",
aliases: ["fireworks-ai"],
docsPath: "/providers/fireworks",
auth: [
{
methodId: "api-key",
label: "Fireworks API key",
hint: "API key",
optionKey: "fireworksApiKey",
flagName: "--fireworks-api-key",
envVar: "FIREWORKS_API_KEY",
promptMessage: "Enter Fireworks API key",
defaultModel: FIREWORKS_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyFireworksConfig(cfg),
},
],
manifestAuth: { applyConfig: applyFireworksConfig },
catalog: {
buildProvider: buildFireworksProvider,
buildStaticProvider: buildFireworksProvider,
allowExplicitBaseUrl: true,
liveModelDiscovery: true,
},

View File

@@ -3,7 +3,7 @@ import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provid
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { GMI_DEFAULT_MODEL_REF } from "./models.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildGmiProvider } from "./provider-catalog.js";
const PROVIDER_ID = "gmi";
@@ -12,25 +12,15 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "GMI Cloud Provider",
description: "GMI Cloud provider plugin",
manifest,
provider: {
label: "GMI Cloud",
docsPath: "/providers/gmi",
aliases: ["gmi-cloud", "gmicloud"],
envVars: ["GMI_API_KEY"],
auth: [
{
methodId: "api-key",
label: "GMI Cloud API key",
hint: "OpenAI-compatible GMI Cloud endpoint",
optionKey: "gmiApiKey",
flagName: "--gmi-api-key",
envVar: "GMI_API_KEY",
promptMessage: "Enter GMI Cloud API key",
defaultModel: GMI_DEFAULT_MODEL_REF,
noteTitle: "GMI Cloud",
noteMessage: "Manage API keys at https://www.gmicloud.ai/",
},
],
manifestAuth: {
noteTitle: "GMI Cloud",
noteMessage: "Manage API keys at https://www.gmicloud.ai/",
},
catalog: {
buildProvider: buildGmiProvider,
buildStaticProvider: buildGmiProvider,

View File

@@ -1,8 +1,5 @@
// Gmi plugin module implements models behavior.
import {
buildManifestModelDefinition,
readManifestProviderDefaultModelRef,
} from "openclaw/plugin-sdk/provider-catalog-shared";
import { buildManifestModelDefinition } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
@@ -15,4 +12,3 @@ export const GMI_MODEL_CATALOG: ModelDefinitionConfig[] = GMI_MANIFEST_CATALOG.m
decorate: (model) => ({ ...model, api: "openai-completions" }),
}),
);
export const GMI_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(manifest, "gmi")!;

View File

@@ -1,6 +1,7 @@
// Huggingface plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyHuggingfaceConfig, HUGGINGFACE_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildHuggingfaceProvider } from "./provider-catalog.js";
const PROVIDER_ID = "huggingface";
@@ -15,23 +16,15 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Hugging Face Provider",
description: "Bundled Hugging Face provider plugin",
manifest,
provider: {
label: "Hugging Face",
docsPath: "/providers/huggingface",
envVars: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
auth: [
{
methodId: "api-key",
label: "Hugging Face API key",
hint: "Inference API (HF token)",
optionKey: "huggingfaceApiKey",
flagName: "--huggingface-api-key",
envVar: "HUGGINGFACE_HUB_TOKEN",
promptMessage: "Enter Hugging Face API key",
defaultModel: HUGGINGFACE_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyHuggingfaceConfig(cfg),
},
],
manifestAuth: {
defaultModel: HUGGINGFACE_DEFAULT_MODEL_REF,
applyConfig: applyHuggingfaceConfig,
},
catalog: {
order: "simple",
run: async (ctx) => {

View File

@@ -3,6 +3,7 @@ import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provid
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { applyKilocodeConfig, KILOCODE_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildKilocodeProvider, buildKilocodeProviderWithDiscovery } from "./provider-catalog.js";
import { wrapKilocodeProviderStream } from "./stream.js";
@@ -12,22 +13,14 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Kilo Gateway Provider",
description: "Bundled Kilo Gateway provider plugin",
manifest,
provider: {
label: "Kilo Gateway",
docsPath: "/providers/kilocode",
auth: [
{
methodId: "api-key",
label: "Kilo Gateway API key",
hint: "API key (OpenRouter-compatible)",
optionKey: "kilocodeApiKey",
flagName: "--kilocode-api-key",
envVar: "KILOCODE_API_KEY",
promptMessage: "Enter Kilo Gateway API key",
defaultModel: KILOCODE_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyKilocodeConfig(cfg),
},
],
manifestAuth: {
defaultModel: KILOCODE_DEFAULT_MODEL_REF,
applyConfig: applyKilocodeConfig,
},
catalog: {
buildProvider: buildKilocodeProviderWithDiscovery,
buildStaticProvider: buildKilocodeProvider,

View File

@@ -4,7 +4,7 @@ import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-mod
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { LONGCAT_DEFAULT_MODEL_REF } from "./models.js";
import { applyLongCatConfig } from "./onboard.js";
import { buildLongCatProvider } from "./provider-catalog.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { createLongCatThinkingWrapper } from "./stream.js";
const PROVIDER_ID = "longcat";
@@ -13,38 +13,18 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "LongCat Provider",
description: "Official LongCat provider plugin",
manifest,
provider: {
label: "LongCat",
docsPath: "/providers/longcat",
aliases: ["meituan-longcat"],
envVars: ["LONGCAT_API_KEY"],
auth: [
{
methodId: "api-key",
label: "LongCat API key",
hint: "API key",
optionKey: "longcatApiKey",
flagName: "--longcat-api-key",
envVar: "LONGCAT_API_KEY",
promptMessage: "Enter LongCat API key",
defaultModel: LONGCAT_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyLongCatConfig(cfg),
noteTitle: "LongCat",
noteMessage: "Manage API keys at https://longcat.chat/platform/api_keys",
wizard: {
choiceId: "longcat-api-key",
choiceLabel: "LongCat API key",
groupId: "longcat",
groupLabel: "LongCat",
groupHint: "API key",
},
},
],
catalog: {
buildProvider: buildLongCatProvider,
buildStaticProvider: buildLongCatProvider,
liveModelDiscovery: true,
manifestAuth: {
defaultModel: LONGCAT_DEFAULT_MODEL_REF,
applyConfig: applyLongCatConfig,
noteTitle: "LongCat",
noteMessage: "Manage API keys at https://longcat.chat/platform/api_keys",
},
catalog: { liveModelDiscovery: true },
...buildProviderReplayFamilyHooks({
family: "openai-compatible",
dropReasoningFromHistory: false,

View File

@@ -3,7 +3,8 @@
*/
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { applyMetaConfig, META_DEFAULT_MODEL_REF } from "./onboard.js";
import { applyMetaConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildMetaProvider } from "./provider-catalog.js";
import { wrapMetaProviderStream } from "./stream.js";
import { resolveMetaThinkingProfile } from "./thinking.js";
@@ -14,28 +15,15 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Meta Provider",
description: "Bundled Meta provider plugin",
manifest,
provider: {
label: "Meta",
docsPath: "/providers/meta",
auth: [
{
methodId: "api-key",
label: "Meta API key",
hint: "Meta (Responses API)",
optionKey: "metaApiKey",
flagName: "--meta-api-key",
envVar: "MODEL_API_KEY",
promptMessage: "Enter Meta API key",
defaultModel: META_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyMetaConfig(cfg),
noteMessage: ["Meta provides Responses API inference."].join("\n"),
noteTitle: "Meta",
wizard: {
groupLabel: "Meta",
groupHint: "Meta (Responses API)",
},
},
],
manifestAuth: {
applyConfig: applyMetaConfig,
noteMessage: "Meta provides Responses API inference.",
noteTitle: "Meta",
},
catalog: {
buildProvider: buildMetaProvider,
buildStaticProvider: buildMetaProvider,

View File

@@ -7,8 +7,8 @@ import {
} from "./api.js";
import { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js";
import { mistralMemoryEmbeddingProviderAdapter } from "./memory-embedding-adapter.js";
import { MISTRAL_DEFAULT_MODEL_REF } from "./model-definitions.js";
import { applyMistralConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildMistralProvider } from "./provider-catalog.js";
import { buildMistralRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
@@ -24,25 +24,11 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Mistral Provider",
description: "Bundled Mistral provider plugin",
manifest,
provider: {
label: "Mistral",
docsPath: "/providers/models",
auth: [
{
methodId: "api-key",
label: "Mistral API key",
hint: "API key",
optionKey: "mistralApiKey",
flagName: "--mistral-api-key",
envVar: "MISTRAL_API_KEY",
promptMessage: "Enter Mistral API key",
defaultModel: MISTRAL_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyMistralConfig(cfg),
wizard: {
groupLabel: "Mistral AI",
},
},
],
manifestAuth: { applyConfig: applyMistralConfig },
catalog: {
buildProvider: buildMistralProvider,
buildStaticProvider: buildMistralProvider,

View File

@@ -3,7 +3,7 @@ import { readConfiguredProviderCatalogEntries } from "openclaw/plugin-sdk/provid
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared";
import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools";
import { NOVITA_DEFAULT_MODEL_REF } from "./models.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildNovitaProvider } from "./provider-catalog.js";
const PROVIDER_ID = "novita";
@@ -12,25 +12,15 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "NovitaAI Provider",
description: "Bundled NovitaAI provider plugin",
manifest,
provider: {
label: "NovitaAI",
docsPath: "/providers/novita",
aliases: ["novita-ai", "novitaai"],
envVars: ["NOVITA_API_KEY"],
auth: [
{
methodId: "api-key",
label: "NovitaAI API key",
hint: "OpenAI-compatible NovitaAI endpoint",
optionKey: "novitaApiKey",
flagName: "--novita-api-key",
envVar: "NOVITA_API_KEY",
promptMessage: "Enter NovitaAI API key",
defaultModel: NOVITA_DEFAULT_MODEL_REF,
noteTitle: "NovitaAI",
noteMessage: "Manage API keys at https://novita.ai/settings/key-management",
},
],
manifestAuth: {
noteTitle: "NovitaAI",
noteMessage: "Manage API keys at https://novita.ai/settings/key-management",
},
catalog: {
buildProvider: buildNovitaProvider,
buildStaticProvider: buildNovitaProvider,

View File

@@ -1,8 +1,5 @@
// Novita plugin module implements models behavior.
import {
buildManifestModelDefinition,
readManifestProviderDefaultModelRef,
} from "openclaw/plugin-sdk/provider-catalog-shared";
import { buildManifestModelDefinition } from "openclaw/plugin-sdk/provider-catalog-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import manifest from "./openclaw.plugin.json" with { type: "json" };
@@ -16,4 +13,3 @@ export const NOVITA_MODEL_CATALOG: ModelDefinitionConfig[] = NOVITA_MANIFEST_CAT
decorate: (model) => ({ ...model, api: "openai-completions" }),
}),
);
export const NOVITA_DEFAULT_MODEL_REF = readManifestProviderDefaultModelRef(manifest, "novita")!;

View File

@@ -1,6 +1,7 @@
// Nvidia plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyNvidiaConfig, NVIDIA_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import {
buildLiveNvidiaProvider,
buildSelectableNvidiaProvider,
@@ -39,24 +40,15 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "NVIDIA Provider",
description: "Bundled NVIDIA provider plugin",
manifest,
provider: {
label: "NVIDIA",
docsPath: "/providers/nvidia",
envVars: ["NVIDIA_API_KEY"],
preserveLiteralProviderPrefix: true,
auth: [
{
methodId: "api-key",
label: "NVIDIA API key",
hint: "Direct API key",
optionKey: "nvidiaApiKey",
flagName: "--nvidia-api-key",
envVar: "NVIDIA_API_KEY",
promptMessage: "Enter NVIDIA API key",
defaultModel: NVIDIA_DEFAULT_MODEL_REF,
applyConfig: applyNvidiaConfig,
},
],
manifestAuth: {
defaultModel: NVIDIA_DEFAULT_MODEL_REF,
applyConfig: applyNvidiaConfig,
},
catalog: {
buildProvider: buildSelectableLiveNvidiaProvider,
buildStaticProvider: buildSelectableNvidiaProvider,

View File

@@ -1,7 +1,7 @@
// Qianfan plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildQianfanProvider } from "./provider-catalog.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
const PROVIDER_ID = "qianfan";
@@ -9,26 +9,14 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Qianfan Provider",
description: "Bundled Qianfan provider plugin",
manifest,
provider: {
label: "Qianfan",
docsPath: "/providers/qianfan",
auth: [
{
methodId: "api-key",
label: "Qianfan API key",
hint: "API key",
optionKey: "qianfanApiKey",
flagName: "--qianfan-api-key",
envVar: "QIANFAN_API_KEY",
promptMessage: "Enter Qianfan API key",
defaultModel: QIANFAN_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyQianfanConfig(cfg),
},
],
catalog: {
buildProvider: buildQianfanProvider,
buildStaticProvider: buildQianfanProvider,
liveModelDiscovery: true,
manifestAuth: {
defaultModel: QIANFAN_DEFAULT_MODEL_REF,
applyConfig: applyQianfanConfig,
},
catalog: { liveModelDiscovery: true },
},
});

View File

@@ -1,6 +1,7 @@
// Synthetic plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applySyntheticConfig, SYNTHETIC_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildSyntheticProvider } from "./provider-catalog.js";
const PROVIDER_ID = "synthetic";
@@ -9,22 +10,14 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Synthetic Provider",
description: "Bundled Synthetic provider plugin",
manifest,
provider: {
label: "Synthetic",
docsPath: "/providers/synthetic",
auth: [
{
methodId: "api-key",
label: "Synthetic API key",
hint: "Anthropic-compatible (multi-model)",
optionKey: "syntheticApiKey",
flagName: "--synthetic-api-key",
envVar: "SYNTHETIC_API_KEY",
promptMessage: "Enter Synthetic API key",
defaultModel: SYNTHETIC_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applySyntheticConfig(cfg),
},
],
manifestAuth: {
defaultModel: SYNTHETIC_DEFAULT_MODEL_REF,
applyConfig: applySyntheticConfig,
},
catalog: {
buildProvider: buildSyntheticProvider,
},

View File

@@ -1,7 +1,7 @@
// Together plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyTogetherConfig, TOGETHER_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildTogetherProvider } from "./provider-catalog.js";
import { applyTogetherConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildTogetherVideoGenerationProvider } from "./video-generation-provider.js";
const PROVIDER_ID = "together";
@@ -10,30 +10,12 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Together Provider",
description: "Bundled Together provider plugin",
manifest,
provider: {
label: "Together",
docsPath: "/providers/together",
auth: [
{
methodId: "api-key",
label: "Together AI API key",
hint: "API key",
optionKey: "togetherApiKey",
flagName: "--together-api-key",
envVar: "TOGETHER_API_KEY",
promptMessage: "Enter Together AI API key",
defaultModel: TOGETHER_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyTogetherConfig(cfg),
wizard: {
groupLabel: "Together AI",
},
},
],
catalog: {
buildProvider: buildTogetherProvider,
buildStaticProvider: buildTogetherProvider,
liveModelDiscovery: true,
},
manifestAuth: { applyConfig: applyTogetherConfig },
catalog: { liveModelDiscovery: true },
classifyFailoverReason: ({ errorMessage }) =>
/\bconcurrency limit\b.*\b(?:breached|reached)\b/i.test(errorMessage)
? "rate_limit"

View File

@@ -5,8 +5,9 @@ import {
type ModelCompatConfig,
} from "openclaw/plugin-sdk/provider-model-shared";
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
import { VENICE_DEFAULT_MODEL_REF, VENICE_MODEL_DISCOVERY_OPTIONS } from "./models.js";
import { VENICE_MODEL_DISCOVERY_OPTIONS } from "./models.js";
import { applyVeniceConfig } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import { buildStaticVeniceProvider } from "./provider-catalog.js";
import { createVeniceDeepSeekV4Wrapper } from "./stream.js";
import { fetchVeniceUsage } from "./usage.js";
@@ -37,31 +38,19 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Venice Provider",
description: "Bundled Venice provider plugin",
manifest,
provider: {
label: "Venice",
docsPath: "/providers/venice",
auth: [
{
methodId: "api-key",
label: "Venice AI API key",
hint: "Privacy-focused (uncensored models)",
optionKey: "veniceApiKey",
flagName: "--venice-api-key",
envVar: "VENICE_API_KEY",
promptMessage: "Enter Venice AI API key",
defaultModel: VENICE_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyVeniceConfig(cfg),
noteMessage: [
"Venice AI provides privacy-focused inference with uncensored models.",
"Get your API key at: https://venice.ai/settings/api",
"Supports 'private' (fully private) and 'anonymized' (proxy) modes.",
].join("\n"),
noteTitle: "Venice AI",
wizard: {
groupLabel: "Venice AI",
},
},
],
manifestAuth: {
applyConfig: applyVeniceConfig,
noteMessage: [
"Venice AI provides privacy-focused inference with uncensored models.",
"Get your API key at: https://venice.ai/settings/api",
"Supports 'private' (fully private) and 'anonymized' (proxy) modes.",
].join("\n"),
noteTitle: "Venice AI",
},
catalog: {
buildProvider: buildStaticVeniceProvider,
liveModelDiscovery: VENICE_MODEL_DISCOVERY_OPTIONS,

View File

@@ -1,6 +1,7 @@
// Vercel Ai Gateway plugin entrypoint registers its OpenClaw integration.
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyVercelAiGatewayConfig, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from "./onboard.js";
import manifest from "./openclaw.plugin.json" with { type: "json" };
import {
buildStaticVercelAiGatewayProvider,
buildVercelAiGatewayProvider,
@@ -14,26 +15,14 @@ export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Vercel AI Gateway Provider",
description: "Bundled Vercel AI Gateway provider plugin",
manifest,
provider: {
label: "Vercel AI Gateway",
docsPath: "/providers/vercel-ai-gateway",
auth: [
{
methodId: "api-key",
label: "Vercel AI Gateway API key",
hint: "API key",
optionKey: "aiGatewayApiKey",
flagName: "--ai-gateway-api-key",
envVar: "AI_GATEWAY_API_KEY",
promptMessage: "Enter Vercel AI Gateway API key",
defaultModel: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
applyConfig: (cfg) => applyVercelAiGatewayConfig(cfg),
wizard: {
choiceId: "ai-gateway-api-key",
groupId: "ai-gateway",
},
},
],
manifestAuth: {
defaultModel: VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF,
applyConfig: applyVercelAiGatewayConfig,
},
catalog: {
buildProvider: buildVercelAiGatewayProvider,
buildStaticProvider: buildStaticVercelAiGatewayProvider,

View File

@@ -21,6 +21,37 @@ function createModel(id: string, name: string): ModelDefinitionConfig {
maxTokens: 8_192,
};
}
function createProviderManifest() {
return {
setup: { providers: [{ id: "demo", envVars: ["DEMO_API_KEY"] }] },
providerAuthChoices: [
{
provider: "demo",
method: "api-key",
choiceId: "demo-api-key",
choiceLabel: "Demo API key",
choiceHint: "Manifest-owned key",
groupId: "demo-group",
groupLabel: "Demo providers",
groupHint: "Manifest-owned setup",
optionKey: "demoApiKey",
cliFlag: "--demo-api-key",
},
],
modelCatalog: {
providers: {
demo: {
api: "openai-completions",
baseUrl: "https://api.demo.test/v1",
defaultModel: "default",
models: [createModel("default", "Default")],
},
},
},
};
}
function createCatalogContext(
config: ProviderCatalogContext["config"] = {},
): ProviderCatalogContext {
@@ -55,6 +86,116 @@ async function captureProviderEntry(params: {
}
describe("defineSingleProviderPluginEntry", () => {
it("derives API-key auth and static and live model catalogs from the provider manifest", async () => {
const manifest = createProviderManifest();
const entry = defineSingleProviderPluginEntry({
id: "demo",
name: "Demo Provider",
description: "Demo provider plugin",
manifest,
provider: {
label: "Demo",
docsPath: "/providers/demo",
aliases: ["demo-alias"],
catalog: {},
},
});
const { provider, catalog, staticCatalog, unifiedCatalog, unifiedStaticCatalog } =
await captureProviderEntry({ entry });
expect(provider).toMatchObject({
id: "demo",
label: "Demo",
aliases: ["demo-alias"],
envVars: ["DEMO_API_KEY"],
});
expect(provider?.auth[0]).toMatchObject({
id: "api-key",
label: "Demo API key",
hint: "Manifest-owned key",
starterModel: "demo/default",
wizard: {
choiceId: "demo-api-key",
choiceLabel: "Demo API key",
choiceHint: "Manifest-owned key",
groupId: "demo-group",
groupLabel: "Demo providers",
groupHint: "Manifest-owned setup",
methodId: "api-key",
},
});
const { defaultModel, ...manifestProvider } = manifest.modelCatalog.providers.demo;
expect(defaultModel).toBe("default");
expect(catalog).toEqual({ provider: { ...manifestProvider, apiKey: "test-key" } });
expect(staticCatalog).toEqual({ provider: manifestProvider });
expect(unifiedCatalog).toEqual([
{ kind: "text", provider: "demo", model: "default", label: "Default", source: "live" },
]);
expect(unifiedStaticCatalog).toEqual([
{ kind: "text", provider: "demo", model: "default", label: "Default", source: "static" },
]);
});
it("honors explicit base URLs and provider-owned manifest auth overrides", async () => {
const entry = defineSingleProviderPluginEntry({
id: "demo",
name: "Demo Provider",
description: "Demo provider plugin",
manifest: createProviderManifest(),
provider: {
label: "Demo",
docsPath: "/providers/demo",
manifestAuth: { defaultModel: "demo/custom", preserveExistingPrimary: true },
catalog: { allowExplicitBaseUrl: true },
},
});
const { provider, catalog, staticCatalog } = await captureProviderEntry({
entry,
config: {
models: {
providers: {
demo: {
baseUrl: "https://override.demo.test/v1",
models: [createModel("configured", "Configured")],
},
},
},
},
});
expect(provider?.auth[0]?.starterModel).toBe("demo/custom");
expect(catalog).toMatchObject({ provider: { baseUrl: "https://override.demo.test/v1" } });
expect(staticCatalog).toMatchObject({ provider: { baseUrl: "https://api.demo.test/v1" } });
});
it("rejects manifest API-key metadata without its declared credential source", () => {
const manifest = createProviderManifest();
const entry = defineSingleProviderPluginEntry({
id: "demo",
name: "Demo Provider",
description: "Demo provider plugin",
manifest: { ...manifest, setup: { providers: [] } },
provider: { label: "Demo", docsPath: "/providers/demo", catalog: {} },
});
expect(() => capturePluginRegistration(entry)).toThrow(
'Incomplete manifest API-key auth for provider "demo"',
);
});
it("rejects a provider catalog without a manifest catalog or explicit builder", () => {
const entry = defineSingleProviderPluginEntry({
id: "demo",
name: "Demo Provider",
description: "Demo provider plugin",
provider: { label: "Demo", docsPath: "/providers/demo", catalog: {} },
});
expect(() => capturePluginRegistration(entry)).toThrow("Missing modelCatalog.providers.demo");
});
it("registers a single provider with default wizard metadata", async () => {
const entry = defineSingleProviderPluginEntry({
id: "demo",

View File

@@ -4,6 +4,10 @@ import {
normalizeStringEntries,
uniqueStrings,
} from "../../packages/normalization-core/src/string-normalization.js";
import type {
PluginManifestProviderAuthChoice,
PluginManifestSetupProvider,
} from "../plugins/manifest-types.js";
import { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js";
import { projectProviderCatalogResultToUnifiedTextRows } from "../plugins/provider-catalog-unified-text.js";
import type {
@@ -30,10 +34,25 @@ import {
buildOpenAICompatibleProviderCatalog,
type OpenAICompatibleModelDiscoveryOptions,
} from "./provider-catalog-live-runtime.js";
import { buildSingleProviderApiKeyCatalog } from "./provider-catalog-shared.js";
import {
buildManifestModelProviderConfig,
buildSingleProviderApiKeyCatalog,
readManifestProviderDefaultModelRef,
} from "./provider-catalog-shared.js";
type ApiKeyAuthMethodOptions = Parameters<typeof createProviderApiKeyAuthMethod>[0];
type SingleProviderPluginManifest = {
setup?: {
providers?: readonly Pick<PluginManifestSetupProvider, "id" | "envVars">[];
};
providerAuthChoices?: readonly PluginManifestProviderAuthChoice[];
modelCatalog?: {
providers?: Readonly<Record<string, unknown>>;
discovery?: Readonly<Record<string, unknown>>;
};
};
/**
* API-key auth options for single-provider plugins, with provider id filled in by the entry helper.
*/
@@ -53,6 +72,13 @@ export type SingleProviderPluginApiKeyAuthOptions = Omit<
wizard?: false | ProviderPluginWizardSetup;
};
type ManifestProviderAuthOptions = Omit<
SingleProviderPluginApiKeyAuthOptions,
"methodId" | "label" | "optionKey" | "flagName" | "envVar" | "promptMessage"
> & {
promptMessage?: string;
};
/**
* Catalog configuration accepted by the single-provider entry helper.
*/
@@ -61,7 +87,7 @@ export type SingleProviderPluginCatalogOptions =
/**
* Builds the live provider catalog through the shared API-key catalog path.
*/
buildProvider: Parameters<typeof buildSingleProviderApiKeyCatalog>[0]["buildProvider"];
buildProvider?: Parameters<typeof buildSingleProviderApiKeyCatalog>[0]["buildProvider"];
/**
* Builds a static catalog for cheap model discovery before credentials are resolved.
*/
@@ -113,6 +139,11 @@ export type SingleProviderPluginOptions = {
* Short plugin description surfaced by plugin registries and setup flows.
*/
description: string;
/**
* Plugin-owned metadata used to derive API-key auth and model catalogs
* without repeating the manifest in the runtime entry.
*/
manifest?: SingleProviderPluginManifest;
/**
* @deprecated Declare exclusive plugin kind in `openclaw.plugin.json` via
* manifest `kind`. Runtime-entry `kind` remains only as a compatibility
@@ -152,6 +183,10 @@ export type SingleProviderPluginOptions = {
* API-key auth methods converted through the shared provider auth helper.
*/
auth?: SingleProviderPluginApiKeyAuthOptions[];
/**
* Provider-owned behavior layered over manifest-derived API-key auth.
*/
manifestAuth?: ManifestProviderAuthOptions;
/**
* Non-API-key auth methods appended after generated API-key methods.
*/
@@ -170,6 +205,50 @@ export type SingleProviderPluginOptions = {
register?: (api: OpenClawPluginApi) => void;
};
function resolveManifestProviderAuth(params: {
manifest: SingleProviderPluginManifest | undefined;
providerId: string;
providerLabel: string;
overrides?: ManifestProviderAuthOptions;
}): SingleProviderPluginApiKeyAuthOptions[] {
const choice = params.manifest?.providerAuthChoices?.find(
(entry) => entry.provider === params.providerId && entry.method === "api-key",
);
if (!choice) {
return [];
}
const envVar = params.manifest?.setup?.providers?.find((entry) => entry.id === params.providerId)
?.envVars?.[0];
if (!choice.choiceLabel || !choice.optionKey || !choice.cliFlag?.startsWith("--") || !envVar) {
throw new Error(`Incomplete manifest API-key auth for provider "${params.providerId}"`);
}
const defaultModel = readManifestProviderDefaultModelRef(params.manifest, params.providerId);
return [
{
methodId: choice.method,
label: choice.choiceLabel,
...(choice.choiceHint || choice.groupHint
? { hint: choice.choiceHint ?? choice.groupHint }
: {}),
optionKey: choice.optionKey,
flagName: choice.cliFlag as `--${string}`,
envVar,
promptMessage: `Enter ${choice.choiceLabel}`,
...(defaultModel ? { defaultModel } : {}),
wizard: {
choiceId: choice.choiceId,
choiceLabel: choice.choiceLabel,
groupId: choice.groupId ?? params.providerId,
groupLabel: choice.groupLabel ?? params.providerLabel,
...(choice.choiceHint ? { choiceHint: choice.choiceHint } : {}),
...(choice.groupHint ? { groupHint: choice.groupHint } : {}),
methodId: choice.method,
},
...params.overrides,
},
];
}
function resolveWizardSetup(params: {
providerId: string;
providerLabel: string;
@@ -244,7 +323,22 @@ export function defineSingleProviderPluginEntry(options: SingleProviderPluginOpt
const provider = options.provider;
if (provider) {
const providerId = provider.id ?? options.id;
const providerAuth = copyProviderAuthOptions(provider.auth);
if (
!("run" in provider.catalog) &&
!provider.catalog.buildProvider &&
!options.manifest?.modelCatalog?.providers?.[providerId]
) {
throw new Error(`Missing modelCatalog.providers.${providerId}`);
}
const providerAuth = copyProviderAuthOptions(
provider.auth ??
resolveManifestProviderAuth({
manifest: options.manifest,
providerId,
providerLabel: provider.label,
overrides: provider.manifestAuth,
}),
);
const acceptedProviderAuth: SingleProviderPluginApiKeyAuthOptions[] = [];
const auth = providerAuth.flatMap((entry) => {
try {
@@ -281,7 +375,13 @@ export function defineSingleProviderPluginEntry(options: SingleProviderPluginOpt
run: catalogRun!,
};
} else {
const buildProvider = provider.catalog.buildProvider;
const buildProvider =
provider.catalog.buildProvider ??
(() =>
buildManifestModelProviderConfig({
providerId,
catalog: options.manifest?.modelCatalog?.providers?.[providerId],
}));
catalog = {
order: "simple",
run: (ctx: ProviderCatalogContext): Promise<ProviderCatalogResult> =>
@@ -307,6 +407,17 @@ export function defineSingleProviderPluginEntry(options: SingleProviderPluginOpt
}),
};
}
const manifestStaticProvider =
"run" in provider.catalog
? undefined
: (provider.catalog.buildStaticProvider ??
(provider.catalog.buildProvider
? undefined
: () =>
buildManifestModelProviderConfig({
providerId,
catalog: options.manifest?.modelCatalog?.providers?.[providerId],
})));
const staticCatalog: ProviderPluginCatalog | undefined =
"run" in provider.catalog
? provider.catalog.staticRun
@@ -315,11 +426,11 @@ export function defineSingleProviderPluginEntry(options: SingleProviderPluginOpt
run: provider.catalog.staticRun,
}
: undefined
: provider.catalog.buildStaticProvider
: manifestStaticProvider
? {
order: "simple",
run: async () => ({
provider: await provider.catalog.buildStaticProvider!(),
provider: await manifestStaticProvider(),
}),
}
: undefined;
@@ -344,6 +455,7 @@ export function defineSingleProviderPluginEntry(options: SingleProviderPluginOpt
"aliases",
"envVars",
"auth",
"manifestAuth",
"extraAuth",
"catalog",
"staticCatalog",