fix(quota,error-logs): match CLIProxyAPI headers and enhance error log display

Quota fetcher:
- loadCodeAssist headers now match antigravity.go exactly
- fetchAvailableModels uses empty body {} and correct User-Agent
- Prevents accounts being flagged for anomalous requests

Error logs:
- Extract status code from end of log (RESPONSE section)
- Display full model names instead of abbreviated
- Show status badges (500/429/4xx) with color coding
- Add provider icons with white background
This commit is contained in:
kaitranntt committed 2025-12-29 15:48:37 -05:00
1 parent 00597b3358
commit ac6f382f6a
9 files changed
+266 -171

No files matched your search

+19 -10
View File
@@ -54,11 +54,19 @@ const ANTIGRAVITY_CLIENT_ID =
'1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com'; '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
const ANTIGRAVITY_CLIENT_SECRET = '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'; const ANTIGRAVITY_CLIENT_SECRET = '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';
/** API client headers */ /** Headers for loadCodeAssist (matches CLIProxyAPI antigravity.go) */
const ANTIGRAVITY_HEADERS = { const LOADCODEASSIST_HEADERS = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'User-Agent': 'antigravity/1.11.5 linux/amd64', 'User-Agent': 'google-api-nodejs-client/9.15.1',
'X-Goog-Api-Client': 'gl-node/20.9.0', 'X-Goog-Api-Client': 'google-cloud-sdk vscode_cloudshelleditor/0.1',
'Client-Metadata':
'{"ideType":"IDE_UNSPECIFIED","platform":"PLATFORM_UNSPECIFIED","pluginType":"GEMINI"}',
};
/** Headers for fetchAvailableModels (matches CLIProxyAPI antigravity_executor.go) */
const FETCHMODELS_HEADERS = {
'Content-Type': 'application/json',
'User-Agent': 'antigravity/1.104.0 darwin/arm64',
}; };
/** Auth file structure */ /** Auth file structure */
@@ -270,7 +278,7 @@ async function getProjectId(
method: 'POST', method: 'POST',
signal: controller.signal, signal: controller.signal,
headers: { headers: {
...ANTIGRAVITY_HEADERS, ...LOADCODEASSIST_HEADERS,
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}, },
body: JSON.stringify({ body: JSON.stringify({
@@ -326,24 +334,25 @@ async function getProjectId(
/** /**
* Fetch available models with quota info * Fetch available models with quota info
* Note: projectId is kept for potential future use but not sent in body
* (CLIProxyAPI sends empty {} body for this endpoint)
*/ */
async function fetchAvailableModels(accessToken: string, projectId: string): Promise<QuotaResult> { async function fetchAvailableModels(accessToken: string, _projectId: string): Promise<QuotaResult> {
const url = `${ANTIGRAVITY_API_BASE}/${ANTIGRAVITY_API_VERSION}:fetchAvailableModels`; const url = `${ANTIGRAVITY_API_BASE}/${ANTIGRAVITY_API_VERSION}:fetchAvailableModels`;
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); const timeoutId = setTimeout(() => controller.abort(), 5000);
try { try {
// Match CLIProxyAPI exactly: empty body, minimal headers
const response = await fetch(url, { const response = await fetch(url, {
method: 'POST', method: 'POST',
signal: controller.signal, signal: controller.signal,
headers: { headers: {
...ANTIGRAVITY_HEADERS, ...FETCHMODELS_HEADERS,
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
}, },
body: JSON.stringify({ body: JSON.stringify({}),
project: projectId,
}),
}); });
clearTimeout(timeoutId); clearTimeout(timeoutId);
+4
View File
@@ -297,6 +297,10 @@ export interface CliproxyErrorLog {
modified: number; modified: number;
/** Absolute path to the log file (injected by backend) */ /** Absolute path to the log file (injected by backend) */
absolutePath?: string; absolutePath?: string;
/** HTTP status code extracted from log (injected by backend) */
statusCode?: number;
/** Model name extracted from request body (injected by backend) */
model?: string;
} }
/** Response from /v0/management/request-error-logs endpoint */ /** Response from /v0/management/request-error-logs endpoint */
+56 -6
View File
@@ -25,6 +25,48 @@ import { checkCliproxyUpdate } from '../../cliproxy/binary-manager';
const router = Router(); const router = Router();
/**
* Extract status code and model from error log file (lightweight parsing)
* Reads first 4KB for model, last 2KB for status code
*/
async function extractErrorLogMetadata(
filePath: string
): Promise<{ statusCode?: number; model?: string }> {
try {
const fd = fs.openSync(filePath, 'r');
const stat = fs.fstatSync(fd);
const fileSize = stat.size;
// Read first 4KB for model (in request body)
const startBuffer = Buffer.alloc(Math.min(4096, fileSize));
fs.readSync(fd, startBuffer, 0, startBuffer.length, 0);
const startContent = startBuffer.toString('utf-8');
// Extract model from request body JSON: "model":"gemini-3-flash-preview"
const modelMatch = startContent.match(/"model"\s*:\s*"([^"]+)"/);
const model = modelMatch ? modelMatch[1] : undefined;
// Read last 2KB for status code (in response section at end)
let statusCode: number | undefined;
if (fileSize > 2048) {
const endBuffer = Buffer.alloc(2048);
fs.readSync(fd, endBuffer, 0, 2048, fileSize - 2048);
const endContent = endBuffer.toString('utf-8');
const statusMatch = endContent.match(/Status:\s*(\d{3})/);
statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined;
} else {
// Small file - check start content for status
const statusMatch = startContent.match(/Status:\s*(\d{3})/);
statusCode = statusMatch ? parseInt(statusMatch[1], 10) : undefined;
}
fs.closeSync(fd);
return { statusCode, model };
} catch {
return {};
}
}
/** /**
* Shared handler for stats/usage endpoint * Shared handler for stats/usage endpoint
*/ */
@@ -214,14 +256,22 @@ router.get('/error-logs', async (_req: Request, res: Response): Promise<void> =>
return; return;
} }
// Inject absolute paths into each file entry // Inject absolute paths and extract metadata from each file
const logsDir = path.join(getCliproxyWritablePath(), 'logs'); const logsDir = path.join(getCliproxyWritablePath(), 'logs');
const filesWithPaths = files.map((file) => ({ const filesWithMetadata = await Promise.all(
...file, files.map(async (file) => {
absolutePath: path.join(logsDir, file.name), const absolutePath = path.join(logsDir, file.name);
})); const metadata = await extractErrorLogMetadata(absolutePath);
return {
...file,
absolutePath,
statusCode: metadata.statusCode,
model: metadata.model,
};
})
);
res.json({ files: filesWithPaths }); res.json({ files: filesWithMetadata });
} catch (error) { } catch (error) {
res.status(500).json({ error: (error as Error).message }); res.status(500).json({ error: (error as Error).message });
} }
@@ -1,71 +1,86 @@
/** /**
* Error Log Item Component * Error Log Item Component
* Individual log entry in the list view * Individual log entry in the list view - shows status code, model, endpoint, time
*/ */
import { useMemo } from 'react'; import { useMemo } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Clock, FileText } from 'lucide-react'; import { Clock } from 'lucide-react';
import { ProviderIcon } from '@/components/shared/provider-icon'; import { ProviderIcon } from '@/components/shared/provider-icon';
import { parseFilename, formatRelativeTime, formatBytes } from '@/lib/error-log-parser'; import { parseFilename, formatRelativeTime } from '@/lib/error-log-parser';
import type { ErrorLogItemProps } from './types'; import type { ErrorLogItemProps } from './types';
export function ErrorLogItem({ name, size, modified, isSelected, onClick }: ErrorLogItemProps) { /** Get status badge color based on HTTP status code */
function getStatusColor(code?: number): string {
if (!code) return 'bg-gray-100 text-gray-600';
if (code === 429) return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400';
if (code >= 500) return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400';
if (code >= 400)
return 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400';
return 'bg-gray-100 text-gray-600';
}
/** Format model name for display (full name) */
function formatModel(model?: string): string {
if (!model) return '';
return model;
}
export function ErrorLogItem({
name,
size: _size,
modified,
isSelected,
onClick,
statusCode,
model,
}: ErrorLogItemProps) {
const parsed = useMemo(() => parseFilename(name), [name]); const parsed = useMemo(() => parseFilename(name), [name]);
const displayModel = useMemo(() => formatModel(model), [model]);
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className={cn( className={cn(
'w-full px-3 py-2.5 flex items-start gap-3 text-left transition-colors', 'w-full px-2.5 py-2 flex items-center gap-2 text-left transition-colors',
'hover:bg-muted/40 border-l-[3px]', 'hover:bg-muted/40 border-l-[3px]',
isSelected ? 'bg-muted/50 border-l-amber-500' : 'border-l-transparent' isSelected ? 'bg-muted/50 border-l-red-500' : 'border-l-transparent'
)} )}
> >
{/* Status Badge - prominent */}
<span
className={cn(
'shrink-0 text-[10px] font-bold px-1.5 py-0.5 rounded min-w-[32px] text-center',
getStatusColor(statusCode)
)}
>
{statusCode || '???'}
</span>
{/* Provider Icon */} {/* Provider Icon */}
<ProviderIcon <ProviderIcon
provider={parsed.provider} provider={parsed.provider}
size={24} size={18}
withBackground={true} withBackground={true}
className="shrink-0 mt-0.5" className="shrink-0"
/> />
<div className="flex-1 min-w-0 space-y-1"> {/* Content */}
{/* Provider / Endpoint */} <div className="flex-1 min-w-0 flex flex-col">
<div className="flex flex-col gap-0.5"> {/* Row 1: Endpoint */}
<div className="flex items-center justify-between gap-2"> <span className="text-[11px] font-medium text-foreground truncate">
<span className="text-xs font-semibold text-foreground truncate"> {parsed.endpoint || 'unknown'}
{parsed.provider} </span>
</span> {/* Row 2: Model (full name) */}
<span {displayModel && (
className={cn( <span className="text-[10px] text-muted-foreground truncate" title={displayModel}>
'text-[9px] px-1 rounded border', {displayModel}
isSelected
? 'bg-amber-500/10 text-amber-600 border-amber-500/20'
: 'bg-muted border-border text-muted-foreground'
)}
>
LOG
</span>
</div>
<span
className="text-[11px] text-muted-foreground truncate font-medium"
title={parsed.endpoint}
>
{parsed.endpoint}
</span>
</div>
{/* Meta row: time + size */}
<div className="flex items-center gap-3 text-[10px] text-muted-foreground/80 mt-1">
<span className="flex items-center gap-1">
<Clock className="w-3 h-3" />
{formatRelativeTime(modified)}
</span>
<span className="flex items-center gap-1">
<FileText className="w-3 h-3" />
{formatBytes(size)}
</span> </span>
)}
{/* Row 3: Time */}
<div className="flex items-center gap-1 text-[9px] text-muted-foreground/60">
<Clock className="w-2.5 h-2.5" />
<span>{formatRelativeTime(modified)}</span>
</div> </div>
</div> </div>
</button> </button>
@@ -168,6 +168,8 @@ export function ErrorLogsMonitor() {
modified={log.modified} modified={log.modified}
isSelected={effectiveSelection === log.name} isSelected={effectiveSelection === log.name}
onClick={() => setSelectedLog(log.name)} onClick={() => setSelectedLog(log.name)}
statusCode={log.statusCode}
model={log.model}
/> />
))} ))}
</div> </div>
@@ -21,132 +21,134 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
formatQuotaResetTimestamp(parsed.quotaResetTimestamp); formatQuotaResetTimestamp(parsed.quotaResetTimestamp);
return ( return (
<div className="p-4 space-y-4"> <ScrollArea className="h-full">
{/* Status row */} <div className="p-4 space-y-4">
<div className="flex items-center gap-3"> {/* Status row */}
<StatusBadge code={parsed.statusCode} /> <div className="flex items-center gap-3">
<span className="text-sm font-medium">{parsed.statusText}</span> <StatusBadge code={parsed.statusCode} />
<span className="text-xs text-muted-foreground px-2 py-0.5 rounded bg-muted/50"> <span className="text-sm font-medium">{parsed.statusText}</span>
{getErrorTypeLabel(parsed.errorType)} <span className="text-xs text-muted-foreground px-2 py-0.5 rounded bg-muted/50">
</span> {getErrorTypeLabel(parsed.errorType)}
</div> </span>
</div>
{/* Model info - prominent display */} {/* Model info - prominent display */}
{parsed.model && ( {parsed.model && (
<div className="flex items-center gap-2.5 p-3 rounded-lg bg-violet-500/10 border border-violet-500/20"> <div className="flex items-center gap-2.5 p-3 rounded-lg bg-violet-500/10 border border-violet-500/20">
<Cpu className="w-4 h-4 text-violet-500 shrink-0" /> <Cpu className="w-4 h-4 text-violet-500 shrink-0" />
<div className="text-sm"> <div className="text-sm">
<span className="text-muted-foreground">Model: </span> <span className="text-muted-foreground">Model: </span>
<span className="font-semibold text-violet-600 dark:text-violet-400"> <span className="font-semibold text-violet-600 dark:text-violet-400">
{parsed.model} {parsed.model}
</span> </span>
</div>
</div>
)}
{/* Quota reset info for 429 errors */}
{parsed.errorType === 'rate_limit' && quotaResetDisplay && (
<div className="flex items-center gap-2.5 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20">
<Clock className="w-4 h-4 text-amber-500 shrink-0" />
<div className="text-sm">
<span className="text-muted-foreground">Quota resets in </span>
<span className="font-semibold text-amber-600 dark:text-amber-400">
{quotaResetDisplay}
</span>
</div>
</div>
)}
{/* Key metrics grid */}
<div className="grid grid-cols-4 gap-3 text-xs">
<div className="p-2.5 rounded bg-muted/30 border border-border/50">
<div className="text-muted-foreground mb-1">Method</div>
<div className="font-medium">{parsed.method || 'N/A'}</div>
</div>
<div className="p-2.5 rounded bg-muted/30 border border-border/50">
<div className="text-muted-foreground mb-1">Provider</div>
<div className="font-medium">{parsed.provider || 'N/A'}</div>
</div>
<div className="p-2.5 rounded bg-muted/30 border border-border/50">
<div className="text-muted-foreground mb-1">Version</div>
<div className="font-medium">{parsed.version || 'N/A'}</div>
</div>
<div className="p-2.5 rounded bg-muted/30 border border-border/50">
<div className="text-muted-foreground mb-1">Endpoint</div>
<div className="font-medium truncate" title={parsed.endpoint}>
{parsed.endpoint || 'N/A'}
</div>
</div> </div>
</div> </div>
)}
{/* Quota reset info for 429 errors */} {/* URL */}
{parsed.errorType === 'rate_limit' && quotaResetDisplay && ( <div className="text-xs">
<div className="flex items-center gap-2.5 p-3 rounded-lg bg-amber-500/10 border border-amber-500/20"> <div className="text-muted-foreground mb-1.5">URL</div>
<Clock className="w-4 h-4 text-amber-500 shrink-0" /> <div className="font-mono p-2.5 rounded bg-muted/30 border border-border/50 break-all leading-relaxed">
<div className="text-sm"> {parsed.url || 'N/A'}
<span className="text-muted-foreground">Quota resets in </span>
<span className="font-semibold text-amber-600 dark:text-amber-400">
{quotaResetDisplay}
</span>
</div> </div>
</div> </div>
)}
{/* Key metrics grid */} {/* Timestamp */}
<div className="grid grid-cols-4 gap-3 text-xs"> <div className="text-xs">
<div className="p-2.5 rounded bg-muted/30 border border-border/50"> <div className="text-muted-foreground mb-1.5">Timestamp</div>
<div className="text-muted-foreground mb-1">Method</div> <div className="font-mono">{parsed.timestamp || 'N/A'}</div>
<div className="font-medium">{parsed.method || 'N/A'}</div>
</div> </div>
<div className="p-2.5 rounded bg-muted/30 border border-border/50">
<div className="text-muted-foreground mb-1">Provider</div>
<div className="font-medium">{parsed.provider || 'N/A'}</div>
</div>
<div className="p-2.5 rounded bg-muted/30 border border-border/50">
<div className="text-muted-foreground mb-1">Version</div>
<div className="font-medium">{parsed.version || 'N/A'}</div>
</div>
<div className="p-2.5 rounded bg-muted/30 border border-border/50">
<div className="text-muted-foreground mb-1">Endpoint</div>
<div className="font-medium truncate" title={parsed.endpoint}>
{parsed.endpoint || 'N/A'}
</div>
</div>
</div>
{/* URL */} {/* Actionable suggestion based on error type */}
<div className="text-xs"> {parsed.errorType !== 'unknown' && (
<div className="text-muted-foreground mb-1.5">URL</div>
<div className="font-mono p-2.5 rounded bg-muted/30 border border-border/50 break-all leading-relaxed">
{parsed.url || 'N/A'}
</div>
</div>
{/* Timestamp */}
<div className="text-xs">
<div className="text-muted-foreground mb-1.5">Timestamp</div>
<div className="font-mono">{parsed.timestamp || 'N/A'}</div>
</div>
{/* Actionable suggestion based on error type */}
{parsed.errorType !== 'unknown' && (
<div
className={cn(
'flex items-start gap-3 p-3 rounded text-xs',
parsed.errorType === 'rate_limit'
? 'bg-amber-500/10 border border-amber-500/20'
: parsed.errorType === 'auth'
? 'bg-red-500/10 border border-red-500/20'
: 'bg-blue-500/10 border border-blue-500/20'
)}
>
{parsed.errorType === 'rate_limit' ? (
<AlertTriangle className="w-4 h-4 mt-0.5 text-amber-500 shrink-0" />
) : parsed.errorType === 'auth' ? (
<AlertTriangle className="w-4 h-4 mt-0.5 text-red-500 shrink-0" />
) : (
<Info className="w-4 h-4 mt-0.5 text-blue-500 shrink-0" />
)}
<div <div
className={cn( className={cn(
'leading-relaxed', 'flex items-start gap-3 p-3 rounded text-xs',
parsed.errorType === 'rate_limit' parsed.errorType === 'rate_limit'
? 'text-amber-600 dark:text-amber-400' ? 'bg-amber-500/10 border border-amber-500/20'
: parsed.errorType === 'auth' : parsed.errorType === 'auth'
? 'text-red-600 dark:text-red-400' ? 'bg-red-500/10 border border-red-500/20'
: 'text-blue-600 dark:text-blue-400' : 'bg-blue-500/10 border border-blue-500/20'
)} )}
> >
{parsed.errorType === 'rate_limit' && ( {parsed.errorType === 'rate_limit' ? (
<> <AlertTriangle className="w-4 h-4 mt-0.5 text-amber-500 shrink-0" />
<strong>Rate Limited.</strong> Switch to a different account or wait for quota ) : parsed.errorType === 'auth' ? (
reset. <AlertTriangle className="w-4 h-4 mt-0.5 text-red-500 shrink-0" />
{parsed.model && ( ) : (
<> <Info className="w-4 h-4 mt-0.5 text-blue-500 shrink-0" />
{' '}
Model <code className="font-mono text-[11px]">{parsed.model}</code> has
exhausted quota.
</>
)}
</>
)} )}
{parsed.errorType === 'auth' && <div
'Authentication failed. Re-authenticate via CLIProxy Settings or check API key.'} className={cn(
{parsed.errorType === 'not_found' && 'leading-relaxed',
'Endpoint not found. This endpoint may not exist on this provider.'} parsed.errorType === 'rate_limit'
{parsed.errorType === 'server' && ? 'text-amber-600 dark:text-amber-400'
'Server error from upstream. Retry later or check provider status page.'} : parsed.errorType === 'auth'
{parsed.errorType === 'timeout' && ? 'text-red-600 dark:text-red-400'
'Request timed out. Check network connection or increase timeout settings.'} : 'text-blue-600 dark:text-blue-400'
)}
>
{parsed.errorType === 'rate_limit' && (
<>
<strong>Rate Limited.</strong> Switch to a different account or wait for quota
reset.
{parsed.model && (
<>
{' '}
Model <code className="font-mono text-[11px]">{parsed.model}</code> has
exhausted quota.
</>
)}
</>
)}
{parsed.errorType === 'auth' &&
'Authentication failed. Re-authenticate via CLIProxy Settings or check API key.'}
{parsed.errorType === 'not_found' &&
'Endpoint not found. This endpoint may not exist on this provider.'}
{parsed.errorType === 'server' &&
'Server error from upstream. Retry later or check provider status page.'}
{parsed.errorType === 'timeout' &&
'Request timed out. Check network connection or increase timeout settings.'}
</div>
</div> </div>
</div> )}
)} </div>
</div> </ScrollArea>
); );
} }
@@ -10,6 +10,10 @@ export interface ErrorLogItemProps {
modified: number; modified: number;
isSelected: boolean; isSelected: boolean;
onClick: () => void; onClick: () => void;
/** HTTP status code from pre-parsed metadata */
statusCode?: number;
/** Model name from pre-parsed metadata */
model?: string;
} }
export interface LogContentPanelProps { export interface LogContentPanelProps {
+4
View File
@@ -139,6 +139,10 @@ export interface CliproxyErrorLog {
modified: number; modified: number;
/** Absolute path to the log file (injected by backend) */ /** Absolute path to the log file (injected by backend) */
absolutePath?: string; absolutePath?: string;
/** HTTP status code extracted from log (injected by backend) */
statusCode?: number;
/** Model name extracted from request body (injected by backend) */
model?: string;
} }
/** /**
+8 -3
View File
@@ -62,9 +62,10 @@ export function parseFilename(name: string): ParsedFilename {
result.provider = providerMatch[1]; result.provider = providerMatch[1];
} }
// Extract endpoint from after provider: ...-api-{ENDPOINT}-{timestamp} // Extract endpoint: after provider, before timestamp
// Example: error-api-provider-agy-api-event_logging-batch-2025-12-18T185041-... // Format: error-api-provider-{provider}-{endpoint}-{timestamp}-{id}.log
const endpointMatch = name.match(/-api-([a-z_]+(?:-[a-z_]+)*)-\d{4}-\d{2}-\d{2}T/i); // Example: error-api-provider-agy-v1-messages-2025-12-29T105823-a12b73f8.log
const endpointMatch = name.match(/error-api-provider-[^-]+-(.+?)-\d{4}-\d{2}-\d{2}T/);
if (endpointMatch) { if (endpointMatch) {
result.endpoint = endpointMatch[1].replace(/-/g, '/'); result.endpoint = endpointMatch[1].replace(/-/g, '/');
} }
@@ -122,6 +123,10 @@ export function parseErrorLog(content: string): ParsedErrorLog {
} else if (part === 'REQUEST BODY') { } else if (part === 'REQUEST BODY') {
currentSection = 'request_body'; currentSection = 'request_body';
continue; continue;
} else if (part === 'API RESPONSE') {
// Skip API RESPONSE section - we parse the actual RESPONSE section instead
currentSection = '';
continue;
} else if (part === 'RESPONSE') { } else if (part === 'RESPONSE') {
currentSection = 'response'; currentSection = 'response';
continue; continue;