mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
feat: resolve model suppressions from manifests
This commit is contained in:
@@ -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,
|
||||
|
||||
91
src/plugins/manifest-model-suppression.test.ts
Normal file
91
src/plugins/manifest-model-suppression.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
117
src/plugins/manifest-model-suppression.ts
Normal file
117
src/plugins/manifest-model-suppression.ts
Normal 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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user