mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
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:
@@ -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<QuotaResult> {
|
||||
async function fetchAvailableModels(accessToken: string, _projectId: string): Promise<QuotaResult> {
|
||||
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);
|
||||
|
||||
@@ -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 */
|
||||
|
||||
@@ -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<void> =>
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<button
|
||||
onClick={onClick}
|
||||
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]',
|
||||
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 */}
|
||||
<ProviderIcon
|
||||
provider={parsed.provider}
|
||||
size={24}
|
||||
size={18}
|
||||
withBackground={true}
|
||||
className="shrink-0 mt-0.5"
|
||||
className="shrink-0"
|
||||
/>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
{/* Provider / Endpoint */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-semibold text-foreground truncate">
|
||||
{parsed.provider}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
'text-[9px] px-1 rounded border',
|
||||
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)}
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0 flex flex-col">
|
||||
{/* Row 1: Endpoint */}
|
||||
<span className="text-[11px] font-medium text-foreground truncate">
|
||||
{parsed.endpoint || 'unknown'}
|
||||
</span>
|
||||
{/* Row 2: Model (full name) */}
|
||||
{displayModel && (
|
||||
<span className="text-[10px] text-muted-foreground truncate" title={displayModel}>
|
||||
{displayModel}
|
||||
</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>
|
||||
</button>
|
||||
|
||||
@@ -168,6 +168,8 @@ export function ErrorLogsMonitor() {
|
||||
modified={log.modified}
|
||||
isSelected={effectiveSelection === log.name}
|
||||
onClick={() => setSelectedLog(log.name)}
|
||||
statusCode={log.statusCode}
|
||||
model={log.model}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -21,132 +21,134 @@ export function OverviewTab({ parsed }: { parsed: ParsedErrorLog }) {
|
||||
formatQuotaResetTimestamp(parsed.quotaResetTimestamp);
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Status row */}
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusBadge code={parsed.statusCode} />
|
||||
<span className="text-sm font-medium">{parsed.statusText}</span>
|
||||
<span className="text-xs text-muted-foreground px-2 py-0.5 rounded bg-muted/50">
|
||||
{getErrorTypeLabel(parsed.errorType)}
|
||||
</span>
|
||||
</div>
|
||||
<ScrollArea className="h-full">
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Status row */}
|
||||
<div className="flex items-center gap-3">
|
||||
<StatusBadge code={parsed.statusCode} />
|
||||
<span className="text-sm font-medium">{parsed.statusText}</span>
|
||||
<span className="text-xs text-muted-foreground px-2 py-0.5 rounded bg-muted/50">
|
||||
{getErrorTypeLabel(parsed.errorType)}
|
||||
</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>
|
||||
{/* 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">
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
{/* URL */}
|
||||
<div className="text-xs">
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
{/* 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>
|
||||
<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 */}
|
||||
<div className="text-xs">
|
||||
<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" />
|
||||
)}
|
||||
{/* Actionable suggestion based on error type */}
|
||||
{parsed.errorType !== 'unknown' && (
|
||||
<div
|
||||
className={cn(
|
||||
'leading-relaxed',
|
||||
'flex items-start gap-3 p-3 rounded text-xs',
|
||||
parsed.errorType === 'rate_limit'
|
||||
? 'text-amber-600 dark:text-amber-400'
|
||||
? 'bg-amber-500/10 border border-amber-500/20'
|
||||
: parsed.errorType === 'auth'
|
||||
? 'text-red-600 dark:text-red-400'
|
||||
: 'text-blue-600 dark:text-blue-400'
|
||||
? 'bg-red-500/10 border border-red-500/20'
|
||||
: 'bg-blue-500/10 border border-blue-500/20'
|
||||
)}
|
||||
>
|
||||
{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 === '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" />
|
||||
)}
|
||||
{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
|
||||
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. 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>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user