diff --git a/docs/plugins/sdk-provider-plugins.md b/docs/plugins/sdk-provider-plugins.md index a169bbb3ef9..03c68fd10b0 100644 --- a/docs/plugins/sdk-provider-plugins.md +++ b/docs/plugins/sdk-provider-plugins.md @@ -241,10 +241,11 @@ API key auth, and dynamic model resolution. `buildProvider` is the live catalog path used when OpenClaw can resolve real provider auth. It may perform provider-specific discovery. Use - `buildStaticProvider` only for bundled/offline rows that are safe to show in - display-only surfaces such as `models list --all` before auth is configured; - it must not require credentials or make network requests. Static catalog - hooks run with an empty config, empty env, and no agent/workspace paths. + `buildStaticProvider` only for offline rows that are safe to show before auth + is configured; it must not require credentials or make network requests. + OpenClaw's `models list --all` display currently executes static catalogs + only for bundled provider plugins, with an empty config, empty env, and no + agent/workspace paths. If your auth flow also needs to patch `models.providers.*`, aliases, and the agent default model during onboarding, use the preset helpers from diff --git a/src/commands/models/list.provider-catalog.test.ts b/src/commands/models/list.provider-catalog.test.ts index a6f9d1d82fc..5dbda8c0120 100644 --- a/src/commands/models/list.provider-catalog.test.ts +++ b/src/commands/models/list.provider-catalog.test.ts @@ -1,5 +1,4 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import type { ProviderPlugin } from "../../plugins/types.js"; import { loadProviderCatalogModelsForList, resolveProviderCatalogPluginIdsForFilter, @@ -24,7 +23,6 @@ const baseParams = { describe("loadProviderCatalogModelsForList", () => { afterEach(() => { - vi.useRealTimers(); vi.restoreAllMocks(); }); @@ -53,48 +51,6 @@ describe("loadProviderCatalogModelsForList", () => { ); }); - it("skips static catalogs that exceed the display budget", async () => { - vi.useFakeTimers(); - const hungProvider = { - id: "hung", - label: "Hung", - auth: [], - staticCatalog: { - run: async () => new Promise(() => {}), - }, - } satisfies ProviderPlugin; - const healthyProvider = { - id: "healthy", - label: "Healthy", - auth: [], - staticCatalog: { - run: async () => ({ - provider: { - baseUrl: "https://healthy.example/v1", - models: [{ id: "healthy-model", name: "Healthy Model" }], - }, - }), - }, - } satisfies ProviderPlugin; - const discovery = await import("../../plugins/provider-discovery.js"); - vi.spyOn(discovery, "resolvePluginDiscoveryProviders").mockResolvedValue([ - hungProvider, - healthyProvider, - ]); - - const rowsPromise = loadProviderCatalogModelsForList({ - ...baseParams, - }); - await vi.advanceTimersByTimeAsync(2_000); - - await expect(rowsPromise).resolves.toEqual([ - expect.objectContaining({ - provider: "healthy", - id: "healthy-model", - }), - ]); - }); - it("recognizes bundled provider hook aliases before the unknown-provider short-circuit", async () => { await expect( resolveProviderCatalogPluginIdsForFilter({ @@ -105,47 +61,46 @@ describe("loadProviderCatalogModelsForList", () => { ).resolves.toEqual(["openai"]); }); - it("recognizes trusted workspace provider aliases before the unknown-provider short-circuit", async () => { - const manifestRegistry = await import("../../plugins/manifest-registry.js"); + it("does not execute workspace provider static catalogs", async () => { const providers = await import("../../plugins/providers.js"); const discovery = await import("../../plugins/provider-discovery.js"); - vi.spyOn(manifestRegistry, "loadPluginManifestRegistry").mockReturnValue({ - plugins: [ - { - id: "workspace-demo", - origin: "workspace", - providers: ["workspace-demo"], - cliBackends: [], - }, - ], - diagnostics: [], - } as never); - vi.spyOn(providers, "resolveDiscoveredProviderPluginIds").mockReturnValue(["workspace-demo"]); + const workspaceStaticCatalog = vi.fn(async () => ({ + provider: { baseUrl: "https://workspace.example/v1", models: [] }, + })); + vi.spyOn(providers, "resolveBundledProviderCompatPluginIds").mockReturnValue(["bundled-demo"]); vi.spyOn(discovery, "resolvePluginDiscoveryProviders").mockResolvedValue([ + { + id: "bundled-demo", + pluginId: "bundled-demo", + label: "Bundled Demo", + auth: [], + staticCatalog: { + run: async () => null, + }, + }, { id: "workspace-demo", pluginId: "workspace-demo", label: "Workspace Demo", - aliases: ["workspace-demo-alias"], auth: [], staticCatalog: { - run: async () => ({ - provider: { - baseUrl: "https://workspace.example/v1", - models: [], - }, - }), + run: workspaceStaticCatalog, }, }, ]); - await expect( - resolveProviderCatalogPluginIdsForFilter({ - cfg: baseParams.cfg, - env: baseParams.env, - providerFilter: "workspace-demo-alias", + const rows = await loadProviderCatalogModelsForList({ + ...baseParams, + }); + + expect(discovery.resolvePluginDiscoveryProviders).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["bundled-demo"], + includeUntrustedWorkspacePlugins: false, }), - ).resolves.toEqual(["workspace-demo"]); + ); + expect(workspaceStaticCatalog).not.toHaveBeenCalled(); + expect(rows).toEqual([]); }); it("keeps unknown provider filters eligible for early empty results", async () => { diff --git a/src/commands/models/list.provider-catalog.ts b/src/commands/models/list.provider-catalog.ts index c0ceda4c80a..2e7cc92a490 100644 --- a/src/commands/models/list.provider-catalog.ts +++ b/src/commands/models/list.provider-catalog.ts @@ -4,7 +4,6 @@ import type { ModelProviderConfig } from "../../config/types.models.js"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { formatErrorMessage } from "../../infra/errors.js"; import { createSubsystemLogger } from "../../logging/subsystem.js"; -import { loadPluginManifestRegistry } from "../../plugins/manifest-registry.js"; import { groupPluginDiscoveryProvidersByOrder, normalizePluginDiscoveryResult, @@ -12,63 +11,14 @@ import { runProviderStaticCatalog, } from "../../plugins/provider-discovery.js"; import { - resolveDiscoveredProviderPluginIds, + resolveBundledProviderCompatPluginIds, resolveOwningPluginIdsForProvider, } from "../../plugins/providers.js"; -import type { ProviderPlugin } from "../../plugins/types.js"; const DISCOVERY_ORDERS = ["simple", "profile", "paired", "late"] as const; const SELF_HOSTED_DISCOVERY_PROVIDER_IDS = new Set(["lmstudio", "ollama", "sglang", "vllm"]); -const STATIC_CATALOG_TIMEOUT_MS = 2_000; const log = createSubsystemLogger("models/list-provider-catalog"); -function providerMatchesFilterAlias(provider: ProviderPlugin, providerFilter: string): boolean { - return [provider.id, ...(provider.aliases ?? []), ...(provider.hookAliases ?? [])].some( - (providerId) => normalizeProviderId(providerId) === providerFilter, - ); -} - -async function resolveWorkspacePluginIdsForProviderAlias(params: { - cfg: OpenClawConfig; - env?: NodeJS.ProcessEnv; - providerFilter: string; -}): Promise { - const discoverablePluginIds = new Set( - resolveDiscoveredProviderPluginIds({ - config: params.cfg, - env: params.env, - includeUntrustedWorkspacePlugins: false, - }), - ); - const workspacePluginIds = loadPluginManifestRegistry({ - config: params.cfg, - env: params.env, - }) - .plugins.filter( - (plugin) => plugin.origin === "workspace" && discoverablePluginIds.has(plugin.id), - ) - .map((plugin) => plugin.id); - if (workspacePluginIds.length === 0) { - return undefined; - } - - const providers = await resolvePluginDiscoveryProviders({ - config: params.cfg, - env: params.env, - onlyPluginIds: workspacePluginIds, - includeUntrustedWorkspacePlugins: false, - }); - const pluginIds = [ - ...new Set( - providers - .filter((provider) => providerMatchesFilterAlias(provider, params.providerFilter)) - .map((provider) => provider.pluginId) - .filter((pluginId): pluginId is string => typeof pluginId === "string" && pluginId !== ""), - ), - ].toSorted((left, right) => left.localeCompare(right)); - return pluginIds.length > 0 ? pluginIds : undefined; -} - export async function resolveProviderCatalogPluginIdsForFilter(params: { cfg: OpenClawConfig; env?: NodeJS.ProcessEnv; @@ -92,11 +42,7 @@ export async function resolveProviderCatalogPluginIdsForFilter(params: { if (bundledAliasPluginIds) { return bundledAliasPluginIds; } - return await resolveWorkspacePluginIdsForProviderAlias({ - cfg: params.cfg, - env: params.env, - providerFilter, - }); + return undefined; } function modelFromProviderCatalog(params: { @@ -121,29 +67,6 @@ function modelFromProviderCatalog(params: { } as Model; } -async function withStaticCatalogTimeout( - providerId: string, - run: () => T | Promise, -): Promise { - let timer: NodeJS.Timeout | undefined; - const timeout = new Promise((_, reject) => { - timer = setTimeout(() => { - reject( - new Error( - `provider static catalog timed out for ${providerId} after ${STATIC_CATALOG_TIMEOUT_MS}ms`, - ), - ); - }, STATIC_CATALOG_TIMEOUT_MS); - }); - try { - return await Promise.race([Promise.resolve().then(run), timeout]); - } finally { - if (timer) { - clearTimeout(timer); - } - } -} - export async function loadProviderCatalogModelsForList(params: { cfg: OpenClawConfig; agentDir: string; @@ -162,12 +85,30 @@ export async function loadProviderCatalogModelsForList(params: { if (providerFilter && !onlyPluginIds) { return []; } - const providers = await resolvePluginDiscoveryProviders({ + + const bundledPluginIds = resolveBundledProviderCompatPluginIds({ config: params.cfg, env, - ...(onlyPluginIds ? { onlyPluginIds } : {}), - includeUntrustedWorkspacePlugins: false, }); + const bundledPluginIdSet = new Set(bundledPluginIds); + const scopedPluginIds = onlyPluginIds + ? onlyPluginIds.filter((pluginId) => bundledPluginIdSet.has(pluginId)) + : bundledPluginIds; + if (scopedPluginIds.length === 0) { + return []; + } + + const providers = ( + await resolvePluginDiscoveryProviders({ + config: params.cfg, + env, + onlyPluginIds: scopedPluginIds, + includeUntrustedWorkspacePlugins: false, + }) + ).filter( + (provider) => + typeof provider.pluginId === "string" && bundledPluginIdSet.has(provider.pluginId), + ); const byOrder = groupPluginDiscoveryProvidersByOrder(providers); const rows: Model[] = []; const seen = new Set(); @@ -179,14 +120,12 @@ export async function loadProviderCatalogModelsForList(params: { } let result: Awaited> | null; try { - result = await withStaticCatalogTimeout(provider.id, () => - runProviderStaticCatalog({ - provider, - config: params.cfg, - agentDir: params.agentDir, - env, - }), - ); + result = await runProviderStaticCatalog({ + provider, + config: params.cfg, + agentDir: params.agentDir, + env, + }); } catch (error) { log.warn(`provider static catalog failed for ${provider.id}: ${formatErrorMessage(error)}`); result = null;