Skip to content

Commit b2685e7

Browse files
committed
refactor: plan manifest catalog aliases and suppressions
1 parent 6d269f6 commit b2685e7

3 files changed

Lines changed: 271 additions & 23 deletions

File tree

src/model-catalog/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ export {
1616
loadOpenClawProviderIndex,
1717
normalizeOpenClawProviderIndex,
1818
} from "./provider-index/index.js";
19-
export { planManifestModelCatalogRows } from "./manifest-planner.js";
19+
export {
20+
planManifestModelCatalogRows,
21+
planManifestModelCatalogSuppressions,
22+
} from "./manifest-planner.js";
2023
export { planProviderIndexModelCatalogRows } from "./provider-index-planner.js";
2124
export type {
2225
ProviderIndexModelCatalogPlan,
@@ -28,6 +31,8 @@ export type {
2831
ManifestModelCatalogPlanEntry,
2932
ManifestModelCatalogPlugin,
3033
ManifestModelCatalogRegistry,
34+
ManifestModelCatalogSuppressionEntry,
35+
ManifestModelCatalogSuppressionPlan,
3136
} from "./manifest-planner.js";
3237
export type {
3338
ModelCatalog,

src/model-catalog/manifest-planner.test.ts

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, expect, it } from "vitest";
2-
import { planManifestModelCatalogRows } from "./index.js";
2+
import { planManifestModelCatalogRows, planManifestModelCatalogSuppressions } from "./index.js";
33

44
describe("manifest model catalog planner", () => {
55
it("builds manifest rows from plugin-owned catalog providers", () => {
@@ -92,6 +92,57 @@ describe("manifest model catalog planner", () => {
9292
expect(plan.conflicts).toEqual([]);
9393
});
9494

95+
it("plans alias-filtered rows from owned provider catalogs", () => {
96+
const plan = planManifestModelCatalogRows({
97+
providerFilter: "azure-openai-responses",
98+
registry: {
99+
plugins: [
100+
{
101+
id: "openai",
102+
providers: ["openai"],
103+
modelCatalog: {
104+
aliases: {
105+
"azure-openai-responses": {
106+
provider: "openai",
107+
api: "azure-openai-responses",
108+
baseUrl: "https://example.openai.azure.com/openai/v1",
109+
},
110+
},
111+
discovery: {
112+
openai: "static",
113+
},
114+
providers: {
115+
openai: {
116+
api: "openai-responses",
117+
baseUrl: "https://api.openai.com/v1",
118+
models: [{ id: "gpt-5.4", name: "GPT-5.4" }],
119+
},
120+
},
121+
},
122+
},
123+
],
124+
},
125+
});
126+
127+
expect(plan.entries).toEqual([
128+
expect.objectContaining({
129+
pluginId: "openai",
130+
provider: "azure-openai-responses",
131+
discovery: "static",
132+
}),
133+
]);
134+
expect(plan.rows).toEqual([
135+
expect.objectContaining({
136+
provider: "azure-openai-responses",
137+
id: "gpt-5.4",
138+
ref: "azure-openai-responses/gpt-5.4",
139+
mergeKey: "azure-openai-responses::gpt-5.4",
140+
api: "azure-openai-responses",
141+
baseUrl: "https://example.openai.azure.com/openai/v1",
142+
}),
143+
]);
144+
});
145+
95146
it("reports duplicate provider/model keys and excludes conflicted rows", () => {
96147
const plan = planManifestModelCatalogRows({
97148
registry: {
@@ -141,3 +192,58 @@ describe("manifest model catalog planner", () => {
141192
});
142193
});
143194
});
195+
196+
describe("manifest model catalog suppression planner", () => {
197+
it("plans suppressions for owned providers and declared provider aliases", () => {
198+
const plan = planManifestModelCatalogSuppressions({
199+
registry: {
200+
plugins: [
201+
{
202+
id: "openai",
203+
providers: ["openai", "openai-codex"],
204+
modelCatalog: {
205+
aliases: {
206+
"azure-openai-responses": {
207+
provider: "openai",
208+
},
209+
},
210+
suppressions: [
211+
{
212+
provider: "openai",
213+
model: "gpt-5.3-codex-spark",
214+
reason: "Use openai/gpt-5.5.",
215+
},
216+
{
217+
provider: "azure-openai-responses",
218+
model: "GPT-5.3-Codex-Spark",
219+
reason: "Use openai/gpt-5.5.",
220+
},
221+
{
222+
provider: "openrouter",
223+
model: "foreign-row",
224+
},
225+
],
226+
},
227+
},
228+
],
229+
},
230+
});
231+
232+
expect(plan.suppressions).toEqual([
233+
{
234+
pluginId: "openai",
235+
provider: "azure-openai-responses",
236+
model: "gpt-5.3-codex-spark",
237+
mergeKey: "azure-openai-responses::gpt-5.3-codex-spark",
238+
reason: "Use openai/gpt-5.5.",
239+
},
240+
{
241+
pluginId: "openai",
242+
provider: "openai",
243+
model: "gpt-5.3-codex-spark",
244+
mergeKey: "openai::gpt-5.3-codex-spark",
245+
reason: "Use openai/gpt-5.5.",
246+
},
247+
]);
248+
});
249+
});

src/model-catalog/manifest-planner.ts

Lines changed: 158 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1+
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
12
import { normalizeModelCatalogProviderRows } from "./normalize.js";
2-
import { normalizeModelCatalogProviderId } from "./refs.js";
3-
import type { ModelCatalog, ModelCatalogDiscovery, NormalizedModelCatalogRow } from "./types.js";
3+
import { buildModelCatalogMergeKey, normalizeModelCatalogProviderId } from "./refs.js";
4+
import type {
5+
ModelCatalog,
6+
ModelCatalogAlias,
7+
ModelCatalogDiscovery,
8+
NormalizedModelCatalogRow,
9+
} from "./types.js";
410

511
export type ManifestModelCatalogPlugin = {
612
id: string;
7-
modelCatalog?: Pick<ModelCatalog, "providers" | "discovery">;
13+
providers?: readonly string[];
14+
modelCatalog?: Pick<ModelCatalog, "providers" | "aliases" | "suppressions" | "discovery">;
815
};
916

1017
export type ManifestModelCatalogRegistry = {
@@ -33,6 +40,18 @@ export type ManifestModelCatalogPlan = {
3340
conflicts: readonly ManifestModelCatalogConflict[];
3441
};
3542

43+
export type ManifestModelCatalogSuppressionEntry = {
44+
pluginId: string;
45+
provider: string;
46+
model: string;
47+
mergeKey: string;
48+
reason?: string;
49+
};
50+
51+
export type ManifestModelCatalogSuppressionPlan = {
52+
suppressions: readonly ManifestModelCatalogSuppressionEntry[];
53+
};
54+
3655
export function planManifestModelCatalogRows(params: {
3756
registry: ManifestModelCatalogRegistry;
3857
providerFilter?: string;
@@ -94,29 +113,147 @@ function planManifestModelCatalogPluginEntries(params: {
94113
return [];
95114
}
96115

116+
const aliasesByTargetProvider = buildModelCatalogProviderAliasTargets(params.plugin);
117+
97118
return Object.entries(providers).flatMap(([provider, providerCatalog]) => {
98119
const normalizedProvider = normalizeModelCatalogProviderId(provider);
99-
if (
100-
!normalizedProvider ||
101-
(params.providerFilter && normalizedProvider !== params.providerFilter)
102-
) {
120+
if (!normalizedProvider) {
103121
return [];
104122
}
105-
const rows = normalizeModelCatalogProviderRows({
106-
provider: normalizedProvider,
107-
providerCatalog,
108-
source: "manifest",
109-
});
110-
if (rows.length === 0) {
123+
const providerAliases = aliasesByTargetProvider.get(normalizedProvider) ?? [];
124+
const plannedProviders = params.providerFilter
125+
? providerAliases.includes(params.providerFilter) ||
126+
normalizedProvider === params.providerFilter
127+
? [params.providerFilter]
128+
: []
129+
: [normalizedProvider];
130+
if (plannedProviders.length === 0) {
111131
return [];
112132
}
113-
return [
114-
{
115-
pluginId: params.plugin.id,
116-
provider: normalizedProvider,
117-
discovery: params.plugin.modelCatalog?.discovery?.[normalizedProvider],
118-
rows,
119-
},
120-
];
133+
return plannedProviders.flatMap((plannedProvider) => {
134+
const rows = normalizeModelCatalogProviderRows({
135+
provider: plannedProvider,
136+
providerCatalog,
137+
source: "manifest",
138+
});
139+
if (rows.length === 0) {
140+
return [];
141+
}
142+
return [
143+
{
144+
pluginId: params.plugin.id,
145+
provider: plannedProvider,
146+
discovery: params.plugin.modelCatalog?.discovery?.[normalizedProvider],
147+
rows: applyModelCatalogAliasOverrides({
148+
rows,
149+
alias: params.plugin.modelCatalog?.aliases?.[plannedProvider],
150+
}),
151+
},
152+
];
153+
});
121154
});
122155
}
156+
157+
function buildOwnedProviderSet(plugin: ManifestModelCatalogPlugin): ReadonlySet<string> {
158+
return new Set((plugin.providers ?? []).map(normalizeModelCatalogProviderId).filter(Boolean));
159+
}
160+
161+
function buildModelCatalogProviderAliasTargets(
162+
plugin: ManifestModelCatalogPlugin,
163+
): ReadonlyMap<string, readonly string[]> {
164+
const ownedProviders = buildOwnedProviderSet(plugin);
165+
const aliasesByTargetProvider = new Map<string, string[]>();
166+
for (const [rawAlias, alias] of Object.entries(plugin.modelCatalog?.aliases ?? {})) {
167+
const aliasProvider = normalizeModelCatalogProviderId(rawAlias);
168+
const targetProvider = normalizeModelCatalogProviderId(alias.provider);
169+
if (!aliasProvider || !targetProvider || !ownedProviders.has(targetProvider)) {
170+
continue;
171+
}
172+
const aliases = aliasesByTargetProvider.get(targetProvider) ?? [];
173+
aliases.push(aliasProvider);
174+
aliasesByTargetProvider.set(targetProvider, aliases);
175+
}
176+
return aliasesByTargetProvider;
177+
}
178+
179+
function applyModelCatalogAliasOverrides(params: {
180+
rows: readonly NormalizedModelCatalogRow[];
181+
alias?: ModelCatalogAlias;
182+
}): readonly NormalizedModelCatalogRow[] {
183+
if (!params.alias) {
184+
return params.rows;
185+
}
186+
return params.rows.map((row) => ({
187+
...row,
188+
...(params.alias.api ? { api: params.alias.api } : {}),
189+
...(params.alias.baseUrl ? { baseUrl: params.alias.baseUrl } : {}),
190+
}));
191+
}
192+
193+
function pluginOwnsModelCatalogProviderRef(params: {
194+
plugin: ManifestModelCatalogPlugin;
195+
provider: string;
196+
}): boolean {
197+
const provider = normalizeModelCatalogProviderId(params.provider);
198+
if (!provider) {
199+
return false;
200+
}
201+
const ownedProviders = buildOwnedProviderSet(params.plugin);
202+
if (ownedProviders.has(provider)) {
203+
return true;
204+
}
205+
return Object.entries(params.plugin.modelCatalog?.aliases ?? {}).some(([rawAlias, alias]) => {
206+
const aliasProvider = normalizeModelCatalogProviderId(rawAlias);
207+
const targetProvider = normalizeModelCatalogProviderId(alias.provider);
208+
return (
209+
aliasProvider === provider && Boolean(targetProvider) && ownedProviders.has(targetProvider)
210+
);
211+
});
212+
}
213+
214+
export function planManifestModelCatalogSuppressions(params: {
215+
registry: ManifestModelCatalogRegistry;
216+
providerFilter?: string;
217+
modelFilter?: string;
218+
}): ManifestModelCatalogSuppressionPlan {
219+
const providerFilter = params.providerFilter
220+
? normalizeModelCatalogProviderId(params.providerFilter)
221+
: undefined;
222+
const modelFilter = params.modelFilter
223+
? normalizeLowercaseStringOrEmpty(params.modelFilter)
224+
: undefined;
225+
const suppressions: ManifestModelCatalogSuppressionEntry[] = [];
226+
for (const plugin of params.registry.plugins) {
227+
for (const suppression of plugin.modelCatalog?.suppressions ?? []) {
228+
const provider = normalizeModelCatalogProviderId(suppression.provider);
229+
const model = normalizeLowercaseStringOrEmpty(suppression.model);
230+
if (!provider || !model) {
231+
continue;
232+
}
233+
if (providerFilter && provider !== providerFilter) {
234+
continue;
235+
}
236+
if (modelFilter && model !== modelFilter) {
237+
continue;
238+
}
239+
if (!pluginOwnsModelCatalogProviderRef({ plugin, provider })) {
240+
continue;
241+
}
242+
suppressions.push({
243+
pluginId: plugin.id,
244+
provider,
245+
model,
246+
mergeKey: buildModelCatalogMergeKey(provider, model),
247+
...(suppression.reason ? { reason: suppression.reason } : {}),
248+
});
249+
}
250+
}
251+
return {
252+
suppressions: suppressions.toSorted(
253+
(left, right) =>
254+
left.provider.localeCompare(right.provider) ||
255+
left.model.localeCompare(right.model) ||
256+
left.pluginId.localeCompare(right.pluginId),
257+
),
258+
};
259+
}

0 commit comments

Comments
 (0)