refactor(providers): share api-key catalog helper

This commit is contained in:
Peter Steinberger
2026-03-17 04:03:07 +00:00
parent 8357372cc7
commit a20b64cd92
12 changed files with 180 additions and 128 deletions

View File

@@ -5,6 +5,7 @@ import {
} from "../../src/agents/pi-embedded-runner/proxy-stream-wrappers.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { applyKilocodeConfig, KILOCODE_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildKilocodeProviderWithDiscovery } from "./provider-catalog.js";
const PROVIDER_ID = "kilocode";
@@ -44,18 +45,12 @@ const kilocodePlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...(await buildKilocodeProviderWithDiscovery()),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildKilocodeProviderWithDiscovery,
}),
},
capabilities: {
geminiThoughtSignatureSanitization: true,

View File

@@ -1,5 +1,6 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import {
applyModelStudioConfig,
applyModelStudioConfigCn,
@@ -78,22 +79,13 @@ const modelStudioPlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : "";
return {
provider: {
...buildModelStudioProvider(),
...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildModelStudioProvider,
allowExplicitBaseUrl: true,
}),
},
});
},

View File

@@ -9,6 +9,7 @@ import {
} from "../../src/agents/tools/web-search-plugin-factory.js";
import { emptyPluginConfigSchema } from "../../src/plugins/config-schema.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import type { OpenClawPluginApi } from "../../src/plugins/types.js";
import { moonshotMediaUnderstandingProvider } from "./media-understanding-provider.js";
import {
@@ -75,22 +76,13 @@ const moonshotPlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
const explicitProvider = ctx.config.models?.providers?.[PROVIDER_ID];
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : "";
return {
provider: {
...buildMoonshotProvider(),
...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildMoonshotProvider,
allowExplicitBaseUrl: true,
}),
},
wrapStreamFn: (ctx) => {
const thinkingType = resolveMoonshotThinkingType({

View File

@@ -1,4 +1,5 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildNvidiaProvider } from "./provider-catalog.js";
const PROVIDER_ID = "nvidia";
@@ -17,18 +18,12 @@ const nvidiaPlugin = {
auth: [],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...buildNvidiaProvider(),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildNvidiaProvider,
}),
},
});
},

View File

@@ -1,6 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildQianfanProvider } from "./provider-catalog.js";
const PROVIDER_ID = "qianfan";
@@ -40,18 +41,12 @@ const qianfanPlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...buildQianfanProvider(),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildQianfanProvider,
}),
},
});
},

View File

@@ -1,6 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { applySyntheticConfig, SYNTHETIC_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildSyntheticProvider } from "./provider-catalog.js";
const PROVIDER_ID = "synthetic";
@@ -40,18 +41,12 @@ const syntheticPlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...buildSyntheticProvider(),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildSyntheticProvider,
}),
},
});
},

View File

@@ -1,6 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { applyTogetherConfig, TOGETHER_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildTogetherProvider } from "./provider-catalog.js";
const PROVIDER_ID = "together";
@@ -40,18 +41,12 @@ const togetherPlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...buildTogetherProvider(),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildTogetherProvider,
}),
},
});
},

View File

@@ -1,6 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { applyVeniceConfig, VENICE_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildVeniceProvider } from "./provider-catalog.js";
const PROVIDER_ID = "venice";
@@ -46,18 +47,12 @@ const venicePlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...(await buildVeniceProvider()),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildVeniceProvider,
}),
},
});
},

View File

@@ -1,6 +1,7 @@
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk/core";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { applyVercelAiGatewayConfig, VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildVercelAiGatewayProvider } from "./provider-catalog.js";
const PROVIDER_ID = "vercel-ai-gateway";
@@ -40,18 +41,12 @@ const vercelAiGatewayPlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...(await buildVercelAiGatewayProvider()),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildVercelAiGatewayProvider,
}),
},
});
},

View File

@@ -2,6 +2,7 @@ import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin
import { PROVIDER_LABELS } from "../../src/infra/provider-usage.shared.js";
import { createProviderApiKeyAuthMethod } from "../../src/plugins/provider-api-key-auth.js";
import { applyXiaomiConfig, XIAOMI_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildSingleProviderApiKeyCatalog } from "../../src/plugins/provider-catalog.js";
import { buildXiaomiProvider } from "./provider-catalog.js";
const PROVIDER_ID = "xiaomi";
@@ -41,18 +42,12 @@ const xiaomiPlugin = {
],
catalog: {
order: "simple",
run: async (ctx) => {
const apiKey = ctx.resolveProviderApiKey(PROVIDER_ID).apiKey;
if (!apiKey) {
return null;
}
return {
provider: {
...buildXiaomiProvider(),
apiKey,
},
};
},
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: PROVIDER_ID,
buildProvider: buildXiaomiProvider,
}),
},
resolveUsageAuth: async (ctx) => {
const apiKey = ctx.resolveApiKeyFromConfigAndStore({

View File

@@ -0,0 +1,80 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { buildSingleProviderApiKeyCatalog } from "./provider-catalog.js";
import type { ProviderCatalogContext } from "./types.js";
function createCatalogContext(params: {
config?: OpenClawConfig;
apiKeys?: Record<string, string | undefined>;
}): ProviderCatalogContext {
return {
config: params.config ?? {},
env: {},
resolveProviderApiKey: (providerId) => ({
apiKey: providerId ? params.apiKeys?.[providerId] : undefined,
}),
};
}
describe("buildSingleProviderApiKeyCatalog", () => {
it("returns null when api key is missing", async () => {
const result = await buildSingleProviderApiKeyCatalog({
ctx: createCatalogContext({}),
providerId: "test-provider",
buildProvider: () => ({ api: "openai-completions", provider: "test-provider" }),
});
expect(result).toBeNull();
});
it("adds api key to the built provider", async () => {
const result = await buildSingleProviderApiKeyCatalog({
ctx: createCatalogContext({
apiKeys: { "test-provider": "secret-key" },
}),
providerId: "test-provider",
buildProvider: async () => ({ api: "openai-completions", provider: "test-provider" }),
});
expect(result).toEqual({
provider: {
api: "openai-completions",
provider: "test-provider",
apiKey: "secret-key",
},
});
});
it("prefers explicit base url when allowed", async () => {
const result = await buildSingleProviderApiKeyCatalog({
ctx: createCatalogContext({
apiKeys: { "test-provider": "secret-key" },
config: {
models: {
providers: {
"test-provider": {
baseUrl: " https://override.example/v1/ ",
},
},
},
},
}),
providerId: "test-provider",
buildProvider: () => ({
api: "openai-completions",
provider: "test-provider",
baseUrl: "https://default.example/v1",
}),
allowExplicitBaseUrl: true,
});
expect(result).toEqual({
provider: {
api: "openai-completions",
provider: "test-provider",
baseUrl: "https://override.example/v1/",
apiKey: "secret-key",
},
});
});
});

View File

@@ -0,0 +1,28 @@
import type { ModelProviderConfig } from "../config/types.js";
import type { ProviderCatalogContext, ProviderCatalogResult } from "./types.js";
export async function buildSingleProviderApiKeyCatalog(params: {
ctx: ProviderCatalogContext;
providerId: string;
buildProvider: () => ModelProviderConfig | Promise<ModelProviderConfig>;
allowExplicitBaseUrl?: boolean;
}): Promise<ProviderCatalogResult> {
const apiKey = params.ctx.resolveProviderApiKey(params.providerId).apiKey;
if (!apiKey) {
return null;
}
const explicitProvider = params.allowExplicitBaseUrl
? params.ctx.config.models?.providers?.[params.providerId]
: undefined;
const explicitBaseUrl =
typeof explicitProvider?.baseUrl === "string" ? explicitProvider.baseUrl.trim() : "";
return {
provider: {
...(await params.buildProvider()),
...(explicitBaseUrl ? { baseUrl: explicitBaseUrl } : {}),
apiKey,
},
};
}