mirror of
https://github.com/larsbaunwall/vscode-copilot-bridge.git
synced 2025-10-05 22:22:59 +00:00
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import polka from 'polka';
|
|
import type { Server, IncomingMessage, ServerResponse } from 'http';
|
|
import { getBridgeConfig } from '../config';
|
|
import { state } from '../state';
|
|
import { isAuthorized } from './auth';
|
|
import { handleHealthCheck } from './routes/health';
|
|
import { handleModelsRequest } from './routes/models';
|
|
import { handleChatCompletion } from './routes/chat';
|
|
import { writeErrorResponse, writeNotFound, writeRateLimit, writeUnauthorized } from './utils';
|
|
import { ensureOutput, verbose } from '../log';
|
|
import { updateStatus } from '../status';
|
|
|
|
export const startServer = async (): Promise<void> => {
|
|
if (state.server) return;
|
|
const config = getBridgeConfig();
|
|
ensureOutput();
|
|
|
|
const app = polka({
|
|
onError: (err, req, res) => {
|
|
const msg = err instanceof Error ? err.message : String(err);
|
|
verbose(`HTTP error: ${msg}`);
|
|
if (!res.headersSent) {
|
|
writeErrorResponse(res, 500, msg || 'internal_error', 'server_error', 'internal_error');
|
|
} else {
|
|
try { res.end(); } catch {/* ignore */}
|
|
}
|
|
},
|
|
onNoMatch: (_req, res) => {
|
|
writeNotFound(res);
|
|
},
|
|
});
|
|
|
|
// Auth middleware - runs before all routes (except /health)
|
|
app.use((req, res, next) => {
|
|
const path = req.url ?? '/';
|
|
if (path === '/health') {
|
|
return next();
|
|
}
|
|
if (!isAuthorized(req, config.token)) {
|
|
writeUnauthorized(res);
|
|
return;
|
|
}
|
|
next();
|
|
});
|
|
|
|
// Verbose logging middleware
|
|
const cfg = getBridgeConfig();
|
|
if (cfg.verbose) {
|
|
app.use((req, res, next) => {
|
|
verbose(`${req.method} ${req.url}`);
|
|
next();
|
|
});
|
|
}
|
|
|
|
app.get('/health', async (_req: IncomingMessage, res: ServerResponse) => {
|
|
await handleHealthCheck(res, config.verbose);
|
|
});
|
|
|
|
app.get('/v1/models', async (_req: IncomingMessage, res: ServerResponse) => {
|
|
await handleModelsRequest(res);
|
|
});
|
|
|
|
app.post('/v1/chat/completions', async (req: IncomingMessage, res: ServerResponse) => {
|
|
// Rate limiting check
|
|
if (state.activeRequests >= config.maxConcurrent) {
|
|
if (config.verbose) {
|
|
verbose(`429 throttled (active=${state.activeRequests}, max=${config.maxConcurrent})`);
|
|
}
|
|
writeRateLimit(res);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await handleChatCompletion(req, res);
|
|
} catch (e) {
|
|
const msg = e instanceof Error ? e.message : String(e);
|
|
writeErrorResponse(res, 500, msg || 'internal_error', 'server_error', 'internal_error');
|
|
}
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
try {
|
|
app.listen(config.port, config.host, () => {
|
|
const srv = app.server as Server | undefined;
|
|
if (!srv) return reject(new Error('Server failed to start'));
|
|
state.server = srv;
|
|
updateStatus('start');
|
|
resolve();
|
|
});
|
|
const srv = app.server as Server | undefined;
|
|
srv?.on('error', reject);
|
|
} catch (err) {
|
|
reject(err);
|
|
}
|
|
});
|
|
};
|
|
|
|
export const stopServer = async (): Promise<void> => {
|
|
await new Promise<void>((resolve) => {
|
|
if (!state.server) return resolve();
|
|
state.server.close(() => resolve());
|
|
});
|
|
state.server = undefined;
|
|
};
|