From dbd5689ea199e6fa858b88cc9e13d41ee903de2c Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Fri, 19 Jun 2026 10:19:23 +0200 Subject: [PATCH] fix(agents): cancel model scan error bodies --- src/agents/model-scan.test.ts | 10 +++ src/agents/model-scan.ts | 126 ++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 59 deletions(-) diff --git a/src/agents/model-scan.test.ts b/src/agents/model-scan.test.ts index b40b1dee771..b9c15f7a9ad 100644 --- a/src/agents/model-scan.test.ts +++ b/src/agents/model-scan.test.ts @@ -103,6 +103,16 @@ describe("scanOpenRouterModels", () => { expect(result?.createdAtMs).toBeNull(); }); + it("cancels catalog error response bodies", async () => { + const response = new Response("unavailable", { status: 503 }); + const cancel = vi.spyOn(response.body!, "cancel").mockResolvedValue(undefined); + const fetchImpl = withFetchPreconnect(async () => response); + + await expect(scanOpenRouterModels({ fetchImpl, probe: false })).rejects.toThrow(/HTTP 503/); + + expect(cancel).toHaveBeenCalledOnce(); + }); + it("requires an API key when probing", async () => { const fetchImpl = createFetchFixture({ data: [] }); await withEnvAsync({ OPENROUTER_API_KEY: undefined }, async () => { diff --git a/src/agents/model-scan.ts b/src/agents/model-scan.ts index b2b0966232e..e9a543e6339 100644 --- a/src/agents/model-scan.ts +++ b/src/agents/model-scan.ts @@ -188,73 +188,81 @@ async function fetchOpenRouterModels( fetchImpl: typeof fetch, timeoutMs: number, ): Promise { - const res = await withTimeout(timeoutMs, (signal) => - fetchImpl(OPENROUTER_MODELS_URL, { - headers: { Accept: "application/json" }, - signal, - }), - ); - if (!res.ok) { - throw new Error(`OpenRouter /models failed: HTTP ${res.status}`); - } - const payload = (await res.json()) as { data?: unknown }; - const entries = Array.isArray(payload.data) ? payload.data : []; + let res: Response | undefined; + try { + res = await withTimeout(timeoutMs, (signal) => + fetchImpl(OPENROUTER_MODELS_URL, { + headers: { Accept: "application/json" }, + signal, + }), + ); + if (!res.ok) { + throw new Error(`OpenRouter /models failed: HTTP ${res.status}`); + } + const payload = (await res.json()) as { data?: unknown }; + const entries = Array.isArray(payload.data) ? payload.data : []; - return entries - .map((entry) => { - if (!entry || typeof entry !== "object") { - return null; - } - const obj = entry as Record; - const id = normalizeOptionalString(obj.id) ?? ""; - if (!id) { - return null; - } - const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id; + return entries + .map((entry) => { + if (!entry || typeof entry !== "object") { + return null; + } + const obj = entry as Record; + const id = normalizeOptionalString(obj.id) ?? ""; + if (!id) { + return null; + } + const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id; - const contextLength = - typeof obj.context_length === "number" && Number.isFinite(obj.context_length) - ? obj.context_length - : null; - - const maxCompletionTokens = - typeof obj.max_completion_tokens === "number" && Number.isFinite(obj.max_completion_tokens) - ? obj.max_completion_tokens - : typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens) - ? obj.max_output_tokens + const contextLength = + typeof obj.context_length === "number" && Number.isFinite(obj.context_length) + ? obj.context_length : null; - const supportedParameters = Array.isArray(obj.supported_parameters) - ? normalizeStringEntries( - obj.supported_parameters.filter((value) => typeof value === "string"), - ) - : []; + const maxCompletionTokens = + typeof obj.max_completion_tokens === "number" && + Number.isFinite(obj.max_completion_tokens) + ? obj.max_completion_tokens + : typeof obj.max_output_tokens === "number" && Number.isFinite(obj.max_output_tokens) + ? obj.max_output_tokens + : null; - const supportedParametersCount = supportedParameters.length; - const supportsToolsMeta = supportedParameters.includes("tools"); + const supportedParameters = Array.isArray(obj.supported_parameters) + ? normalizeStringEntries( + obj.supported_parameters.filter((value) => typeof value === "string"), + ) + : []; - const modality = - typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null; + const supportedParametersCount = supportedParameters.length; + const supportsToolsMeta = supportedParameters.includes("tools"); - const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`); - const createdAtMs = normalizeCreatedAtMs(obj.created_at); - const pricing = parseOpenRouterPricing(obj.pricing); + const modality = + typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null; - return { - id, - name, - contextLength, - maxCompletionTokens, - supportedParameters, - supportedParametersCount, - supportsToolsMeta, - modality, - inferredParamB, - createdAtMs, - pricing, - } satisfies OpenRouterModelMeta; - }) - .filter((entry): entry is OpenRouterModelMeta => Boolean(entry)); + const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`); + const createdAtMs = normalizeCreatedAtMs(obj.created_at); + const pricing = parseOpenRouterPricing(obj.pricing); + + return { + id, + name, + contextLength, + maxCompletionTokens, + supportedParameters, + supportedParametersCount, + supportsToolsMeta, + modality, + inferredParamB, + createdAtMs, + pricing, + } satisfies OpenRouterModelMeta; + }) + .filter((entry): entry is OpenRouterModelMeta => Boolean(entry)); + } finally { + if (res && !res.bodyUsed) { + await res.body?.cancel().catch(() => undefined); + } + } } async function probeTool(