diff --git a/src/gateway/model-pricing-cache.test.ts b/src/gateway/model-pricing-cache.test.ts index a9519fcde2e9..1d63deef0489 100644 --- a/src/gateway/model-pricing-cache.test.ts +++ b/src/gateway/model-pricing-cache.test.ts @@ -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(); + + 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 () => { const config = { agents: { diff --git a/src/gateway/model-pricing-cache.ts b/src/gateway/model-pricing-cache.ts index 27acbc2b8d58..68302d15ea45 100644 --- a/src/gateway/model-pricing-cache.ts +++ b/src/gateway/model-pricing-cache.ts @@ -1,4 +1,8 @@ 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 { buildModelAliasIndex, @@ -26,7 +30,6 @@ import { } from "../plugins/plugin-metadata-snapshot.js"; import type { PluginMetadataRegistryView } from "../plugins/plugin-metadata-snapshot.types.js"; import type { PluginRegistrySnapshot } from "../plugins/plugin-registry.js"; -import { normalizeOptionalString, resolvePrimaryStringValue } from "../../packages/normalization-core/src/string-coerce.js"; import { clearGatewayModelPricingCacheState, clearGatewayModelPricingFailures, @@ -433,6 +436,45 @@ function normalizeExternalPricingPolicy( }; } +function listManifestModelPricingPolicies( + plugin: PluginManifestRecord, +): Array<{ provider: string; policy: PluginManifestModelPricingProvider }> { + let providers: Record | 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: { registry: PluginManifestRegistry; index: PluginRegistrySnapshot; @@ -504,8 +546,8 @@ function loadManifestPricingContext( } { const policies = new Map(); for (const plugin of registry.plugins) { - for (const [provider, rawPolicy] of Object.entries(plugin.modelPricing?.providers ?? {})) { - const policy = normalizeExternalPricingPolicy(rawPolicy, normalizationOptions); + for (const { provider, policy: rawPolicy } of listManifestModelPricingPolicies(plugin)) { + const policy = normalizeManifestModelPricingPolicy(rawPolicy, normalizationOptions); if (policy) { policies.set(provider, policy); }