From 39db00f896c64ba06bfbde40523eb5c98f7e58eb Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 28 May 2026 17:59:42 -0400 Subject: [PATCH] fix: validate vercel gateway model token metadata --- extensions/vercel-ai-gateway/models.ts | 13 +++--- .../provider-catalog.test.ts | 45 +++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/extensions/vercel-ai-gateway/models.ts b/extensions/vercel-ai-gateway/models.ts index f84541e1476..5e4a751368c 100644 --- a/extensions/vercel-ai-gateway/models.ts +++ b/extensions/vercel-ai-gateway/models.ts @@ -3,6 +3,7 @@ import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env"; import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; +import { asPositiveSafeInteger } from "openclaw/plugin-sdk/string-coerce-runtime"; export const VERCEL_AI_GATEWAY_PROVIDER_ID = "vercel-ai-gateway"; export const VERCEL_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh"; @@ -151,13 +152,13 @@ function buildDiscoveredModelDefinition( const fallback = getStaticFallbackModel(id); const contextWindow = - typeof model.context_window === "number" && Number.isFinite(model.context_window) - ? model.context_window - : (fallback?.contextWindow ?? VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW); + asPositiveSafeInteger(model.context_window) ?? + fallback?.contextWindow ?? + VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW; const maxTokens = - typeof model.max_tokens === "number" && Number.isFinite(model.max_tokens) - ? model.max_tokens - : (fallback?.maxTokens ?? VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS); + asPositiveSafeInteger(model.max_tokens) ?? + fallback?.maxTokens ?? + VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS; const normalizedCost = normalizeCost(model.pricing); return { diff --git a/extensions/vercel-ai-gateway/provider-catalog.test.ts b/extensions/vercel-ai-gateway/provider-catalog.test.ts index 3ec3ea0e287..dcaba99b70b 100644 --- a/extensions/vercel-ai-gateway/provider-catalog.test.ts +++ b/extensions/vercel-ai-gateway/provider-catalog.test.ts @@ -12,6 +12,8 @@ import { discoverVercelAiGatewayModels, getStaticVercelAiGatewayModelCatalog, VERCEL_AI_GATEWAY_BASE_URL, + VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW, + VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS, } from "./api.js"; import { buildStaticVercelAiGatewayProvider, @@ -93,4 +95,47 @@ describe("vercel ai gateway provider catalog", () => { }); } }); + + it("falls back from malformed live token metadata", async () => { + fetchWithSsrFGuardMock.mockResolvedValueOnce({ + response: { + ok: true, + status: 200, + json: async () => ({ + data: [ + { + id: "anthropic/claude-opus-4.6", + name: "Claude Opus 4.6", + context_window: -1, + max_tokens: 128_000.5, + tags: ["vision", "reasoning"], + }, + { + id: "custom/provider-model", + name: "Custom model", + context_window: Number.POSITIVE_INFINITY, + max_tokens: 0, + tags: ["reasoning"], + }, + ], + }), + }, + release: async () => {}, + }); + + await withLiveDiscovery(async () => { + const models = await discoverVercelAiGatewayModels(); + + expect(models[0]).toMatchObject({ + id: "anthropic/claude-opus-4.6", + contextWindow: 1_000_000, + maxTokens: 128_000, + }); + expect(models[1]).toMatchObject({ + id: "custom/provider-model", + contextWindow: VERCEL_AI_GATEWAY_DEFAULT_CONTEXT_WINDOW, + maxTokens: VERCEL_AI_GATEWAY_DEFAULT_MAX_TOKENS, + }); + }); + }); });