Spaces:
Sleeping
Sleeping
File size: 11,273 Bytes
f6266b9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | /**
* OpenAI-compatible HTTP server with multi-account management.
*
* POST /v1/chat/completions β chat completions
* GET /v1/models β list models
* POST /auth/login β add account (email+password / token / api_key)
* GET /auth/accounts β list all accounts
* DELETE /auth/accounts/:id β remove account
* GET /auth/status β pool status summary
* GET /health β health check
*/
import http from 'http';
import { readFileSync, existsSync } from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import {
validateApiKey, isAuthenticated, getAccountList, getAccountCount,
addAccountByEmail, addAccountByToken, addAccountByKey, removeAccount,
} from './auth.js';
import { handleChatCompletions } from './handlers/chat.js';
import { handleMessages } from './handlers/messages.js';
import { handleModels } from './handlers/models.js';
import { handleDashboardApi } from './dashboard/api.js';
import { config, log } from './config.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(__dirname, '..');
// Cache version info at boot β git queries are slow and this never changes
// until a restart (and self-update restarts us, so always fresh).
const VERSION_INFO = (() => {
let pkgVersion = '1.2.0';
try {
const pkg = JSON.parse(readFileSync(join(REPO_ROOT, 'package.json'), 'utf-8'));
if (pkg.version) pkgVersion = pkg.version;
} catch {}
let commit = '', commitMessage = '', commitDate = '', branch = 'unknown';
if (existsSync(join(REPO_ROOT, '.git'))) {
try { commit = execSync('git rev-parse --short HEAD', { cwd: REPO_ROOT, timeout: 2000 }).toString().trim(); } catch {}
try { commitMessage = execSync('git log -1 --pretty=format:%s', { cwd: REPO_ROOT, timeout: 2000 }).toString().trim(); } catch {}
try { commitDate = execSync('git log -1 --pretty=format:%cI', { cwd: REPO_ROOT, timeout: 2000 }).toString().trim(); } catch {}
try { branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: REPO_ROOT, timeout: 2000 }).toString().trim(); } catch {}
}
return { version: pkgVersion, commit, commitMessage, commitDate, branch };
})();
function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on('data', c => chunks.push(c));
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
req.on('error', reject);
});
}
function extractToken(req) {
// Anthropic SDK + OAI SDK compatibility: accept either header.
const authHeader = req.headers['authorization'] || '';
if (authHeader.startsWith('Bearer ')) return authHeader.slice(7);
if (authHeader) return authHeader;
const xApiKey = req.headers['x-api-key'] || '';
return xApiKey;
}
function json(res, status, body) {
const data = JSON.stringify(body);
res.writeHead(status, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
});
res.end(data);
}
async function route(req, res) {
const { method } = req;
const path = req.url.split('?')[0];
if (method === 'OPTIONS') return json(res, 204, '');
if (path === '/health') {
const counts = getAccountCount();
return json(res, 200, {
status: 'ok',
provider: 'WindsurfAPI bydwgx1337',
version: VERSION_INFO.version,
commit: VERSION_INFO.commit,
commitMessage: VERSION_INFO.commitMessage,
commitDate: VERSION_INFO.commitDate,
branch: VERSION_INFO.branch,
uptime: Math.round(process.uptime()),
accounts: counts,
});
}
// βββ Dashboard βββββββββββββββββββββββββββββββββββββββββ
// Silent 204 for favicon β browsers request it from every page; otherwise
// the later Bearer-token check produces noise in the dashboard console.
if (path === '/favicon.ico') {
res.writeHead(204);
return res.end();
}
if (path === '/dashboard' || path === '/dashboard/') {
try {
const html = readFileSync(join(__dirname, 'dashboard', 'index.html'));
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
return res.end(html);
} catch {
return json(res, 500, { error: 'Dashboard not found' });
}
}
if (path.startsWith('/dashboard/api/')) {
let body = {};
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
try { body = JSON.parse(await readBody(req)); } catch {}
}
const subpath = path.slice('/dashboard/api'.length);
return handleDashboardApi(method, subpath, body, req, res);
}
// βββ Auth management (no API key required) βββββββββββββ
if (path === '/auth/status') {
return json(res, 200, { authenticated: isAuthenticated(), ...getAccountCount() });
}
if (path === '/auth/accounts' && method === 'GET') {
return json(res, 200, { accounts: getAccountList() });
}
// DELETE /auth/accounts/:id
if (path.startsWith('/auth/accounts/') && method === 'DELETE') {
const id = path.split('/')[3];
const ok = removeAccount(id);
return json(res, ok ? 200 : 404, { success: ok });
}
if (path === '/auth/login' && method === 'POST') {
let body;
try { body = JSON.parse(await readBody(req)); } catch {
return json(res, 400, { error: 'Invalid JSON' });
}
try {
// Support batch: { accounts: [{email,password}, ...] }
if (Array.isArray(body.accounts)) {
const results = [];
for (const acct of body.accounts) {
try {
let result;
if (acct.api_key) {
result = addAccountByKey(acct.api_key, acct.label);
} else if (acct.token) {
result = await addAccountByToken(acct.token, acct.label);
} else if (acct.email && acct.password) {
result = await addAccountByEmail(acct.email, acct.password);
} else {
results.push({ error: 'Missing credentials' });
continue;
}
results.push({ id: result.id, email: result.email, status: result.status });
} catch (err) {
results.push({ email: acct.email, error: err.message });
}
}
return json(res, 200, { results, ...getAccountCount() });
}
// Single account
let account;
if (body.api_key) {
account = addAccountByKey(body.api_key, body.label);
} else if (body.token) {
account = await addAccountByToken(body.token, body.label);
} else if (body.email && body.password) {
account = await addAccountByEmail(body.email, body.password);
} else {
return json(res, 400, { error: 'Provide api_key, token, or email+password' });
}
return json(res, 200, {
success: true,
account: { id: account.id, email: account.email, method: account.method, status: account.status },
...getAccountCount(),
});
} catch (err) {
log.error('Login failed:', err.message);
return json(res, 401, { error: err.message });
}
}
// βββ API endpoints (require API key) ββββββββββββββββββββ
if (!validateApiKey(extractToken(req))) {
return json(res, 401, { error: { message: 'Invalid API key', type: 'auth_error' } });
}
if (path === '/v1/models' && method === 'GET') {
return json(res, 200, handleModels());
}
if (path === '/v1/chat/completions' && method === 'POST') {
if (!isAuthenticated()) {
return json(res, 503, {
error: { message: 'No active accounts. POST /auth/login to add accounts.', type: 'auth_error' },
});
}
let body;
try { body = JSON.parse(await readBody(req)); } catch {
return json(res, 400, { error: { message: 'Invalid JSON', type: 'invalid_request' } });
}
if (!Array.isArray(body.messages)) {
return json(res, 400, { error: { message: 'messages must be an array', type: 'invalid_request' } });
}
if (body.messages.length === 0) {
return json(res, 400, { error: { message: 'messages must contain at least 1 item', type: 'invalid_request' } });
}
const result = await handleChatCompletions(body);
if (result.stream) {
res.writeHead(result.status, { 'Access-Control-Allow-Origin': '*', ...result.headers });
await result.handler(res);
} else {
json(res, result.status, result.body);
}
return;
}
// Anthropic Messages API β Claude Code compatibility
if (path === '/v1/messages' && method === 'POST') {
if (!isAuthenticated()) {
return json(res, 503, { type: 'error', error: { type: 'api_error', message: 'No active accounts' } });
}
let body;
try { body = JSON.parse(await readBody(req)); } catch {
return json(res, 400, { type: 'error', error: { type: 'invalid_request_error', message: 'Invalid JSON' } });
}
if (!Array.isArray(body.messages) || body.messages.length === 0) {
return json(res, 400, { type: 'error', error: { type: 'invalid_request_error', message: 'messages must be a non-empty array' } });
}
const result = await handleMessages(body);
if (result.stream) {
res.writeHead(result.status, { 'Access-Control-Allow-Origin': '*', ...result.headers });
await result.handler(res);
} else {
json(res, result.status, result.body);
}
return;
}
json(res, 404, { error: { message: `${method} ${path} not found`, type: 'not_found' } });
}
export function startServer() {
const activeRequests = new Set();
const server = http.createServer(async (req, res) => {
activeRequests.add(res);
res.on('close', () => activeRequests.delete(res));
try {
await route(req, res);
} catch (err) {
log.error('Handler error:', err);
if (!res.headersSent) json(res, 500, { error: { message: 'Internal error', type: 'server_error' } });
}
});
server.keepAliveTimeout = 65_000;
server.headersTimeout = 66_000;
let retryCount = 0;
const maxRetries = 10;
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
retryCount++;
if (retryCount > maxRetries) {
log.error(`Port ${config.port} still in use after ${maxRetries} retries. Exiting.`);
process.exit(1);
}
log.warn(`Port ${config.port} in use, retry ${retryCount}/${maxRetries} in 3s...`);
setTimeout(() => server.listen(config.port, '0.0.0.0'), 3000);
} else {
log.error('Server error:', err);
}
});
server.getActiveRequests = () => activeRequests.size;
server.listen({ port: config.port, host: '0.0.0.0' }, () => {
log.info(`Server on http://0.0.0.0:${config.port}`);
log.info(' POST /v1/chat/completions');
log.info(' GET /v1/models');
log.info(' POST /auth/login (add account)');
log.info(' GET /auth/accounts (list accounts)');
log.info(' DELETE /auth/accounts/:id (remove account)');
});
return server;
}
|