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
This commit is contained in:
kaitranntt
2025-12-29 13:40:17 -05:00
parent 4be8e927a0
commit e3a71fc893
2 changed files with 237 additions and 11 deletions
@@ -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 (
<div className="p-4 space-y-4">
{/* Status row */}
@@ -22,6 +31,32 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
</span>
</div>
{/* Model info - prominent display */}
{parsed.model && (
<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" />
<div className="text-sm">
<span className="text-muted-foreground">Model: </span>
<span className="font-semibold text-violet-600 dark:text-violet-400">
{parsed.model}
</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">
@@ -58,21 +93,56 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
<div className="font-mono">{parsed.timestamp || 'N/A'}</div>
</div>
{/* Suggestion based on error type */}
{/* Actionable suggestion based on error type */}
{parsed.errorType !== 'unknown' && (
<div className="flex items-start gap-3 p-3 rounded bg-blue-500/10 border border-blue-500/20 text-xs">
<Info className="w-4 h-4 mt-0.5 text-blue-500 shrink-0" />
<div className="text-blue-500/90 leading-relaxed">
{parsed.errorType === 'rate_limit' &&
'Rate limited. Consider using multiple accounts or reducing request frequency.'}
<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
className={cn(
'leading-relaxed',
parsed.errorType === 'rate_limit'
? 'text-amber-600 dark:text-amber-400'
: parsed.errorType === 'auth'
? 'text-red-600 dark:text-red-400'
: '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. 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.'}
</div>
</div>
)}
+156
View File
@@ -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<string, unknown>;
// 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<string, unknown>;
// 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;
}
}