mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 11:10:45 +00:00
fix: tighten model catalog manifest validation
This commit is contained in:
@@ -594,24 +594,24 @@ is required.
|
||||
|
||||
```json
|
||||
{
|
||||
"providers": ["moonshot"],
|
||||
"providers": ["openai"],
|
||||
"modelCatalog": {
|
||||
"providers": {
|
||||
"moonshot": {
|
||||
"baseUrl": "https://api.moonshot.ai/v1",
|
||||
"openai": {
|
||||
"baseUrl": "https://api.openai.com/v1",
|
||||
"api": "openai-responses",
|
||||
"models": [
|
||||
{
|
||||
"id": "kimi-k2.6",
|
||||
"name": "Kimi K2.6",
|
||||
"id": "gpt-5.4",
|
||||
"name": "GPT-5.4",
|
||||
"input": ["text", "image"],
|
||||
"reasoning": true,
|
||||
"contextWindow": 256000,
|
||||
"maxTokens": 128000,
|
||||
"cost": {
|
||||
"input": 0.6,
|
||||
"output": 2.5,
|
||||
"cacheRead": 0.15
|
||||
"input": 1.25,
|
||||
"output": 10,
|
||||
"cacheRead": 0.125
|
||||
},
|
||||
"status": "available",
|
||||
"tags": ["default"]
|
||||
@@ -627,13 +627,13 @@ is required.
|
||||
},
|
||||
"suppressions": [
|
||||
{
|
||||
"provider": "openai",
|
||||
"provider": "azure-openai-responses",
|
||||
"model": "gpt-5.3-codex-spark",
|
||||
"reason": "not available on Azure OpenAI Responses"
|
||||
}
|
||||
],
|
||||
"discovery": {
|
||||
"moonshot": "static"
|
||||
"openai": "static"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,6 +498,7 @@ describe("loadPluginManifestRegistry", () => {
|
||||
input: ["text", "image", "bogus"],
|
||||
reasoning: true,
|
||||
contextWindow: 256000,
|
||||
contextTokens: 200000,
|
||||
maxTokens: 128000,
|
||||
cost: {
|
||||
input: 0.6,
|
||||
@@ -513,18 +514,26 @@ describe("loadPluginManifestRegistry", () => {
|
||||
},
|
||||
compat: {
|
||||
supportsTools: true,
|
||||
supportsStore: "yes",
|
||||
unknownFlag: true,
|
||||
},
|
||||
status: "available",
|
||||
tags: ["default"],
|
||||
},
|
||||
],
|
||||
},
|
||||
openai: {
|
||||
models: [{ id: "gpt-5.4" }],
|
||||
},
|
||||
},
|
||||
aliases: {
|
||||
kimi: {
|
||||
provider: "moonshot",
|
||||
api: "openai-responses",
|
||||
},
|
||||
openai: {
|
||||
provider: "openai",
|
||||
},
|
||||
},
|
||||
suppressions: [
|
||||
{
|
||||
@@ -535,6 +544,7 @@ describe("loadPluginManifestRegistry", () => {
|
||||
],
|
||||
discovery: {
|
||||
moonshot: "static",
|
||||
openai: "static",
|
||||
ignored: "unknown",
|
||||
},
|
||||
},
|
||||
@@ -562,6 +572,7 @@ describe("loadPluginManifestRegistry", () => {
|
||||
input: ["text", "image"],
|
||||
reasoning: true,
|
||||
contextWindow: 256000,
|
||||
contextTokens: 200000,
|
||||
maxTokens: 128000,
|
||||
cost: {
|
||||
input: 0.6,
|
||||
|
||||
@@ -696,6 +696,14 @@ function normalizeModelCatalogNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeModelCatalogPositiveNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeModelCatalogPositiveInteger(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
||||
}
|
||||
|
||||
function normalizeModelCatalogTieredCost(
|
||||
value: unknown,
|
||||
): PluginManifestModelCatalogTieredCost[] | undefined {
|
||||
@@ -756,6 +764,72 @@ function normalizeModelCatalogCost(value: unknown): PluginManifestModelCatalogCo
|
||||
return Object.keys(cost).length > 0 ? cost : undefined;
|
||||
}
|
||||
|
||||
function normalizeModelCatalogCompat(value: unknown): ModelCompatConfig | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const compat: Record<string, unknown> = {};
|
||||
const booleanFields = [
|
||||
"supportsStore",
|
||||
"supportsPromptCacheKey",
|
||||
"supportsDeveloperRole",
|
||||
"supportsReasoningEffort",
|
||||
"supportsUsageInStreaming",
|
||||
"supportsTools",
|
||||
"supportsStrictMode",
|
||||
"requiresStringContent",
|
||||
"requiresToolResultName",
|
||||
"requiresAssistantAfterToolResult",
|
||||
"requiresThinkingAsText",
|
||||
"nativeWebSearchTool",
|
||||
"requiresMistralToolIds",
|
||||
"requiresOpenAiAnthropicToolPayload",
|
||||
] as const;
|
||||
for (const field of booleanFields) {
|
||||
if (typeof value[field] === "boolean") {
|
||||
compat[field] = value[field];
|
||||
}
|
||||
}
|
||||
|
||||
const stringFields = ["toolSchemaProfile", "toolCallArgumentsEncoding"] as const;
|
||||
for (const field of stringFields) {
|
||||
const normalized = normalizeOptionalString(value[field]) ?? "";
|
||||
if (normalized) {
|
||||
compat[field] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
const stringListFields = [
|
||||
"visibleReasoningDetailTypes",
|
||||
"unsupportedToolSchemaKeywords",
|
||||
] as const;
|
||||
for (const field of stringListFields) {
|
||||
const normalized = normalizeTrimmedStringList(value[field]);
|
||||
if (normalized.length > 0) {
|
||||
compat[field] = normalized;
|
||||
}
|
||||
}
|
||||
|
||||
const maxTokensField = normalizeOptionalString(value.maxTokensField) ?? "";
|
||||
if (maxTokensField === "max_completion_tokens" || maxTokensField === "max_tokens") {
|
||||
compat.maxTokensField = maxTokensField;
|
||||
}
|
||||
|
||||
const thinkingFormat = normalizeOptionalString(value.thinkingFormat) ?? "";
|
||||
if (
|
||||
thinkingFormat === "openai" ||
|
||||
thinkingFormat === "openrouter" ||
|
||||
thinkingFormat === "deepseek" ||
|
||||
thinkingFormat === "zai" ||
|
||||
thinkingFormat === "qwen" ||
|
||||
thinkingFormat === "qwen-chat-template"
|
||||
) {
|
||||
compat.thinkingFormat = thinkingFormat;
|
||||
}
|
||||
|
||||
return Object.keys(compat).length > 0 ? (compat as ModelCompatConfig) : undefined;
|
||||
}
|
||||
|
||||
function normalizeModelCatalogStatus(value: unknown): PluginManifestModelCatalogStatus | undefined {
|
||||
const status = normalizeOptionalString(value) ?? "";
|
||||
return MODEL_CATALOG_STATUSES.has(status)
|
||||
@@ -777,11 +851,11 @@ function normalizeModelCatalogModel(value: unknown): PluginManifestModelCatalogM
|
||||
const headers = normalizeStringMap(value.headers);
|
||||
const input = normalizeModelCatalogInputs(value.input);
|
||||
const reasoning = typeof value.reasoning === "boolean" ? value.reasoning : undefined;
|
||||
const contextWindow = normalizeModelCatalogNumber(value.contextWindow);
|
||||
const contextTokens = normalizeModelCatalogNumber(value.contextTokens);
|
||||
const maxTokens = normalizeModelCatalogNumber(value.maxTokens);
|
||||
const contextWindow = normalizeModelCatalogPositiveNumber(value.contextWindow);
|
||||
const contextTokens = normalizeModelCatalogPositiveInteger(value.contextTokens);
|
||||
const maxTokens = normalizeModelCatalogPositiveNumber(value.maxTokens);
|
||||
const cost = normalizeModelCatalogCost(value.cost);
|
||||
const compat = isRecord(value.compat) ? (value.compat as ModelCompatConfig) : undefined;
|
||||
const compat = normalizeModelCatalogCompat(value.compat);
|
||||
const status = normalizeModelCatalogStatus(value.status);
|
||||
const statusReason = normalizeOptionalString(value.statusReason) ?? "";
|
||||
const replaces = normalizeTrimmedStringList(value.replaces);
|
||||
@@ -810,6 +884,7 @@ function normalizeModelCatalogModel(value: unknown): PluginManifestModelCatalogM
|
||||
|
||||
function normalizeModelCatalogProviders(
|
||||
value: unknown,
|
||||
ownedProviders: ReadonlySet<string>,
|
||||
): Record<string, PluginManifestModelCatalogProvider> | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
@@ -817,7 +892,7 @@ function normalizeModelCatalogProviders(
|
||||
const providers: Record<string, PluginManifestModelCatalogProvider> = {};
|
||||
for (const [rawProviderId, rawProvider] of Object.entries(value)) {
|
||||
const providerId = normalizeSafeRecordKey(rawProviderId);
|
||||
if (!providerId || !isRecord(rawProvider)) {
|
||||
if (!providerId || !ownedProviders.has(providerId) || !isRecord(rawProvider)) {
|
||||
continue;
|
||||
}
|
||||
const models = Array.isArray(rawProvider.models)
|
||||
@@ -843,6 +918,7 @@ function normalizeModelCatalogProviders(
|
||||
|
||||
function normalizeModelCatalogAliases(
|
||||
value: unknown,
|
||||
ownedProviders: ReadonlySet<string>,
|
||||
): Record<string, PluginManifestModelCatalogAlias> | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
@@ -854,7 +930,7 @@ function normalizeModelCatalogAliases(
|
||||
continue;
|
||||
}
|
||||
const provider = normalizeOptionalString(rawTarget.provider) ?? "";
|
||||
if (!provider) {
|
||||
if (!provider || !ownedProviders.has(provider)) {
|
||||
continue;
|
||||
}
|
||||
const api = normalizeModelCatalogApi(rawTarget.api);
|
||||
@@ -896,6 +972,7 @@ function normalizeModelCatalogSuppressions(
|
||||
|
||||
function normalizeModelCatalogDiscovery(
|
||||
value: unknown,
|
||||
ownedProviders: ReadonlySet<string>,
|
||||
): Record<string, PluginManifestModelCatalogDiscovery> | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
@@ -904,21 +981,24 @@ function normalizeModelCatalogDiscovery(
|
||||
for (const [rawProviderId, rawMode] of Object.entries(value)) {
|
||||
const providerId = normalizeSafeRecordKey(rawProviderId);
|
||||
const mode = normalizeOptionalString(rawMode) ?? "";
|
||||
if (providerId && MODEL_CATALOG_DISCOVERY_MODES.has(mode)) {
|
||||
if (providerId && ownedProviders.has(providerId) && MODEL_CATALOG_DISCOVERY_MODES.has(mode)) {
|
||||
discovery[providerId] = mode as PluginManifestModelCatalogDiscovery;
|
||||
}
|
||||
}
|
||||
return Object.keys(discovery).length > 0 ? discovery : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestModelCatalog(value: unknown): PluginManifestModelCatalog | undefined {
|
||||
function normalizeManifestModelCatalog(
|
||||
value: unknown,
|
||||
ownedProviders: ReadonlySet<string>,
|
||||
): PluginManifestModelCatalog | undefined {
|
||||
if (!isRecord(value)) {
|
||||
return undefined;
|
||||
}
|
||||
const providers = normalizeModelCatalogProviders(value.providers);
|
||||
const aliases = normalizeModelCatalogAliases(value.aliases);
|
||||
const providers = normalizeModelCatalogProviders(value.providers, ownedProviders);
|
||||
const aliases = normalizeModelCatalogAliases(value.aliases, ownedProviders);
|
||||
const suppressions = normalizeModelCatalogSuppressions(value.suppressions);
|
||||
const discovery = normalizeModelCatalogDiscovery(value.discovery);
|
||||
const discovery = normalizeModelCatalogDiscovery(value.discovery, ownedProviders);
|
||||
const modelCatalog = {
|
||||
...(providers ? { providers } : {}),
|
||||
...(aliases ? { aliases } : {}),
|
||||
@@ -1237,7 +1317,7 @@ export function loadPluginManifest(
|
||||
const providers = normalizeTrimmedStringList(raw.providers);
|
||||
const providerDiscoveryEntry = normalizeOptionalString(raw.providerDiscoveryEntry);
|
||||
const modelSupport = normalizeManifestModelSupport(raw.modelSupport);
|
||||
const modelCatalog = normalizeManifestModelCatalog(raw.modelCatalog);
|
||||
const modelCatalog = normalizeManifestModelCatalog(raw.modelCatalog, new Set(providers));
|
||||
const providerEndpoints = normalizeManifestProviderEndpoints(raw.providerEndpoints);
|
||||
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
|
||||
const syntheticAuthRefs = normalizeTrimmedStringList(raw.syntheticAuthRefs);
|
||||
|
||||
Reference in New Issue
Block a user