fix(gateway): guard model pricing metadata

This commit is contained in:
Vincent Koc
2026-06-04 02:54:19 +02:00
parent d1fef1d50d
commit 57504feec7
2 changed files with 89 additions and 3 deletions

View File

@@ -649,6 +649,50 @@ describe("model-pricing-cache", () => {
}); });
}); });
it("skips malformed manifest model-pricing metadata while preserving healthy policies", async () => {
const config = {
agents: {
defaults: {
model: { primary: "custom/gpt-remote" },
},
},
models: {
providers: {
custom: {
baseUrl: "https://models.example/v1",
api: "openai-completions",
models: [{ id: "gpt-remote" }],
},
},
},
} as unknown as OpenClawConfig;
const poisoned = createManifestRecord({ id: "poisoned-pricing" });
Object.defineProperty(poisoned, "modelPricing", {
get() {
throw new Error("model pricing metadata exploded");
},
});
const healthy = createManifestRecord({
id: "healthy-pricing",
modelPricing: {
providers: {
custom: { external: false },
},
},
});
const fetchImpl = vi.fn<typeof fetch>();
await expect(
refreshGatewayModelPricingCache({
config,
fetchImpl,
manifestRegistry: { diagnostics: [], plugins: [poisoned, healthy] },
}),
).resolves.toBeUndefined();
expect(fetchImpl).not.toHaveBeenCalled();
});
it("loads openrouter pricing and maps provider aliases, wrappers, and anthropic dotted ids", async () => { it("loads openrouter pricing and maps provider aliases, wrappers, and anthropic dotted ids", async () => {
const config = { const config = {
agents: { agents: {

View File

@@ -1,4 +1,8 @@
import type { ModelCatalogCost } from "@openclaw/model-catalog-core/model-catalog-types"; import type { ModelCatalogCost } from "@openclaw/model-catalog-core/model-catalog-types";
import {
normalizeOptionalString,
resolvePrimaryStringValue,
} from "../../packages/normalization-core/src/string-coerce.js";
import { DEFAULT_PROVIDER } from "../agents/defaults.js"; import { DEFAULT_PROVIDER } from "../agents/defaults.js";
import { import {
buildModelAliasIndex, buildModelAliasIndex,
@@ -26,7 +30,6 @@ import {
} from "../plugins/plugin-metadata-snapshot.js"; } from "../plugins/plugin-metadata-snapshot.js";
import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js"; import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js";
import type { PluginRegistrySnapshot } from "../plugins/plugin-registry.js"; import type { PluginRegistrySnapshot } from "../plugins/plugin-registry.js";
import { normalizeOptionalString, resolvePrimaryStringValue } from "../../packages/normalization-core/src/string-coerce.js";
import { import {
clearGatewayModelPricingCacheState, clearGatewayModelPricingCacheState,
clearGatewayModelPricingFailures, clearGatewayModelPricingFailures,
@@ -433,6 +436,45 @@ function normalizeExternalPricingPolicy(
}; };
} }
function listManifestModelPricingPolicies(
plugin: PluginManifestRecord,
): Array<{ provider: string; policy: PluginManifestModelPricingProvider }> {
let providers: Record<string, PluginManifestModelPricingProvider> | undefined;
try {
providers = plugin.modelPricing?.providers;
} catch {
return [];
}
if (!providers || typeof providers !== "object") {
return [];
}
let providerIds: string[];
try {
providerIds = Object.keys(providers);
} catch {
return [];
}
return providerIds.flatMap((provider) => {
try {
const policy = providers[provider];
return policy ? [{ provider, policy }] : [];
} catch {
return [];
}
});
}
function normalizeManifestModelPricingPolicy(
value: PluginManifestModelPricingProvider,
options: PricingModelNormalizationOptions,
): ExternalPricingPolicy | undefined {
try {
return normalizeExternalPricingPolicy(value, options);
} catch {
return undefined;
}
}
function filterActiveManifestRegistry(params: { function filterActiveManifestRegistry(params: {
registry: PluginManifestRegistry; registry: PluginManifestRegistry;
index: PluginRegistrySnapshot; index: PluginRegistrySnapshot;
@@ -504,8 +546,8 @@ function loadManifestPricingContext(
} { } {
const policies = new Map<string, ExternalPricingPolicy>(); const policies = new Map<string, ExternalPricingPolicy>();
for (const plugin of registry.plugins) { for (const plugin of registry.plugins) {
for (const [provider, rawPolicy] of Object.entries(plugin.modelPricing?.providers ?? {})) { for (const { provider, policy: rawPolicy } of listManifestModelPricingPolicies(plugin)) {
const policy = normalizeExternalPricingPolicy(rawPolicy, normalizationOptions); const policy = normalizeManifestModelPricingPolicy(rawPolicy, normalizationOptions);
if (policy) { if (policy) {
policies.set(provider, policy); policies.set(provider, policy);
} }