fix(agents): cancel model scan error bodies

This commit is contained in:
Vincent Koc
2026-06-19 10:19:23 +02:00
parent 44b0644e88
commit dbd5689ea1
2 changed files with 77 additions and 59 deletions
+10
View File
@@ -103,6 +103,16 @@ describe("scanOpenRouterModels", () => {
expect(result?.createdAtMs).toBeNull(); 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 () => { it("requires an API key when probing", async () => {
const fetchImpl = createFetchFixture({ data: [] }); const fetchImpl = createFetchFixture({ data: [] });
await withEnvAsync({ OPENROUTER_API_KEY: undefined }, async () => { await withEnvAsync({ OPENROUTER_API_KEY: undefined }, async () => {
+67 -59
View File
@@ -188,73 +188,81 @@ async function fetchOpenRouterModels(
fetchImpl: typeof fetch, fetchImpl: typeof fetch,
timeoutMs: number, timeoutMs: number,
): Promise<OpenRouterModelMeta[]> { ): Promise<OpenRouterModelMeta[]> {
const res = await withTimeout(timeoutMs, (signal) => let res: Response | undefined;
fetchImpl(OPENROUTER_MODELS_URL, { try {
headers: { Accept: "application/json" }, res = await withTimeout(timeoutMs, (signal) =>
signal, fetchImpl(OPENROUTER_MODELS_URL, {
}), headers: { Accept: "application/json" },
); signal,
if (!res.ok) { }),
throw new Error(`OpenRouter /models failed: HTTP ${res.status}`); );
} if (!res.ok) {
const payload = (await res.json()) as { data?: unknown }; throw new Error(`OpenRouter /models failed: HTTP ${res.status}`);
const entries = Array.isArray(payload.data) ? payload.data : []; }
const payload = (await res.json()) as { data?: unknown };
const entries = Array.isArray(payload.data) ? payload.data : [];
return entries return entries
.map((entry) => { .map((entry) => {
if (!entry || typeof entry !== "object") { if (!entry || typeof entry !== "object") {
return null; return null;
} }
const obj = entry as Record<string, unknown>; const obj = entry as Record<string, unknown>;
const id = normalizeOptionalString(obj.id) ?? ""; const id = normalizeOptionalString(obj.id) ?? "";
if (!id) { if (!id) {
return null; return null;
} }
const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id; const name = typeof obj.name === "string" && obj.name.trim() ? obj.name.trim() : id;
const contextLength = const contextLength =
typeof obj.context_length === "number" && Number.isFinite(obj.context_length) typeof obj.context_length === "number" && Number.isFinite(obj.context_length)
? 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
: null; : null;
const supportedParameters = Array.isArray(obj.supported_parameters) const maxCompletionTokens =
? normalizeStringEntries( typeof obj.max_completion_tokens === "number" &&
obj.supported_parameters.filter((value) => typeof value === "string"), 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 supportedParameters = Array.isArray(obj.supported_parameters)
const supportsToolsMeta = supportedParameters.includes("tools"); ? normalizeStringEntries(
obj.supported_parameters.filter((value) => typeof value === "string"),
)
: [];
const modality = const supportedParametersCount = supportedParameters.length;
typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null; const supportsToolsMeta = supportedParameters.includes("tools");
const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`); const modality =
const createdAtMs = normalizeCreatedAtMs(obj.created_at); typeof obj.modality === "string" && obj.modality.trim() ? obj.modality.trim() : null;
const pricing = parseOpenRouterPricing(obj.pricing);
return { const inferredParamB = inferParamBFromIdOrName(`${id} ${name}`);
id, const createdAtMs = normalizeCreatedAtMs(obj.created_at);
name, const pricing = parseOpenRouterPricing(obj.pricing);
contextLength,
maxCompletionTokens, return {
supportedParameters, id,
supportedParametersCount, name,
supportsToolsMeta, contextLength,
modality, maxCompletionTokens,
inferredParamB, supportedParameters,
createdAtMs, supportedParametersCount,
pricing, supportsToolsMeta,
} satisfies OpenRouterModelMeta; modality,
}) inferredParamB,
.filter((entry): entry is OpenRouterModelMeta => Boolean(entry)); 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( async function probeTool(