diff --git a/packages/ai-contracts/src/gemini.ts b/packages/ai-contracts/src/gemini.ts index e10a2e5..0e0f9ad 100644 --- a/packages/ai-contracts/src/gemini.ts +++ b/packages/ai-contracts/src/gemini.ts @@ -23,6 +23,61 @@ import type { LocaleContext } from "@app/shared-types"; 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 { + 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 = {}; + 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); + } + } + 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). */ const COST_ESTIMATE_PER_IMAGE_USD = 0.0015; @@ -160,7 +215,7 @@ export class GeminiAamosClient implements AamosClient { timeoutMs: config.timeoutMs ?? 60_000, dailyBudgetUsd: config.dailyBudgetUsd ?? 0, promptVersion: config.promptVersion ?? "gemini-fridge-v1", - fetchImpl: config.fetchImpl ?? fetch, + fetchImpl: config.fetchImpl ?? customNodeFetch, budgetStore: config.budgetStore, }; }