fix(ai-contracts): customNodeFetch för att kringgå IPv6-häng i WSL

Node.js fetch() hänger på POST till Gemini via IPv6 i WSL.
Ersätter default fetch med customNodeFetch som använder node:https
och därmed kör över IPv4. Signifikant hastighetsförbättring
(~1 sek vs timeout).

Relaterat till scale-batch.ts i Fas A Steg 2.
This commit is contained in:
Sven (AAMOS AI)
2026-08-09 22:10:36 +07:00
parent 84f8ff3c45
commit 344ec8ee3a
+56 -1
View File
@@ -23,6 +23,61 @@ import type { LocaleContext } from "@app/shared-types";
const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta"; const GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
/** Custom fetch using Node https module — works around IPv6 hangs on WSL. */
function customNodeFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
return new Promise((resolve, reject) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
const u = new URL(url);
const isHttps = u.protocol === "https:";
const mod = isHttps ? import("node:https") : import("node:http");
mod.then((httpMod) => {
const postData = init?.body ? String(init.body) : undefined;
const headers: Record<string, string> = {};
if (init?.headers) {
if (init.headers instanceof Headers) {
init.headers.forEach((v, k) => { headers[k] = v; });
} else if (Array.isArray(init.headers)) {
(init.headers as [string, string][]).forEach(([k, v]) => { if (k != null) headers[k] = v; });
} else {
Object.assign(headers, init.headers as Record<string, string>);
}
}
if (postData && !headers["content-length"]) {
headers["content-length"] = String(Buffer.byteLength(postData));
}
const req = httpMod.request(
{
hostname: u.hostname,
port: u.port || (isHttps ? 443 : 80),
path: u.pathname + u.search,
method: init?.method || "GET",
headers,
timeout: 120_000,
},
(res) => {
let body = "";
res.setEncoding("utf8");
res.on("data", (chunk) => { body += chunk; });
res.on("end", () => {
resolve(
new Response(body, {
status: res.statusCode ?? 200,
statusText: res.statusMessage ?? "OK",
headers: new Headers(Object.entries(res.headers).map(([k, v]) => [k, String(v)])),
}),
);
});
},
);
req.on("error", (err) => reject(err));
req.on("timeout", () => { req.destroy(); reject(new Error("Request timeout")); });
if (postData) req.write(postData);
req.end();
}).catch(reject);
});
}
/** Estimated max cost per image call in USD (pessimistic). */ /** Estimated max cost per image call in USD (pessimistic). */
const COST_ESTIMATE_PER_IMAGE_USD = 0.0015; const COST_ESTIMATE_PER_IMAGE_USD = 0.0015;
@@ -160,7 +215,7 @@ export class GeminiAamosClient implements AamosClient {
timeoutMs: config.timeoutMs ?? 60_000, timeoutMs: config.timeoutMs ?? 60_000,
dailyBudgetUsd: config.dailyBudgetUsd ?? 0, dailyBudgetUsd: config.dailyBudgetUsd ?? 0,
promptVersion: config.promptVersion ?? "gemini-fridge-v1", promptVersion: config.promptVersion ?? "gemini-fridge-v1",
fetchImpl: config.fetchImpl ?? fetch, fetchImpl: config.fetchImpl ?? customNodeFetch,
budgetStore: config.budgetStore, budgetStore: config.budgetStore,
}; };
} }