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:
195
src/server.js
195
src/server.js
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user