feat(plugins): auto-load provider plugins from model support

This commit is contained in:
Peter Steinberger
2026-04-04 04:52:14 +01:00
parent 5b144655f2
commit fff7e610df
13 changed files with 599 additions and 11 deletions

View File

@@ -15,18 +15,21 @@ const loadPluginManifestRegistryMock = vi.fn<LoadPluginManifestRegistry>();
const applyPluginAutoEnableMock = vi.fn<ApplyPluginAutoEnable>();
let resolveOwningPluginIdsForProvider: typeof import("./providers.js").resolveOwningPluginIdsForProvider;
let resolveOwningPluginIdsForModelRef: typeof import("./providers.js").resolveOwningPluginIdsForModelRef;
let resolvePluginProviders: typeof import("./providers.runtime.js").resolvePluginProviders;
function createManifestProviderPlugin(params: {
id: string;
providerIds: string[];
origin?: "bundled" | "workspace";
modelSupport?: { modelPrefixes?: string[]; modelPatterns?: string[] };
}): PluginManifestRecord {
return {
id: params.id,
channels: [],
cliBackends: [],
providers: params.providerIds,
modelSupport: params.modelSupport,
skills: [],
hooks: [],
origin: params.origin ?? "bundled",
@@ -52,6 +55,47 @@ function setOwningProviderManifestPlugins() {
createManifestProviderPlugin({
id: "openai",
providerIds: ["openai", "openai-codex"],
modelSupport: {
modelPrefixes: ["gpt-", "o1", "o3", "o4"],
},
}),
createManifestProviderPlugin({
id: "anthropic",
providerIds: ["anthropic"],
modelSupport: {
modelPrefixes: ["claude-"],
},
}),
]);
}
function setOwningProviderManifestPluginsWithWorkspace() {
setManifestPlugins([
createManifestProviderPlugin({
id: "minimax",
providerIds: ["minimax", "minimax-portal"],
}),
createManifestProviderPlugin({
id: "openai",
providerIds: ["openai", "openai-codex"],
modelSupport: {
modelPrefixes: ["gpt-", "o1", "o3", "o4"],
},
}),
createManifestProviderPlugin({
id: "anthropic",
providerIds: ["anthropic"],
modelSupport: {
modelPrefixes: ["claude-"],
},
}),
createManifestProviderPlugin({
id: "workspace-provider",
providerIds: ["workspace-provider"],
origin: "workspace",
modelSupport: {
modelPrefixes: ["workspace-model-"],
},
}),
]);
}
@@ -158,6 +202,10 @@ function expectOwningPluginIds(provider: string, expectedPluginIds?: readonly st
expect(resolveOwningPluginIdsForProvider({ provider })).toEqual(expectedPluginIds);
}
function expectModelOwningPluginIds(model: string, expectedPluginIds?: readonly string[]) {
expect(resolveOwningPluginIdsForModelRef({ model })).toEqual(expectedPluginIds);
}
function expectProviderRuntimeRegistryLoad(params?: { config?: unknown; env?: NodeJS.ProcessEnv }) {
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
expect.objectContaining({
@@ -182,7 +230,8 @@ describe("resolvePluginProviders", () => {
loadPluginManifestRegistry: (...args: Parameters<LoadPluginManifestRegistry>) =>
loadPluginManifestRegistryMock(...args),
}));
({ resolveOwningPluginIdsForProvider } = await import("./providers.js"));
({ resolveOwningPluginIdsForProvider, resolveOwningPluginIdsForModelRef } =
await import("./providers.js"));
({ resolvePluginProviders } = await import("./providers.runtime.js"));
});
@@ -215,6 +264,9 @@ describe("resolvePluginProviders", () => {
id: "workspace-provider",
providerIds: ["workspace-provider"],
origin: "workspace",
modelSupport: {
modelPrefixes: ["workspace-model-"],
},
}),
]);
});
@@ -433,4 +485,112 @@ describe("resolvePluginProviders", () => {
expectOwningPluginIds(provider, expectedPluginIds);
},
);
it.each([
{
model: "gpt-5.4",
expectedPluginIds: ["openai"],
},
{
model: "claude-sonnet-4-6",
expectedPluginIds: ["anthropic"],
},
{
model: "openai/gpt-5.4",
expectedPluginIds: ["openai"],
},
{
model: "workspace-model-fast",
expectedPluginIds: ["workspace-provider"],
},
{
model: "unknown-model",
expectedPluginIds: undefined,
},
] as const)(
"maps $model to owning plugin ids via modelSupport",
({ model, expectedPluginIds }) => {
setOwningProviderManifestPluginsWithWorkspace();
expectModelOwningPluginIds(model, expectedPluginIds);
},
);
it("refuses ambiguous bundled shorthand model ownership", () => {
setManifestPlugins([
createManifestProviderPlugin({
id: "openai",
providerIds: ["openai"],
modelSupport: { modelPrefixes: ["gpt-"] },
}),
createManifestProviderPlugin({
id: "proxy-openai",
providerIds: ["proxy-openai"],
modelSupport: { modelPrefixes: ["gpt-"] },
}),
]);
expectModelOwningPluginIds("gpt-5.4", undefined);
});
it("prefers non-bundled shorthand model ownership over bundled matches", () => {
setManifestPlugins([
createManifestProviderPlugin({
id: "openai",
providerIds: ["openai"],
modelSupport: { modelPrefixes: ["gpt-"] },
}),
createManifestProviderPlugin({
id: "workspace-openai",
providerIds: ["workspace-openai"],
origin: "workspace",
modelSupport: { modelPrefixes: ["gpt-"] },
}),
]);
expectModelOwningPluginIds("gpt-5.4", ["workspace-openai"]);
});
it("auto-loads a model-owned provider plugin from shorthand model refs", () => {
setManifestPlugins([
createManifestProviderPlugin({
id: "openai",
providerIds: ["openai", "openai-codex"],
modelSupport: {
modelPrefixes: ["gpt-", "o1", "o3", "o4"],
},
}),
]);
const provider: ProviderPlugin = {
id: "openai",
label: "OpenAI",
auth: [],
};
const registry = createEmptyPluginRegistry();
registry.providers.push({ pluginId: "openai", provider, source: "bundled" });
resolveRuntimePluginRegistryMock.mockReturnValue(registry);
const providers = resolvePluginProviders({
config: {},
modelRefs: ["gpt-5.4"],
bundledProviderAllowlistCompat: true,
});
expectResolvedProviders(providers, [
{ id: "openai", label: "OpenAI", auth: [], pluginId: "openai" },
]);
expect(resolveRuntimePluginRegistryMock).toHaveBeenCalledWith(
expect.objectContaining({
onlyPluginIds: ["openai"],
config: expect.objectContaining({
plugins: expect.objectContaining({
allow: ["openai"],
entries: {
openai: { enabled: true },
},
}),
}),
}),
);
});
});