initial commit

This commit is contained in:
Badri Narayanan S
2025-12-19 19:20:28 +05:30
parent 52d72b7bff
commit 5ae29947b1
18 changed files with 3925 additions and 494 deletions

View File

@@ -1,16 +1,43 @@
/**
* Express Server - Anthropic-compatible API
* Proxies to Google Cloud Code via Antigravity
* Supports multi-account load balancing
*/
import express from 'express';
import cors from 'cors';
import { sendMessage, sendMessageStream, listModels, clearProjectCache, getProject } from './cloudcode-client.js';
import { getToken, forceRefresh } from './token-extractor.js';
import { AVAILABLE_MODELS, REQUEST_BODY_LIMIT } from './constants.js';
import { sendMessage, sendMessageStream, listModels } from './cloudcode-client.js';
import { forceRefresh } from './token-extractor.js';
import { REQUEST_BODY_LIMIT } from './constants.js';
import { AccountManager } from './account-manager.js';
const app = express();
// Initialize account manager (will be fully initialized on first request or startup)
const accountManager = new AccountManager();
// Track initialization status
let isInitialized = false;
let initError = null;
/**
* Ensure account manager is initialized
*/
async function ensureInitialized() {
if (isInitialized) return;
try {
await accountManager.initialize();
isInitialized = true;
const status = accountManager.getStatus();
console.log(`[Server] Account pool initialized: ${status.summary}`);
} catch (error) {
initError = error;
console.error('[Server] Failed to initialize account manager:', error.message);
throw error;
}
}
// Middleware
app.use(cors());
app.use(express.json({ limit: REQUEST_BODY_LIMIT }));
@@ -27,13 +54,20 @@ function parseError(error) {
errorType = 'authentication_error';
statusCode = 401;
errorMessage = 'Authentication failed. Make sure Antigravity is running with a valid token.';
} else if (error.message.includes('429') || error.message.includes('RESOURCE_EXHAUSTED')) {
errorType = 'rate_limit_error';
statusCode = 429;
const resetMatch = error.message.match(/quota will reset after (\d+h\d+m\d+s|\d+s)/i);
errorMessage = resetMatch
? `Rate limited. Quota will reset after ${resetMatch[1]}.`
: 'Rate limited. Please wait and try again.';
} else if (error.message.includes('429') || error.message.includes('RESOURCE_EXHAUSTED') || error.message.includes('QUOTA_EXHAUSTED')) {
errorType = 'overloaded_error'; // Claude Code recognizes this type
statusCode = 529; // Use 529 for overloaded (Claude API convention)
// Try to extract the quota reset time from the error
const resetMatch = error.message.match(/quota will reset after (\d+h\d+m\d+s|\d+m\d+s|\d+s)/i);
const modelMatch = error.message.match(/"model":\s*"([^"]+)"/);
const model = modelMatch ? modelMatch[1] : 'the model';
if (resetMatch) {
errorMessage = `You have exhausted your capacity on ${model}. Quota will reset after ${resetMatch[1]}.`;
} else {
errorMessage = `You have exhausted your capacity on ${model}. Please wait for your quota to reset.`;
}
} else if (error.message.includes('invalid_request_error') || error.message.includes('INVALID_ARGUMENT')) {
errorType = 'invalid_request_error';
statusCode = 400;
@@ -63,19 +97,15 @@ app.use((req, res, next) => {
*/
app.get('/health', async (req, res) => {
try {
const token = await getToken();
let project = null;
try {
project = await getProject(token);
} catch (e) {
// Project fetch might fail if token just refreshed
}
await ensureInitialized();
const status = accountManager.getStatus();
res.json({
status: 'ok',
hasToken: !!token,
tokenPrefix: token ? token.substring(0, 10) + '...' : null,
project: project || 'unknown',
accounts: status.summary,
available: status.available,
rateLimited: status.rateLimited,
invalid: status.invalid,
timestamp: new Date().toISOString()
});
} catch (error) {
@@ -87,16 +117,55 @@ app.get('/health', async (req, res) => {
}
});
/**
* Account pool status endpoint
*/
app.get('/accounts', async (req, res) => {
try {
await ensureInitialized();
const status = accountManager.getStatus();
res.json({
total: status.total,
available: status.available,
rateLimited: status.rateLimited,
invalid: status.invalid,
accounts: status.accounts.map(a => ({
email: a.email,
source: a.source,
isRateLimited: a.isRateLimited,
rateLimitResetTime: a.rateLimitResetTime
? new Date(a.rateLimitResetTime).toISOString()
: null,
isInvalid: a.isInvalid,
invalidReason: a.invalidReason,
lastUsed: a.lastUsed
? new Date(a.lastUsed).toISOString()
: null
}))
});
} catch (error) {
res.status(500).json({
status: 'error',
error: error.message
});
}
});
/**
* Force token refresh endpoint
*/
app.post('/refresh-token', async (req, res) => {
try {
clearProjectCache();
await ensureInitialized();
// Clear all caches
accountManager.clearTokenCache();
accountManager.clearProjectCache();
// Force refresh default token
const token = await forceRefresh();
res.json({
status: 'ok',
message: 'Token refreshed successfully',
message: 'Token caches cleared and refreshed',
tokenPrefix: token.substring(0, 10) + '...'
});
} catch (error) {
@@ -119,6 +188,9 @@ app.get('/v1/models', (req, res) => {
*/
app.post('/v1/messages', async (req, res) => {
try {
// Ensure account manager is initialized
await ensureInitialized();
const {
model,
messages,
@@ -161,6 +233,15 @@ app.post('/v1/messages', async (req, res) => {
console.log(`[API] Request for model: ${request.model}, stream: ${!!stream}`);
// Debug: Log message structure to diagnose tool_use/tool_result ordering
console.log('[API] Message structure:');
messages.forEach((msg, i) => {
const contentTypes = Array.isArray(msg.content)
? msg.content.map(c => c.type || 'text').join(', ')
: (typeof msg.content === 'string' ? 'text' : 'unknown');
console.log(` [${i}] ${msg.role}: ${contentTypes}`);
});
if (stream) {
// Handle streaming response
res.setHeader('Content-Type', 'text/event-stream');
@@ -168,10 +249,15 @@ app.post('/v1/messages', async (req, res) => {
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
// Flush headers immediately to start the stream
res.flushHeaders();
try {
// Use the streaming generator
for await (const event of sendMessageStream(request)) {
// Use the streaming generator with account manager
for await (const event of sendMessageStream(request, accountManager)) {
res.write(`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`);
// Flush after each event for real-time streaming
if (res.flush) res.flush();
}
res.end();
@@ -189,7 +275,7 @@ app.post('/v1/messages', async (req, res) => {
} else {
// Handle non-streaming response
const response = await sendMessage(request);
const response = await sendMessage(request, accountManager);
res.json(response);
}
@@ -202,7 +288,8 @@ app.post('/v1/messages', async (req, res) => {
if (errorType === 'authentication_error') {
console.log('[API] Token might be expired, attempting refresh...');
try {
clearProjectCache();
accountManager.clearProjectCache();
accountManager.clearTokenCache();
await forceRefresh();
errorMessage = 'Token was expired and has been refreshed. Please retry your request.';
} catch (refreshError) {
@@ -210,13 +297,25 @@ app.post('/v1/messages', async (req, res) => {
}
}
res.status(statusCode).json({
type: 'error',
error: {
type: errorType,
message: errorMessage
}
});
console.log(`[API] Returning error response: ${statusCode} ${errorType} - ${errorMessage}`);
// Check if headers have already been sent (for streaming that failed mid-way)
if (res.headersSent) {
console.log('[API] Headers already sent, writing error as SSE event');
res.write(`event: error\ndata: ${JSON.stringify({
type: 'error',
error: { type: errorType, message: errorMessage }
})}\n\n`);
res.end();
} else {
res.status(statusCode).json({
type: 'error',
error: {
type: errorType,
message: errorMessage
}
});
}
}
});