diff --git a/extensions/huggingface/index.ts b/extensions/huggingface/index.ts index 61d87686afd4..3451212c2229 100644 --- a/extensions/huggingface/index.ts +++ b/extensions/huggingface/index.ts @@ -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, + }, + }; + }, + }, }, }); diff --git a/extensions/huggingface/models.ts b/extensions/huggingface/models.ts index 9bad187bad85..74502ec72051 100644 --- a/extensions/huggingface/models.ts +++ b/extensions/huggingface/models.ts @@ -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; diff --git a/extensions/huggingface/provider-catalog.ts b/extensions/huggingface/provider-catalog.ts index c27cf5eb97d9..4948ede17cbf 100644 --- a/extensions/huggingface/provider-catalog.ts +++ b/extensions/huggingface/provider-catalog.ts @@ -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, diff --git a/extensions/tsconfig.package-boundary.paths.json b/extensions/tsconfig.package-boundary.paths.json index 46daa688e59d..567968c754e9 100644 --- a/extensions/tsconfig.package-boundary.paths.json +++ b/extensions/tsconfig.package-boundary.paths.json @@ -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"] diff --git a/extensions/xai/tsconfig.json b/extensions/xai/tsconfig.json index 1f842b591d92..5fc82ac0bf93 100644 --- a/extensions/xai/tsconfig.json +++ b/extensions/xai/tsconfig.json @@ -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"], diff --git a/package.json b/package.json index 7ebcd6b372a9..a49238c0d93a 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/scripts/lib/extension-package-boundary.ts b/scripts/lib/extension-package-boundary.ts index e0d67f0a27de..5291e2195eec 100644 --- a/scripts/lib/extension-package-boundary.ts +++ b/scripts/lib/extension-package-boundary.ts @@ -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"], diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index 22eb5e6fe47e..5341a9272d1c 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -205,6 +205,7 @@ "provider-entry", "provider-env-vars", "provider-http", + "provider-model-types", "provider-model-shared", "volc-model-catalog-shared", "provider-onboard", diff --git a/scripts/prepare-extension-package-boundary-artifacts.mjs b/scripts/prepare-extension-package-boundary-artifacts.mjs new file mode 100644 index 000000000000..cebde2d9143a --- /dev/null +++ b/scripts/prepare-extension-package-boundary-artifacts.mjs @@ -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, +); diff --git a/src/plugin-sdk/provider-entry.ts b/src/plugin-sdk/provider-entry.ts index 75a449209c31..eea243feac5e 100644 --- a/src/plugin-sdk/provider-entry.ts +++ b/src/plugin-sdk/provider-entry.ts @@ -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[0]["buildProvider"]; - allowExplicitBaseUrl?: boolean; -}; +export type SingleProviderPluginCatalogOptions = + | { + buildProvider: Parameters[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 => + 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]) => diff --git a/src/plugin-sdk/provider-model-types.ts b/src/plugin-sdk/provider-model-types.ts new file mode 100644 index 000000000000..690ad475b28c --- /dev/null +++ b/src/plugin-sdk/provider-model-types.ts @@ -0,0 +1,7 @@ +export type { + BedrockDiscoveryConfig, + ModelApi, + ModelCompatConfig, + ModelDefinitionConfig, + ModelProviderConfig, +} from "../config/types.models.js"; diff --git a/test/extension-package-tsc-boundary.test.ts b/test/extension-package-tsc-boundary.test.ts index b14c162cc629..14186e63ef7d 100644 --- a/test/extension-package-tsc-boundary.test.ts +++ b/test/extension-package-tsc-boundary.test.ts @@ -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 }); } - }, - ); + } + }); }); diff --git a/tsconfig.plugin-sdk.dts.json b/tsconfig.plugin-sdk.dts.json index b3245912c27d..017bd7812309 100644 --- a/tsconfig.plugin-sdk.dts.json +++ b/tsconfig.plugin-sdk.dts.json @@ -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"] }