mirror of
https://github.com/YouXam/claude-code-usage-dashboard.git
synced 2026-02-04 15:10:16 +08:00
feat: add ai accounts status
This commit is contained in:
317
client/components/AIAccounts.tsx
Normal file
317
client/components/AIAccounts.tsx
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { UsageProgressBar } from './UsageProgressBar';
|
||||||
|
|
||||||
|
interface ClaudeAccount {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
accountType: string;
|
||||||
|
lastUsedAt: string | null;
|
||||||
|
usage?: {
|
||||||
|
daily?: {
|
||||||
|
tokens: number;
|
||||||
|
requests: number;
|
||||||
|
cost: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
claudeUsage?: {
|
||||||
|
fiveHour?: {
|
||||||
|
utilization: number;
|
||||||
|
resetsAt: string;
|
||||||
|
remainingSeconds: number;
|
||||||
|
};
|
||||||
|
sevenDay?: {
|
||||||
|
utilization: number;
|
||||||
|
resetsAt: string;
|
||||||
|
remainingSeconds: number;
|
||||||
|
};
|
||||||
|
sevenDayOpus?: {
|
||||||
|
utilization: number;
|
||||||
|
resetsAt: string;
|
||||||
|
remainingSeconds: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIAccount {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
accountType: string;
|
||||||
|
lastUsedAt: string | null;
|
||||||
|
usage?: {
|
||||||
|
daily?: {
|
||||||
|
tokens: number;
|
||||||
|
requests: number;
|
||||||
|
cost: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
codexUsage?: {
|
||||||
|
primary?: {
|
||||||
|
usedPercent: number;
|
||||||
|
resetAfterSeconds: number;
|
||||||
|
resetAt: string;
|
||||||
|
};
|
||||||
|
secondary?: {
|
||||||
|
usedPercent: number;
|
||||||
|
resetAfterSeconds: number;
|
||||||
|
resetAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type UnifiedAccount = (ClaudeAccount | OpenAIAccount) & {
|
||||||
|
platform: 'claude' | 'openai';
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AIAccountsProps {
|
||||||
|
apiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AIAccounts({ apiKey }: AIAccountsProps) {
|
||||||
|
const [claudeAccounts, setClaudeAccounts] = useState<ClaudeAccount[]>([]);
|
||||||
|
const [openaiAccounts, setOpenaiAccounts] = useState<OpenAIAccount[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAccounts();
|
||||||
|
const interval = setInterval(fetchAccounts, 30000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [apiKey]);
|
||||||
|
|
||||||
|
const fetchAccounts = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/ai-accounts', {
|
||||||
|
headers: {
|
||||||
|
'X-API-Key': apiKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch AI accounts');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setClaudeAccounts(data.claude || []);
|
||||||
|
setOpenaiAccounts(data.openai || []);
|
||||||
|
setError('');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching AI accounts:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to load AI accounts');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLastUsed = (lastUsedAt: string | null) => {
|
||||||
|
if (!lastUsedAt) return 'Never';
|
||||||
|
const date = new Date(lastUsedAt);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now.getTime() - date.getTime();
|
||||||
|
const diffMins = Math.floor(diffMs / 60000);
|
||||||
|
|
||||||
|
if (diffMins < 1) return 'Just now';
|
||||||
|
if (diffMins < 60) return `${diffMins}m ago`;
|
||||||
|
const diffHours = Math.floor(diffMins / 60);
|
||||||
|
if (diffHours < 24) return `${diffHours}h ago`;
|
||||||
|
const diffDays = Math.floor(diffHours / 24);
|
||||||
|
return `${diffDays}d ago`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
if (seconds <= 0) return '0m';
|
||||||
|
const days = Math.floor(seconds / 86400);
|
||||||
|
const hours = Math.floor((seconds % 86400) / 3600);
|
||||||
|
const minutes = Math.floor((seconds % 3600) / 60);
|
||||||
|
|
||||||
|
if (days > 0) {
|
||||||
|
if (hours > 0) return `${days}d ${hours}h`;
|
||||||
|
return `${days}d`;
|
||||||
|
}
|
||||||
|
if (hours > 0) return `${hours}h ${minutes}m`;
|
||||||
|
return `${minutes}m`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'active':
|
||||||
|
return 'bg-chart-1 text-white';
|
||||||
|
case 'blocked':
|
||||||
|
return 'bg-chart-2 text-white';
|
||||||
|
case 'unauthorized':
|
||||||
|
return 'bg-chart-3 text-white';
|
||||||
|
case 'temp_error':
|
||||||
|
return 'bg-chart-4 text-white';
|
||||||
|
default:
|
||||||
|
return 'bg-chart-5 text-white';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAccountName = (account: UnifiedAccount, totalCount: number) => {
|
||||||
|
if (totalCount === 1) {
|
||||||
|
return account.platform === 'claude' ? 'Claude' : 'OpenAI';
|
||||||
|
}
|
||||||
|
return `${account.platform === 'claude' ? 'Claude' : 'OpenAI'} (${account.name})`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge accounts
|
||||||
|
const allAccounts: UnifiedAccount[] = [
|
||||||
|
...claudeAccounts.map(acc => ({ ...acc, platform: 'claude' as const })),
|
||||||
|
...openaiAccounts.map(acc => ({ ...acc, platform: 'openai' as const })),
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card rounded-lg shadow-sm border border-border overflow-hidden mb-6">
|
||||||
|
<div className="px-6 py-4 border-b border-border">
|
||||||
|
<h3 className="text-lg font-medium text-card-foreground">AI Accounts Status</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<svg className="animate-spin h-6 w-6 text-muted-foreground" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="ml-3 text-muted-foreground">Loading accounts...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="bg-card rounded-lg shadow-sm border border-border overflow-hidden mb-6">
|
||||||
|
<div className="px-6 py-4 border-b border-border">
|
||||||
|
<h3 className="text-lg font-medium text-card-foreground">AI Accounts Status</h3>
|
||||||
|
</div>
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-destructive">{error}</p>
|
||||||
|
<button
|
||||||
|
onClick={fetchAccounts}
|
||||||
|
className="mt-3 text-sm text-primary hover:underline"
|
||||||
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-card rounded-lg shadow-sm border border-border overflow-hidden mb-6">
|
||||||
|
<div className="px-6 py-4 border-b border-border">
|
||||||
|
<h3 className="text-lg font-medium text-card-foreground">AI Accounts Status</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{allAccounts.length > 0 ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-border">
|
||||||
|
<thead className="bg-muted/50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Account</th>
|
||||||
|
<th scope="col" className="px-3 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Status</th>
|
||||||
|
<th scope="col" className="px-3 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Daily Usage</th>
|
||||||
|
<th scope="col" className="px-3 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Usage Windows</th>
|
||||||
|
<th scope="col" className="px-3 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">Last Used</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-card divide-y divide-border">
|
||||||
|
{allAccounts.map((account) => {
|
||||||
|
const platformCount = account.platform === 'claude' ? claudeAccounts.length : openaiAccounts.length;
|
||||||
|
const isClaudeAccount = account.platform === 'claude';
|
||||||
|
const claudeAcc = isClaudeAccount ? (account as ClaudeAccount) : null;
|
||||||
|
const openaiAcc = !isClaudeAccount ? (account as OpenAIAccount) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={account.id} className="hover:bg-muted/50 transition-colors">
|
||||||
|
<td className="px-6 py-3 whitespace-nowrap">
|
||||||
|
<div className="text-sm font-medium text-card-foreground">
|
||||||
|
{getAccountName(account, platformCount)}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(account.status)}`}>
|
||||||
|
{account.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3">
|
||||||
|
{account.usage?.daily && (isClaudeAccount || account.usage.daily.requests > 0) ? (
|
||||||
|
<div className="text-xs">
|
||||||
|
<div className="text-muted-foreground">{account.usage.daily.requests} reqs</div>
|
||||||
|
<div className="text-muted-foreground">${account.usage.daily.cost.toFixed(2)} cost</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-xs">{isClaudeAccount ? 'No data' : 'No usage today'}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3">
|
||||||
|
{claudeAcc?.claudeUsage ? (
|
||||||
|
<div className="space-y-1.5 min-w-[180px]">
|
||||||
|
{claudeAcc.claudeUsage.fiveHour && (
|
||||||
|
<UsageProgressBar
|
||||||
|
label="5h Window"
|
||||||
|
resetTime={formatTime(claudeAcc.claudeUsage.fiveHour.remainingSeconds)}
|
||||||
|
percentage={claudeAcc.claudeUsage.fiveHour.utilization}
|
||||||
|
resetAfterSeconds={claudeAcc.claudeUsage.fiveHour.remainingSeconds}
|
||||||
|
resetAt={claudeAcc.claudeUsage.fiveHour.resetsAt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{claudeAcc.claudeUsage.sevenDay && (
|
||||||
|
<UsageProgressBar
|
||||||
|
label="7d Window"
|
||||||
|
resetTime={formatTime(claudeAcc.claudeUsage.sevenDay.remainingSeconds)}
|
||||||
|
percentage={claudeAcc.claudeUsage.sevenDay.utilization}
|
||||||
|
resetAfterSeconds={claudeAcc.claudeUsage.sevenDay.remainingSeconds}
|
||||||
|
resetAt={claudeAcc.claudeUsage.sevenDay.resetsAt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{claudeAcc.claudeUsage.sevenDayOpus && (
|
||||||
|
<UsageProgressBar
|
||||||
|
label="Opus Window"
|
||||||
|
resetTime={formatTime(claudeAcc.claudeUsage.sevenDayOpus.remainingSeconds)}
|
||||||
|
percentage={claudeAcc.claudeUsage.sevenDayOpus.utilization}
|
||||||
|
resetAfterSeconds={claudeAcc.claudeUsage.sevenDayOpus.remainingSeconds}
|
||||||
|
resetAt={claudeAcc.claudeUsage.sevenDayOpus.resetsAt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : openaiAcc?.codexUsage ? (
|
||||||
|
<div className="space-y-1.5 min-w-[180px]">
|
||||||
|
{openaiAcc.codexUsage.primary && (
|
||||||
|
<UsageProgressBar
|
||||||
|
label="5h Window"
|
||||||
|
resetTime={formatTime(openaiAcc.codexUsage.primary.resetAfterSeconds)}
|
||||||
|
percentage={openaiAcc.codexUsage.primary.usedPercent}
|
||||||
|
resetAfterSeconds={openaiAcc.codexUsage.primary.resetAfterSeconds}
|
||||||
|
resetAt={openaiAcc.codexUsage.primary.resetAt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{openaiAcc.codexUsage.secondary && (
|
||||||
|
<UsageProgressBar
|
||||||
|
label="7d Window"
|
||||||
|
resetTime={formatTime(openaiAcc.codexUsage.secondary.resetAfterSeconds)}
|
||||||
|
percentage={openaiAcc.codexUsage.secondary.usedPercent}
|
||||||
|
resetAfterSeconds={openaiAcc.codexUsage.secondary.resetAfterSeconds}
|
||||||
|
resetAt={openaiAcc.codexUsage.secondary.resetAt}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground text-xs">No data</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-3 py-3 text-muted-foreground text-xs text-left">{formatLastUsed(account.lastUsedAt)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
No shared AI accounts found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -193,7 +193,7 @@ export function CurrentPeriod({ period, apiKey, userId }: CurrentPeriodProps) {
|
|||||||
<h3 className="text-lg font-medium text-card-foreground mb-2">Failed to Load Period Data</h3>
|
<h3 className="text-lg font-medium text-card-foreground mb-2">Failed to Load Period Data</h3>
|
||||||
<p className="text-muted-foreground mb-4">{error}</p>
|
<p className="text-muted-foreground mb-4">{error}</p>
|
||||||
<button
|
<button
|
||||||
onClick={fetchSummary}
|
onClick={() => fetchSummary()}
|
||||||
className="bg-primary text-primary-foreground px-4 py-2 rounded-md hover:bg-primary/90 transition-colors"
|
className="bg-primary text-primary-foreground px-4 py-2 rounded-md hover:bg-primary/90 transition-colors"
|
||||||
>
|
>
|
||||||
Retry
|
Retry
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { CurrentPeriod } from './CurrentPeriod';
|
import { CurrentPeriod } from './CurrentPeriod';
|
||||||
import { HistoricalPeriods } from './HistoricalPeriods';
|
import { HistoricalPeriods } from './HistoricalPeriods';
|
||||||
|
import { AIAccounts } from './AIAccounts';
|
||||||
|
|
||||||
interface DashboardProps {
|
interface DashboardProps {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -110,6 +111,8 @@ export function Dashboard({ apiKey, userId, onLogout }: DashboardProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="container mx-auto px-4 lg:px-8 max-w-6xl py-8">
|
<div className="container mx-auto px-4 lg:px-8 max-w-6xl py-8">
|
||||||
|
<AIAccounts apiKey={apiKey} />
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="border-b border-border">
|
<div className="border-b border-border">
|
||||||
<nav className="-mb-px flex space-x-8">
|
<nav className="-mb-px flex space-x-8">
|
||||||
|
|||||||
36
client/components/UsageProgressBar.tsx
Normal file
36
client/components/UsageProgressBar.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
interface UsageProgressBarProps {
|
||||||
|
label: string;
|
||||||
|
resetTime: string;
|
||||||
|
percentage: number;
|
||||||
|
resetAfterSeconds?: number;
|
||||||
|
resetAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function UsageProgressBar({ label, resetTime, percentage, resetAfterSeconds, resetAt }: UsageProgressBarProps) {
|
||||||
|
// Check if reset time has passed
|
||||||
|
const resetElapsed =
|
||||||
|
resetAfterSeconds !== undefined && (
|
||||||
|
resetAfterSeconds <= 0 ||
|
||||||
|
(resetAt && !isNaN(Date.parse(resetAt)) && Date.now() >= Date.parse(resetAt))
|
||||||
|
);
|
||||||
|
|
||||||
|
// If reset time has passed, show 0%
|
||||||
|
const displayPercentage = resetElapsed ? 0 : percentage;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-muted-foreground mb-1">
|
||||||
|
{label}{!resetElapsed && ` · Resets in ${resetTime}`}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 h-2 bg-chart-3 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-chart-1"
|
||||||
|
style={{ width: `${displayPercentage}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground min-w-[30px]">{displayPercentage}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -67,6 +67,84 @@ interface ApiKeyBatchStats {
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI Account types
|
||||||
|
interface ClaudeAccount {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
accountType: string;
|
||||||
|
lastUsedAt: string | null;
|
||||||
|
usage?: {
|
||||||
|
daily?: {
|
||||||
|
tokens: number;
|
||||||
|
requests: number;
|
||||||
|
cost: number;
|
||||||
|
};
|
||||||
|
sessionWindow?: {
|
||||||
|
totalTokens: number;
|
||||||
|
totalRequests: number;
|
||||||
|
totalCost: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sessionWindow?: {
|
||||||
|
hasActiveWindow: boolean;
|
||||||
|
windowStart: string;
|
||||||
|
windowEnd: string;
|
||||||
|
progress: number;
|
||||||
|
remainingTime: number;
|
||||||
|
sessionWindowStatus: string;
|
||||||
|
};
|
||||||
|
claudeUsage?: {
|
||||||
|
fiveHour?: {
|
||||||
|
utilization: number;
|
||||||
|
resetsAt: string;
|
||||||
|
remainingSeconds: number;
|
||||||
|
};
|
||||||
|
sevenDay?: {
|
||||||
|
utilization: number;
|
||||||
|
resetsAt: string;
|
||||||
|
remainingSeconds: number;
|
||||||
|
};
|
||||||
|
sevenDayOpus?: {
|
||||||
|
utilization: number;
|
||||||
|
resetsAt: string;
|
||||||
|
remainingSeconds: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OpenAIAccount {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
status: string;
|
||||||
|
accountType: string;
|
||||||
|
lastUsedAt: string | null;
|
||||||
|
usage?: {
|
||||||
|
daily?: {
|
||||||
|
tokens: number;
|
||||||
|
requests: number;
|
||||||
|
cost: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
codexUsage?: {
|
||||||
|
primary?: {
|
||||||
|
usedPercent: number;
|
||||||
|
resetAfterSeconds: number;
|
||||||
|
resetAt: string;
|
||||||
|
};
|
||||||
|
secondary?: {
|
||||||
|
usedPercent: number;
|
||||||
|
resetAfterSeconds: number;
|
||||||
|
resetAt: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AIAccountsResponse {
|
||||||
|
claude: ClaudeAccount[];
|
||||||
|
openai: OpenAIAccount[];
|
||||||
|
}
|
||||||
|
|
||||||
export class ApiClient {
|
export class ApiClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
private username: string;
|
private username: string;
|
||||||
@@ -304,6 +382,123 @@ export class ApiClient {
|
|||||||
return await this.getKeyIdFromList(apiKey);
|
return await this.getKeyIdFromList(apiKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAIAccounts(): Promise<AIAccountsResponse> {
|
||||||
|
await this.ensureValidToken();
|
||||||
|
|
||||||
|
// Fetch Claude accounts
|
||||||
|
const claudeResponse = await fetch(`${this.baseUrl}/admin/claude-accounts`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!claudeResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch claude accounts: ${claudeResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const claudeData = await claudeResponse.json();
|
||||||
|
|
||||||
|
// Fetch OpenAI accounts
|
||||||
|
const openaiResponse = await fetch(`${this.baseUrl}/admin/openai-accounts`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${this.token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!openaiResponse.ok) {
|
||||||
|
throw new Error(`Failed to fetch openai accounts: ${openaiResponse.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const openaiData = await openaiResponse.json();
|
||||||
|
|
||||||
|
// Filter and sanitize Claude accounts
|
||||||
|
const claudeAccounts = (claudeData.data || [])
|
||||||
|
.filter((acc: any) => acc.accountType === 'shared')
|
||||||
|
.map((acc: any) => ({
|
||||||
|
id: acc.id,
|
||||||
|
name: acc.name,
|
||||||
|
status: acc.status,
|
||||||
|
accountType: acc.accountType,
|
||||||
|
lastUsedAt: acc.lastUsedAt,
|
||||||
|
usage: acc.usage ? {
|
||||||
|
daily: acc.usage.daily ? {
|
||||||
|
tokens: acc.usage.daily.tokens || 0,
|
||||||
|
requests: acc.usage.daily.requests || 0,
|
||||||
|
cost: acc.usage.daily.cost || 0,
|
||||||
|
} : undefined,
|
||||||
|
sessionWindow: acc.usage.sessionWindow ? {
|
||||||
|
totalTokens: acc.usage.sessionWindow.totalTokens || 0,
|
||||||
|
totalRequests: acc.usage.sessionWindow.totalRequests || 0,
|
||||||
|
totalCost: acc.usage.sessionWindow.totalCost || 0,
|
||||||
|
} : undefined,
|
||||||
|
} : undefined,
|
||||||
|
sessionWindow: acc.sessionWindow ? {
|
||||||
|
hasActiveWindow: acc.sessionWindow.hasActiveWindow || false,
|
||||||
|
windowStart: acc.sessionWindow.windowStart || '',
|
||||||
|
windowEnd: acc.sessionWindow.windowEnd || '',
|
||||||
|
progress: acc.sessionWindow.progress || 0,
|
||||||
|
remainingTime: acc.sessionWindow.remainingTime || 0,
|
||||||
|
sessionWindowStatus: acc.sessionWindow.sessionWindowStatus || 'unknown',
|
||||||
|
} : undefined,
|
||||||
|
claudeUsage: acc.claudeUsage ? {
|
||||||
|
fiveHour: acc.claudeUsage.fiveHour ? {
|
||||||
|
utilization: acc.claudeUsage.fiveHour.utilization || 0,
|
||||||
|
resetsAt: acc.claudeUsage.fiveHour.resetsAt || '',
|
||||||
|
remainingSeconds: acc.claudeUsage.fiveHour.remainingSeconds || 0,
|
||||||
|
} : undefined,
|
||||||
|
sevenDay: acc.claudeUsage.sevenDay ? {
|
||||||
|
utilization: acc.claudeUsage.sevenDay.utilization || 0,
|
||||||
|
resetsAt: acc.claudeUsage.sevenDay.resetsAt || '',
|
||||||
|
remainingSeconds: acc.claudeUsage.sevenDay.remainingSeconds || 0,
|
||||||
|
} : undefined,
|
||||||
|
sevenDayOpus: acc.claudeUsage.sevenDayOpus ? {
|
||||||
|
utilization: acc.claudeUsage.sevenDayOpus.utilization || 0,
|
||||||
|
resetsAt: acc.claudeUsage.sevenDayOpus.resetsAt || '',
|
||||||
|
remainingSeconds: acc.claudeUsage.sevenDayOpus.remainingSeconds || 0,
|
||||||
|
} : undefined,
|
||||||
|
} : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Filter and sanitize OpenAI accounts
|
||||||
|
const openaiAccounts = (openaiData.data || [])
|
||||||
|
.filter((acc: any) => acc.accountType === 'shared')
|
||||||
|
.map((acc: any) => ({
|
||||||
|
id: acc.id,
|
||||||
|
name: acc.name,
|
||||||
|
status: acc.status,
|
||||||
|
accountType: acc.accountType,
|
||||||
|
lastUsedAt: acc.lastUsedAt,
|
||||||
|
usage: acc.usage ? {
|
||||||
|
daily: acc.usage.daily ? {
|
||||||
|
tokens: acc.usage.daily.tokens || 0,
|
||||||
|
requests: acc.usage.daily.requests || 0,
|
||||||
|
cost: acc.usage.daily.cost || 0,
|
||||||
|
} : undefined,
|
||||||
|
} : undefined,
|
||||||
|
codexUsage: acc.codexUsage ? {
|
||||||
|
primary: acc.codexUsage.primary ? {
|
||||||
|
usedPercent: acc.codexUsage.primary.usedPercent || 0,
|
||||||
|
resetAfterSeconds: acc.codexUsage.primary.resetAfterSeconds || 0,
|
||||||
|
resetAt: acc.codexUsage.primary.resetAt || '',
|
||||||
|
} : undefined,
|
||||||
|
secondary: acc.codexUsage.secondary ? {
|
||||||
|
usedPercent: acc.codexUsage.secondary.usedPercent || 0,
|
||||||
|
resetAfterSeconds: acc.codexUsage.secondary.resetAfterSeconds || 0,
|
||||||
|
resetAt: acc.codexUsage.secondary.resetAt || '',
|
||||||
|
} : undefined,
|
||||||
|
} : undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
claude: claudeAccounts,
|
||||||
|
openai: openaiAccounts,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const apiClient = new ApiClient();
|
export const apiClient = new ApiClient();
|
||||||
|
|||||||
@@ -157,6 +157,31 @@ Bun.serve({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
'/api/ai-accounts': {
|
||||||
|
async GET(req: Request) {
|
||||||
|
const validation = await validateApiKey(req);
|
||||||
|
if (!validation.valid) {
|
||||||
|
return new Response(JSON.stringify({ error: validation.error }), {
|
||||||
|
status: 401,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accounts = await apiClient.getAIAccounts();
|
||||||
|
return new Response(JSON.stringify(accounts), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting AI accounts:', error);
|
||||||
|
return new Response(JSON.stringify({ error: 'Failed to get AI accounts' }), {
|
||||||
|
status: 500,
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
development: {
|
development: {
|
||||||
|
|||||||
Reference in New Issue
Block a user