refactor: plan manifest catalog aliases and suppressions

This commit is contained in:
Shakker
2026-04-27 16:18:23 +01:00
parent 6d269f62d6
commit b2685e72c1
3 changed files with 272 additions and 24 deletions

View File

@@ -16,7 +16,10 @@ export {
loadOpenClawProviderIndex,
normalizeOpenClawProviderIndex,
} from "./provider-index/index.js";
export { planManifestModelCatalogRows } from "./manifest-planner.js";
export {
planManifestModelCatalogRows,
planManifestModelCatalogSuppressions,
} from "./manifest-planner.js";
export { planProviderIndexModelCatalogRows } from "./provider-index-planner.js";
export type {
ProviderIndexModelCatalogPlan,
@@ -28,6 +31,8 @@ export type {
ManifestModelCatalogPlanEntry,
ManifestModelCatalogPlugin,
ManifestModelCatalogRegistry,
ManifestModelCatalogSuppressionEntry,
ManifestModelCatalogSuppressionPlan,
} from "./manifest-planner.js";
export type {
ModelCatalog,

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { planManifestModelCatalogRows } from "./index.js";
import { planManifestModelCatalogRows, planManifestModelCatalogSuppressions } from "./index.js";
describe("manifest model catalog planner", () => {
it("builds manifest rows from plugin-owned catalog providers", () => {
@@ -92,6 +92,57 @@ describe("manifest model catalog planner", () => {
expect(plan.conflicts).toEqual([]);
});
it("plans alias-filtered rows from owned provider catalogs", () => {
const plan = planManifestModelCatalogRows({
providerFilter: "azure-openai-responses",
registry: {
plugins: [
{
id: "openai",
providers: ["openai"],
modelCatalog: {
aliases: {
"azure-openai-responses": {
provider: "openai",
api: "azure-openai-responses",
baseUrl: "https://example.openai.azure.com/openai/v1",
},
},
discovery: {
openai: "static",
},
providers: {
openai: {
api: "openai-responses",
baseUrl: "https://api.openai.com/v1",
models: [{ id: "gpt-5.4", name: "GPT-5.4" }],
},
},
},
},
],
},
});
expect(plan.entries).toEqual([
expect.objectContaining({
pluginId: "openai",
provider: "azure-openai-responses",
discovery: "static",
}),
]);
expect(plan.rows).toEqual([
expect.objectContaining({
provider: "azure-openai-responses",
id: "gpt-5.4",
ref: "azure-openai-responses/gpt-5.4",
mergeKey: "azure-openai-responses::gpt-5.4",
api: "azure-openai-responses",
baseUrl: "https://example.openai.azure.com/openai/v1",
}),
]);
});
it("reports duplicate provider/model keys and excludes conflicted rows", () => {
const plan = planManifestModelCatalogRows({
registry: {
@@ -141,3 +192,58 @@ describe("manifest model catalog planner", () => {
});
});
});
describe("manifest model catalog suppression planner", () => {
it("plans suppressions for owned providers and declared provider aliases", () => {
const plan = planManifestModelCatalogSuppressions({
registry: {
plugins: [
{
id: "openai",
providers: ["openai", "openai-codex"],
modelCatalog: {
aliases: {
"azure-openai-responses": {
provider: "openai",
},
},
suppressions: [
{
provider: "openai",
model: "gpt-5.3-codex-spark",
reason: "Use openai/gpt-5.5.",
},
{
provider: "azure-openai-responses",
model: "GPT-5.3-Codex-Spark",
reason: "Use openai/gpt-5.5.",
},
{
provider: "openrouter",
model: "foreign-row",
},
],
},
},
],
},
});
expect(plan.suppressions).toEqual([
{
pluginId: "openai",
provider: "azure-openai-responses",
model: "gpt-5.3-codex-spark",
mergeKey: "azure-openai-responses::gpt-5.3-codex-spark",
reason: "Use openai/gpt-5.5.",
},
{
pluginId: "openai",
provider: "openai",
model: "gpt-5.3-codex-spark",
mergeKey: "openai::gpt-5.3-codex-spark",
reason: "Use openai/gpt-5.5.",
},
]);
});
});

View File

@@ -1,10 +1,17 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { normalizeModelCatalogProviderRows } from "./normalize.js";
import { normalizeModelCatalogProviderId } from "./refs.js";
import type { ModelCatalog, ModelCatalogDiscovery, NormalizedModelCatalogRow } from "./types.js";
import { buildModelCatalogMergeKey, normalizeModelCatalogProviderId } from "./refs.js";
import type {
ModelCatalog,
ModelCatalogAlias,
ModelCatalogDiscovery,
NormalizedModelCatalogRow,
} from "./types.js";
export type ManifestModelCatalogPlugin = {
id: string;
modelCatalog?: Pick<ModelCatalog, "providers" | "discovery">;
providers?: readonly string[];
modelCatalog?: Pick<ModelCatalog, "providers" | "aliases" | "suppressions" | "discovery">;
};
export type ManifestModelCatalogRegistry = {
@@ -33,6 +40,18 @@ export type ManifestModelCatalogPlan = {
conflicts: readonly ManifestModelCatalogConflict[];
};
export type ManifestModelCatalogSuppressionEntry = {
pluginId: string;
provider: string;
model: string;
mergeKey: string;
reason?: string;
};
export type ManifestModelCatalogSuppressionPlan = {
suppressions: readonly ManifestModelCatalogSuppressionEntry[];
};
export function planManifestModelCatalogRows(params: {
registry: ManifestModelCatalogRegistry;
providerFilter?: string;
@@ -94,29 +113,147 @@ function planManifestModelCatalogPluginEntries(params: {
return [];
}
const aliasesByTargetProvider = buildModelCatalogProviderAliasTargets(params.plugin);
return Object.entries(providers).flatMap(([provider, providerCatalog]) => {
const normalizedProvider = normalizeModelCatalogProviderId(provider);
if (
!normalizedProvider ||
(params.providerFilter && normalizedProvider !== params.providerFilter)
) {
if (!normalizedProvider) {
return [];
}
const rows = normalizeModelCatalogProviderRows({
provider: normalizedProvider,
providerCatalog,
source: "manifest",
const providerAliases = aliasesByTargetProvider.get(normalizedProvider) ?? [];
const plannedProviders = params.providerFilter
? providerAliases.includes(params.providerFilter) ||
normalizedProvider === params.providerFilter
? [params.providerFilter]
: []
: [normalizedProvider];
if (plannedProviders.length === 0) {
return [];
}
return plannedProviders.flatMap((plannedProvider) => {
const rows = normalizeModelCatalogProviderRows({
provider: plannedProvider,
providerCatalog,
source: "manifest",
});
if (rows.length === 0) {
return [];
}
return [
{
pluginId: params.plugin.id,
provider: plannedProvider,
discovery: params.plugin.modelCatalog?.discovery?.[normalizedProvider],
rows: applyModelCatalogAliasOverrides({
rows,
alias: params.plugin.modelCatalog?.aliases?.[plannedProvider],
}),
},
];
});
if (rows.length === 0) {
return [];
}
return [
{
pluginId: params.plugin.id,
provider: normalizedProvider,
discovery: params.plugin.modelCatalog?.discovery?.[normalizedProvider],
rows,
},
];
});
}
function buildOwnedProviderSet(plugin: ManifestModelCatalogPlugin): ReadonlySet<string> {
return new Set((plugin.providers ?? []).map(normalizeModelCatalogProviderId).filter(Boolean));
}
function buildModelCatalogProviderAliasTargets(
plugin: ManifestModelCatalogPlugin,
): ReadonlyMap<string, readonly string[]> {
const ownedProviders = buildOwnedProviderSet(plugin);
const aliasesByTargetProvider = new Map<string, string[]>();
for (const [rawAlias, alias] of Object.entries(plugin.modelCatalog?.aliases ?? {})) {
const aliasProvider = normalizeModelCatalogProviderId(rawAlias);
const targetProvider = normalizeModelCatalogProviderId(alias.provider);
if (!aliasProvider || !targetProvider || !ownedProviders.has(targetProvider)) {
continue;
}
const aliases = aliasesByTargetProvider.get(targetProvider) ?? [];
aliases.push(aliasProvider);
aliasesByTargetProvider.set(targetProvider, aliases);
}
return aliasesByTargetProvider;
}
function applyModelCatalogAliasOverrides(params: {
rows: readonly NormalizedModelCatalogRow[];
alias?: ModelCatalogAlias;
}): readonly NormalizedModelCatalogRow[] {
if (!params.alias) {
return params.rows;
}
return params.rows.map((row) => ({
...row,
...(params.alias.api ? { api: params.alias.api } : {}),
...(params.alias.baseUrl ? { baseUrl: params.alias.baseUrl } : {}),
}));
}
function pluginOwnsModelCatalogProviderRef(params: {
plugin: ManifestModelCatalogPlugin;
provider: string;
}): boolean {
const provider = normalizeModelCatalogProviderId(params.provider);
if (!provider) {
return false;
}
const ownedProviders = buildOwnedProviderSet(params.plugin);
if (ownedProviders.has(provider)) {
return true;
}
return Object.entries(params.plugin.modelCatalog?.aliases ?? {}).some(([rawAlias, alias]) => {
const aliasProvider = normalizeModelCatalogProviderId(rawAlias);
const targetProvider = normalizeModelCatalogProviderId(alias.provider);
return (
aliasProvider === provider && Boolean(targetProvider) && ownedProviders.has(targetProvider)
);
});
}
export function planManifestModelCatalogSuppressions(params: {
registry: ManifestModelCatalogRegistry;
providerFilter?: string;
modelFilter?: string;
}): ManifestModelCatalogSuppressionPlan {
const providerFilter = params.providerFilter
? normalizeModelCatalogProviderId(params.providerFilter)
: undefined;
const modelFilter = params.modelFilter
? normalizeLowercaseStringOrEmpty(params.modelFilter)
: undefined;
const suppressions: ManifestModelCatalogSuppressionEntry[] = [];
for (const plugin of params.registry.plugins) {
for (const suppression of plugin.modelCatalog?.suppressions ?? []) {
const provider = normalizeModelCatalogProviderId(suppression.provider);
const model = normalizeLowercaseStringOrEmpty(suppression.model);
if (!provider || !model) {
continue;
}
if (providerFilter && provider !== providerFilter) {
continue;
}
if (modelFilter && model !== modelFilter) {
continue;
}
if (!pluginOwnsModelCatalogProviderRef({ plugin, provider })) {
continue;
}
suppressions.push({
pluginId: plugin.id,
provider,
model,
mergeKey: buildModelCatalogMergeKey(provider, model),
...(suppression.reason ? { reason: suppression.reason } : {}),
});
}
}
return {
suppressions: suppressions.toSorted(
(left, right) =>
left.provider.localeCompare(right.provider) ||
left.model.localeCompare(right.model) ||
left.pluginId.localeCompare(right.pluginId),
),
};
}