diff --git a/extensions/anthropic/provider-runtime.contract.test.ts b/extensions/anthropic/provider-runtime.contract.test.ts new file mode 100644 index 00000000000..1e82b5481f9 --- /dev/null +++ b/extensions/anthropic/provider-runtime.contract.test.ts @@ -0,0 +1,3 @@ +import { describeAnthropicProviderRuntimeContract } from "../../test/helpers/plugins/provider-runtime-contract.js"; + +describeAnthropicProviderRuntimeContract(() => import("./index.js")); diff --git a/extensions/cloudflare-ai-gateway/provider-discovery.contract.test.ts b/extensions/cloudflare-ai-gateway/provider-discovery.contract.test.ts new file mode 100644 index 00000000000..98cc720920a --- /dev/null +++ b/extensions/cloudflare-ai-gateway/provider-discovery.contract.test.ts @@ -0,0 +1,3 @@ +import { describeCloudflareAiGatewayProviderDiscoveryContract } from "../../test/helpers/plugins/provider-discovery-contract.js"; + +describeCloudflareAiGatewayProviderDiscoveryContract(() => import("./index.js")); diff --git a/extensions/github-copilot/provider-auth.contract.test.ts b/extensions/github-copilot/provider-auth.contract.test.ts new file mode 100644 index 00000000000..31a5b0717dd --- /dev/null +++ b/extensions/github-copilot/provider-auth.contract.test.ts @@ -0,0 +1,3 @@ +import { describeGithubCopilotProviderAuthContract } from "../../test/helpers/plugins/provider-auth-contract.js"; + +describeGithubCopilotProviderAuthContract(() => import("./index.js")); diff --git a/extensions/github-copilot/provider-discovery.contract.test.ts b/extensions/github-copilot/provider-discovery.contract.test.ts new file mode 100644 index 00000000000..a02486e0f56 --- /dev/null +++ b/extensions/github-copilot/provider-discovery.contract.test.ts @@ -0,0 +1,7 @@ +import { fileURLToPath } from "node:url"; +import { describeGithubCopilotProviderDiscoveryContract } from "../../test/helpers/plugins/provider-discovery-contract.js"; + +describeGithubCopilotProviderDiscoveryContract({ + load: () => import("./index.js"), + registerRuntimeModuleId: fileURLToPath(new URL("./register.runtime.js", import.meta.url)), +}); diff --git a/extensions/github-copilot/provider-runtime.contract.test.ts b/extensions/github-copilot/provider-runtime.contract.test.ts new file mode 100644 index 00000000000..dc840ae4762 --- /dev/null +++ b/extensions/github-copilot/provider-runtime.contract.test.ts @@ -0,0 +1,3 @@ +import { describeGithubCopilotProviderRuntimeContract } from "../../test/helpers/plugins/provider-runtime-contract.js"; + +describeGithubCopilotProviderRuntimeContract(() => import("./index.js")); diff --git a/extensions/google/provider-runtime.contract.test.ts b/extensions/google/provider-runtime.contract.test.ts new file mode 100644 index 00000000000..85a729bf514 --- /dev/null +++ b/extensions/google/provider-runtime.contract.test.ts @@ -0,0 +1,3 @@ +import { describeGoogleProviderRuntimeContract } from "../../test/helpers/plugins/provider-runtime-contract.js"; + +describeGoogleProviderRuntimeContract(() => import("./index.js")); diff --git a/extensions/minimax/provider-discovery.contract.test.ts b/extensions/minimax/provider-discovery.contract.test.ts new file mode 100644 index 00000000000..b38fd6a9bc2 --- /dev/null +++ b/extensions/minimax/provider-discovery.contract.test.ts @@ -0,0 +1,3 @@ +import { describeMinimaxProviderDiscoveryContract } from "../../test/helpers/plugins/provider-discovery-contract.js"; + +describeMinimaxProviderDiscoveryContract(() => import("./index.js")); diff --git a/extensions/openai/provider-auth.contract.test.ts b/extensions/openai/provider-auth.contract.test.ts new file mode 100644 index 00000000000..301068e9def --- /dev/null +++ b/extensions/openai/provider-auth.contract.test.ts @@ -0,0 +1,3 @@ +import { describeOpenAICodexProviderAuthContract } from "../../test/helpers/plugins/provider-auth-contract.js"; + +describeOpenAICodexProviderAuthContract(() => import("./index.js")); diff --git a/extensions/openai/provider-runtime.contract.test.ts b/extensions/openai/provider-runtime.contract.test.ts new file mode 100644 index 00000000000..8ec59b70973 --- /dev/null +++ b/extensions/openai/provider-runtime.contract.test.ts @@ -0,0 +1,3 @@ +import { describeOpenAIProviderRuntimeContract } from "../../test/helpers/plugins/provider-runtime-contract.js"; + +describeOpenAIProviderRuntimeContract(() => import("./index.js")); diff --git a/extensions/openrouter/provider-runtime.contract.test.ts b/extensions/openrouter/provider-runtime.contract.test.ts new file mode 100644 index 00000000000..dca90ed129f --- /dev/null +++ b/extensions/openrouter/provider-runtime.contract.test.ts @@ -0,0 +1,3 @@ +import { describeOpenRouterProviderRuntimeContract } from "../../test/helpers/plugins/provider-runtime-contract.js"; + +describeOpenRouterProviderRuntimeContract(() => import("./index.js")); diff --git a/extensions/qwen/provider-discovery.contract.test.ts b/extensions/qwen/provider-discovery.contract.test.ts new file mode 100644 index 00000000000..ed017ec9799 --- /dev/null +++ b/extensions/qwen/provider-discovery.contract.test.ts @@ -0,0 +1,3 @@ +import { describeModelStudioProviderDiscoveryContract } from "../../test/helpers/plugins/provider-discovery-contract.js"; + +describeModelStudioProviderDiscoveryContract(() => import("./index.js")); diff --git a/extensions/sglang/provider-discovery.contract.test.ts b/extensions/sglang/provider-discovery.contract.test.ts new file mode 100644 index 00000000000..3a5a6483764 --- /dev/null +++ b/extensions/sglang/provider-discovery.contract.test.ts @@ -0,0 +1,7 @@ +import { fileURLToPath } from "node:url"; +import { describeSglangProviderDiscoveryContract } from "../../test/helpers/plugins/provider-discovery-contract.js"; + +describeSglangProviderDiscoveryContract({ + load: () => import("./index.js"), + apiModuleId: fileURLToPath(new URL("./api.js", import.meta.url)), +}); diff --git a/extensions/venice/provider-runtime.contract.test.ts b/extensions/venice/provider-runtime.contract.test.ts new file mode 100644 index 00000000000..f26769833c5 --- /dev/null +++ b/extensions/venice/provider-runtime.contract.test.ts @@ -0,0 +1,3 @@ +import { describeVeniceProviderRuntimeContract } from "../../test/helpers/plugins/provider-runtime-contract.js"; + +describeVeniceProviderRuntimeContract(() => import("./index.js")); diff --git a/extensions/vllm/provider-discovery.contract.test.ts b/extensions/vllm/provider-discovery.contract.test.ts new file mode 100644 index 00000000000..c62404f7893 --- /dev/null +++ b/extensions/vllm/provider-discovery.contract.test.ts @@ -0,0 +1,7 @@ +import { fileURLToPath } from "node:url"; +import { describeVllmProviderDiscoveryContract } from "../../test/helpers/plugins/provider-discovery-contract.js"; + +describeVllmProviderDiscoveryContract({ + load: () => import("./index.js"), + apiModuleId: fileURLToPath(new URL("./api.js", import.meta.url)), +}); diff --git a/extensions/zai/provider-runtime.contract.test.ts b/extensions/zai/provider-runtime.contract.test.ts new file mode 100644 index 00000000000..f6830b26605 --- /dev/null +++ b/extensions/zai/provider-runtime.contract.test.ts @@ -0,0 +1,3 @@ +import { describeZAIProviderRuntimeContract } from "../../test/helpers/plugins/provider-runtime-contract.js"; + +describeZAIProviderRuntimeContract(() => import("./index.js")); diff --git a/src/plugins/contracts/provider-auth.contract.test.ts b/src/plugins/contracts/provider-auth.contract.test.ts deleted file mode 100644 index 347bca3745f..00000000000 --- a/src/plugins/contracts/provider-auth.contract.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { - describeGithubCopilotProviderAuthContract, - describeOpenAICodexProviderAuthContract, -} from "../../../test/helpers/plugins/provider-auth-contract.js"; - -describeOpenAICodexProviderAuthContract(); -describeGithubCopilotProviderAuthContract(); diff --git a/src/plugins/contracts/provider-discovery.contract.test.ts b/src/plugins/contracts/provider-discovery.contract.test.ts deleted file mode 100644 index e067634e551..00000000000 --- a/src/plugins/contracts/provider-discovery.contract.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { - describeCloudflareAiGatewayProviderDiscoveryContract, - describeGithubCopilotProviderDiscoveryContract, - describeMinimaxProviderDiscoveryContract, - describeModelStudioProviderDiscoveryContract, - describeSglangProviderDiscoveryContract, - describeVllmProviderDiscoveryContract, -} from "../../../test/helpers/plugins/provider-discovery-contract.js"; - -describeCloudflareAiGatewayProviderDiscoveryContract(); -describeGithubCopilotProviderDiscoveryContract(); -describeMinimaxProviderDiscoveryContract(); -describeModelStudioProviderDiscoveryContract(); -describeSglangProviderDiscoveryContract(); -describeVllmProviderDiscoveryContract(); diff --git a/src/plugins/contracts/provider-runtime.contract.test.ts b/src/plugins/contracts/provider-runtime.contract.test.ts deleted file mode 100644 index 0fe8423855f..00000000000 --- a/src/plugins/contracts/provider-runtime.contract.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { - describeAnthropicProviderRuntimeContract, - describeGithubCopilotProviderRuntimeContract, - describeGoogleProviderRuntimeContract, - describeOpenAIProviderRuntimeContract, - describeOpenRouterProviderRuntimeContract, - describeVeniceProviderRuntimeContract, - describeZAIProviderRuntimeContract, -} from "../../../test/helpers/plugins/provider-runtime-contract.js"; - -describeAnthropicProviderRuntimeContract(); -describeGithubCopilotProviderRuntimeContract(); -describeGoogleProviderRuntimeContract(); -describeOpenAIProviderRuntimeContract(); -describeOpenRouterProviderRuntimeContract(); -describeVeniceProviderRuntimeContract(); -describeZAIProviderRuntimeContract(); diff --git a/test/helpers/plugins/provider-auth-contract.ts b/test/helpers/plugins/provider-auth-contract.ts index ba3e5638875..75c833032de 100644 --- a/test/helpers/plugins/provider-auth-contract.ts +++ b/test/helpers/plugins/provider-auth-contract.ts @@ -2,7 +2,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearRuntimeAuthProfileStoreSnapshots } from "../../../src/agents/auth-profiles/store.js"; import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.js"; import { createNonExitingRuntime } from "../../../src/runtime.js"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; import type { WizardMultiSelectParams, WizardPrompter, @@ -26,18 +25,10 @@ const loginOpenAICodexOAuthMock = vi.hoisted(() => vi.fn( const githubCopilotLoginCommandMock = vi.hoisted(() => vi.fn()); const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn()); const listProfilesForProviderMock = vi.hoisted(() => vi.fn()); -const providerAuthContractModules = { - githubCopilotIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "github-copilot", - artifactBasename: "index.js", - }), - openAIIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "openai", - artifactBasename: "index.js", - }), -}; + +export type ProviderAuthContractPluginLoader = () => Promise<{ + default: Parameters[0]; +}>; vi.mock("openclaw/plugin-sdk/provider-auth-login", async () => { const actual = await vi.importActual( @@ -61,10 +52,6 @@ vi.mock("openclaw/plugin-sdk/provider-auth", async () => { }; }); -async function importBundledProviderPlugin(moduleUrl: string): Promise { - return (await import(`${moduleUrl}?t=${Date.now()}`)) as T; -} - function buildPrompter(): WizardPrompter { const progress: WizardProgress = { update() {}, @@ -164,7 +151,7 @@ function installSharedAuthProfileStoreHooks(state: { authStore: AuthProfileStore }); } -export function describeOpenAICodexProviderAuthContract() { +export function describeOpenAICodexProviderAuthContract(load: ProviderAuthContractPluginLoader) { const state = { authStore: { version: 1, profiles: {} } as AuthProfileStore, }; @@ -173,9 +160,7 @@ export function describeOpenAICodexProviderAuthContract() { installSharedAuthProfileStoreHooks(state); async function expectStableFallbackProfile(params: { access: string; profileId: string }) { - const { default: openAIPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(providerAuthContractModules.openAIIndexModuleUrl); + const { default: openAIPlugin } = await load(); const provider = requireProvider(await registerProviders(openAIPlugin), "openai-codex"); loginOpenAICodexOAuthMock.mockResolvedValueOnce({ refresh: "refresh-token", @@ -194,9 +179,7 @@ export function describeOpenAICodexProviderAuthContract() { } async function getProvider() { - const { default: openAIPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(providerAuthContractModules.openAIIndexModuleUrl); + const { default: openAIPlugin } = await load(); return requireProvider(await registerProviders(openAIPlugin), "openai-codex"); } @@ -317,7 +300,7 @@ export function describeOpenAICodexProviderAuthContract() { }); } -export function describeGithubCopilotProviderAuthContract() { +export function describeGithubCopilotProviderAuthContract(load: ProviderAuthContractPluginLoader) { const state = { authStore: { version: 1, profiles: {} } as AuthProfileStore, }; @@ -326,9 +309,7 @@ export function describeGithubCopilotProviderAuthContract() { installSharedAuthProfileStoreHooks(state); async function getProvider() { - const { default: githubCopilotPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(providerAuthContractModules.githubCopilotIndexModuleUrl); + const { default: githubCopilotPlugin } = await load(); return requireProvider(await registerProviders(githubCopilotPlugin), "github-copilot"); } diff --git a/test/helpers/plugins/provider-discovery-contract.ts b/test/helpers/plugins/provider-discovery-contract.ts index ca435335e21..eb8e3e0fa85 100644 --- a/test/helpers/plugins/provider-discovery-contract.ts +++ b/test/helpers/plugins/provider-discovery-contract.ts @@ -1,10 +1,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { AuthProfileStore } from "../../../src/agents/auth-profiles/types.js"; import type { OpenClawConfig } from "../../../src/config/config.js"; -import { - resolveBundledPluginPublicModulePath, - resolveRelativeBundledPluginPublicModuleId, -} from "../../../src/test-utils/bundled-plugin-public-surface.js"; import { registerProviders, requireProvider } from "./contracts-testkit.js"; const resolveCopilotApiTokenMock = vi.hoisted(() => vi.fn()); @@ -12,50 +8,10 @@ const buildVllmProviderMock = vi.hoisted(() => vi.fn()); const buildSglangProviderMock = vi.hoisted(() => vi.fn()); const ensureAuthProfileStoreMock = vi.hoisted(() => vi.fn()); const listProfilesForProviderMock = vi.hoisted(() => vi.fn()); -const bundledProviderModules = { - cloudflareAiGatewayIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "cloudflare-ai-gateway", - artifactBasename: "index.js", - }), - githubCopilotIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "github-copilot", - artifactBasename: "index.js", - }), - githubCopilotRegisterRuntimeModuleId: resolveBundledPluginPublicModulePath({ - pluginId: "github-copilot", - artifactBasename: "register.runtime.js", - }), - minimaxIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "minimax", - artifactBasename: "index.js", - }), - qwenIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "qwen", - artifactBasename: "index.js", - }), - sglangApiModuleId: resolveBundledPluginPublicModulePath({ - pluginId: "sglang", - artifactBasename: "api.js", - }), - sglangIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "sglang", - artifactBasename: "index.js", - }), - vllmApiModuleId: resolveBundledPluginPublicModulePath({ - pluginId: "vllm", - artifactBasename: "api.js", - }), - vllmIndexModuleUrl: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "vllm", - artifactBasename: "index.js", - }), -}; + +export type ProviderDiscoveryContractPluginLoader = () => Promise<{ + default: Parameters[0]; +}>; type ProviderHandle = Awaited>; @@ -78,6 +34,19 @@ type BundledProviderUnderTest = | "modelstudio" | "cloudflare-ai-gateway"; +type DiscoveryContractOptions = { + providerIds: readonly BundledProviderUnderTest[]; + loadGithubCopilot?: ProviderDiscoveryContractPluginLoader; + loadVllm?: ProviderDiscoveryContractPluginLoader; + loadSglang?: ProviderDiscoveryContractPluginLoader; + loadMinimax?: ProviderDiscoveryContractPluginLoader; + loadModelStudio?: ProviderDiscoveryContractPluginLoader; + loadCloudflareAiGateway?: ProviderDiscoveryContractPluginLoader; + githubCopilotRegisterRuntimeModuleId?: string; + vllmApiModuleId?: string; + sglangApiModuleId?: string; +}; + function setRuntimeAuthStore(store?: AuthProfileStore) { const resolvedStore = store ?? { version: 1, @@ -140,14 +109,7 @@ function runCatalog( }); } -async function importBundledProviderPlugin(moduleUrl: string): Promise { - return (await import(moduleUrl)) as T; -} - -function installDiscoveryHooks( - state: DiscoveryState, - providerIds: readonly BundledProviderUnderTest[], -) { +function installDiscoveryHooks(state: DiscoveryState, options: DiscoveryContractOptions) { beforeAll(async () => { vi.resetModules(); vi.doMock("openclaw/plugin-sdk/agent-runtime", () => { @@ -187,80 +149,72 @@ function installDiscoveryHooks( validateApiKeyInput: () => undefined, }; }); - vi.doMock(bundledProviderModules.githubCopilotRegisterRuntimeModuleId, async () => { - const actual = await vi.importActual( - bundledProviderModules.githubCopilotRegisterRuntimeModuleId, - ); - return { - ...actual, - resolveCopilotApiToken: resolveCopilotApiTokenMock, - }; - }); - vi.doMock(bundledProviderModules.vllmApiModuleId, async () => { - return { - VLLM_DEFAULT_API_KEY_ENV_VAR: "VLLM_API_KEY", - VLLM_DEFAULT_BASE_URL: "http://127.0.0.1:8000/v1", - VLLM_MODEL_PLACEHOLDER: "meta-llama/Meta-Llama-3-8B-Instruct", - VLLM_PROVIDER_LABEL: "vLLM", - buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args), - }; - }); - vi.doMock(bundledProviderModules.sglangApiModuleId, async () => { - return { - SGLANG_DEFAULT_API_KEY_ENV_VAR: "SGLANG_API_KEY", - SGLANG_DEFAULT_BASE_URL: "http://127.0.0.1:30000/v1", - SGLANG_MODEL_PLACEHOLDER: "Qwen/Qwen3-8B", - SGLANG_PROVIDER_LABEL: "SGLang", - buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args), - }; - }); + if (options.githubCopilotRegisterRuntimeModuleId) { + vi.doMock(options.githubCopilotRegisterRuntimeModuleId, async () => { + const actual = await vi.importActual(options.githubCopilotRegisterRuntimeModuleId!); + return { + ...actual, + resolveCopilotApiToken: resolveCopilotApiTokenMock, + }; + }); + } + if (options.vllmApiModuleId) { + vi.doMock(options.vllmApiModuleId, async () => { + return { + VLLM_DEFAULT_API_KEY_ENV_VAR: "VLLM_API_KEY", + VLLM_DEFAULT_BASE_URL: "http://127.0.0.1:8000/v1", + VLLM_MODEL_PLACEHOLDER: "meta-llama/Meta-Llama-3-8B-Instruct", + VLLM_PROVIDER_LABEL: "vLLM", + buildVllmProvider: (...args: unknown[]) => buildVllmProviderMock(...args), + }; + }); + } + if (options.sglangApiModuleId) { + vi.doMock(options.sglangApiModuleId, async () => { + return { + SGLANG_DEFAULT_API_KEY_ENV_VAR: "SGLANG_API_KEY", + SGLANG_DEFAULT_BASE_URL: "http://127.0.0.1:30000/v1", + SGLANG_MODEL_PLACEHOLDER: "Qwen/Qwen3-8B", + SGLANG_PROVIDER_LABEL: "SGLang", + buildSglangProvider: (...args: unknown[]) => buildSglangProviderMock(...args), + }; + }); + } ({ runProviderCatalog: state.runProviderCatalog } = await import("../../../src/plugins/provider-discovery.js")); - if (providerIds.includes("github-copilot")) { - const { default: githubCopilotPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(bundledProviderModules.githubCopilotIndexModuleUrl); + if (options.providerIds.includes("github-copilot")) { + const { default: githubCopilotPlugin } = await options.loadGithubCopilot!(); state.githubCopilotProvider = requireProvider( await registerProviders(githubCopilotPlugin), "github-copilot", ); } - if (providerIds.includes("vllm")) { - const { default: vllmPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(bundledProviderModules.vllmIndexModuleUrl); + if (options.providerIds.includes("vllm")) { + const { default: vllmPlugin } = await options.loadVllm!(); state.vllmProvider = requireProvider(await registerProviders(vllmPlugin), "vllm"); } - if (providerIds.includes("sglang")) { - const { default: sglangPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(bundledProviderModules.sglangIndexModuleUrl); + if (options.providerIds.includes("sglang")) { + const { default: sglangPlugin } = await options.loadSglang!(); state.sglangProvider = requireProvider(await registerProviders(sglangPlugin), "sglang"); } - if (providerIds.includes("minimax")) { - const { default: minimaxPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(bundledProviderModules.minimaxIndexModuleUrl); + if (options.providerIds.includes("minimax")) { + const { default: minimaxPlugin } = await options.loadMinimax!(); const registeredProviders = await registerProviders(minimaxPlugin); state.minimaxProvider = requireProvider(registeredProviders, "minimax"); state.minimaxPortalProvider = requireProvider(registeredProviders, "minimax-portal"); } - if (providerIds.includes("modelstudio")) { - const { default: qwenPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(bundledProviderModules.qwenIndexModuleUrl); + if (options.providerIds.includes("modelstudio")) { + const { default: qwenPlugin } = await options.loadModelStudio!(); state.modelStudioProvider = requireProvider(await registerProviders(qwenPlugin), "qwen"); } - if (providerIds.includes("cloudflare-ai-gateway")) { - const { default: cloudflareAiGatewayPlugin } = await importBundledProviderPlugin<{ - default: Parameters[0]; - }>(bundledProviderModules.cloudflareAiGatewayIndexModuleUrl); + if (options.providerIds.includes("cloudflare-ai-gateway")) { + const { default: cloudflareAiGatewayPlugin } = await options.loadCloudflareAiGateway!(); state.cloudflareAiGatewayProvider = requireProvider( await registerProviders(cloudflareAiGatewayPlugin), "cloudflare-ai-gateway", @@ -283,11 +237,18 @@ function installDiscoveryHooks( }); } -export function describeGithubCopilotProviderDiscoveryContract() { +export function describeGithubCopilotProviderDiscoveryContract(params: { + load: ProviderDiscoveryContractPluginLoader; + registerRuntimeModuleId: string; +}) { const state = {} as DiscoveryState; describe("github-copilot provider discovery contract", () => { - installDiscoveryHooks(state, ["github-copilot"]); + installDiscoveryHooks(state, { + providerIds: ["github-copilot"], + loadGithubCopilot: params.load, + githubCopilotRegisterRuntimeModuleId: params.registerRuntimeModuleId, + }); it("keeps catalog disabled without env tokens or profiles", async () => { await expect( @@ -341,11 +302,18 @@ export function describeGithubCopilotProviderDiscoveryContract() { }); } -export function describeVllmProviderDiscoveryContract() { +export function describeVllmProviderDiscoveryContract(params: { + load: ProviderDiscoveryContractPluginLoader; + apiModuleId: string; +}) { const state = {} as DiscoveryState; describe("vllm provider discovery contract", () => { - installDiscoveryHooks(state, ["vllm"]); + installDiscoveryHooks(state, { + providerIds: ["vllm"], + loadVllm: params.load, + vllmApiModuleId: params.apiModuleId, + }); it("keeps self-hosted discovery provider-owned", async () => { buildVllmProviderMock.mockResolvedValueOnce({ @@ -387,11 +355,18 @@ export function describeVllmProviderDiscoveryContract() { }); } -export function describeSglangProviderDiscoveryContract() { +export function describeSglangProviderDiscoveryContract(params: { + load: ProviderDiscoveryContractPluginLoader; + apiModuleId: string; +}) { const state = {} as DiscoveryState; describe("sglang provider discovery contract", () => { - installDiscoveryHooks(state, ["sglang"]); + installDiscoveryHooks(state, { + providerIds: ["sglang"], + loadSglang: params.load, + sglangApiModuleId: params.apiModuleId, + }); it("keeps self-hosted discovery provider-owned", async () => { buildSglangProviderMock.mockResolvedValueOnce({ @@ -433,11 +408,13 @@ export function describeSglangProviderDiscoveryContract() { }); } -export function describeMinimaxProviderDiscoveryContract() { +export function describeMinimaxProviderDiscoveryContract( + load: ProviderDiscoveryContractPluginLoader, +) { const state = {} as DiscoveryState; describe("minimax provider discovery contract", () => { - installDiscoveryHooks(state, ["minimax"]); + installDiscoveryHooks(state, { providerIds: ["minimax"], loadMinimax: load }); it("keeps API catalog provider-owned", async () => { await expect( @@ -542,11 +519,13 @@ export function describeMinimaxProviderDiscoveryContract() { }); } -export function describeModelStudioProviderDiscoveryContract() { +export function describeModelStudioProviderDiscoveryContract( + load: ProviderDiscoveryContractPluginLoader, +) { const state = {} as DiscoveryState; describe("modelstudio provider discovery contract", () => { - installDiscoveryHooks(state, ["modelstudio"]); + installDiscoveryHooks(state, { providerIds: ["modelstudio"], loadModelStudio: load }); it("keeps catalog provider-owned", async () => { await expect( @@ -589,11 +568,16 @@ export function describeModelStudioProviderDiscoveryContract() { }); } -export function describeCloudflareAiGatewayProviderDiscoveryContract() { +export function describeCloudflareAiGatewayProviderDiscoveryContract( + load: ProviderDiscoveryContractPluginLoader, +) { const state = {} as DiscoveryState; describe("cloudflare-ai-gateway provider discovery contract", () => { - installDiscoveryHooks(state, ["cloudflare-ai-gateway"]); + installDiscoveryHooks(state, { + providerIds: ["cloudflare-ai-gateway"], + loadCloudflareAiGateway: load, + }); it("keeps catalog disabled without stored metadata", async () => { await expect( diff --git a/test/helpers/plugins/provider-runtime-contract.ts b/test/helpers/plugins/provider-runtime-contract.ts index 72a30ad6b28..5e71814a117 100644 --- a/test/helpers/plugins/provider-runtime-contract.ts +++ b/test/helpers/plugins/provider-runtime-contract.ts @@ -3,7 +3,6 @@ import os from "node:os"; import path from "node:path"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ProviderPlugin, ProviderRuntimeModel } from "../../../src/plugins/types.js"; -import { resolveRelativeBundledPluginPublicModuleId } from "../../../src/test-utils/bundled-plugin-public-surface.js"; import { createProviderUsageFetch, makeResponse, @@ -20,43 +19,6 @@ const getOAuthProvidersMock = vi.hoisted(() => { id: "openai-codex", envApiKey: "OPENAI_API_KEY", oauthTokenEnv: "OPENAI_OAUTH_TOKEN" }, ]), ); -const providerRuntimeContractModules = { - anthropicIndexModuleId: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "anthropic", - artifactBasename: "index.js", - }), - githubCopilotIndexModuleId: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "github-copilot", - artifactBasename: "index.js", - }), - googleIndexModuleId: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "google", - artifactBasename: "index.js", - }), - openAIIndexModuleId: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "openai", - artifactBasename: "index.js", - }), - openRouterIndexModuleId: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "openrouter", - artifactBasename: "index.js", - }), - veniceIndexModuleId: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "venice", - artifactBasename: "index.js", - }), - zaiIndexModuleId: resolveRelativeBundledPluginPublicModuleId({ - fromModuleUrl: import.meta.url, - pluginId: "zai", - artifactBasename: "index.js", - }), -}; vi.mock("@mariozechner/pi-ai/oauth", async () => { const actual = await vi.importActual( @@ -69,10 +31,6 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { }; }); -async function importBundledProviderPlugin(moduleUrl: string): Promise { - return (await import(moduleUrl)) as T; -} - function createModel(overrides: Partial & Pick) { return { id: overrides.id, @@ -92,133 +50,77 @@ type ProviderRuntimeContractFixture = { providerIds: string[]; pluginId: string; name: string; - load: () => Promise<{ default: Parameters[0]["plugin"] }>; + load: ProviderRuntimeContractPluginLoader; }; -const PROVIDER_RUNTIME_CONTRACT_FIXTURES: readonly ProviderRuntimeContractFixture[] = [ - { - providerIds: ["anthropic"], - pluginId: "anthropic", - name: "Anthropic", - load: async () => - await importBundledProviderPlugin<{ - default: Parameters[0]["plugin"]; - }>(providerRuntimeContractModules.anthropicIndexModuleId), - }, - { - providerIds: ["github-copilot"], - pluginId: "github-copilot", - name: "GitHub Copilot", - load: async () => - await importBundledProviderPlugin<{ - default: Parameters[0]["plugin"]; - }>(providerRuntimeContractModules.githubCopilotIndexModuleId), - }, - { - providerIds: ["google", "google-gemini-cli"], - pluginId: "google", - name: "Google", - load: async () => - await importBundledProviderPlugin<{ - default: Parameters[0]["plugin"]; - }>(providerRuntimeContractModules.googleIndexModuleId), - }, - { - providerIds: ["openai", "openai-codex"], - pluginId: "openai", - name: "OpenAI", - load: async () => - await importBundledProviderPlugin<{ - default: Parameters[0]["plugin"]; - }>(providerRuntimeContractModules.openAIIndexModuleId), - }, - { - providerIds: ["openrouter"], - pluginId: "openrouter", - name: "OpenRouter", - load: async () => - await importBundledProviderPlugin<{ - default: Parameters[0]["plugin"]; - }>(providerRuntimeContractModules.openRouterIndexModuleId), - }, - { - providerIds: ["venice"], - pluginId: "venice", - name: "Venice", - load: async () => - await importBundledProviderPlugin<{ - default: Parameters[0]["plugin"]; - }>(providerRuntimeContractModules.veniceIndexModuleId), - }, - { - providerIds: ["zai"], - pluginId: "zai", - name: "Z.AI", - load: async () => - await importBundledProviderPlugin<{ - default: Parameters[0]["plugin"]; - }>(providerRuntimeContractModules.zaiIndexModuleId), - }, -] as const; +export type ProviderRuntimeContractPluginLoader = () => Promise<{ + default: Parameters[0]["plugin"]; +}>; -const providerRuntimeContractProviders = new Map(); -let providerRuntimeContractLoadPromise: Promise | null = null; +function installRuntimeHooks(fixtures: readonly ProviderRuntimeContractFixture[]) { + const providers = new Map(); + let loadPromise: Promise | null = null; -function requireProviderContractProvider(providerId: string): ProviderPlugin { - const provider = providerRuntimeContractProviders.get(providerId); - if (!provider) { - throw new Error(`provider runtime contract fixture missing for ${providerId}`); + function requireProviderContractProvider(providerId: string): ProviderPlugin { + const provider = providers.get(providerId); + if (!provider) { + throw new Error(`provider runtime contract fixture missing for ${providerId}`); + } + return provider; } - return provider; -} -async function ensureProviderRuntimeContractProvidersLoaded() { - if (!providerRuntimeContractLoadPromise) { - providerRuntimeContractLoadPromise = (async () => { - providerRuntimeContractProviders.clear(); - const registeredFixtures = await Promise.all( - PROVIDER_RUNTIME_CONTRACT_FIXTURES.map(async (fixture) => { - const plugin = await fixture.load(); - return { - fixture, - providers: ( - await registerProviderPlugin({ - plugin: plugin.default, - id: fixture.pluginId, - name: fixture.name, - }) - ).providers, - }; - }), - ); - for (const { fixture, providers } of registeredFixtures) { - for (const providerId of fixture.providerIds) { - providerRuntimeContractProviders.set( - providerId, - requireRegisteredProvider(providers, providerId, "provider"), - ); + async function ensureProvidersLoaded() { + if (!loadPromise) { + loadPromise = (async () => { + providers.clear(); + const registeredFixtures = await Promise.all( + fixtures.map(async (fixture) => { + const plugin = await fixture.load(); + return { + fixture, + providers: ( + await registerProviderPlugin({ + plugin: plugin.default, + id: fixture.pluginId, + name: fixture.name, + }) + ).providers, + }; + }), + ); + for (const { fixture, providers: registeredProviders } of registeredFixtures) { + for (const providerId of fixture.providerIds) { + providers.set( + providerId, + requireRegisteredProvider(registeredProviders, providerId, "provider"), + ); + } } - } - })(); + })(); + } + + await loadPromise; } - await providerRuntimeContractLoadPromise; -} - -function installRuntimeHooks() { beforeAll(async () => { - await ensureProviderRuntimeContractProvidersLoaded(); + await ensureProvidersLoaded(); }, CONTRACT_SETUP_TIMEOUT_MS); beforeEach(() => { refreshOpenAICodexTokenMock.mockReset(); getOAuthProvidersMock.mockClear(); }, CONTRACT_SETUP_TIMEOUT_MS); + + return requireProviderContractProvider; } -export function describeAnthropicProviderRuntimeContract() { +export function describeAnthropicProviderRuntimeContract( + load: ProviderRuntimeContractPluginLoader, +) { describe("anthropic provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { - installRuntimeHooks(); + const requireProviderContractProvider = installRuntimeHooks([ + { providerIds: ["anthropic"], pluginId: "anthropic", name: "Anthropic", load }, + ]); it("owns anthropic 4.6 forward-compat resolution", () => { const provider = requireProviderContractProvider("anthropic"); @@ -329,12 +231,21 @@ export function describeAnthropicProviderRuntimeContract() { }); } -export function describeGithubCopilotProviderRuntimeContract() { +export function describeGithubCopilotProviderRuntimeContract( + load: ProviderRuntimeContractPluginLoader, +) { describe( "github-copilot provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { - installRuntimeHooks(); + const requireProviderContractProvider = installRuntimeHooks([ + { + providerIds: ["github-copilot"], + pluginId: "github-copilot", + name: "GitHub Copilot", + load, + }, + ]); it("owns Copilot-specific forward-compat fallbacks", () => { const provider = requireProviderContractProvider("github-copilot"); @@ -364,9 +275,11 @@ export function describeGithubCopilotProviderRuntimeContract() { ); } -export function describeGoogleProviderRuntimeContract() { +export function describeGoogleProviderRuntimeContract(load: ProviderRuntimeContractPluginLoader) { describe("google provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { - installRuntimeHooks(); + const requireProviderContractProvider = installRuntimeHooks([ + { providerIds: ["google", "google-gemini-cli"], pluginId: "google", name: "Google", load }, + ]); it("owns google direct gemini 3.1 forward-compat resolution", () => { const provider = requireProviderContractProvider("google"); @@ -494,9 +407,11 @@ export function describeGoogleProviderRuntimeContract() { }); } -export function describeOpenAIProviderRuntimeContract() { +export function describeOpenAIProviderRuntimeContract(load: ProviderRuntimeContractPluginLoader) { describe("openai provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { - installRuntimeHooks(); + const requireProviderContractProvider = installRuntimeHooks([ + { providerIds: ["openai", "openai-codex"], pluginId: "openai", name: "OpenAI", load }, + ]); it("owns openai gpt-5.4 forward-compat resolution", () => { const provider = requireProviderContractProvider("openai"); @@ -702,9 +617,13 @@ export function describeOpenAIProviderRuntimeContract() { }); } -export function describeOpenRouterProviderRuntimeContract() { +export function describeOpenRouterProviderRuntimeContract( + load: ProviderRuntimeContractPluginLoader, +) { describe("openrouter provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { - installRuntimeHooks(); + const requireProviderContractProvider = installRuntimeHooks([ + { providerIds: ["openrouter"], pluginId: "openrouter", name: "OpenRouter", load }, + ]); it("owns dynamic OpenRouter model defaults", () => { const provider = requireProviderContractProvider("openrouter"); @@ -727,9 +646,11 @@ export function describeOpenRouterProviderRuntimeContract() { }); } -export function describeVeniceProviderRuntimeContract() { +export function describeVeniceProviderRuntimeContract(load: ProviderRuntimeContractPluginLoader) { describe("venice provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { - installRuntimeHooks(); + const requireProviderContractProvider = installRuntimeHooks([ + { providerIds: ["venice"], pluginId: "venice", name: "Venice", load }, + ]); it("owns xai downstream compat flags for grok-backed Venice models", () => { const provider = requireProviderContractProvider("venice"); @@ -755,9 +676,11 @@ export function describeVeniceProviderRuntimeContract() { }); } -export function describeZAIProviderRuntimeContract() { +export function describeZAIProviderRuntimeContract(load: ProviderRuntimeContractPluginLoader) { describe("zai provider runtime contract", { timeout: CONTRACT_SETUP_TIMEOUT_MS }, () => { - installRuntimeHooks(); + const requireProviderContractProvider = installRuntimeHooks([ + { providerIds: ["zai"], pluginId: "zai", name: "Z.AI", load }, + ]); it("owns glm-5 forward-compat resolution", () => { const provider = requireProviderContractProvider("zai");