From ac6f382f6a6cd64aa3fa0727d11bcf498aae28fc Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 29 Dec 2025 15:48:37 -0500 Subject: [PATCH] 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 --- src/cliproxy/quota-fetcher.ts | 29 ++- src/cliproxy/stats-fetcher.ts | 4 + .../routes/cliproxy-stats-routes.ts | 62 ++++- .../monitoring/error-logs/error-log-item.tsx | 101 ++++---- .../monitoring/error-logs/index.tsx | 2 + .../monitoring/error-logs/tab-components.tsx | 220 +++++++++--------- .../components/monitoring/error-logs/types.ts | 4 + ui/src/hooks/use-cliproxy-stats.ts | 4 + ui/src/lib/error-log-parser.ts | 11 +- 9 files changed, 266 insertions(+), 171 deletions(-) diff --git a/src/cliproxy/quota-fetcher.ts b/src/cliproxy/quota-fetcher.ts index e2f7e014..749d9393 100644 --- a/src/cliproxy/quota-fetcher.ts +++ b/src/cliproxy/quota-fetcher.ts @@ -54,11 +54,19 @@ const ANTIGRAVITY_CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com'; const ANTIGRAVITY_CLIENT_SECRET = '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'; -/** API client headers */ -const ANTIGRAVITY_HEADERS = { +/** Headers for loadCodeAssist (matches CLIProxyAPI antigravity.go) */ +const LOADCODEASSIST_HEADERS = { 'Content-Type': 'application/json', - 'User-Agent': 'antigravity/1.11.5 linux/amd64', - 'X-Goog-Api-Client': 'gl-node/20.9.0', + 'User-Agent': 'google-api-nodejs-client/9.15.1', + '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 */ @@ -270,7 +278,7 @@ async function getProjectId( method: 'POST', signal: controller.signal, headers: { - ...ANTIGRAVITY_HEADERS, + ...LOADCODEASSIST_HEADERS, Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ @@ -326,24 +334,25 @@ async function getProjectId( /** * 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 { +async function fetchAvailableModels(accessToken: string, _projectId: string): Promise { const url = `${ANTIGRAVITY_API_BASE}/${ANTIGRAVITY_API_VERSION}:fetchAvailableModels`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); try { + // Match CLIProxyAPI exactly: empty body, minimal headers const response = await fetch(url, { method: 'POST', signal: controller.signal, headers: { - ...ANTIGRAVITY_HEADERS, + ...FETCHMODELS_HEADERS, Authorization: `Bearer ${accessToken}`, }, - body: JSON.stringify({ - project: projectId, - }), + body: JSON.stringify({}), }); clearTimeout(timeoutId); diff --git a/src/cliproxy/stats-fetcher.ts b/src/cliproxy/stats-fetcher.ts index ee74b8a9..58c20a4b 100644 --- a/src/cliproxy/stats-fetcher.ts +++ b/src/cliproxy/stats-fetcher.ts @@ -297,6 +297,10 @@ export interface CliproxyErrorLog { modified: number; /** Absolute path to the log file (injected by backend) */ 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 */ diff --git a/src/web-server/routes/cliproxy-stats-routes.ts b/src/web-server/routes/cliproxy-stats-routes.ts index 95d65263..ac11ca84 100644 --- a/src/web-server/routes/cliproxy-stats-routes.ts +++ b/src/web-server/routes/cliproxy-stats-routes.ts @@ -25,6 +25,48 @@ import { checkCliproxyUpdate } from '../../cliproxy/binary-manager'; 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 */ @@ -214,14 +256,22 @@ router.get('/error-logs', async (_req: Request, res: Response): Promise => return; } - // Inject absolute paths into each file entry + // Inject absolute paths and extract metadata from each file const logsDir = path.join(getCliproxyWritablePath(), 'logs'); - const filesWithPaths = files.map((file) => ({ - ...file, - absolutePath: path.join(logsDir, file.name), - })); + const filesWithMetadata = await Promise.all( + files.map(async (file) => { + 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) { res.status(500).json({ error: (error as Error).message }); } diff --git a/ui/src/components/monitoring/error-logs/error-log-item.tsx b/ui/src/components/monitoring/error-logs/error-log-item.tsx index a4d4b2e1..5f2f170b 100644 --- a/ui/src/components/monitoring/error-logs/error-log-item.tsx +++ b/ui/src/components/monitoring/error-logs/error-log-item.tsx @@ -1,71 +1,86 @@ /** * 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 { cn } from '@/lib/utils'; -import { Clock, FileText } from 'lucide-react'; +import { Clock } from 'lucide-react'; 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'; -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 displayModel = useMemo(() => formatModel(model), [model]); return ( diff --git a/ui/src/components/monitoring/error-logs/index.tsx b/ui/src/components/monitoring/error-logs/index.tsx index b96a65c7..8f8cea22 100644 --- a/ui/src/components/monitoring/error-logs/index.tsx +++ b/ui/src/components/monitoring/error-logs/index.tsx @@ -168,6 +168,8 @@ export function ErrorLogsMonitor() { modified={log.modified} isSelected={effectiveSelection === log.name} onClick={() => setSelectedLog(log.name)} + statusCode={log.statusCode} + model={log.model} /> ))} diff --git a/ui/src/components/monitoring/error-logs/tab-components.tsx b/ui/src/components/monitoring/error-logs/tab-components.tsx index 00f4de41..bba80c37 100644 --- a/ui/src/components/monitoring/error-logs/tab-components.tsx +++ b/ui/src/components/monitoring/error-logs/tab-components.tsx @@ -21,132 +21,134 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) { formatQuotaResetTimestamp(parsed.quotaResetTimestamp); return ( -
- {/* Status row */} -
- - {parsed.statusText} - - {getErrorTypeLabel(parsed.errorType)} - -
+ +
+ {/* Status row */} +
+ + {parsed.statusText} + + {getErrorTypeLabel(parsed.errorType)} + +
- {/* Model info - prominent display */} - {parsed.model && ( -
- -
- Model: - - {parsed.model} - + {/* Model info - prominent display */} + {parsed.model && ( +
+ +
+ Model: + + {parsed.model} + +
+
+ )} + + {/* Quota reset info for 429 errors */} + {parsed.errorType === 'rate_limit' && quotaResetDisplay && ( +
+ +
+ Quota resets in + + {quotaResetDisplay} + +
+
+ )} + + {/* Key metrics grid */} +
+
+
Method
+
{parsed.method || 'N/A'}
+
+
+
Provider
+
{parsed.provider || 'N/A'}
+
+
+
Version
+
{parsed.version || 'N/A'}
+
+
+
Endpoint
+
+ {parsed.endpoint || 'N/A'} +
- )} - {/* Quota reset info for 429 errors */} - {parsed.errorType === 'rate_limit' && quotaResetDisplay && ( -
- -
- Quota resets in - - {quotaResetDisplay} - + {/* URL */} +
+
URL
+
+ {parsed.url || 'N/A'}
- )} - {/* Key metrics grid */} -
-
-
Method
-
{parsed.method || 'N/A'}
+ {/* Timestamp */} +
+
Timestamp
+
{parsed.timestamp || 'N/A'}
-
-
Provider
-
{parsed.provider || 'N/A'}
-
-
-
Version
-
{parsed.version || 'N/A'}
-
-
-
Endpoint
-
- {parsed.endpoint || 'N/A'} -
-
-
- {/* URL */} -
-
URL
-
- {parsed.url || 'N/A'} -
-
- - {/* Timestamp */} -
-
Timestamp
-
{parsed.timestamp || 'N/A'}
-
- - {/* Actionable suggestion based on error type */} - {parsed.errorType !== 'unknown' && ( -
- {parsed.errorType === 'rate_limit' ? ( - - ) : parsed.errorType === 'auth' ? ( - - ) : ( - - )} + {/* Actionable suggestion based on error type */} + {parsed.errorType !== 'unknown' && (
- {parsed.errorType === 'rate_limit' && ( - <> - Rate Limited. Switch to a different account or wait for quota - reset. - {parsed.model && ( - <> - {' '} - Model {parsed.model} has - exhausted quota. - - )} - + {parsed.errorType === 'rate_limit' ? ( + + ) : parsed.errorType === 'auth' ? ( + + ) : ( + )} - {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.'} +
+ {parsed.errorType === 'rate_limit' && ( + <> + Rate Limited. Switch to a different account or wait for quota + reset. + {parsed.model && ( + <> + {' '} + Model {parsed.model} 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.'} +
-
- )} -
+ )} +
+ ); } diff --git a/ui/src/components/monitoring/error-logs/types.ts b/ui/src/components/monitoring/error-logs/types.ts index 0cae04f1..e5cd35be 100644 --- a/ui/src/components/monitoring/error-logs/types.ts +++ b/ui/src/components/monitoring/error-logs/types.ts @@ -10,6 +10,10 @@ export interface ErrorLogItemProps { modified: number; isSelected: boolean; onClick: () => void; + /** HTTP status code from pre-parsed metadata */ + statusCode?: number; + /** Model name from pre-parsed metadata */ + model?: string; } export interface LogContentPanelProps { diff --git a/ui/src/hooks/use-cliproxy-stats.ts b/ui/src/hooks/use-cliproxy-stats.ts index 014e5f4e..cc9cf222 100644 --- a/ui/src/hooks/use-cliproxy-stats.ts +++ b/ui/src/hooks/use-cliproxy-stats.ts @@ -139,6 +139,10 @@ export interface CliproxyErrorLog { modified: number; /** Absolute path to the log file (injected by backend) */ absolutePath?: string; + /** HTTP status code extracted from log (injected by backend) */ + statusCode?: number; + /** Model name extracted from request body (injected by backend) */ + model?: string; } /** diff --git a/ui/src/lib/error-log-parser.ts b/ui/src/lib/error-log-parser.ts index 3bf8de82..ff2116b9 100644 --- a/ui/src/lib/error-log-parser.ts +++ b/ui/src/lib/error-log-parser.ts @@ -62,9 +62,10 @@ export function parseFilename(name: string): ParsedFilename { result.provider = providerMatch[1]; } - // Extract endpoint from after provider: ...-api-{ENDPOINT}-{timestamp} - // Example: error-api-provider-agy-api-event_logging-batch-2025-12-18T185041-... - const endpointMatch = name.match(/-api-([a-z_]+(?:-[a-z_]+)*)-\d{4}-\d{2}-\d{2}T/i); + // Extract endpoint: after provider, before timestamp + // Format: error-api-provider-{provider}-{endpoint}-{timestamp}-{id}.log + // 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) { result.endpoint = endpointMatch[1].replace(/-/g, '/'); } @@ -122,6 +123,10 @@ export function parseErrorLog(content: string): ParsedErrorLog { } else if (part === 'REQUEST BODY') { currentSection = 'request_body'; continue; + } else if (part === 'API RESPONSE') { + // Skip API RESPONSE section - we parse the actual RESPONSE section instead + currentSection = ''; + continue; } else if (part === 'RESPONSE') { currentSection = 'response'; continue;