From dc8fdb17564595d5fcbc2bdc60878e9f9a72bcd7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Jul 2026 01:29:53 -0400 Subject: [PATCH] refactor(providers): derive simple providers from manifests (#114331) --- extensions/baseten/index.ts | 38 ++----- extensions/cerebras/index.ts | 41 +++----- extensions/cohere/index.test.ts | 21 ++-- extensions/cohere/index.ts | 26 +---- extensions/cohere/onboard.test.ts | 3 +- extensions/cohere/onboard.ts | 2 +- extensions/cohere/provider-catalog.ts | 11 +- extensions/deepseek/index.ts | 25 +---- extensions/deepseek/onboard.test.ts | 4 +- extensions/deepseek/onboard.ts | 5 +- extensions/featherless/index.ts | 33 ++---- extensions/fireworks/index.ts | 20 +--- extensions/gmi/index.ts | 22 ++-- extensions/gmi/models.ts | 6 +- extensions/huggingface/index.ts | 19 ++-- extensions/kilocode/index.ts | 19 ++-- extensions/longcat/index.ts | 36 ++----- extensions/meta/index.ts | 28 ++--- extensions/mistral/index.ts | 20 +--- extensions/novita/index.ts | 22 ++-- extensions/novita/models.ts | 6 +- extensions/nvidia/index.ts | 20 ++-- extensions/qianfan/index.ts | 24 ++--- extensions/synthetic/index.ts | 19 ++-- extensions/together/index.ts | 28 +---- extensions/venice/index.ts | 35 +++---- extensions/vercel-ai-gateway/index.ts | 23 ++--- src/plugin-sdk/provider-entry.test.ts | 141 ++++++++++++++++++++++++++ src/plugin-sdk/provider-entry.ts | 124 ++++++++++++++++++++-- 29 files changed, 409 insertions(+), 412 deletions(-) diff --git a/extensions/baseten/index.ts b/extensions/baseten/index.ts index 0dcea5685e42..8d8b9752f7ef 100644 --- a/extensions/baseten/index.ts +++ b/extensions/baseten/index.ts @@ -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) => { diff --git a/extensions/cerebras/index.ts b/extensions/cerebras/index.ts index 2ec508887e6b..10c079b13fb1 100644 --- a/extensions/cerebras/index.ts +++ b/extensions/cerebras/index.ts @@ -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 }, }, }); diff --git a/extensions/cohere/index.test.ts b/extensions/cohere/index.test.ts index b387f44b0b31..9cfb63737189 100644 --- a/extensions/cohere/index.test.ts +++ b/extensions/cohere/index.test.ts @@ -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", () => { diff --git a/extensions/cohere/index.ts b/extensions/cohere/index.ts index 6110428790ed..204b432f0cb1 100644 --- a/extensions/cohere/index.ts +++ b/extensions/cohere/index.ts @@ -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), diff --git a/extensions/cohere/onboard.test.ts b/extensions/cohere/onboard.test.ts index 28413c2143e3..52b57af5b6ef 100644 --- a/extensions/cohere/onboard.test.ts +++ b/extensions/cohere/onboard.test.ts @@ -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"; diff --git a/extensions/cohere/onboard.ts b/extensions/cohere/onboard.ts index 5936b894d0dc..d80b562972ba 100644 --- a/extensions/cohere/onboard.ts +++ b/extensions/cohere/onboard.ts @@ -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, diff --git a/extensions/cohere/provider-catalog.ts b/extensions/cohere/provider-catalog.ts index 550ff53fa2ae..1cd3cdbc13e4 100644 --- a/extensions/cohere/provider-catalog.ts +++ b/extensions/cohere/provider-catalog.ts @@ -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(), - }; -} diff --git a/extensions/deepseek/index.ts b/extensions/deepseek/index.ts index cda1a5d3ecc5..b3a3ead7a60e 100644 --- a/extensions/deepseek/index.ts +++ b/extensions/deepseek/index.ts @@ -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, diff --git a/extensions/deepseek/onboard.test.ts b/extensions/deepseek/onboard.test.ts index 447ae432b3af..b6cfd3ea49ce 100644 --- a/extensions/deepseek/onboard.test.ts +++ b/extensions/deepseek/onboard.test.ts @@ -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({}); diff --git a/extensions/deepseek/onboard.ts b/extensions/deepseek/onboard.ts index eebfd356c0d7..356a4c464ac4 100644 --- a/extensions/deepseek/onboard.ts +++ b/extensions/deepseek/onboard.ts @@ -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, diff --git a/extensions/featherless/index.ts b/extensions/featherless/index.ts index ab00c5a96740..31d32ba3dc2b 100644 --- a/extensions/featherless/index.ts +++ b/extensions/featherless/index.ts @@ -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", diff --git a/extensions/fireworks/index.ts b/extensions/fireworks/index.ts index da0c248690ba..86e0e7f61dd5 100644 --- a/extensions/fireworks/index.ts +++ b/extensions/fireworks/index.ts @@ -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, }, diff --git a/extensions/gmi/index.ts b/extensions/gmi/index.ts index ee691228f497..f013f314b4db 100644 --- a/extensions/gmi/index.ts +++ b/extensions/gmi/index.ts @@ -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, diff --git a/extensions/gmi/models.ts b/extensions/gmi/models.ts index 6069498e4302..642ddbd6b8cc 100644 --- a/extensions/gmi/models.ts +++ b/extensions/gmi/models.ts @@ -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")!; diff --git a/extensions/huggingface/index.ts b/extensions/huggingface/index.ts index 7ab83fa4bdea..5325f4b0a76d 100644 --- a/extensions/huggingface/index.ts +++ b/extensions/huggingface/index.ts @@ -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) => { diff --git a/extensions/kilocode/index.ts b/extensions/kilocode/index.ts index fac6ee34b2fc..68b749dd5a94 100644 --- a/extensions/kilocode/index.ts +++ b/extensions/kilocode/index.ts @@ -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, diff --git a/extensions/longcat/index.ts b/extensions/longcat/index.ts index e30d5cf7c617..fa88f38a5c34 100644 --- a/extensions/longcat/index.ts +++ b/extensions/longcat/index.ts @@ -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, diff --git a/extensions/meta/index.ts b/extensions/meta/index.ts index f9d14ec1ec05..f33377f8bf2e 100644 --- a/extensions/meta/index.ts +++ b/extensions/meta/index.ts @@ -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, diff --git a/extensions/mistral/index.ts b/extensions/mistral/index.ts index 0771268017c4..349c4d141977 100644 --- a/extensions/mistral/index.ts +++ b/extensions/mistral/index.ts @@ -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, diff --git a/extensions/novita/index.ts b/extensions/novita/index.ts index f03148800ad8..c1020424d006 100644 --- a/extensions/novita/index.ts +++ b/extensions/novita/index.ts @@ -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, diff --git a/extensions/novita/models.ts b/extensions/novita/models.ts index 48810282cf4b..1fc9c0b30e1c 100644 --- a/extensions/novita/models.ts +++ b/extensions/novita/models.ts @@ -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")!; diff --git a/extensions/nvidia/index.ts b/extensions/nvidia/index.ts index e49cb70241fb..a4483fd16bab 100644 --- a/extensions/nvidia/index.ts +++ b/extensions/nvidia/index.ts @@ -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, diff --git a/extensions/qianfan/index.ts b/extensions/qianfan/index.ts index be2bb68827e8..c507b91124f8 100644 --- a/extensions/qianfan/index.ts +++ b/extensions/qianfan/index.ts @@ -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 }, }, }); diff --git a/extensions/synthetic/index.ts b/extensions/synthetic/index.ts index 74adaca355a6..32e57a5b7426 100644 --- a/extensions/synthetic/index.ts +++ b/extensions/synthetic/index.ts @@ -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, }, diff --git a/extensions/together/index.ts b/extensions/together/index.ts index b951ad6c07e2..6be7e5a10fba 100644 --- a/extensions/together/index.ts +++ b/extensions/together/index.ts @@ -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" diff --git a/extensions/venice/index.ts b/extensions/venice/index.ts index 4c960c69681a..24874f6a9cd6 100644 --- a/extensions/venice/index.ts +++ b/extensions/venice/index.ts @@ -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, diff --git a/extensions/vercel-ai-gateway/index.ts b/extensions/vercel-ai-gateway/index.ts index 215032950622..324ec44d73f5 100644 --- a/extensions/vercel-ai-gateway/index.ts +++ b/extensions/vercel-ai-gateway/index.ts @@ -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, diff --git a/src/plugin-sdk/provider-entry.test.ts b/src/plugin-sdk/provider-entry.test.ts index 841f770b1619..6a47f23b2a98 100644 --- a/src/plugin-sdk/provider-entry.test.ts +++ b/src/plugin-sdk/provider-entry.test.ts @@ -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", diff --git a/src/plugin-sdk/provider-entry.ts b/src/plugin-sdk/provider-entry.ts index af3bf761e230..2000f6bbabd0 100644 --- a/src/plugin-sdk/provider-entry.ts +++ b/src/plugin-sdk/provider-entry.ts @@ -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[0]; +type SingleProviderPluginManifest = { + setup?: { + providers?: readonly Pick[]; + }; + providerAuthChoices?: readonly PluginManifestProviderAuthChoice[]; + modelCatalog?: { + providers?: Readonly>; + discovery?: Readonly>; + }; +}; + /** * 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[0]["buildProvider"]; + buildProvider?: Parameters[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 => @@ -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",