From 956fe72b39e683546ea358ddb2c8a59f87d0522e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 23 Mar 2026 01:34:24 +0000 Subject: [PATCH] refactor: extract single-provider plugin entry helper --- docs/plugins/sdk-provider-plugins.md | 37 +++++ extensions/kilocode/index.ts | 85 +++++------- extensions/mistral/index.ts | 92 ++++++------- extensions/modelstudio/index.ts | 130 ++++++++---------- extensions/moonshot/index.ts | 116 +++++++--------- extensions/nvidia/index.ts | 30 ++--- extensions/qianfan/index.ts | 61 +++------ extensions/synthetic/index.ts | 61 +++------ extensions/together/index.ts | 64 ++++----- extensions/venice/index.ts | 80 +++++------ extensions/vercel-ai-gateway/index.ts | 65 ++++----- extensions/xai/index.ts | 104 ++++++-------- extensions/xiaomi/index.ts | 83 +++++------- src/plugin-sdk/provider-entry.test.ts | 186 ++++++++++++++++++++++++++ src/plugin-sdk/provider-entry.ts | 142 ++++++++++++++++++++ 15 files changed, 739 insertions(+), 597 deletions(-) create mode 100644 src/plugin-sdk/provider-entry.test.ts create mode 100644 src/plugin-sdk/provider-entry.ts diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index 7b22264b8e9..56c8ce50cab 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -148,6 +148,43 @@ API key auth, and dynamic model resolution. `openclaw onboard --acme-ai-api-key ` and select `acme-ai/acme-large` as their model. + For bundled providers that only register one text provider with API-key + auth plus a single catalog-backed runtime, prefer the narrower + `defineSingleProviderPluginEntry(...)` helper: + + ```typescript + import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; + + export default defineSingleProviderPluginEntry({ + id: "acme-ai", + name: "Acme AI", + description: "Acme AI model provider", + provider: { + label: "Acme AI", + docsPath: "/providers/acme-ai", + auth: [ + { + methodId: "api-key", + label: "Acme AI API key", + hint: "API key from your Acme AI dashboard", + optionKey: "acmeAiApiKey", + flagName: "--acme-ai-api-key", + envVar: "ACME_AI_API_KEY", + promptMessage: "Enter your Acme AI API key", + defaultModel: "acme-ai/acme-large", + }, + ], + catalog: { + buildProvider: () => ({ + api: "openai-completions", + baseUrl: "https://api.acme-ai.com/v1", + models: [{ id: "acme-large", name: "Acme Large" }], + }), + }, + }, + }); + ``` + diff --git a/extensions/kilocode/index.ts b/extensions/kilocode/index.ts index 1261afe9ace..48166bebeb1 100644 --- a/extensions/kilocode/index.ts +++ b/extensions/kilocode/index.ts @@ -1,6 +1,4 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { createKilocodeWrapper, isProxyReasoningUnsupported, @@ -10,59 +8,40 @@ import { buildKilocodeProviderWithDiscovery } from "./provider-catalog.js"; const PROVIDER_ID = "kilocode"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Kilo Gateway Provider", description: "Bundled Kilo Gateway provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Kilo Gateway", - docsPath: "/providers/kilocode", - envVars: ["KILOCODE_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - 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, - expectedProviders: ["kilocode"], - applyConfig: (cfg) => applyKilocodeConfig(cfg), - wizard: { - choiceId: "kilocode-api-key", - choiceLabel: "Kilo Gateway API key", - groupId: "kilocode", - groupLabel: "Kilo Gateway", - groupHint: "API key (OpenRouter-compatible)", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildKilocodeProviderWithDiscovery, - }), + 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), }, - capabilities: { - geminiThoughtSignatureSanitization: true, - geminiThoughtSignatureModelHints: ["gemini"], - }, - wrapStreamFn: (ctx) => { - const thinkingLevel = - ctx.modelId === "kilo/auto" || isProxyReasoningUnsupported(ctx.modelId) - ? undefined - : ctx.thinkingLevel; - return createKilocodeWrapper(ctx.streamFn, thinkingLevel); - }, - isCacheTtlEligible: (ctx) => ctx.modelId.startsWith("anthropic/"), - }); + ], + catalog: { + buildProvider: buildKilocodeProviderWithDiscovery, + }, + capabilities: { + geminiThoughtSignatureSanitization: true, + geminiThoughtSignatureModelHints: ["gemini"], + }, + wrapStreamFn: (ctx) => { + const thinkingLevel = + ctx.modelId === "kilo/auto" || isProxyReasoningUnsupported(ctx.modelId) + ? undefined + : ctx.thinkingLevel; + return createKilocodeWrapper(ctx.streamFn, thinkingLevel); + }, + isCacheTtlEligible: (ctx) => ctx.modelId.startsWith("anthropic/"), }, }); diff --git a/extensions/mistral/index.ts b/extensions/mistral/index.ts index 27e58726685..53a31b5e91c 100644 --- a/extensions/mistral/index.ts +++ b/extensions/mistral/index.ts @@ -1,67 +1,51 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js"; import { applyMistralConfig, MISTRAL_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildMistralProvider } from "./provider-catalog.js"; const PROVIDER_ID = "mistral"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Mistral Provider", description: "Bundled Mistral provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Mistral", - docsPath: "/providers/models", - envVars: ["MISTRAL_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - 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, - expectedProviders: ["mistral"], - applyConfig: (cfg) => applyMistralConfig(cfg), - wizard: { - choiceId: "mistral-api-key", - choiceLabel: "Mistral API key", - groupId: "mistral", - groupLabel: "Mistral AI", - groupHint: "API key", - }, - }), + 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", + }, + }, + ], + catalog: { + buildProvider: buildMistralProvider, + allowExplicitBaseUrl: true, + }, + capabilities: { + transcriptToolCallIdMode: "strict9", + transcriptToolCallIdModelHints: [ + "mistral", + "mixtral", + "codestral", + "pixtral", + "devstral", + "ministral", + "mistralai", ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildMistralProvider, - allowExplicitBaseUrl: true, - }), - }, - capabilities: { - transcriptToolCallIdMode: "strict9", - transcriptToolCallIdModelHints: [ - "mistral", - "mixtral", - "codestral", - "pixtral", - "devstral", - "ministral", - "mistralai", - ], - }, - }); + }, + }, + register(api) { api.registerMediaUnderstandingProvider(mistralMediaUnderstandingProvider); }, }); diff --git a/extensions/modelstudio/index.ts b/extensions/modelstudio/index.ts index eeb0b46e89f..db4ac83baf6 100644 --- a/extensions/modelstudio/index.ts +++ b/extensions/modelstudio/index.ts @@ -1,6 +1,4 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyModelStudioConfig, applyModelStudioConfigCn, @@ -10,82 +8,62 @@ import { buildModelStudioProvider } from "./provider-catalog.js"; const PROVIDER_ID = "modelstudio"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Model Studio Provider", description: "Bundled Model Studio provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Model Studio", - docsPath: "/providers/models", - envVars: ["MODELSTUDIO_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - methodId: "api-key-cn", - label: "Coding Plan API Key for China (subscription)", - hint: "Endpoint: coding.dashscope.aliyuncs.com", - optionKey: "modelstudioApiKeyCn", - flagName: "--modelstudio-api-key-cn", - envVar: "MODELSTUDIO_API_KEY", - promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (China)", - defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF, - expectedProviders: ["modelstudio"], - applyConfig: (cfg) => applyModelStudioConfigCn(cfg), - noteMessage: [ - "Get your API key at: https://bailian.console.aliyun.com/", - "Endpoint: coding.dashscope.aliyuncs.com", - "Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.", - ].join("\n"), - noteTitle: "Alibaba Cloud Model Studio Coding Plan (China)", - wizard: { - choiceId: "modelstudio-api-key-cn", - choiceLabel: "Coding Plan API Key for China (subscription)", - choiceHint: "Endpoint: coding.dashscope.aliyuncs.com", - groupId: "modelstudio", - groupLabel: "Alibaba Cloud Model Studio", - groupHint: "Coding Plan API key (CN / Global)", - }, - }), - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - methodId: "api-key", - label: "Coding Plan API Key for Global/Intl (subscription)", - hint: "Endpoint: coding-intl.dashscope.aliyuncs.com", - optionKey: "modelstudioApiKey", - flagName: "--modelstudio-api-key", - envVar: "MODELSTUDIO_API_KEY", - promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (Global/Intl)", - defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF, - expectedProviders: ["modelstudio"], - applyConfig: (cfg) => applyModelStudioConfig(cfg), - noteMessage: [ - "Get your API key at: https://bailian.console.aliyun.com/", - "Endpoint: coding-intl.dashscope.aliyuncs.com", - "Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.", - ].join("\n"), - noteTitle: "Alibaba Cloud Model Studio Coding Plan (Global/Intl)", - wizard: { - choiceId: "modelstudio-api-key", - choiceLabel: "Coding Plan API Key for Global/Intl (subscription)", - choiceHint: "Endpoint: coding-intl.dashscope.aliyuncs.com", - groupId: "modelstudio", - groupLabel: "Alibaba Cloud Model Studio", - groupHint: "Coding Plan API key (CN / Global)", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildModelStudioProvider, - allowExplicitBaseUrl: true, - }), + provider: { + label: "Model Studio", + docsPath: "/providers/models", + auth: [ + { + methodId: "api-key-cn", + label: "Coding Plan API Key for China (subscription)", + hint: "Endpoint: coding.dashscope.aliyuncs.com", + optionKey: "modelstudioApiKeyCn", + flagName: "--modelstudio-api-key-cn", + envVar: "MODELSTUDIO_API_KEY", + promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (China)", + defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyModelStudioConfigCn(cfg), + noteMessage: [ + "Get your API key at: https://bailian.console.aliyun.com/", + "Endpoint: coding.dashscope.aliyuncs.com", + "Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.", + ].join("\n"), + noteTitle: "Alibaba Cloud Model Studio Coding Plan (China)", + wizard: { + choiceHint: "Endpoint: coding.dashscope.aliyuncs.com", + groupLabel: "Alibaba Cloud Model Studio", + groupHint: "Coding Plan API key (CN / Global)", + }, }, - }); + { + methodId: "api-key", + label: "Coding Plan API Key for Global/Intl (subscription)", + hint: "Endpoint: coding-intl.dashscope.aliyuncs.com", + optionKey: "modelstudioApiKey", + flagName: "--modelstudio-api-key", + envVar: "MODELSTUDIO_API_KEY", + promptMessage: "Enter Alibaba Cloud Model Studio Coding Plan API key (Global/Intl)", + defaultModel: MODELSTUDIO_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyModelStudioConfig(cfg), + noteMessage: [ + "Get your API key at: https://bailian.console.aliyun.com/", + "Endpoint: coding-intl.dashscope.aliyuncs.com", + "Models: qwen3.5-plus, glm-4.7, kimi-k2.5, MiniMax-M2.5, etc.", + ].join("\n"), + noteTitle: "Alibaba Cloud Model Studio Coding Plan (Global/Intl)", + wizard: { + choiceHint: "Endpoint: coding-intl.dashscope.aliyuncs.com", + groupLabel: "Alibaba Cloud Model Studio", + groupHint: "Coding Plan API key (CN / Global)", + }, + }, + ], + catalog: { + buildProvider: buildModelStudioProvider, + allowExplicitBaseUrl: true, + }, }, }); diff --git a/extensions/moonshot/index.ts b/extensions/moonshot/index.ts index dd23e9a6309..a5908547acd 100644 --- a/extensions/moonshot/index.ts +++ b/extensions/moonshot/index.ts @@ -1,6 +1,4 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { createMoonshotThinkingWrapper, resolveMoonshotThinkingType, @@ -16,76 +14,56 @@ import { createKimiWebSearchProvider } from "./src/kimi-web-search-provider.js"; const PROVIDER_ID = "moonshot"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Moonshot Provider", description: "Bundled Moonshot provider plugin", + provider: { + label: "Moonshot", + docsPath: "/providers/moonshot", + auth: [ + { + methodId: "api-key", + label: "Kimi API key (.ai)", + hint: "Kimi K2.5 + Kimi", + optionKey: "moonshotApiKey", + flagName: "--moonshot-api-key", + envVar: "MOONSHOT_API_KEY", + promptMessage: "Enter Moonshot API key", + defaultModel: MOONSHOT_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyMoonshotConfig(cfg), + wizard: { + groupLabel: "Moonshot AI (Kimi K2.5)", + }, + }, + { + methodId: "api-key-cn", + label: "Kimi API key (.cn)", + hint: "Kimi K2.5 + Kimi", + optionKey: "moonshotApiKey", + flagName: "--moonshot-api-key", + envVar: "MOONSHOT_API_KEY", + promptMessage: "Enter Moonshot API key (.cn)", + defaultModel: MOONSHOT_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyMoonshotConfigCn(cfg), + wizard: { + groupLabel: "Moonshot AI (Kimi K2.5)", + }, + }, + ], + catalog: { + buildProvider: buildMoonshotProvider, + allowExplicitBaseUrl: true, + }, + wrapStreamFn: (ctx) => { + const thinkingType = resolveMoonshotThinkingType({ + configuredThinking: ctx.extraParams?.thinking, + thinkingLevel: ctx.thinkingLevel, + }); + return createMoonshotThinkingWrapper(ctx.streamFn, thinkingType); + }, + }, register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Moonshot", - docsPath: "/providers/moonshot", - envVars: ["MOONSHOT_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - methodId: "api-key", - label: "Kimi API key (.ai)", - hint: "Kimi K2.5 + Kimi", - optionKey: "moonshotApiKey", - flagName: "--moonshot-api-key", - envVar: "MOONSHOT_API_KEY", - promptMessage: "Enter Moonshot API key", - defaultModel: MOONSHOT_DEFAULT_MODEL_REF, - expectedProviders: ["moonshot"], - applyConfig: (cfg) => applyMoonshotConfig(cfg), - wizard: { - choiceId: "moonshot-api-key", - choiceLabel: "Kimi API key (.ai)", - groupId: "moonshot", - groupLabel: "Moonshot AI (Kimi K2.5)", - groupHint: "Kimi K2.5 + Kimi", - }, - }), - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - methodId: "api-key-cn", - label: "Kimi API key (.cn)", - hint: "Kimi K2.5 + Kimi", - optionKey: "moonshotApiKey", - flagName: "--moonshot-api-key", - envVar: "MOONSHOT_API_KEY", - promptMessage: "Enter Moonshot API key (.cn)", - defaultModel: MOONSHOT_DEFAULT_MODEL_REF, - expectedProviders: ["moonshot"], - applyConfig: (cfg) => applyMoonshotConfigCn(cfg), - wizard: { - choiceId: "moonshot-api-key-cn", - choiceLabel: "Kimi API key (.cn)", - groupId: "moonshot", - groupLabel: "Moonshot AI (Kimi K2.5)", - groupHint: "Kimi K2.5 + Kimi", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildMoonshotProvider, - allowExplicitBaseUrl: true, - }), - }, - wrapStreamFn: (ctx) => { - const thinkingType = resolveMoonshotThinkingType({ - configuredThinking: ctx.extraParams?.thinking, - thinkingLevel: ctx.thinkingLevel, - }); - return createMoonshotThinkingWrapper(ctx.streamFn, thinkingType); - }, - }); api.registerMediaUnderstandingProvider(moonshotMediaUnderstandingProvider); api.registerWebSearchProvider(createKimiWebSearchProvider()); }, diff --git a/extensions/nvidia/index.ts b/extensions/nvidia/index.ts index ce67ef562d4..7f0cb36a872 100644 --- a/extensions/nvidia/index.ts +++ b/extensions/nvidia/index.ts @@ -1,29 +1,19 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { buildNvidiaProvider } from "./provider-catalog.js"; const PROVIDER_ID = "nvidia"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "NVIDIA Provider", description: "Bundled NVIDIA provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "NVIDIA", - docsPath: "/providers/nvidia", - envVars: ["NVIDIA_API_KEY"], - auth: [], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildNvidiaProvider, - }), - }, - }); + provider: { + label: "NVIDIA", + docsPath: "/providers/nvidia", + envVars: ["NVIDIA_API_KEY"], + auth: [], + catalog: { + buildProvider: buildNvidiaProvider, + }, }, }); diff --git a/extensions/qianfan/index.ts b/extensions/qianfan/index.ts index c2f3147f135..5daa47f1fee 100644 --- a/extensions/qianfan/index.ts +++ b/extensions/qianfan/index.ts @@ -1,52 +1,31 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildQianfanProvider } from "./provider-catalog.js"; const PROVIDER_ID = "qianfan"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Qianfan Provider", description: "Bundled Qianfan provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Qianfan", - docsPath: "/providers/qianfan", - envVars: ["QIANFAN_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - 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, - expectedProviders: ["qianfan"], - applyConfig: (cfg) => applyQianfanConfig(cfg), - wizard: { - choiceId: "qianfan-api-key", - choiceLabel: "Qianfan API key", - groupId: "qianfan", - groupLabel: "Qianfan", - groupHint: "API key", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildQianfanProvider, - }), + 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, + }, }, }); diff --git a/extensions/synthetic/index.ts b/extensions/synthetic/index.ts index 5a88f19309a..7ef2fa4688c 100644 --- a/extensions/synthetic/index.ts +++ b/extensions/synthetic/index.ts @@ -1,52 +1,31 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applySyntheticConfig, SYNTHETIC_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildSyntheticProvider } from "./provider-catalog.js"; const PROVIDER_ID = "synthetic"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Synthetic Provider", description: "Bundled Synthetic provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Synthetic", - docsPath: "/providers/synthetic", - envVars: ["SYNTHETIC_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - 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, - expectedProviders: ["synthetic"], - applyConfig: (cfg) => applySyntheticConfig(cfg), - wizard: { - choiceId: "synthetic-api-key", - choiceLabel: "Synthetic API key", - groupId: "synthetic", - groupLabel: "Synthetic", - groupHint: "Anthropic-compatible (multi-model)", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildSyntheticProvider, - }), + 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), }, - }); + ], + catalog: { + buildProvider: buildSyntheticProvider, + }, }, }); diff --git a/extensions/together/index.ts b/extensions/together/index.ts index 30ca167003d..42ecad1a3a2 100644 --- a/extensions/together/index.ts +++ b/extensions/together/index.ts @@ -1,52 +1,34 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyTogetherConfig, TOGETHER_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildTogetherProvider } from "./provider-catalog.js"; const PROVIDER_ID = "together"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Together Provider", description: "Bundled Together provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Together", - docsPath: "/providers/together", - envVars: ["TOGETHER_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - 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, - expectedProviders: ["together"], - applyConfig: (cfg) => applyTogetherConfig(cfg), - wizard: { - choiceId: "together-api-key", - choiceLabel: "Together AI API key", - groupId: "together", - groupLabel: "Together AI", - groupHint: "API key", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildTogetherProvider, - }), + 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, + }, }, }); diff --git a/extensions/venice/index.ts b/extensions/venice/index.ts index e47cbf629f2..4d56a2e59d5 100644 --- a/extensions/venice/index.ts +++ b/extensions/venice/index.ts @@ -1,6 +1,4 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyXaiModelCompat } from "openclaw/plugin-sdk/provider-models"; import { applyVeniceConfig, VENICE_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildVeniceProvider } from "./provider-catalog.js"; @@ -11,55 +9,39 @@ function isXaiBackedVeniceModel(modelId: string): boolean { return modelId.trim().toLowerCase().includes("grok"); } -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Venice Provider", description: "Bundled Venice provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Venice", - docsPath: "/providers/venice", - envVars: ["VENICE_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - 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, - expectedProviders: ["venice"], - 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: { - choiceId: "venice-api-key", - choiceLabel: "Venice AI API key", - groupId: "venice", - groupLabel: "Venice AI", - groupHint: "Privacy-focused (uncensored models)", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildVeniceProvider, - }), + 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", + }, }, - normalizeResolvedModel: ({ modelId, model }) => - isXaiBackedVeniceModel(modelId) ? applyXaiModelCompat(model) : undefined, - }); + ], + catalog: { + buildProvider: buildVeniceProvider, + }, + normalizeResolvedModel: ({ modelId, model }) => + isXaiBackedVeniceModel(modelId) ? applyXaiModelCompat(model) : undefined, }, }); diff --git a/extensions/vercel-ai-gateway/index.ts b/extensions/vercel-ai-gateway/index.ts index 8cf329ef140..f224698e1d7 100644 --- a/extensions/vercel-ai-gateway/index.ts +++ b/extensions/vercel-ai-gateway/index.ts @@ -1,52 +1,35 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyVercelAiGatewayConfig, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildVercelAiGatewayProvider } from "./provider-catalog.js"; const PROVIDER_ID = "vercel-ai-gateway"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Vercel AI Gateway Provider", description: "Bundled Vercel AI Gateway provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Vercel AI Gateway", - docsPath: "/providers/vercel-ai-gateway", - envVars: ["AI_GATEWAY_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - 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, - expectedProviders: ["vercel-ai-gateway"], - applyConfig: (cfg) => applyVercelAiGatewayConfig(cfg), - wizard: { - choiceId: "ai-gateway-api-key", - choiceLabel: "Vercel AI Gateway API key", - groupId: "ai-gateway", - groupLabel: "Vercel AI Gateway", - groupHint: "API key", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildVercelAiGatewayProvider, - }), + 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", + }, }, - }); + ], + catalog: { + buildProvider: buildVercelAiGatewayProvider, + }, }, }); diff --git a/extensions/xai/index.ts b/extensions/xai/index.ts index 6dc646a2cad..82d817a358b 100644 --- a/extensions/xai/index.ts +++ b/extensions/xai/index.ts @@ -1,6 +1,4 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { applyXaiModelCompat } from "openclaw/plugin-sdk/provider-models"; import { createToolStreamWrapper } from "openclaw/plugin-sdk/provider-stream"; import { applyXaiConfig, XAI_DEFAULT_MODEL_REF } from "./onboard.js"; @@ -14,68 +12,54 @@ import { createXaiWebSearchProvider } from "./web-search.js"; const PROVIDER_ID = "xai"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: "xai", name: "xAI Plugin", description: "Bundled xAI plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "xAI", - aliases: ["x-ai"], - docsPath: "/providers/xai", - envVars: ["XAI_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - methodId: "api-key", - label: "xAI API key", - hint: "API key", - optionKey: "xaiApiKey", - flagName: "--xai-api-key", - envVar: "XAI_API_KEY", - promptMessage: "Enter xAI API key", - defaultModel: XAI_DEFAULT_MODEL_REF, - expectedProviders: ["xai"], - applyConfig: (cfg) => applyXaiConfig(cfg), - wizard: { - choiceId: "xai-api-key", - choiceLabel: "xAI API key", - groupId: "xai", - groupLabel: "xAI (Grok)", - groupHint: "API key", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildXaiProvider, - }), + provider: { + label: "xAI", + aliases: ["x-ai"], + docsPath: "/providers/xai", + auth: [ + { + methodId: "api-key", + label: "xAI API key", + hint: "API key", + optionKey: "xaiApiKey", + flagName: "--xai-api-key", + envVar: "XAI_API_KEY", + promptMessage: "Enter xAI API key", + defaultModel: XAI_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyXaiConfig(cfg), + wizard: { + groupLabel: "xAI (Grok)", + }, }, - prepareExtraParams: (ctx) => { - if (ctx.extraParams?.tool_stream !== undefined) { - return ctx.extraParams; - } - return { - ...ctx.extraParams, - tool_stream: true, - }; - }, - wrapStreamFn: (ctx) => - createToolStreamWrapper( - createXaiToolCallArgumentDecodingWrapper( - createXaiToolPayloadCompatibilityWrapper(ctx.streamFn), - ), - ctx.extraParams?.tool_stream !== false, + ], + catalog: { + buildProvider: buildXaiProvider, + }, + prepareExtraParams: (ctx) => { + if (ctx.extraParams?.tool_stream !== undefined) { + return ctx.extraParams; + } + return { + ...ctx.extraParams, + tool_stream: true, + }; + }, + wrapStreamFn: (ctx) => + createToolStreamWrapper( + createXaiToolCallArgumentDecodingWrapper( + createXaiToolPayloadCompatibilityWrapper(ctx.streamFn), ), - normalizeResolvedModel: ({ model }) => applyXaiModelCompat(model), - resolveDynamicModel: (ctx) => resolveXaiForwardCompatModel({ providerId: PROVIDER_ID, ctx }), - isModernModelRef: ({ modelId }) => isModernXaiModel(modelId), - }); + ctx.extraParams?.tool_stream !== false, + ), + normalizeResolvedModel: ({ model }) => applyXaiModelCompat(model), + resolveDynamicModel: (ctx) => resolveXaiForwardCompatModel({ providerId: PROVIDER_ID, ctx }), + isModernModelRef: ({ modelId }) => isModernXaiModel(modelId), + }, + register(api) { api.registerWebSearchProvider(createXaiWebSearchProvider()); }, }); diff --git a/extensions/xiaomi/index.ts b/extensions/xiaomi/index.ts index 513581c0332..350e812d577 100644 --- a/extensions/xiaomi/index.ts +++ b/extensions/xiaomi/index.ts @@ -1,64 +1,43 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; -import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth"; -import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog"; +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; import { PROVIDER_LABELS } from "openclaw/plugin-sdk/provider-usage"; import { applyXiaomiConfig, XIAOMI_DEFAULT_MODEL_REF } from "./onboard.js"; import { buildXiaomiProvider } from "./provider-catalog.js"; const PROVIDER_ID = "xiaomi"; -export default definePluginEntry({ +export default defineSingleProviderPluginEntry({ id: PROVIDER_ID, name: "Xiaomi Provider", description: "Bundled Xiaomi provider plugin", - register(api) { - api.registerProvider({ - id: PROVIDER_ID, - label: "Xiaomi", - docsPath: "/providers/xiaomi", - envVars: ["XIAOMI_API_KEY"], - auth: [ - createProviderApiKeyAuthMethod({ - providerId: PROVIDER_ID, - methodId: "api-key", - label: "Xiaomi API key", - hint: "API key", - optionKey: "xiaomiApiKey", - flagName: "--xiaomi-api-key", - envVar: "XIAOMI_API_KEY", - promptMessage: "Enter Xiaomi API key", - defaultModel: XIAOMI_DEFAULT_MODEL_REF, - expectedProviders: ["xiaomi"], - applyConfig: (cfg) => applyXiaomiConfig(cfg), - wizard: { - choiceId: "xiaomi-api-key", - choiceLabel: "Xiaomi API key", - groupId: "xiaomi", - groupLabel: "Xiaomi", - groupHint: "API key", - }, - }), - ], - catalog: { - order: "simple", - run: (ctx) => - buildSingleProviderApiKeyCatalog({ - ctx, - providerId: PROVIDER_ID, - buildProvider: buildXiaomiProvider, - }), + provider: { + label: "Xiaomi", + docsPath: "/providers/xiaomi", + auth: [ + { + methodId: "api-key", + label: "Xiaomi API key", + hint: "API key", + optionKey: "xiaomiApiKey", + flagName: "--xiaomi-api-key", + envVar: "XIAOMI_API_KEY", + promptMessage: "Enter Xiaomi API key", + defaultModel: XIAOMI_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyXiaomiConfig(cfg), }, - resolveUsageAuth: async (ctx) => { - const apiKey = ctx.resolveApiKeyFromConfigAndStore({ - envDirect: [ctx.env.XIAOMI_API_KEY], - }); - return apiKey ? { token: apiKey } : null; - }, - fetchUsageSnapshot: async () => ({ - provider: "xiaomi", - displayName: PROVIDER_LABELS.xiaomi, - windows: [], - }), - }); + ], + catalog: { + buildProvider: buildXiaomiProvider, + }, + resolveUsageAuth: async (ctx) => { + const apiKey = ctx.resolveApiKeyFromConfigAndStore({ + envDirect: [ctx.env.XIAOMI_API_KEY], + }); + return apiKey ? { token: apiKey } : null; + }, + fetchUsageSnapshot: async () => ({ + provider: "xiaomi", + displayName: PROVIDER_LABELS.xiaomi, + windows: [], + }), }, }); diff --git a/src/plugin-sdk/provider-entry.test.ts b/src/plugin-sdk/provider-entry.test.ts new file mode 100644 index 00000000000..d09a62ab876 --- /dev/null +++ b/src/plugin-sdk/provider-entry.test.ts @@ -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" }], + }, + }); + }); +}); diff --git a/src/plugin-sdk/provider-entry.ts b/src/plugin-sdk/provider-entry.ts new file mode 100644 index 00000000000..273b71e8a7e --- /dev/null +++ b/src/plugin-sdk/provider-entry.ts @@ -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[0]; + +export type SingleProviderPluginApiKeyAuthOptions = Omit< + ApiKeyAuthMethodOptions, + "providerId" | "expectedProviders" | "wizard" +> & { + expectedProviders?: string[]; + wizard?: false | ProviderPluginWizardSetup; +}; + +export type SingleProviderPluginCatalogOptions = { + buildProvider: Parameters[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); + }, + }); +}