import http from "node:http"; import https from "node:https"; /** Bounded GET with no redirects, no response-body errors, and loopback proxy bypass. */ export function getCatalogText( url: URL, signal: AbortSignal, headers: Record = {}, maxBytes = 5 * 1024 * 1024, ): Promise { return new Promise((resolve, reject) => { const transport = url.protocol === "http:" ? http : url.protocol === "https:" ? https : undefined; if (!transport || url.username || url.password) { reject(new Error("Invalid catalog URL")); return; } const loopback = ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname); const req = transport.get(url, { signal, headers: { Accept: "application/json, text/html, application/javascript", ...headers }, ...(loopback ? { agent: false } : {}), }, res => { if (res.statusCode !== 200) { res.destroy(); reject(new Error(`HTTP ${res.statusCode ?? 0}`)); return; } const chunks: Buffer[] = []; let bytes = 0; res.on("data", (chunk: Buffer) => { bytes += chunk.length; if (bytes > maxBytes) { reject(new Error("Catalog response too large")); res.destroy(); return; } chunks.push(chunk); }); res.on("end", () => resolve(Buffer.concat(chunks).toString("utf8"))); res.on("error", reject); res.on("aborted", () => reject(new Error("Catalog response interrupted"))); }); req.on("error", reject); }); } export function modelsUrl(baseUrl: string): URL { const url = new URL(baseUrl.trim()); let path = url.pathname.replace(/\/+$/, ""); path = path.replace(/\/v1\/(?:chat\/completions|responses|models)$/, "/v1"); if (!path.endsWith("/v1")) path += "/v1"; url.pathname = `${path}/models`; url.search = ""; url.hash = ""; return url; }