refactor(plugins): harden package boundary sdk prep

This commit is contained in:
Vincent Koc
2026-04-07 09:40:31 +01:00
parent 81f48384cb
commit fb64ba7bf7
13 changed files with 165 additions and 140 deletions

View File

@@ -1,5 +1,4 @@
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key";
import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry";
import { applyHuggingfaceConfig, HUGGINGFACE_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildHuggingfaceProvider } from "./provider-catalog.js";
@@ -11,60 +10,51 @@ type HuggingFacePluginConfig = {
};
};
export default definePluginEntry({
export default defineSingleProviderPluginEntry({
id: PROVIDER_ID,
name: "Hugging Face Provider",
description: "Bundled Hugging Face provider plugin",
register(api) {
const pluginConfig = (api.pluginConfig ?? {}) as HuggingFacePluginConfig;
api.registerProvider({
id: PROVIDER_ID,
label: "Hugging Face",
docsPath: "/providers/huggingface",
envVars: ["HUGGINGFACE_HUB_TOKEN", "HF_TOKEN"],
auth: [
createProviderApiKeyAuthMethod({
providerId: PROVIDER_ID,
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,
expectedProviders: ["huggingface"],
applyConfig: (cfg) => applyHuggingfaceConfig(cfg),
wizard: {
choiceId: "huggingface-api-key",
choiceLabel: "Hugging Face API key",
choiceHint: "Inference API (HF token)",
groupId: "huggingface",
groupLabel: "Hugging Face",
groupHint: "Inference API (HF token)",
},
}),
],
catalog: {
order: "simple",
run: async (ctx) => {
const discoveryEnabled =
pluginConfig.discovery?.enabled ?? ctx.config?.models?.huggingfaceDiscovery?.enabled;
if (discoveryEnabled === false) {
return null;
}
const { apiKey, discoveryApiKey } = ctx.resolveProviderApiKey(PROVIDER_ID);
if (!apiKey) {
return null;
}
return {
provider: {
...(await buildHuggingfaceProvider(discoveryApiKey)),
apiKey,
},
};
},
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),
},
});
],
catalog: {
order: "simple",
run: async (ctx) => {
const pluginEntry = ctx.config?.plugins?.entries?.[PROVIDER_ID];
const pluginConfig =
pluginEntry && typeof pluginEntry === "object" && pluginEntry.config
? (pluginEntry.config as HuggingFacePluginConfig)
: undefined;
const discoveryEnabled =
pluginConfig?.discovery?.enabled ?? ctx.config?.models?.huggingfaceDiscovery?.enabled;
if (discoveryEnabled === false) {
return null;
}
const { apiKey, discoveryApiKey } = ctx.resolveProviderApiKey(PROVIDER_ID);
if (!apiKey) {
return null;
}
return {
provider: {
...(await buildHuggingfaceProvider(discoveryApiKey)),
apiKey,
},
};
},
},
},
});

View File

@@ -1,4 +1,4 @@
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-types";
export const HUGGINGFACE_BASE_URL = "https://router.huggingface.co/v1";
export const HUGGINGFACE_POLICY_SUFFIXES = ["cheapest", "fastest"] as const;

View File

@@ -1,4 +1,4 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-types";
import {
buildHuggingfaceModelDefinition,
discoverHuggingfaceModels,

View File

@@ -5,22 +5,6 @@
"openclaw/extension-api": ["../src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["../dist/plugin-sdk/index.d.ts"],
"openclaw/plugin-sdk/*": ["../dist/plugin-sdk/*.d.ts"],
"openclaw/plugin-sdk/account-id": ["../dist/plugin-sdk/account-id.d.ts"],
"openclaw/plugin-sdk/channel-entry-contract": [
"../packages/plugin-sdk/dist/src/plugin-sdk/channel-entry-contract.d.ts"
],
"openclaw/plugin-sdk/browser-maintenance": [
"../packages/plugin-sdk/dist/extensions/browser/browser-maintenance.d.ts"
],
"openclaw/plugin-sdk/provider-catalog-shared": [
"../packages/plugin-sdk/dist/src/plugin-sdk/provider-catalog-shared.d.ts"
],
"openclaw/plugin-sdk/provider-entry": [
"../packages/plugin-sdk/dist/src/plugin-sdk/provider-entry.d.ts"
],
"openclaw/plugin-sdk/secret-ref-runtime": [
"../dist/plugin-sdk/src/plugin-sdk/secret-ref-runtime.d.ts"
],
"@openclaw/*.js": ["../packages/plugin-sdk/dist/extensions/*.d.ts", "../extensions/*"],
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
"@openclaw/plugin-sdk/*": ["../dist/plugin-sdk/*.d.ts"]

View File

@@ -6,22 +6,6 @@
"openclaw/extension-api": ["../../src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["../../dist/plugin-sdk/index.d.ts"],
"openclaw/plugin-sdk/*": ["../../dist/plugin-sdk/*.d.ts"],
"openclaw/plugin-sdk/account-id": ["../../dist/plugin-sdk/account-id.d.ts"],
"openclaw/plugin-sdk/channel-entry-contract": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/channel-entry-contract.d.ts"
],
"openclaw/plugin-sdk/browser-maintenance": [
"../../packages/plugin-sdk/dist/extensions/browser/browser-maintenance.d.ts"
],
"openclaw/plugin-sdk/provider-catalog-shared": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/provider-catalog-shared.d.ts"
],
"openclaw/plugin-sdk/provider-entry": [
"../../packages/plugin-sdk/dist/src/plugin-sdk/provider-entry.d.ts"
],
"openclaw/plugin-sdk/secret-ref-runtime": [
"../../dist/plugin-sdk/src/plugin-sdk/secret-ref-runtime.d.ts"
],
"@openclaw/*.js": ["../../packages/plugin-sdk/dist/extensions/*.d.ts", "../*"],
"@openclaw/*": ["../*"],
"@openclaw/plugin-sdk/*": ["../../dist/plugin-sdk/*.d.ts"],

View File

@@ -864,6 +864,10 @@
"types": "./dist/plugin-sdk/provider-http.d.ts",
"default": "./dist/plugin-sdk/provider-http.js"
},
"./plugin-sdk/provider-model-types": {
"types": "./dist/plugin-sdk/provider-model-types.d.ts",
"default": "./dist/plugin-sdk/provider-model-types.js"
},
"./plugin-sdk/provider-model-shared": {
"types": "./dist/plugin-sdk/provider-model-shared.d.ts",
"default": "./dist/plugin-sdk/provider-model-shared.js"

View File

@@ -18,22 +18,6 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"openclaw/extension-api": ["../src/extensionAPI.ts"],
"openclaw/plugin-sdk": ["../dist/plugin-sdk/index.d.ts"],
"openclaw/plugin-sdk/*": ["../dist/plugin-sdk/*.d.ts"],
"openclaw/plugin-sdk/account-id": ["../dist/plugin-sdk/account-id.d.ts"],
"openclaw/plugin-sdk/channel-entry-contract": [
"../packages/plugin-sdk/dist/src/plugin-sdk/channel-entry-contract.d.ts",
],
"openclaw/plugin-sdk/browser-maintenance": [
"../packages/plugin-sdk/dist/extensions/browser/browser-maintenance.d.ts",
],
"openclaw/plugin-sdk/provider-catalog-shared": [
"../packages/plugin-sdk/dist/src/plugin-sdk/provider-catalog-shared.d.ts",
],
"openclaw/plugin-sdk/provider-entry": [
"../packages/plugin-sdk/dist/src/plugin-sdk/provider-entry.d.ts",
],
"openclaw/plugin-sdk/secret-ref-runtime": [
"../dist/plugin-sdk/src/plugin-sdk/secret-ref-runtime.d.ts",
],
"@openclaw/*.js": ["../packages/plugin-sdk/dist/extensions/*.d.ts", "../extensions/*"],
"@openclaw/*": ["../packages/plugin-sdk/dist/extensions/*", "../extensions/*"],
"@openclaw/plugin-sdk/*": ["../dist/plugin-sdk/*.d.ts"],

View File

@@ -205,6 +205,7 @@
"provider-entry",
"provider-env-vars",
"provider-http",
"provider-model-types",
"provider-model-shared",
"volc-model-catalog-shared",
"provider-onboard",

View File

@@ -0,0 +1,35 @@
import { spawnSync } from "node:child_process";
import { createRequire } from "node:module";
import { resolve } from "node:path";
const require = createRequire(import.meta.url);
const repoRoot = resolve(import.meta.dirname, "..");
const tscBin = require.resolve("typescript/bin/tsc");
function runNodeStep(label, args, timeoutMs) {
const result = spawnSync(process.execPath, args, {
cwd: repoRoot,
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
timeout: timeoutMs,
});
if (result.status === 0 && !result.error) {
return;
}
const timeoutSuffix =
result.error?.name === "Error" && result.error.message.includes("ETIMEDOUT")
? `\n${label} timed out after ${timeoutMs}ms`
: "";
const errorSuffix = result.error ? `\n${result.error.message}` : "";
process.stderr.write(`${label}\n${result.stdout}${result.stderr}${timeoutSuffix}${errorSuffix}`);
process.exit(result.status ?? 1);
}
runNodeStep("plugin-sdk boundary dts", [tscBin, "-p", "tsconfig.plugin-sdk.dts.json"], 300_000);
runNodeStep(
"plugin-sdk boundary root shims",
["--import", "tsx", resolve(repoRoot, "scripts/write-plugin-sdk-entry-dts.ts")],
120_000,
);

View File

@@ -1,5 +1,11 @@
import { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js";
import type { ProviderPlugin, ProviderPluginWizardSetup } from "../plugins/types.js";
import type {
ProviderPlugin,
ProviderCatalogContext,
ProviderCatalogResult,
ProviderPluginCatalog,
ProviderPluginWizardSetup,
} from "../plugins/types.js";
import { definePluginEntry } from "./plugin-entry.js";
import type {
OpenClawPluginApi,
@@ -18,10 +24,19 @@ export type SingleProviderPluginApiKeyAuthOptions = Omit<
wizard?: false | ProviderPluginWizardSetup;
};
export type SingleProviderPluginCatalogOptions = {
buildProvider: Parameters<typeof buildSingleProviderApiKeyCatalog>[0]["buildProvider"];
allowExplicitBaseUrl?: boolean;
};
export type SingleProviderPluginCatalogOptions =
| {
buildProvider: Parameters<typeof buildSingleProviderApiKeyCatalog>[0]["buildProvider"];
allowExplicitBaseUrl?: boolean;
run?: never;
order?: never;
}
| {
run: ProviderPluginCatalog["run"];
order?: ProviderPluginCatalog["order"];
buildProvider?: never;
allowExplicitBaseUrl?: never;
};
export type SingleProviderPluginOptions = {
id: string;
@@ -111,6 +126,26 @@ export function defineSingleProviderPluginEntry(options: SingleProviderPluginOpt
...(wizard ? { wizard } : {}),
});
});
let catalog: ProviderPluginCatalog;
if ("run" in provider.catalog) {
const catalogRun = provider.catalog.run;
catalog = {
order: provider.catalog.order ?? "simple",
run: catalogRun!,
};
} else {
const buildProvider = provider.catalog.buildProvider;
catalog = {
order: "simple",
run: (ctx: ProviderCatalogContext): Promise<ProviderCatalogResult> =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId,
buildProvider,
...(provider.catalog.allowExplicitBaseUrl ? { allowExplicitBaseUrl: true } : {}),
}),
};
}
api.registerProvider({
id: providerId,
label: provider.label,
@@ -118,16 +153,7 @@ export function defineSingleProviderPluginEntry(options: SingleProviderPluginOpt
...(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 } : {}),
}),
},
catalog,
...Object.fromEntries(
Object.entries(provider).filter(
([key]) =>

View File

@@ -0,0 +1,7 @@
export type {
BedrockDiscoveryConfig,
ModelApi,
ModelCompatConfig,
ModelDefinitionConfig,
ModelProviderConfig,
} from "../config/types.models.js";

View File

@@ -3,39 +3,53 @@ import { rmSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import { collectOptInExtensionPackageBoundaries } from "../scripts/lib/extension-package-boundary.ts";
import {
collectOptInExtensionPackageBoundaries,
readExtensionPackageBoundaryTsconfig,
} from "../scripts/lib/extension-package-boundary.ts";
const REPO_ROOT = resolve(import.meta.dirname, "..");
const PREPARE_BOUNDARY_ARTIFACTS_BIN = resolve(
REPO_ROOT,
"scripts/prepare-extension-package-boundary-artifacts.mjs",
);
const require = createRequire(import.meta.url);
const TSC_BIN = require.resolve("typescript/bin/tsc");
const PLUGIN_SDK_DTS_TSCONFIG = resolve(REPO_ROOT, "tsconfig.plugin-sdk.dts.json");
const OPT_IN_EXTENSION_IDS = collectOptInExtensionPackageBoundaries(REPO_ROOT);
const CANARY_EXTENSION_IDS = [
...new Map(
OPT_IN_EXTENSION_IDS.map((extensionId) => [
JSON.stringify(readExtensionPackageBoundaryTsconfig(extensionId, REPO_ROOT)),
extensionId,
]),
).values(),
];
function runTsc(args: string[]) {
return spawnSync(process.execPath, [TSC_BIN, ...args], {
function runNode(args: string[], timeout: number) {
return spawnSync(process.execPath, args, {
cwd: REPO_ROOT,
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
timeout,
});
}
describe("opt-in extension package TypeScript boundaries", () => {
it("typechecks each opt-in extension cleanly through @openclaw/plugin-sdk", () => {
const prepareResult = runTsc(["-p", PLUGIN_SDK_DTS_TSCONFIG]);
const prepareResult = runNode([PREPARE_BOUNDARY_ARTIFACTS_BIN], 420_000);
expect(prepareResult.status, `${prepareResult.stdout}\n${prepareResult.stderr}`).toBe(0);
for (const extensionId of OPT_IN_EXTENSION_IDS) {
const result = runTsc([
"-p",
resolve(REPO_ROOT, "extensions", extensionId, "tsconfig.json"),
"--noEmit",
]);
const result = runNode(
[TSC_BIN, "-p", resolve(REPO_ROOT, "extensions", extensionId, "tsconfig.json"), "--noEmit"],
120_000,
);
expect(result.status, `${extensionId}\n${result.stdout}\n${result.stderr}`).toBe(0);
}
});
}, 300_000);
it.each(OPT_IN_EXTENSION_IDS)(
"fails when %s imports src/cli through a relative path",
(extensionId) => {
it("fails when opt-in extensions import src/cli through a relative path", () => {
for (const extensionId of CANARY_EXTENSION_IDS) {
const extensionRoot = resolve(REPO_ROOT, "extensions", extensionId);
const canaryPath = resolve(extensionRoot, "__rootdir_boundary_canary__.ts");
const tsconfigPath = resolve(extensionRoot, "tsconfig.rootdir-canary.json");
@@ -60,7 +74,7 @@ describe("opt-in extension package TypeScript boundaries", () => {
"utf8",
);
const result = runTsc(["-p", tsconfigPath, "--noEmit"]);
const result = runNode([TSC_BIN, "-p", tsconfigPath, "--noEmit"], 120_000);
const output = `${result.stdout}\n${result.stderr}`;
expect(result.status).not.toBe(0);
expect(output).toContain("TS6059");
@@ -69,6 +83,6 @@ describe("opt-in extension package TypeScript boundaries", () => {
rmSync(canaryPath, { force: true });
rmSync(tsconfigPath, { force: true });
}
},
);
}
});
});

View File

@@ -10,10 +10,6 @@
"rootDir": ".",
"tsBuildInfoFile": "dist/plugin-sdk/.tsbuildinfo"
},
"include": [
"src/plugin-sdk/**/*.ts",
"src/types/**/*.d.ts",
"packages/memory-host-sdk/src/**/*.ts"
],
"include": ["src/**/*.ts", "src/types/**/*.d.ts", "packages/memory-host-sdk/src/**/*.ts"],
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}