127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
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<RequestBody | null> {
|
|
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}`);
|
|
});
|