mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:00:43 +00:00
refactor: plan manifest catalog aliases and suppressions
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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.",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user