import consola from 'consola'; import http from 'http'; import { listModels, sendChat, sendChatStream } from './chatwrapper'; import { mapRequest, mapResponse, mapStreamChunk } from './mapper.js'; import { RequestBody, GeminiResponse } from './types'; import { config } from './config'; /* ── basic config ─────────────────────────────────────────────────── */ const PORT = config.PORT; const VERBOSE = config.VERBOSE; /* ── Consola setup ────────────────────────────────────────────────── */ if (VERBOSE) { consola.level = 5; consola.info('Verbose logging enabled'); } consola.info('Google CLI OpenAI proxy'); /* ── CORS helper ──────────────────────────────────────────────────── */ function allowCors(res: http.ServerResponse) { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Headers', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); } /* ── JSON body helper ─────────────────────────────────────────────── */ function readJSON( req: http.IncomingMessage, res: http.ServerResponse, ): Promise { return new Promise((resolve) => { let data = ''; req.on('data', (c) => (data += c)); req.on('end', () => { if (!data) { if (req.method === 'POST') { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ error: { message: 'Request body is missing for POST request' }, }), ); } return resolve(null); } try { resolve(JSON.parse(data) as RequestBody); } catch { // malformed JSON res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Malformed JSON' } })); resolve(null); } }); }); } /* ── server ───────────────────────────────────────────────────────── */ http .createServer(async (req, res) => { allowCors(res); const url = new URL(req.url ?? '/', `http://${req.headers.host}`); const pathname = url.pathname.replace(/\/$/, '') || '/'; consola.info(`${req.method} ${url.pathname}`); /* -------- pre-flight ---------- */ if (req.method === 'OPTIONS') { res.writeHead(204).end(); return; } /* -------- /v1/models ---------- */ if (pathname === '/v1/models' || pathname === '/models') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ data: listModels(), }), ); return; } /* ---- /v1/chat/completions ---- */ if ( (pathname === '/chat/completions' || pathname === '/v1/chat/completions') && req.method === 'POST' ) { const body = await readJSON(req, res); if (!body) return; try { const { geminiReq, tools } = await mapRequest(body); if (body.stream) { res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); for await (const chunk of sendChatStream({ ...geminiReq, tools })) { res.write(`data: ${JSON.stringify(mapStreamChunk(chunk))}\n\n`); } res.end('data: [DONE]\n\n'); } else { const gResp: GeminiResponse = await sendChat({ ...geminiReq, tools }); res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(mapResponse(gResp))); } } catch (err) { const error = err as Error; consola.error('Proxy error ➜', error); res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); } return; } /* ---- anything else ---------- */ res.writeHead(404).end(); }) .listen(PORT, () => { consola.info(`Listening on port :${PORT}`); });