mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
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:
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user