feat: consolidate /accounts into /account-limits endpoint

- Remove redundant /accounts endpoint
- Enhance /account-limits table output with account status, last used time, and quota reset time
- Filter model list to show only Claude models
- Use local time format for timestamps
- Update documentation (README.md, CLAUDE.md, index.js)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Badri Narayanan S
2025-12-25 21:55:03 +05:30
parent ed6bd3af2b
commit 9b0b756e72
6 changed files with 255 additions and 27 deletions

View File

@@ -6,7 +6,7 @@
import express from 'express';
import cors from 'cors';
import { sendMessage, sendMessageStream, listModels } from './cloudcode-client.js';
import { sendMessage, sendMessageStream, listModels, getModelQuotas } from './cloudcode-client.js';
import { forceRefresh } from './token-extractor.js';
import { REQUEST_BODY_LIMIT } from './constants.js';
import { AccountManager } from './account-manager.js';
@@ -127,30 +127,185 @@ app.get('/health', async (req, res) => {
});
/**
* Account pool status endpoint
* Account limits endpoint - fetch quota/limits for all accounts × all models
* Returns a table showing remaining quota and reset time for each combination
* Use ?format=table for ASCII table output, default is JSON
*/
app.get('/accounts', async (req, res) => {
app.get('/account-limits', async (req, res) => {
try {
await ensureInitialized();
const status = accountManager.getStatus();
const allAccounts = accountManager.getAllAccounts();
const format = req.query.format || 'json';
// Fetch quotas for each account in parallel
const results = await Promise.allSettled(
allAccounts.map(async (account) => {
// Skip invalid accounts
if (account.isInvalid) {
return {
email: account.email,
status: 'invalid',
error: account.invalidReason,
models: {}
};
}
try {
const token = await accountManager.getTokenForAccount(account);
const quotas = await getModelQuotas(token);
return {
email: account.email,
status: 'ok',
models: quotas
};
} catch (error) {
return {
email: account.email,
status: 'error',
error: error.message,
models: {}
};
}
})
);
// Process results
const accountLimits = results.map((result, index) => {
if (result.status === 'fulfilled') {
return result.value;
} else {
return {
email: allAccounts[index].email,
status: 'error',
error: result.reason?.message || 'Unknown error',
models: {}
};
}
});
// Collect all unique model IDs
const allModelIds = new Set();
for (const account of accountLimits) {
for (const modelId of Object.keys(account.models || {})) {
allModelIds.add(modelId);
}
}
const sortedModels = Array.from(allModelIds).filter(m => m.includes('claude')).sort();
// Return ASCII table format
if (format === 'table') {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
// Build table
const lines = [];
const timestamp = new Date().toLocaleString();
lines.push(`Account Limits (${timestamp})`);
// Get account status info
const status = accountManager.getStatus();
lines.push(`Accounts: ${status.total} total, ${status.available} available, ${status.rateLimited} rate-limited, ${status.invalid} invalid`);
lines.push('');
// Table 1: Account status
const accColWidth = 25;
const statusColWidth = 15;
const lastUsedColWidth = 25;
const resetColWidth = 25;
let accHeader = 'Account'.padEnd(accColWidth) + 'Status'.padEnd(statusColWidth) + 'Last Used'.padEnd(lastUsedColWidth) + 'Quota Reset';
lines.push(accHeader);
lines.push('─'.repeat(accColWidth + statusColWidth + lastUsedColWidth + resetColWidth));
for (const acc of status.accounts) {
const shortEmail = acc.email.split('@')[0].slice(0, 22);
const lastUsed = acc.lastUsed ? new Date(acc.lastUsed).toLocaleString() : 'never';
// Get status and error from accountLimits
const accLimit = accountLimits.find(a => a.email === acc.email);
const accStatus = acc.isInvalid ? 'invalid' : (acc.isRateLimited ? 'rate-limited' : (accLimit?.status || 'ok'));
// Get reset time from quota API
const claudeModel = sortedModels.find(m => m.includes('claude'));
const quota = claudeModel && accLimit?.models?.[claudeModel];
const resetTime = quota?.resetTime
? new Date(quota.resetTime).toLocaleString()
: '-';
let row = shortEmail.padEnd(accColWidth) + accStatus.padEnd(statusColWidth) + lastUsed.padEnd(lastUsedColWidth) + resetTime;
// Add error on next line if present
if (accLimit?.error) {
lines.push(row);
lines.push(' └─ ' + accLimit.error);
} else {
lines.push(row);
}
}
lines.push('');
// Calculate column widths
const modelColWidth = Math.max(25, ...sortedModels.map(m => m.length)) + 2;
const accountColWidth = 22;
// Header row
let header = 'Model'.padEnd(modelColWidth);
for (const acc of accountLimits) {
const shortEmail = acc.email.split('@')[0].slice(0, 18);
header += shortEmail.padEnd(accountColWidth);
}
lines.push(header);
lines.push('─'.repeat(modelColWidth + accountLimits.length * accountColWidth));
// Data rows
for (const modelId of sortedModels) {
let row = modelId.padEnd(modelColWidth);
for (const acc of accountLimits) {
const quota = acc.models?.[modelId];
let cell;
if (acc.status !== 'ok') {
cell = `[${acc.status}]`;
} else if (!quota) {
cell = '-';
} else if (quota.remainingFraction === null) {
cell = '0% (exhausted)';
} else {
const pct = Math.round(quota.remainingFraction * 100);
cell = `${pct}%`;
}
row += cell.padEnd(accountColWidth);
}
lines.push(row);
}
return res.send(lines.join('\n'));
}
// Default: JSON format
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
timestamp: new Date().toLocaleString(),
totalAccounts: allAccounts.length,
models: sortedModels,
accounts: accountLimits.map(acc => ({
email: acc.email,
status: acc.status,
error: acc.error || null,
limits: Object.fromEntries(
sortedModels.map(modelId => {
const quota = acc.models?.[modelId];
if (!quota) {
return [modelId, null];
}
return [modelId, {
remaining: quota.remainingFraction !== null
? `${Math.round(quota.remainingFraction * 100)}%`
: 'N/A',
remainingFraction: quota.remainingFraction,
resetTime: quota.resetTime || null
}];
})
)
}))
});
} catch (error) {