diff --git a/config/knip.config.ts b/config/knip.config.ts index 527e73757bd2..989ecf854e91 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -52,12 +52,23 @@ const bundledPluginEntries = [ "*.ts!", "index.ts!", "setup-entry.ts!", + // Core resolves these public plugin artifacts by basename rather than by a + // static import from the plugin entry module. + "*-api.ts!", + "cli-metadata.ts!", + "channel-entry.ts!", + // Provider catalogs and web tools resolve these manifest/convention-owned + // modules from the plugin root at runtime. + "provider-discovery.ts!", + "{web-search,web-fetch}-provider.ts!", "{api,contract-api,helper-api,runtime-api,light-runtime-api,update-offset-runtime-api,channel-plugin-api,provider-plugin-api,setup-api}.ts!", "subagent-hooks-api.ts!", "src/{api,runtime-api,light-runtime-api,update-offset-runtime-api,channel-plugin-api,provider-plugin-api,doctor-contract,setup-surface,mcp-serve}.ts!", "src/subagent-hooks-api.ts!", ] as const; +const strictBundledPluginEntries = bundledPluginEntries.filter((entry) => entry !== "*.ts!"); + const bundledPluginIgnoredRuntimeDependencies = [ "@agentclientprotocol/claude-agent-acp", "@a2ui/lit", @@ -102,6 +113,14 @@ const rootBundledPluginRuntimeDependencies = [ "tokenjuice", ] as const; +function strictBundledPluginWorkspace() { + return { + entry: strictBundledPluginEntries, + project: ["*.ts!", "src/**/*.{js,mjs,ts}!"], + ignoreDependencies: bundledPluginIgnoredRuntimeDependencies, + } as const; +} + // These files are test infrastructure, so their exports are intentionally // available to tests without becoming part of the production dead-code scan. const ignoredTestSupportFiles = [ @@ -353,6 +372,19 @@ const config = { entry: ["index.js!", "scripts/postinstall.js!"], project: ["index.js!", "scripts/**/*.js!"], }, + [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock-mantle`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/cohere`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/featherless`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/fireworks`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/huggingface`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/kilocode`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/nvidia`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/qianfan`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/qwen`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/senseaudio`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/tencent`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/vllm`]: strictBundledPluginWorkspace(), + [`${BUNDLED_PLUGIN_ROOT_DIR}/xiaomi`]: strictBundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/llama-cpp`]: { entry: bundledPluginEntries, project: ["index.ts!", "src/**/*.{js,mjs,ts}!"], diff --git a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts index 9a4941d60d10..30dc250c46b8 100644 --- a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts +++ b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.test.ts @@ -1,10 +1,7 @@ // Amazon Bedrock Mantle tests cover mantle anthropic plugin behavior. import type { Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it, vi } from "vitest"; -import { - createMantleAnthropicStreamFn, - resolveMantleAnthropicBaseUrl, -} from "./mantle-anthropic.runtime.js"; +import { createMantleAnthropicStreamFn } from "./mantle-anthropic.runtime.js"; function createTestModel(overrides: Partial = {}): Model { return { @@ -267,13 +264,4 @@ describe("createMantleAnthropicStreamFn", () => { expect(streamOptions).not.toHaveProperty("thinkingBudgetTokens"); expect(streamOptions.temperature).toBeUndefined(); }); - - it("normalizes Mantle provider URLs to the Anthropic endpoint", () => { - expect(resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/v1")).toBe( - "https://bedrock-mantle.us-east-1.api.aws/anthropic", - ); - expect( - resolveMantleAnthropicBaseUrl("https://bedrock-mantle.us-east-1.api.aws/anthropic/"), - ).toBe("https://bedrock-mantle.us-east-1.api.aws/anthropic"); - }); }); diff --git a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts index 938e946243a6..1756bd788da3 100644 --- a/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts +++ b/extensions/amazon-bedrock-mantle/mantle-anthropic.runtime.ts @@ -22,7 +22,7 @@ type AnthropicOptions = ConstructorParameters[0]; type MantleAnthropicStream = typeof stream; /** Resolve the Anthropic-compatible Mantle base URL from a provider base URL. */ -export function resolveMantleAnthropicBaseUrl(baseUrl: string): string { +function resolveMantleAnthropicBaseUrl(baseUrl: string): string { const trimmed = baseUrl.replace(/\/+$/, ""); if (trimmed.endsWith("/anthropic")) { return trimmed; diff --git a/extensions/cohere/index.test.ts b/extensions/cohere/index.test.ts index 04cff6ced64c..156e1eaa8c16 100644 --- a/extensions/cohere/index.test.ts +++ b/extensions/cohere/index.test.ts @@ -5,15 +5,14 @@ import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-ru import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime"; import { describe, expect, it } from "vitest"; import plugin from "./index.js"; -import { - COHERE_COMMAND_A_PLUS_MODEL_ID, - COHERE_COMMAND_A_REASONING_MODEL_ID, - COHERE_COMMAND_A_VISION_MODEL_ID, - COHERE_NORTH_MINI_CODE_MODEL_ID, -} from "./models.js"; +import { COHERE_COMMAND_A_PLUS_MODEL_ID } from "./models.js"; import { buildCohereProvider } from "./provider-catalog.js"; import { createCohereCompletionsWrapper } from "./stream.js"; +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 }>; diff --git a/extensions/cohere/models.ts b/extensions/cohere/models.ts index ac7e777329ab..5a7aaf73ce30 100644 --- a/extensions/cohere/models.ts +++ b/extensions/cohere/models.ts @@ -11,9 +11,8 @@ const COHERE_MANIFEST_CATALOG = manifest.modelCatalog.providers.cohere; export const COHERE_BASE_URL = COHERE_MANIFEST_CATALOG.baseUrl; export const COHERE_MODEL_CATALOG = COHERE_MANIFEST_CATALOG.models; export const COHERE_COMMAND_A_PLUS_MODEL_ID = "command-a-plus-05-2026"; -export const COHERE_COMMAND_A_REASONING_MODEL_ID = "command-a-reasoning-08-2025"; -export const COHERE_COMMAND_A_VISION_MODEL_ID = "command-a-vision-07-2025"; -export const COHERE_NORTH_MINI_CODE_MODEL_ID = "north-mini-code-1-0"; +const COHERE_COMMAND_A_REASONING_MODEL_ID = "command-a-reasoning-08-2025"; +const COHERE_NORTH_MINI_CODE_MODEL_ID = "north-mini-code-1-0"; const COHERE_MODERN_MODEL_IDS = new Set([ COHERE_COMMAND_A_PLUS_MODEL_ID, diff --git a/extensions/cohere/onboard.test.ts b/extensions/cohere/onboard.test.ts index ea1d5145b57b..0f7883abcdee 100644 --- a/extensions/cohere/onboard.test.ts +++ b/extensions/cohere/onboard.test.ts @@ -1,15 +1,13 @@ 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, - COHERE_COMMAND_A_REASONING_MODEL_ID, - COHERE_COMMAND_A_VISION_MODEL_ID, - COHERE_MODEL_CATALOG, - COHERE_NORTH_MINI_CODE_MODEL_ID, -} from "./models.js"; -import { applyCohereConfig, COHERE_DEFAULT_MODEL_ID, COHERE_DEFAULT_MODEL_REF } from "./onboard.js"; +import { buildCohereCatalogModels, COHERE_BASE_URL, COHERE_MODEL_CATALOG } from "./models.js"; +import { applyCohereConfig, COHERE_DEFAULT_MODEL_REF } from "./onboard.js"; + +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"; +const COHERE_NORTH_MINI_CODE_MODEL_ID = "north-mini-code-1-0"; describe("Cohere onboarding", () => { it("registers the manifest catalog through the onboarding preset", () => { diff --git a/extensions/cohere/onboard.ts b/extensions/cohere/onboard.ts index 521655b79c00..0ca04e0bc109 100644 --- a/extensions/cohere/onboard.ts +++ b/extensions/cohere/onboard.ts @@ -9,7 +9,7 @@ import { COHERE_MODEL_CATALOG, } from "./models.js"; -export const COHERE_DEFAULT_MODEL_ID = COHERE_COMMAND_A_PLUS_MODEL_ID; +const COHERE_DEFAULT_MODEL_ID = COHERE_COMMAND_A_PLUS_MODEL_ID; export const COHERE_DEFAULT_MODEL_REF = `cohere/${COHERE_DEFAULT_MODEL_ID}`; const coherePresetAppliers = createModelCatalogPresetAppliers({ diff --git a/extensions/featherless/provider-catalog.ts b/extensions/featherless/provider-catalog.ts index 999c0e2b9e7b..f7704f0f8ea9 100644 --- a/extensions/featherless/provider-catalog.ts +++ b/extensions/featherless/provider-catalog.ts @@ -5,8 +5,6 @@ import manifest from "./openclaw.plugin.json" with { type: "json" }; export { FEATHERLESS_BASE_URL, - FEATHERLESS_DEFAULT_CONTEXT_WINDOW, - FEATHERLESS_DEFAULT_MAX_TOKENS, FEATHERLESS_DEFAULT_MODEL_ID, FEATHERLESS_DYNAMIC_COMPAT, FEATHERLESS_DYNAMIC_CONTEXT_WINDOW, diff --git a/extensions/fireworks/stream.test.ts b/extensions/fireworks/stream.test.ts index 9aa667f9e59c..24786ab37109 100644 --- a/extensions/fireworks/stream.test.ts +++ b/extensions/fireworks/stream.test.ts @@ -2,10 +2,7 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import type { Context, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; -import { - createFireworksKimiThinkingDisabledWrapper, - wrapFireworksProviderStream, -} from "./stream.js"; +import { wrapFireworksProviderStream } from "./stream.js"; function capturePayload(params: { provider: string; @@ -21,16 +18,21 @@ function capturePayload(params: { return {} as ReturnType; }; - const wrapped = createFireworksKimiThinkingDisabledWrapper(baseStreamFn); - void wrapped( - { - api: params.api, - provider: params.provider, - id: params.modelId, - } as Model<"openai-completions">, - { messages: [] } as Context, - {}, - ); + const model = { + api: params.api, + provider: params.provider, + id: params.modelId, + } as Model<"openai-completions">; + const wrapped = wrapFireworksProviderStream({ + provider: params.provider, + modelId: params.modelId, + model, + streamFn: baseStreamFn, + } as never); + if (!wrapped) { + throw new Error("expected Fireworks stream wrapper"); + } + void wrapped(model, { messages: [] } as Context, {}); return captured; } @@ -111,20 +113,25 @@ describe("createFireworksKimiThinkingDisabledWrapper", () => { return {} as ReturnType; }; - const wrapped = createFireworksKimiThinkingDisabledWrapper(baseStreamFn); - void wrapped( - { - api: "openai-completions", - provider: "fireworks", - id: "accounts/fireworks/routers/kimi-k2p5-turbo", - } as Model<"openai-completions">, - { messages: [] } as Context, - { - onPayload: (payload) => { - callbackPayload = payload as Record; - }, + const model = { + api: "openai-completions", + provider: "fireworks", + id: "accounts/fireworks/routers/kimi-k2p5-turbo", + } as Model<"openai-completions">; + const wrapped = wrapFireworksProviderStream({ + provider: "fireworks", + modelId: model.id, + model, + streamFn: baseStreamFn, + } as never); + if (!wrapped) { + throw new Error("expected Fireworks stream wrapper"); + } + void wrapped(model, { messages: [] } as Context, { + onPayload: (payload) => { + callbackPayload = payload as Record; }, - ); + }); expect(callbackPayload).toEqual({ thinking: { type: "disabled" } }); }); diff --git a/extensions/fireworks/stream.ts b/extensions/fireworks/stream.ts index 4fb1b16ead88..31ffa4ef064e 100644 --- a/extensions/fireworks/stream.ts +++ b/extensions/fireworks/stream.ts @@ -11,9 +11,7 @@ function isFireworksProviderId(providerId: string): boolean { return normalized === "fireworks" || normalized === "fireworks-ai"; } -export function createFireworksKimiThinkingDisabledWrapper( - baseStreamFn: StreamFn | undefined, -): StreamFn { +function createFireworksKimiThinkingDisabledWrapper(baseStreamFn: StreamFn | undefined): StreamFn { const underlying = baseStreamFn ?? streamSimple; return (model, context, options) => streamWithPayloadPatch(underlying, model, context, options, (payloadObj) => { diff --git a/extensions/huggingface/models.test.ts b/extensions/huggingface/models.test.ts index 9a7c12523c2e..3bc4d1e3f236 100644 --- a/extensions/huggingface/models.test.ts +++ b/extensions/huggingface/models.test.ts @@ -8,7 +8,6 @@ import { HUGGINGFACE_MODEL_CATALOG, isHuggingfacePolicyLocked, } from "./api.js"; -import { HUGGINGFACE_DISCOVERY_TIMEOUT_MS } from "./models.js"; const ORIGINAL_VITEST = process.env.VITEST; const ORIGINAL_NODE_ENV = process.env.NODE_ENV; @@ -81,7 +80,7 @@ describe("huggingface models", () => { await discoverHuggingfaceModels("hf_test_token"); - expect(timeoutSpy).toHaveBeenCalledWith(HUGGINGFACE_DISCOVERY_TIMEOUT_MS); + expect(timeoutSpy).toHaveBeenCalledWith(30_000); }); it("accepts a custom discovery timeout override", async () => { diff --git a/extensions/huggingface/models.ts b/extensions/huggingface/models.ts index fc856eca880c..0ba88dcbab47 100644 --- a/extensions/huggingface/models.ts +++ b/extensions/huggingface/models.ts @@ -11,7 +11,7 @@ import { isHuggingfaceModelDiscoveryTestEnvironment } from "./model-discovery-en export const HUGGINGFACE_BASE_URL = "https://router.huggingface.co/v1"; export const HUGGINGFACE_POLICY_SUFFIXES = ["cheapest", "fastest"] as const; -export const HUGGINGFACE_DISCOVERY_TIMEOUT_MS = 30_000; +const HUGGINGFACE_DISCOVERY_TIMEOUT_MS = 30_000; const HUGGINGFACE_DEFAULT_COST = { input: 0, diff --git a/extensions/kilocode/onboard.test.ts b/extensions/kilocode/onboard.test.ts index e4d258b33b68..34b1c01a8910 100644 --- a/extensions/kilocode/onboard.test.ts +++ b/extensions/kilocode/onboard.test.ts @@ -10,7 +10,8 @@ import { KILOCODE_DEFAULT_COST, KILOCODE_DEFAULT_MODEL_ID, } from "./api.js"; -import { applyKilocodeConfig, KILOCODE_BASE_URL, KILOCODE_DEFAULT_MODEL_REF } from "./onboard.js"; +import { applyKilocodeConfig, KILOCODE_DEFAULT_MODEL_REF } from "./onboard.js"; +import { KILOCODE_BASE_URL } from "./provider-models.js"; const emptyCfg: OpenClawConfig = {}; const KILOCODE_MODEL_IDS = ["kilo/auto"]; diff --git a/extensions/kilocode/onboard.ts b/extensions/kilocode/onboard.ts index fdfa03eafd78..303037e8d96c 100644 --- a/extensions/kilocode/onboard.ts +++ b/extensions/kilocode/onboard.ts @@ -6,7 +6,7 @@ import { import { buildKilocodeProvider } from "./provider-catalog.js"; import { KILOCODE_BASE_URL, KILOCODE_DEFAULT_MODEL_REF } from "./provider-models.js"; -export { KILOCODE_BASE_URL, KILOCODE_DEFAULT_MODEL_REF }; +export { KILOCODE_DEFAULT_MODEL_REF }; const kilocodePresetAppliers = createModelCatalogPresetAppliers({ primaryModelRef: KILOCODE_DEFAULT_MODEL_REF, diff --git a/extensions/nvidia/index.test.ts b/extensions/nvidia/index.test.ts index 828a0290cbe7..ac85e35b9a34 100644 --- a/extensions/nvidia/index.test.ts +++ b/extensions/nvidia/index.test.ts @@ -5,12 +5,12 @@ import { registerSingleProviderPlugin, resolveProviderPluginChoice, } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import { afterEach, describe, expect, it, vi } from "vitest"; import plugin from "./index.js"; -import { - clearNvidiaFeaturedModelCacheForTests, - NVIDIA_FEATURED_MODELS_URL, -} from "./provider-catalog.js"; + +const NVIDIA_FEATURED_MODELS_URL = + "https://assets.ngc.nvidia.com/products/api-catalog/featured-models.json"; const ssrfRuntimeMocks = vi.hoisted(() => ({ fetchWithSsrFGuard: vi.fn(), @@ -39,7 +39,7 @@ async function registerNvidiaProvider() { } afterEach(() => { - clearNvidiaFeaturedModelCacheForTests(); + clearLiveCatalogCacheForTests(); ssrfRuntimeMocks.fetchWithSsrFGuard.mockReset(); ssrfRuntimeMocks.ssrfPolicyFromHttpBaseUrlAllowedHostname.mockClear(); }); diff --git a/extensions/nvidia/provider-catalog.test.ts b/extensions/nvidia/provider-catalog.test.ts index 5c1e511e343d..fd4cb1400c77 100644 --- a/extensions/nvidia/provider-catalog.test.ts +++ b/extensions/nvidia/provider-catalog.test.ts @@ -1,3 +1,4 @@ +import { clearLiveCatalogCacheForTests } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; // Nvidia tests cover provider catalog plugin behavior. import { afterEach, describe, expect, it, vi } from "vitest"; import manifest from "./openclaw.plugin.json" with { type: "json" }; @@ -6,10 +7,11 @@ import { buildNvidiaProvider, buildSelectableNvidiaProvider, buildSelectableLiveNvidiaProvider, - clearNvidiaFeaturedModelCacheForTests, - NVIDIA_FEATURED_MODELS_URL, } from "./provider-catalog.js"; +const NVIDIA_FEATURED_MODELS_URL = + "https://assets.ngc.nvidia.com/products/api-catalog/featured-models.json"; + const EXPECTED_FEATURED_MODELS = [ { id: "nvidia/nemotron-3-ultra-550b-a55b", @@ -94,7 +96,7 @@ vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ssrfRuntimeMocks); afterEach(() => { vi.useRealTimers(); - clearNvidiaFeaturedModelCacheForTests(); + clearLiveCatalogCacheForTests(); ssrfRuntimeMocks.fetchWithSsrFGuard.mockReset(); ssrfRuntimeMocks.ssrfPolicyFromHttpBaseUrlAllowedHostname.mockClear(); }); diff --git a/extensions/nvidia/provider-catalog.ts b/extensions/nvidia/provider-catalog.ts index 85d4efec6751..ba65bd88ee3c 100644 --- a/extensions/nvidia/provider-catalog.ts +++ b/extensions/nvidia/provider-catalog.ts @@ -1,9 +1,6 @@ // Nvidia provider module implements model/runtime integration. import { lookup as dnsLookup } from "node:dns/promises"; -import { - clearLiveCatalogCacheForTests, - getCachedLiveProviderModelRows, -} from "openclaw/plugin-sdk/provider-catalog-live-runtime"; +import { getCachedLiveProviderModelRows } from "openclaw/plugin-sdk/provider-catalog-live-runtime"; import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; import type { ModelDefinitionConfig, @@ -17,7 +14,7 @@ import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime"; import manifest from "./openclaw.plugin.json" with { type: "json" }; export const NVIDIA_DEFAULT_MODEL_ID = "nvidia/nemotron-3-ultra-550b-a55b"; -export const NVIDIA_FEATURED_MODELS_URL = +const NVIDIA_FEATURED_MODELS_URL = "https://assets.ngc.nvidia.com/products/api-catalog/featured-models.json"; const FEATURED_MODEL_CACHE_TTL_MS = 24 * 60 * 60 * 1000; @@ -119,10 +116,6 @@ export async function buildSelectableLiveNvidiaProvider(): Promise { try { const rows = await getCachedLiveProviderModelRows({ diff --git a/extensions/qianfan/index.test.ts b/extensions/qianfan/index.test.ts index eb1bc4c3a78a..9ea84f6c230b 100644 --- a/extensions/qianfan/index.test.ts +++ b/extensions/qianfan/index.test.ts @@ -7,11 +7,7 @@ import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onbo import { describe, expect, it } from "vitest"; import { runSingleProviderCatalog } from "../test-support/provider-model-test-helpers.js"; import qianfanPlugin from "./index.js"; -import { - applyQianfanConfig, - applyQianfanProviderConfig, - QIANFAN_DEFAULT_MODEL_REF, -} from "./onboard.js"; +import { applyQianfanConfig, QIANFAN_DEFAULT_MODEL_REF } from "./onboard.js"; function expectRecord(value: T | null | undefined, label: string): NonNullable { if (!value) { @@ -94,36 +90,6 @@ describe("qianfan provider plugin", () => { }); }); - it("adds Qianfan provider defaults without changing primary model in provider-only mode", () => { - const cfg = applyQianfanProviderConfig({ - agents: { - defaults: { - model: { primary: "anthropic/claude-opus-4-6" }, - }, - }, - }); - - const modelsConfig = expectRecord(cfg.models, "models config"); - const providers = expectRecord(modelsConfig.providers, "model providers"); - const providerConfig = expectRecord(providers.qianfan, "Qianfan provider config"); - expect(providerConfig.api).toBe("openai-completions"); - expect(providerConfig.baseUrl).toBe("https://qianfan.baidubce.com/v2"); - const providerModels = expectRecord(providerConfig.models, "Qianfan provider models"); - expect(providerModels.map((model) => model.id)).toEqual([ - "deepseek-v3.2", - "ernie-5.0-thinking-preview", - ]); - const agentsConfig = expectRecord(cfg.agents, "agents config"); - const agentDefaults = expectRecord(agentsConfig.defaults, "agent defaults"); - const agentModelAliases = expectRecord(agentDefaults.models, "agent model aliases"); - const qianfanAlias = expectRecord( - agentModelAliases[QIANFAN_DEFAULT_MODEL_REF], - "Qianfan model alias", - ); - expect(qianfanAlias.alias).toBe("QIANFAN"); - expect(resolveAgentModelPrimaryValue(agentDefaults.model)).toBe("anthropic/claude-opus-4-6"); - }); - it("sets Qianfan as the agent primary model in full onboarding mode", () => { const cfg = applyQianfanConfig({}); diff --git a/extensions/qianfan/onboard.ts b/extensions/qianfan/onboard.ts index 89abfb61d777..2cc5d2dcada6 100644 --- a/extensions/qianfan/onboard.ts +++ b/extensions/qianfan/onboard.ts @@ -53,10 +53,6 @@ const qianfanPresetAppliers = createDefaultModelsPresetAppliers({ }, }); -export function applyQianfanProviderConfig(cfg: OpenClawConfig): OpenClawConfig { - return qianfanPresetAppliers.applyProviderConfig(cfg); -} - export function applyQianfanConfig(cfg: OpenClawConfig): OpenClawConfig { return qianfanPresetAppliers.applyConfig(cfg); } diff --git a/extensions/qwen/media-understanding-provider.test.ts b/extensions/qwen/media-understanding-provider.test.ts index acdee732f249..d9d9de85656b 100644 --- a/extensions/qwen/media-understanding-provider.test.ts +++ b/extensions/qwen/media-understanding-provider.test.ts @@ -4,10 +4,16 @@ import { installPinnedHostnameTestHooks, } from "openclaw/plugin-sdk/test-env"; import { describe, expect, it } from "vitest"; -import { describeQwenVideo } from "./media-understanding-provider.js"; +import { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js"; installPinnedHostnameTestHooks(); +const qwenProvider = buildQwenMediaUnderstandingProvider(); +const describeQwenVideo = qwenProvider.describeVideo; +if (!describeQwenVideo) { + throw new Error("expected Qwen video description capability"); +} + function oversizedJsonResponse(params: { chunkCount: number; chunkSize: number }): { response: Response; getReadCount: () => number; diff --git a/extensions/qwen/media-understanding-provider.ts b/extensions/qwen/media-understanding-provider.ts index b1e22a9174ec..cce161fb1113 100644 --- a/extensions/qwen/media-understanding-provider.ts +++ b/extensions/qwen/media-understanding-provider.ts @@ -21,9 +21,7 @@ import { QWEN_STANDARD_GLOBAL_BASE_URL } from "./models.js"; const DEFAULT_QWEN_VIDEO_MODEL = "qwen-vl-max-latest"; const DEFAULT_QWEN_VIDEO_PROMPT = "Describe the video in detail."; -export async function describeQwenVideo( - params: VideoDescriptionRequest, -): Promise { +async function describeQwenVideo(params: VideoDescriptionRequest): Promise { const fetchFn = params.fetchFn ?? fetch; const model = resolveMediaUnderstandingString(params.model, DEFAULT_QWEN_VIDEO_MODEL); const mime = resolveMediaUnderstandingString(params.mime, "video/mp4"); diff --git a/extensions/senseaudio/media-understanding-provider.test.ts b/extensions/senseaudio/media-understanding-provider.test.ts index 96562edd9f2e..2c2358647fbb 100644 --- a/extensions/senseaudio/media-understanding-provider.test.ts +++ b/extensions/senseaudio/media-understanding-provider.test.ts @@ -10,10 +10,15 @@ import { installPinnedHostnameTestHooks, } from "openclaw/plugin-sdk/test-env"; import { describe, expect, it } from "vitest"; -import { transcribeSenseAudioAudio } from "./media-understanding-provider.js"; +import { senseaudioMediaUnderstandingProvider } from "./media-understanding-provider.js"; installPinnedHostnameTestHooks(); +const transcribeSenseAudioAudio = senseaudioMediaUnderstandingProvider.transcribeAudio; +if (!transcribeSenseAudioAudio) { + throw new Error("expected SenseAudio transcription capability"); +} + describe("transcribeSenseAudioAudio", () => { it("uses SenseAudio base URL by default", async () => { const { fetchFn, getRequest } = createRequestCaptureJsonFetch({ text: "ok" }); diff --git a/extensions/senseaudio/media-understanding-provider.ts b/extensions/senseaudio/media-understanding-provider.ts index 1d4e1ce03e1c..162daf65ac4d 100644 --- a/extensions/senseaudio/media-understanding-provider.ts +++ b/extensions/senseaudio/media-understanding-provider.ts @@ -8,7 +8,7 @@ import { const DEFAULT_SENSEAUDIO_AUDIO_BASE_URL = "https://api.senseaudio.cn/v1"; const DEFAULT_SENSEAUDIO_AUDIO_MODEL = "senseaudio-asr-pro-1.5-260319"; -export async function transcribeSenseAudioAudio(params: AudioTranscriptionRequest) { +async function transcribeSenseAudioAudio(params: AudioTranscriptionRequest) { return await transcribeOpenAiCompatibleAudio({ ...params, provider: "senseaudio", diff --git a/extensions/tencent/config-compat.test.ts b/extensions/tencent/config-compat.test.ts index a6e3376ec022..d7ecdcd9de8c 100644 --- a/extensions/tencent/config-compat.test.ts +++ b/extensions/tencent/config-compat.test.ts @@ -1,11 +1,10 @@ // Tencent tests cover config compatibility repair behavior. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; import { describe, expect, it } from "vitest"; -import { - migrateTencentTokenHubModelDefaults, - TENCENT_TOKENHUB_DEFAULT_MODEL_REF, - TENCENT_TOKENHUB_PREVIEW_MODEL_REF, -} from "./config-compat.js"; +import { migrateTencentTokenHubModelDefaults } from "./config-compat.js"; + +const TENCENT_TOKENHUB_DEFAULT_MODEL_REF = "tencent-tokenhub/hy3"; +const TENCENT_TOKENHUB_PREVIEW_MODEL_REF = "tencent-tokenhub/hy3-preview"; describe("Tencent config compatibility", () => { it("adds the stable TokenHub model and makes it primary for old preview defaults", () => { diff --git a/extensions/tencent/config-compat.ts b/extensions/tencent/config-compat.ts index 2e7b54ee8667..71f3c1e39756 100644 --- a/extensions/tencent/config-compat.ts +++ b/extensions/tencent/config-compat.ts @@ -1,8 +1,8 @@ // Tencent config compatibility repairs shipped TokenHub model allowlists. import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; -export const TENCENT_TOKENHUB_DEFAULT_MODEL_REF = "tencent-tokenhub/hy3"; -export const TENCENT_TOKENHUB_PREVIEW_MODEL_REF = "tencent-tokenhub/hy3-preview"; +const TENCENT_TOKENHUB_DEFAULT_MODEL_REF = "tencent-tokenhub/hy3"; +const TENCENT_TOKENHUB_PREVIEW_MODEL_REF = "tencent-tokenhub/hy3-preview"; const TOKENHUB_DEFAULT_ALIAS = "Hy3 (TokenHub)"; const TOKENHUB_PREVIEW_ALIAS = "Hy3 preview (TokenHub)"; @@ -111,10 +111,3 @@ export function migrateTencentTokenHubModelDefaults(cfg: OpenClawConfig): { return { config: nextConfig, changes }; } - -export function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }): { - config: OpenClawConfig; - changes: string[]; -} { - return migrateTencentTokenHubModelDefaults(cfg); -} diff --git a/extensions/vllm/stream.test.ts b/extensions/vllm/stream.test.ts index 5bf1b46c373f..db9f7b305d87 100644 --- a/extensions/vllm/stream.test.ts +++ b/extensions/vllm/stream.test.ts @@ -2,11 +2,7 @@ import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; import type { Context, Model } from "openclaw/plugin-sdk/llm"; import { describe, expect, it } from "vitest"; -import { - createVllmProviderThinkingWrapper, - createVllmQwenThinkingWrapper, - wrapVllmProviderStream, -} from "./stream.js"; +import { createVllmQwenThinkingWrapper, wrapVllmProviderStream } from "./stream.js"; function capturePayload(params: { format: "chat-template" | "top-level"; @@ -135,21 +131,21 @@ describe("createVllmProviderThinkingWrapper", () => { return {} as ReturnType; }; - const wrapped = createVllmProviderThinkingWrapper({ - baseStreamFn, + const model = { + api: "openai-completions", + provider: "vllm", + id: "nemotron-3-super", + reasoning: true, + ...params.model, + } as Model<"openai-completions">; + const wrapped = wrapVllmProviderStream({ + provider: "vllm", + modelId: model.id, + model, thinkingLevel: params.thinkingLevel ?? "high", - }); - void wrapped( - { - api: "openai-completions", - provider: "vllm", - id: "nemotron-3-super", - reasoning: true, - ...params.model, - } as Model<"openai-completions">, - { messages: [] } as Context, - {}, - ); + streamFn: baseStreamFn, + } as never); + void wrapped?.(model, { messages: [] } as Context, {}); return captured; } diff --git a/extensions/vllm/stream.ts b/extensions/vllm/stream.ts index 9f30e113eff4..110315a83890 100644 --- a/extensions/vllm/stream.ts +++ b/extensions/vllm/stream.ts @@ -76,7 +76,7 @@ export function createVllmQwenThinkingWrapper(params: { ); } -export function createVllmProviderThinkingWrapper(params: { +function createVllmProviderThinkingWrapper(params: { baseStreamFn: StreamFn | undefined; qwenFormat?: VllmQwenThinkingFormat; thinkingLevel: VllmThinkingLevel; diff --git a/extensions/xiaomi/onboard.test.ts b/extensions/xiaomi/onboard.test.ts index bfbf5490dc0f..efe9125b08d9 100644 --- a/extensions/xiaomi/onboard.test.ts +++ b/extensions/xiaomi/onboard.test.ts @@ -8,7 +8,6 @@ import { applyXiaomiConfig, applyXiaomiProviderConfig, applyXiaomiTokenPlanConfig, - applyXiaomiTokenPlanProviderConfig, } from "./onboard.js"; import { buildXiaomiProvider, buildXiaomiTokenPlanProvider } from "./provider-catalog.js"; @@ -68,7 +67,7 @@ describe("xiaomi onboard", () => { it("merges Xiaomi Token Plan models and rewrites the selected regional base URL", () => { const provider = expectProviderOnboardMergedLegacyConfig({ - applyProviderConfig: (config) => applyXiaomiTokenPlanProviderConfig(config, "sgp"), + applyProviderConfig: (config) => applyXiaomiTokenPlanConfig(config, "sgp"), providerId: "xiaomi-token-plan", providerApi: "openai-completions", baseUrl: "https://token-plan-sgp.xiaomimimo.com/v1", diff --git a/extensions/xiaomi/onboard.ts b/extensions/xiaomi/onboard.ts index edbebe57f2b2..f80ecb201272 100644 --- a/extensions/xiaomi/onboard.ts +++ b/extensions/xiaomi/onboard.ts @@ -86,17 +86,6 @@ export function applyXiaomiConfig(cfg: OpenClawConfig): OpenClawConfig { return xiaomiPresetAppliers.applyConfig(cfg); } -export function applyXiaomiTokenPlanProviderConfig( - cfg: OpenClawConfig, - region: XiaomiTokenPlanRegion, -): OpenClawConfig { - return withProviderBaseUrl( - xiaomiTokenPlanPresetAppliers.applyProviderConfig(cfg), - XIAOMI_TOKEN_PLAN_PROVIDER_ID, - resolveXiaomiTokenPlanBaseUrl(region), - ); -} - export function applyXiaomiTokenPlanConfig( cfg: OpenClawConfig, region: XiaomiTokenPlanRegion, diff --git a/test/scripts/check-deadcode-exports.test.ts b/test/scripts/check-deadcode-exports.test.ts index 293db8b42e50..ccc4c136d008 100644 --- a/test/scripts/check-deadcode-exports.test.ts +++ b/test/scripts/check-deadcode-exports.test.ts @@ -40,6 +40,43 @@ describe("check-deadcode-exports", () => { expect(knipConfig.workspaces["."].entry).toContain("src/mcp/openclaw-tools-serve.ts!"); }); + it.each([ + "amazon-bedrock-mantle", + "cohere", + "featherless", + "fireworks", + "huggingface", + "kilocode", + "nvidia", + "qianfan", + "qwen", + "senseaudio", + "tencent", + "vllm", + "xiaomi", + ])("removes the bundled-plugin root catch-all from migrated %s workspace", (pluginId) => { + const workspace = ( + knipConfig.workspaces as Record + )[`extensions/${pluginId}`]; + if (!workspace) { + throw new Error(`missing Knip workspace for ${pluginId}`); + } + const entries = workspace.entry; + expect(entries).not.toContain("*.ts!"); + expect(entries).toEqual( + expect.arrayContaining([ + "index.ts!", + "setup-entry.ts!", + "*-api.ts!", + "cli-metadata.ts!", + "channel-entry.ts!", + "provider-discovery.ts!", + "{web-search,web-fetch}-provider.ts!", + ]), + ); + expect(knipConfig.workspaces["extensions/*"].entry).toContain("*.ts!"); + }); + it.each([ "packages/agent-core", "packages/markdown-core",