From e3a71fc89372e81af3c425c5bf8e42630b4c1b6b Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Mon, 29 Dec 2025 13:40:17 -0500 Subject: [PATCH] feat(error-logs): extract model and quota reset info from error logs - Add model, quotaResetDelay, quotaResetTimestamp fields to ParsedErrorLog - Extract model from requestBody JSON - Parse quota reset delay/timestamp from 429 response bodies - Display model prominently in Overview tab with violet highlight - Show quota reset countdown for rate limit errors - Improve actionable suggestions with color-coded error types --- .../monitoring/error-logs/tab-components.tsx | 92 +++++++++-- ui/src/lib/error-log-parser.ts | 156 ++++++++++++++++++ 2 files changed, 237 insertions(+), 11 deletions(-) diff --git a/ui/src/components/monitoring/error-logs/tab-components.tsx b/ui/src/components/monitoring/error-logs/tab-components.tsx index 42043731..00f4de41 100644 --- a/ui/src/components/monitoring/error-logs/tab-components.tsx +++ b/ui/src/components/monitoring/error-logs/tab-components.tsx @@ -4,13 +4,22 @@ */ import { ScrollArea } from '@/components/ui/scroll-area'; -import { Info } from 'lucide-react'; +import { Info, Clock, Cpu, AlertTriangle } from 'lucide-react'; import { cn } from '@/lib/utils'; -import { getErrorTypeLabel, type ParsedErrorLog } from '@/lib/error-log-parser'; +import { + getErrorTypeLabel, + formatQuotaResetDelay, + formatQuotaResetTimestamp, + type ParsedErrorLog, +} from '@/lib/error-log-parser'; import { StatusBadge } from './ui-primitives'; /** Overview tab content */ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) { + const quotaResetDisplay = + formatQuotaResetDelay(parsed.quotaResetDelay) || + formatQuotaResetTimestamp(parsed.quotaResetTimestamp); + return (
{/* Status row */} @@ -22,6 +31,32 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
+ {/* 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 */}
@@ -58,21 +93,56 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
{parsed.timestamp || 'N/A'}
- {/* Suggestion based on error type */} + {/* Actionable suggestion based on error type */} {parsed.errorType !== 'unknown' && ( -
- -
- {parsed.errorType === 'rate_limit' && - 'Rate limited. Consider using multiple accounts or reducing request frequency.'} +
+ {parsed.errorType === 'rate_limit' ? ( + + ) : parsed.errorType === 'auth' ? ( + + ) : ( + + )} +
+ {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. Check credentials or re-authenticate with the provider.'} + '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 or check provider status.'} + 'Server error from upstream. Retry later or check provider status page.'} {parsed.errorType === 'timeout' && - 'Request timed out. Check network or increase timeout settings.'} + 'Request timed out. Check network connection or increase timeout settings.'}
)} diff --git a/ui/src/lib/error-log-parser.ts b/ui/src/lib/error-log-parser.ts index 3afa030c..5e29fd64 100644 --- a/ui/src/lib/error-log-parser.ts +++ b/ui/src/lib/error-log-parser.ts @@ -29,6 +29,11 @@ export interface ParsedErrorLog { isClientError: boolean; isServerError: boolean; errorType: 'rate_limit' | 'auth' | 'not_found' | 'server' | 'timeout' | 'unknown'; + + // Extracted from request/response bodies + model: string | null; + quotaResetDelay: number | null; // seconds until reset + quotaResetTimestamp: string | null; // ISO timestamp when quota resets } /** Parsed filename metadata */ @@ -96,6 +101,9 @@ export function parseErrorLog(content: string): ParsedErrorLog { isClientError: false, isServerError: false, errorType: 'unknown', + model: null, + quotaResetDelay: null, + quotaResetTimestamp: null, }; // Split into sections @@ -251,6 +259,113 @@ function computeDerivedFields(result: ParsedErrorLog): void { } else if (result.statusCode === 408 || result.statusCode === 504) { result.errorType = 'timeout'; } + + // Extract model from request body + extractModelFromRequestBody(result); + + // Extract quota reset info from response body (for 429 errors) + if (result.statusCode === 429) { + extractQuotaResetInfo(result); + } +} + +/** Extract model name from request body JSON */ +function extractModelFromRequestBody(result: ParsedErrorLog): void { + if (!result.requestBody) return; + try { + const body = JSON.parse(result.requestBody); + if (typeof body.model === 'string') { + result.model = body.model; + } + } catch { + // Not valid JSON, skip + } +} + +/** Extract quota reset info from 429 response body */ +function extractQuotaResetInfo(result: ParsedErrorLog): void { + if (!result.responseBody) return; + try { + const body = JSON.parse(result.responseBody); + // Look for quotaResetDelay in various response formats + // Format 1: { error: { details: [{ quotaResetDelay: "123s" }] } } + // Format 2: { error: { quotaResetDelay: 123 } } + // Format 3: { quotaResetDelay: 123, quotaResetTimeStamp: "..." } + const delay = findQuotaResetDelay(body); + if (delay !== null) { + result.quotaResetDelay = delay; + } + const timestamp = findQuotaResetTimestamp(body); + if (timestamp) { + result.quotaResetTimestamp = timestamp; + } + } catch { + // Not valid JSON, skip + } +} + +/** Recursively find quotaResetDelay in response object */ +function findQuotaResetDelay(obj: unknown): number | null { + if (typeof obj !== 'object' || obj === null) return null; + + const record = obj as Record; + + // Check direct properties + if ('quotaResetDelay' in record) { + const val = record.quotaResetDelay; + if (typeof val === 'number') return val; + if (typeof val === 'string') { + // Handle "123s" format + const match = val.match(/^(\d+)s?$/); + if (match) return parseInt(match[1], 10); + } + } + + // Check nested error object + if ('error' in record && typeof record.error === 'object') { + const found = findQuotaResetDelay(record.error); + if (found !== null) return found; + } + + // Check details array + if ('details' in record && Array.isArray(record.details)) { + for (const detail of record.details) { + const found = findQuotaResetDelay(detail); + if (found !== null) return found; + } + } + + return null; +} + +/** Recursively find quotaResetTimeStamp in response object */ +function findQuotaResetTimestamp(obj: unknown): string | null { + if (typeof obj !== 'object' || obj === null) return null; + + const record = obj as Record; + + // Check direct properties (various casing) + for (const key of ['quotaResetTimeStamp', 'quotaResetTimestamp', 'resetTime', 'reset_time']) { + if (key in record && typeof record[key] === 'string') { + return record[key] as string; + } + } + + // Check nested error object + if ('error' in record && typeof record.error === 'object') { + const found = findQuotaResetTimestamp(record.error); + if (found) return found; + } + + // Check details array + if ('details' in record && Array.isArray(record.details)) { + for (const detail of record.details) { + const found = findQuotaResetTimestamp(detail); + if (found) return found; + } + } + + return null; } /** Get status text for common codes */ @@ -326,3 +441,44 @@ export function getErrorTypeLabel(type: ParsedErrorLog['errorType']): string { }; return labels[type] || 'Error'; } + +/** + * Format quota reset delay as human-readable string + */ +export function formatQuotaResetDelay(seconds: number | null): string | null { + if (seconds === null || seconds <= 0) return null; + + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) { + const mins = Math.floor(seconds / 60); + return `${mins}m`; + } + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; +} + +/** + * Format quota reset timestamp as relative time + */ +export function formatQuotaResetTimestamp(timestamp: string | null): string | null { + if (!timestamp) return null; + try { + const resetDate = new Date(timestamp); + const now = new Date(); + const diff = resetDate.getTime() - now.getTime(); + if (diff <= 0) return 'now'; + + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return `${seconds}s`; + if (seconds < 3600) { + const mins = Math.floor(seconds / 60); + return `${mins}m`; + } + const hours = Math.floor(seconds / 3600); + const mins = Math.floor((seconds % 3600) / 60); + return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`; + } catch { + return null; + } +}