/** * @fileoverview This file sets up and runs the HTTP server that acts as a * proxy between an OpenAI-compatible client and the Gemini API. */ import consola from 'consola'; import http from 'http'; import { listModels, sendChat, sendChatStream } from './chatwrapper'; import { mapRequest, mapResponse, mapStreamChunk } from './mapper.js'; import { RequestBody, GeminiResponse, GeminiStreamChunk, Part } from './types'; import { config } from './config'; import { isAuthorized } from './auth'; // ================================================================== // Server Configuration // ================================================================== const PORT = config.PORT; const VERBOSE = config.VERBOSE; // ================================================================== // Logger Setup // ================================================================== if (VERBOSE) { consola.level = 5; consola.info('Verbose logging enabled'); } consola.info('Google CLI OpenAI proxy'); // ================================================================== // HTTP Server Helpers // ================================================================== /** * Sets CORS headers to allow cross-origin requests. * @param res - The HTTP server response object. */ 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'); } /** * Reads and parses a JSON request body. * @param req - The HTTP incoming message object. * @param res - The HTTP server response object. * @returns A promise that resolves to the parsed request body * or null if invalid. */ 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' }, }), ); resolve(null); return; } resolve(null); return; } try { resolve(JSON.parse(data) as RequestBody); } catch { // Handle malformed JSON. res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: 'Malformed JSON' } })); resolve(null); return; } }); }); } // ================================================================== // Main Server Logic // ================================================================== 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}`); // Handle pre-flight CORS requests. if (req.method === 'OPTIONS') { res.writeHead(204).end(); return; } if (!isAuthorized(req, res)) { return; } // Route for listing available models. if (pathname === '/v1/models' || pathname === '/models') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ data: listModels(), }), ); return; } // Route for 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 })) { // Transform the chunk to match the expected stream format. const transformedParts = chunk.candidates?.[0]?.content?.parts?.map((part) => { const transformedPart: Part = { text: part.text, thought: part.text?.startsWith?.('') ?? false, }; if (part.inlineData?.data) { transformedPart.inlineData = { mimeType: part.inlineData.mimeType ?? 'text/plain', data: part.inlineData.data, }; } return transformedPart; }) ?? []; const streamChunk: GeminiStreamChunk = { candidates: [ { content: { parts: transformedParts, }, }, ], }; res.write( `data: ${JSON.stringify(mapStreamChunk(streamChunk))}\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, body))); } } catch (err) { const error = err as Error; consola.error('Proxy error ➜', error); // Handle errors, sending them in the appropriate format for streaming // or non-streaming responses. if (body.stream && res.headersSent) { res.write( `data: ${JSON.stringify({ error: { message: error.message, type: 'error', }, })}\n\n`, ); res.end('data: [DONE]\n\n'); return; } else { res.writeHead(500, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: { message: error.message } })); } } } }) .listen(PORT, () => { consola.info(`Listening on port :${PORT}`); });