feat: resolve model suppressions from manifests

This commit is contained in:
Shakker
2026-04-27 16:19:58 +01:00
parent b2685e72c1
commit d014b36347
3 changed files with 218 additions and 0 deletions

View File

@@ -1,4 +1,5 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveManifestBuiltInModelSuppression } from "../plugins/manifest-model-suppression.js";
import { resolveProviderBuiltInModelSuppression } from "../plugins/provider-runtime.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { normalizeProviderId } from "./provider-id.js";
@@ -14,6 +15,15 @@ function resolveBuiltInModelSuppression(params: {
if (!provider || !modelId) {
return undefined;
}
const manifestResult = resolveManifestBuiltInModelSuppression({
provider,
id: modelId,
...(params.config ? { config: params.config } : {}),
env: process.env,
});
if (manifestResult?.suppress) {
return manifestResult;
}
return resolveProviderBuiltInModelSuppression({
...(params.config ? { config: params.config } : {}),
env: process.env,

View File

@@ -0,0 +1,91 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
loadPluginManifestRegistryForPluginRegistry: vi.fn(),
}));
vi.mock("./plugin-registry.js", () => ({
loadPluginManifestRegistryForPluginRegistry: mocks.loadPluginManifestRegistryForPluginRegistry,
}));
import {
clearManifestModelSuppressionCacheForTest,
resolveManifestBuiltInModelSuppression,
} from "./manifest-model-suppression.js";
describe("manifest model suppression", () => {
beforeEach(() => {
clearManifestModelSuppressionCacheForTest();
mocks.loadPluginManifestRegistryForPluginRegistry.mockReset();
mocks.loadPluginManifestRegistryForPluginRegistry.mockReturnValue({
diagnostics: [],
plugins: [
{
id: "openai",
providers: ["openai"],
modelCatalog: {
aliases: {
"azure-openai-responses": {
provider: "openai",
},
},
suppressions: [
{
provider: "azure-openai-responses",
model: "gpt-5.3-codex-spark",
reason: "Use openai/gpt-5.5.",
},
{
provider: "openrouter",
model: "foreign-row",
},
],
},
},
],
});
});
it("resolves manifest suppressions for declared provider aliases", () => {
expect(
resolveManifestBuiltInModelSuppression({
provider: "azure-openai-responses",
id: "GPT-5.3-Codex-Spark",
env: process.env,
}),
).toEqual({
suppress: true,
errorMessage:
"Unknown model: azure-openai-responses/gpt-5.3-codex-spark. Use openai/gpt-5.5.",
});
});
it("ignores suppressions for providers the plugin does not own", () => {
expect(
resolveManifestBuiltInModelSuppression({
provider: "openrouter",
id: "foreign-row",
env: process.env,
}),
).toBeUndefined();
});
it("caches planned manifest suppressions per config and environment", () => {
const config = { plugins: { entries: { openai: { enabled: true } } } };
resolveManifestBuiltInModelSuppression({
provider: "azure-openai-responses",
id: "gpt-5.3-codex-spark",
config,
env: process.env,
});
resolveManifestBuiltInModelSuppression({
provider: "azure-openai-responses",
id: "gpt-5.3-codex-spark",
config,
env: process.env,
});
expect(mocks.loadPluginManifestRegistryForPluginRegistry).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,117 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
buildModelCatalogMergeKey,
planManifestModelCatalogSuppressions,
type ManifestModelCatalogSuppressionEntry,
} from "../model-catalog/index.js";
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js";
type ManifestSuppressionCache = Map<string, readonly ManifestModelCatalogSuppressionEntry[]>;
let cacheWithoutConfig = new WeakMap<NodeJS.ProcessEnv, ManifestSuppressionCache>();
let cacheByConfig = new WeakMap<
OpenClawConfig,
WeakMap<NodeJS.ProcessEnv, ManifestSuppressionCache>
>();
function resolveSuppressionCache(params: {
config?: OpenClawConfig;
env: NodeJS.ProcessEnv;
}): ManifestSuppressionCache {
if (!params.config) {
let cache = cacheWithoutConfig.get(params.env);
if (!cache) {
cache = new Map();
cacheWithoutConfig.set(params.env, cache);
}
return cache;
}
let envCaches = cacheByConfig.get(params.config);
if (!envCaches) {
envCaches = new WeakMap();
cacheByConfig.set(params.config, envCaches);
}
let cache = envCaches.get(params.env);
if (!cache) {
cache = new Map();
envCaches.set(params.env, cache);
}
return cache;
}
function cacheKey(params: { workspaceDir?: string }): string {
return params.workspaceDir ?? "";
}
function listManifestModelCatalogSuppressions(params: {
config?: OpenClawConfig;
workspaceDir?: string;
env: NodeJS.ProcessEnv;
}): readonly ManifestModelCatalogSuppressionEntry[] {
const cache = resolveSuppressionCache({
config: params.config,
env: params.env,
});
const key = cacheKey(params);
const cached = cache.get(key);
if (cached) {
return cached;
}
const registry = loadPluginManifestRegistryForPluginRegistry({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
});
const planned = planManifestModelCatalogSuppressions({ registry });
cache.set(key, planned.suppressions);
return planned.suppressions;
}
function buildManifestSuppressionError(params: {
provider: string;
modelId: string;
reason?: string;
}): string {
const ref = `${params.provider}/${params.modelId}`;
return params.reason ? `Unknown model: ${ref}. ${params.reason}` : `Unknown model: ${ref}.`;
}
export function clearManifestModelSuppressionCacheForTest(): void {
cacheWithoutConfig = new WeakMap<NodeJS.ProcessEnv, ManifestSuppressionCache>();
cacheByConfig = new WeakMap<
OpenClawConfig,
WeakMap<NodeJS.ProcessEnv, ManifestSuppressionCache>
>();
}
export function resolveManifestBuiltInModelSuppression(params: {
provider?: string | null;
id?: string | null;
config?: OpenClawConfig;
workspaceDir?: string;
env?: NodeJS.ProcessEnv;
}) {
const provider = normalizeLowercaseStringOrEmpty(params.provider);
const modelId = normalizeLowercaseStringOrEmpty(params.id);
if (!provider || !modelId) {
return undefined;
}
const mergeKey = buildModelCatalogMergeKey(provider, modelId);
const suppression = listManifestModelCatalogSuppressions({
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env ?? process.env,
}).find((entry) => entry.mergeKey === mergeKey);
if (!suppression) {
return undefined;
}
return {
suppress: true,
errorMessage: buildManifestSuppressionError({
provider,
modelId,
reason: suppression.reason,
}),
};
}