forked from jenslys/opencode-gemini-auth
-
Notifications
You must be signed in to change notification settings - Fork 22
Open
Description
We can't easily check the upstream rate limits ourselves, however we can with a running Antigravity instance (Or at least the language server?)
This would allow us to potentially add something to /status showing the rate limit status.
Here's an example script showing rate limit status if you have a running Antigravity instance, based on https://github.com/Zendevve/antigravity-usage
#!/usr/bin/env ts-node
/**
* Standalone CLI Quota Checker
* Queries a running AntiGravity language server for quota information
* No GUI required - works with headless AntiGravity instances
*/
import { exec } from 'child_process';
import { promisify } from 'util';
import * as https from 'https';
import * as os from 'os';
const execAsync = promisify(exec);
interface ProcessInfo {
pid: number;
extensionPort: number | null;
csrfToken: string;
}
function getProcessName(): string {
const platform = os.platform();
if (platform === 'linux') return 'language_server_linux_x64';
if (platform === 'darwin') return 'language_server_macos_arm';
if (platform === 'win32') return 'language_server_windows_x64.exe';
throw new Error(`Unsupported platform: ${platform}`);
}
function getProcessListCommand(processName: string): string {
const platform = os.platform();
if (platform === 'linux' || platform === 'darwin') {
return `pgrep -fa ${processName}`;
}
if (platform === 'win32') {
return `wmic process where "name='${processName}'" get ProcessId,CommandLine /format:list`;
}
throw new Error(`Unsupported platform: ${platform}`);
}
function parseProcessInfo(output: string): ProcessInfo | null {
const platform = os.platform();
if (platform === 'linux' || platform === 'darwin') {
// Format: "PID /path/to/language_server --api_server_port 12345 --csrf_token abc123..."
const lines = output.trim().split('\n');
for (const line of lines) {
const match = line.match(/^(\d+)\s+.*--csrf_token\s+([a-f0-9-]+)/i);
if (match) {
const pid = parseInt(match[1], 10);
const csrfToken = match[2];
const portMatch = line.match(/--api_server_port\s+(\d+)/);
const extensionPort = portMatch ? parseInt(portMatch[1], 10) : null;
return { pid, extensionPort, csrfToken };
}
}
} else if (platform === 'win32') {
// Windows format from wmic
const pidMatch = output.match(/ProcessId=(\d+)/);
const csrfMatch = output.match(/--csrf_token\s+([a-f0-9-]+)/i);
const portMatch = output.match(/--api_server_port\s+(\d+)/);
if (pidMatch && csrfMatch) {
return {
pid: parseInt(pidMatch[1], 10),
extensionPort: portMatch ? parseInt(portMatch[1], 10) : null,
csrfToken: csrfMatch[1]
};
}
}
return null;
}
function getPortListCommand(pid: number): string {
const platform = os.platform();
if (platform === 'linux') {
return `ss -tlnp 2>/dev/null | grep "pid=${pid},"`;
}
if (platform === 'darwin') {
return `lsof -nP -iTCP -sTCP:LISTEN -a -p ${pid}`;
}
if (platform === 'win32') {
return `netstat -ano | findstr ${pid}`;
}
throw new Error(`Unsupported platform: ${platform}`);
}
function parseListeningPorts(output: string): number[] {
const platform = os.platform();
const ports: number[] = [];
if (platform === 'linux') {
// ss format: "LISTEN 0 128 127.0.0.1:12345 0.0.0.0:*"
const regex = /127\.0\.0\.1:(\d+)/g;
let match;
while ((match = regex.exec(output)) !== null) {
ports.push(parseInt(match[1], 10));
}
} else if (platform === 'darwin') {
// lsof format: "language 12345 user 3u IPv4 0x... 0t0 TCP 127.0.0.1:12345 (LISTEN)"
const regex = /127\.0\.0\.1:(\d+)/g;
let match;
while ((match = regex.exec(output)) !== null) {
ports.push(parseInt(match[1], 10));
}
} else if (platform === 'win32') {
// netstat format: "TCP 127.0.0.1:12345 0.0.0.0:0 LISTENING 12345"
const regex = /127\.0\.0\.1:(\d+).*LISTENING/g;
let match;
while ((match = regex.exec(output)) !== null) {
ports.push(parseInt(match[1], 10));
}
}
return [...new Set(ports)];
}
interface QuotaData {
model: string;
remainingPercent: number;
usedPercent: number;
resetTime?: string;
isExhausted: boolean;
}
async function main() {
try {
const processName = getProcessName();
const { stdout } = await execAsync(getProcessListCommand(processName), { timeout: 15000 });
const processInfo = parseProcessInfo(stdout);
if (!processInfo) {
console.error('❌ AntiGravity language server not found. Make sure AntiGravity is running.\n');
process.exit(1);
}
const { pid, extensionPort, csrfToken } = processInfo;
const { stdout: portOutput } = await execAsync(getPortListCommand(pid), { timeout: 5000 });
const ports = parseListeningPorts(portOutput);
if (ports.length === 0) {
console.error('❌ No listening ports found.\n');
process.exit(1);
}
const portsToTest = extensionPort && ports.includes(extensionPort)
? [extensionPort, ...ports.filter(p => p !== extensionPort)]
: ports;
let quotaData: QuotaData[] | null = null;
let lastError: Error | null = null;
for (const port of portsToTest) {
try {
quotaData = await fetchQuotaData(port, csrfToken);
break;
} catch (error: any) {
lastError = error;
}
}
if (!quotaData) {
throw lastError || new Error('Failed to fetch quota data from any port');
}
displayQuotaData(quotaData);
} catch (error: any) {
console.error(`❌ ${error.message}\n`);
process.exit(1);
}
}
async function fetchQuotaData(port: number, csrfToken: string): Promise<QuotaData[]> {
return new Promise((resolve, reject) => {
const requestBody = JSON.stringify({
metadata: {
ideName: 'antigravity',
extensionName: 'antigravity',
locale: 'en'
}
});
const options: https.RequestOptions = {
hostname: '127.0.0.1',
port: port,
path: '/exa.language_server_pb.LanguageServerService/GetUserStatus',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody),
'Connect-Protocol-Version': '1',
'X-Codeium-Csrf-Token': csrfToken
},
rejectUnauthorized: false,
timeout: 5000
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
if (res.statusCode !== 200) {
reject(new Error(`HTTP ${res.statusCode}: ${data.substring(0, 100)}`));
return;
}
try {
const parsed = JSON.parse(data);
const quotas = parseQuotaResponse(parsed);
resolve(quotas);
} catch (e) {
reject(new Error(`JSON parse error: ${data.substring(0, 100)}`));
}
});
});
req.on('error', (error) => reject(error));
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
req.write(requestBody);
req.end();
});
}
function parseQuotaResponse(data: any): QuotaData[] {
const userStatus = data.userStatus || data;
const cascadeData = userStatus.cascadeModelConfigData;
const rawModels = cascadeData?.clientModelConfigs || [];
const quotas: QuotaData[] = [];
for (const model of rawModels) {
if (!model.quotaInfo) {
continue;
}
const label = model.label || 'Unknown Model';
const remainingFraction = model.quotaInfo.remainingFraction ?? 1;
const remainingPercent = Math.round(remainingFraction * 100);
const usedPercent = 100 - remainingPercent;
const isExhausted = remainingFraction === 0;
let resetTime: string | undefined;
if (model.quotaInfo.resetTime) {
const resetDate = new Date(model.quotaInfo.resetTime);
const diff = resetDate.getTime() - Date.now();
resetTime = formatTime(diff);
}
quotas.push({
model: label,
remainingPercent,
usedPercent,
resetTime,
isExhausted
});
}
return quotas;
}
function formatTime(ms: number): string {
if (ms <= 0) return 'Ready';
const mins = Math.ceil(ms / 60000);
return mins < 60 ? `${mins}m` : `${Math.floor(mins / 60)}h ${mins % 60}m`;
}
function displayQuotaData(quotas: QuotaData[]) {
if (quotas.length === 0) {
console.log('⚠️ No quota data available.\n');
return;
}
console.log('\n─── Quota Status ───\n');
for (const quota of quotas) {
const status = quota.isExhausted ? '🔴 EXHAUSTED' :
quota.remainingPercent < 15 ? '🔴 CRITICAL' :
quota.remainingPercent < 25 ? '🟡 LOW' :
'🟢 HEALTHY';
console.log(`${status} ${quota.model}`);
console.log(` ${quota.remainingPercent}% remaining | ${quota.usedPercent}% used${quota.resetTime ? ' | Resets: ' + quota.resetTime : ''}`);
console.log('');
}
}
if (require.main === module) {
main().catch(console.error);
}Upstream doesn't currently support a nice status display, but I believe there is a PR for it. I'll try do a preliminary PR tomorrow
Metadata
Metadata
Assignees
Labels
No labels