diff --git a/src/plugins/contracts/catalog.contract.test.ts b/src/plugins/contracts/catalog.contract.test.ts index 292fcaccb49..3c5ff9b8868 100644 --- a/src/plugins/contracts/catalog.contract.test.ts +++ b/src/plugins/contracts/catalog.contract.test.ts @@ -15,22 +15,22 @@ const PROVIDER_CATALOG_CONTRACT_TIMEOUT_MS = 300_000; type ResolvePluginProviders = typeof import("../providers.runtime.js").resolvePluginProviders; type ResolveOwningPluginIdsForProvider = typeof import("../providers.js").resolveOwningPluginIdsForProvider; -type ResolveNonBundledProviderPluginIds = - typeof import("../providers.js").resolveNonBundledProviderPluginIds; +type ResolveCatalogHookProviderPluginIds = + typeof import("../providers.js").resolveCatalogHookProviderPluginIds; const resolvePluginProvidersMock = vi.hoisted(() => vi.fn(() => [])); const resolveOwningPluginIdsForProviderMock = vi.hoisted(() => vi.fn(() => undefined), ); -const resolveNonBundledProviderPluginIdsMock = vi.hoisted(() => - vi.fn((_) => [] as string[]), +const resolveCatalogHookProviderPluginIdsMock = vi.hoisted(() => + vi.fn((_) => [] as string[]), ); vi.mock("../providers.js", () => ({ resolveOwningPluginIdsForProvider: (params: unknown) => resolveOwningPluginIdsForProviderMock(params as never), - resolveNonBundledProviderPluginIds: (params: unknown) => - resolveNonBundledProviderPluginIdsMock(params as never), + resolveCatalogHookProviderPluginIds: (params: unknown) => + resolveCatalogHookProviderPluginIdsMock(params as never), })); vi.mock("../providers.runtime.js", () => ({ @@ -83,8 +83,8 @@ describe("provider catalog contract", { timeout: PROVIDER_CATALOG_CONTRACT_TIMEO } }); - resolveNonBundledProviderPluginIdsMock.mockReset(); - resolveNonBundledProviderPluginIdsMock.mockReturnValue([]); + resolveCatalogHookProviderPluginIdsMock.mockReset(); + resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]); }); it("keeps codex-only missing-auth hints wired through the provider runtime", () => { diff --git a/src/plugins/provider-catalog-metadata.ts b/src/plugins/provider-catalog-metadata.ts deleted file mode 100644 index c895613fac5..00000000000 --- a/src/plugins/provider-catalog-metadata.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { normalizeProviderId } from "../agents/provider-id.js"; -import type { - ProviderAugmentModelCatalogContext, - ProviderBuiltInModelSuppressionContext, -} from "./types.js"; - -const OPENAI_PROVIDER_ID = "openai"; -const OPENAI_CODEX_PROVIDER_ID = "openai-codex"; -const OPENAI_DIRECT_SPARK_MODEL_ID = "gpt-5.3-codex-spark"; -const SUPPRESSED_SPARK_PROVIDERS = new Set(["openai", "azure-openai-responses"]); - -function findCatalogTemplate(params: { - entries: ReadonlyArray<{ provider: string; id: string }>; - providerId: string; - templateIds: readonly string[]; -}) { - return params.templateIds - .map((templateId) => - params.entries.find( - (entry) => - entry.provider.toLowerCase() === params.providerId.toLowerCase() && - entry.id.toLowerCase() === templateId.toLowerCase(), - ), - ) - .find((entry) => entry !== undefined); -} - -export function resolveBundledProviderBuiltInModelSuppression( - context: ProviderBuiltInModelSuppressionContext, -) { - if ( - !SUPPRESSED_SPARK_PROVIDERS.has(normalizeProviderId(context.provider)) || - context.modelId.toLowerCase() !== OPENAI_DIRECT_SPARK_MODEL_ID - ) { - return undefined; - } - return { - suppress: true, - errorMessage: `Unknown model: ${context.provider}/${OPENAI_DIRECT_SPARK_MODEL_ID}. ${OPENAI_DIRECT_SPARK_MODEL_ID} is only supported via openai-codex OAuth. Use openai-codex/${OPENAI_DIRECT_SPARK_MODEL_ID}.`, - }; -} - -export function augmentBundledProviderCatalog( - context: ProviderAugmentModelCatalogContext, -): ProviderAugmentModelCatalogContext["entries"] { - const openAiGpt54Template = findCatalogTemplate({ - entries: context.entries, - providerId: OPENAI_PROVIDER_ID, - templateIds: ["gpt-5.2"], - }); - const openAiGpt54ProTemplate = findCatalogTemplate({ - entries: context.entries, - providerId: OPENAI_PROVIDER_ID, - templateIds: ["gpt-5.2-pro", "gpt-5.2"], - }); - const openAiGpt54MiniTemplate = findCatalogTemplate({ - entries: context.entries, - providerId: OPENAI_PROVIDER_ID, - templateIds: ["gpt-5-mini"], - }); - const openAiGpt54NanoTemplate = findCatalogTemplate({ - entries: context.entries, - providerId: OPENAI_PROVIDER_ID, - templateIds: ["gpt-5-nano", "gpt-5-mini"], - }); - const openAiCodexGpt54Template = findCatalogTemplate({ - entries: context.entries, - providerId: OPENAI_CODEX_PROVIDER_ID, - templateIds: ["gpt-5.4", "gpt-5.3-codex", "gpt-5.2-codex"], - }); - const openAiCodexSparkTemplate = findCatalogTemplate({ - entries: context.entries, - providerId: OPENAI_CODEX_PROVIDER_ID, - templateIds: ["gpt-5.4", "gpt-5.3-codex", "gpt-5.2-codex"], - }); - - return [ - openAiGpt54Template - ? { - ...openAiGpt54Template, - id: "gpt-5.4", - name: "gpt-5.4", - } - : undefined, - openAiGpt54ProTemplate - ? { - ...openAiGpt54ProTemplate, - id: "gpt-5.4-pro", - name: "gpt-5.4-pro", - } - : undefined, - openAiGpt54MiniTemplate - ? { - ...openAiGpt54MiniTemplate, - id: "gpt-5.4-mini", - name: "gpt-5.4-mini", - } - : undefined, - openAiGpt54NanoTemplate - ? { - ...openAiGpt54NanoTemplate, - id: "gpt-5.4-nano", - name: "gpt-5.4-nano", - } - : undefined, - openAiCodexGpt54Template - ? { - ...openAiCodexGpt54Template, - id: "gpt-5.4", - name: "gpt-5.4", - } - : undefined, - openAiCodexSparkTemplate - ? { - ...openAiCodexSparkTemplate, - id: OPENAI_DIRECT_SPARK_MODEL_ID, - name: OPENAI_DIRECT_SPARK_MODEL_ID, - } - : undefined, - ].filter((entry): entry is NonNullable => entry !== undefined); -} diff --git a/src/plugins/provider-runtime.test.ts b/src/plugins/provider-runtime.test.ts index 6a941019c6d..f8039369a13 100644 --- a/src/plugins/provider-runtime.test.ts +++ b/src/plugins/provider-runtime.test.ts @@ -7,13 +7,13 @@ import { import type { ProviderPlugin, ProviderRuntimeModel } from "./types.js"; type ResolvePluginProviders = typeof import("./providers.runtime.js").resolvePluginProviders; -type ResolveNonBundledProviderPluginIds = - typeof import("./providers.js").resolveNonBundledProviderPluginIds; +type ResolveCatalogHookProviderPluginIds = + typeof import("./providers.js").resolveCatalogHookProviderPluginIds; type ResolveOwningPluginIdsForProvider = typeof import("./providers.js").resolveOwningPluginIdsForProvider; const resolvePluginProvidersMock = vi.fn((_) => [] as ProviderPlugin[]); -const resolveNonBundledProviderPluginIdsMock = vi.fn( +const resolveCatalogHookProviderPluginIdsMock = vi.fn( (_) => [] as string[], ); const resolveOwningPluginIdsForProviderMock = vi.fn( @@ -64,8 +64,8 @@ describe("provider-runtime", () => { beforeEach(async () => { vi.resetModules(); vi.doMock("./providers.js", () => ({ - resolveNonBundledProviderPluginIds: (params: unknown) => - resolveNonBundledProviderPluginIdsMock(params as never), + resolveCatalogHookProviderPluginIds: (params: unknown) => + resolveCatalogHookProviderPluginIdsMock(params as never), resolveOwningPluginIdsForProvider: (params: unknown) => resolveOwningPluginIdsForProviderMock(params as never), })); @@ -103,8 +103,8 @@ describe("provider-runtime", () => { resetProviderRuntimeHookCacheForTest(); resolvePluginProvidersMock.mockReset(); resolvePluginProvidersMock.mockReturnValue([]); - resolveNonBundledProviderPluginIdsMock.mockReset(); - resolveNonBundledProviderPluginIdsMock.mockReturnValue([]); + resolveCatalogHookProviderPluginIdsMock.mockReset(); + resolveCatalogHookProviderPluginIdsMock.mockReturnValue([]); resolveOwningPluginIdsForProviderMock.mockReset(); resolveOwningPluginIdsForProviderMock.mockReturnValue(undefined); }); @@ -150,6 +150,7 @@ describe("provider-runtime", () => { }); it("dispatches runtime hooks for the matched provider", async () => { + resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]); resolveOwningPluginIdsForProviderMock.mockImplementation((params) => { if (params.provider === "demo") { return ["demo"]; @@ -248,6 +249,8 @@ describe("provider-runtime", () => { augmentModelCatalog: () => [ { provider: "openai", id: "gpt-5.4", name: "gpt-5.4" }, { provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" }, + { provider: "openai", id: "gpt-5.4-mini", name: "gpt-5.4-mini" }, + { provider: "openai", id: "gpt-5.4-nano", name: "gpt-5.4-nano" }, { provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" }, { provider: "openai-codex", @@ -540,7 +543,40 @@ describe("provider-runtime", () => { expect(fetchUsageSnapshot).toHaveBeenCalledTimes(1); }); - it("resolves bundled catalog hooks without loading provider plugins", async () => { + it("resolves bundled catalog hooks through provider plugins", async () => { + resolveCatalogHookProviderPluginIdsMock.mockReturnValue(["openai"]); + resolvePluginProvidersMock.mockImplementation((params?: { onlyPluginIds?: string[] }) => { + const onlyPluginIds = params?.onlyPluginIds; + if (!onlyPluginIds || !onlyPluginIds.includes("openai")) { + return []; + } + return [ + { + id: "openai", + label: "OpenAI", + auth: [], + suppressBuiltInModel: ({ provider, modelId }) => + provider === "openai" && modelId === "gpt-5.3-codex-spark" + ? { suppress: true, errorMessage: "openai-codex/gpt-5.3-codex-spark" } + : provider === "azure-openai-responses" && modelId === "gpt-5.3-codex-spark" + ? { suppress: true, errorMessage: "openai-codex/gpt-5.3-codex-spark" } + : undefined, + augmentModelCatalog: () => [ + { provider: "openai", id: "gpt-5.4", name: "gpt-5.4" }, + { provider: "openai", id: "gpt-5.4-pro", name: "gpt-5.4-pro" }, + { provider: "openai", id: "gpt-5.4-mini", name: "gpt-5.4-mini" }, + { provider: "openai", id: "gpt-5.4-nano", name: "gpt-5.4-nano" }, + { provider: "openai-codex", id: "gpt-5.4", name: "gpt-5.4" }, + { + provider: "openai-codex", + id: "gpt-5.3-codex-spark", + name: "gpt-5.3-codex-spark", + }, + ], + }, + ]; + }); + expect( resolveProviderBuiltInModelSuppression({ env: process.env, @@ -581,6 +617,12 @@ describe("provider-runtime", () => { }, ]); - expect(resolvePluginProvidersMock).not.toHaveBeenCalled(); + expect(resolvePluginProvidersMock).toHaveBeenCalledWith( + expect.objectContaining({ + onlyPluginIds: ["openai"], + activate: false, + cache: false, + }), + ); }); }); diff --git a/src/plugins/provider-runtime.ts b/src/plugins/provider-runtime.ts index 9aa9a62b7a6..34fe3ff195e 100644 --- a/src/plugins/provider-runtime.ts +++ b/src/plugins/provider-runtime.ts @@ -2,11 +2,7 @@ import type { AuthProfileCredential, OAuthCredential } from "../agents/auth-prof import { normalizeProviderId } from "../agents/provider-id.js"; import type { OpenClawConfig } from "../config/config.js"; import { - augmentBundledProviderCatalog, - resolveBundledProviderBuiltInModelSuppression, -} from "./provider-catalog-metadata.js"; -import { - resolveNonBundledProviderPluginIds, + resolveCatalogHookProviderPluginIds, resolveOwningPluginIdsForProvider, } from "./providers.js"; import { resolvePluginProviders } from "./providers.runtime.js"; @@ -145,7 +141,7 @@ function resolveProviderPluginsForCatalogHooks(params: { workspaceDir?: string; env?: NodeJS.ProcessEnv; }): ProviderPlugin[] { - const onlyPluginIds = resolveNonBundledProviderPluginIds({ + const onlyPluginIds = resolveCatalogHookProviderPluginIds({ config: params.config, workspaceDir: params.workspaceDir, env: params.env, @@ -416,10 +412,6 @@ export function resolveProviderBuiltInModelSuppression(params: { env?: NodeJS.ProcessEnv; context: ProviderBuiltInModelSuppressionContext; }) { - const bundledResult = resolveBundledProviderBuiltInModelSuppression(params.context); - if (bundledResult?.suppress) { - return bundledResult; - } for (const plugin of resolveProviderPluginsForCatalogHooks(params)) { const result = plugin.suppressBuiltInModel?.(params.context); if (result?.suppress) { @@ -435,9 +427,7 @@ export async function augmentModelCatalogWithProviderPlugins(params: { env?: NodeJS.ProcessEnv; context: ProviderAugmentModelCatalogContext; }) { - const supplemental = [ - ...augmentBundledProviderCatalog(params.context), - ] as ProviderAugmentModelCatalogContext["entries"]; + const supplemental = [] as ProviderAugmentModelCatalogContext["entries"]; for (const plugin of resolveProviderPluginsForCatalogHooks(params)) { const next = await plugin.augmentModelCatalog?.(params.context); if (!next || next.length === 0) { diff --git a/src/plugins/providers.ts b/src/plugins/providers.ts index 9e6a2cba4e1..e4d2637819c 100644 --- a/src/plugins/providers.ts +++ b/src/plugins/providers.ts @@ -91,3 +91,32 @@ export function resolveNonBundledProviderPluginIds(params: { .map((plugin) => plugin.id) .toSorted((left, right) => left.localeCompare(right)); } + +export function resolveCatalogHookProviderPluginIds(params: { + config?: PluginLoadOptions["config"]; + workspaceDir?: string; + env?: PluginLoadOptions["env"]; +}): string[] { + const registry = loadPluginManifestRegistry({ + config: params.config, + workspaceDir: params.workspaceDir, + env: params.env, + }); + const normalizedConfig = normalizePluginsConfig(params.config?.plugins); + const enabledProviderPluginIds = registry.plugins + .filter( + (plugin) => + plugin.providers.length > 0 && + resolveEffectiveEnableState({ + id: plugin.id, + origin: plugin.origin, + config: normalizedConfig, + rootConfig: params.config, + }).enabled, + ) + .map((plugin) => plugin.id); + const bundledCompatPluginIds = resolveBundledProviderCompatPluginIds(params); + return [...new Set([...enabledProviderPluginIds, ...bundledCompatPluginIds])].toSorted( + (left, right) => left.localeCompare(right), + ); +}