- Added new model "Claude Opus 4.6" to the provider models.
This commit is contained in:
parent
39c555ca7e
commit
e8aa3e21fe
10 changed files with 159 additions and 143 deletions
|
|
@ -222,7 +222,7 @@ export const BACKOFF_CONFIG = {
|
||||||
export const COOLDOWN_MS = {
|
export const COOLDOWN_MS = {
|
||||||
unauthorized: 2 * 60 * 1000, // 401 → 30 min
|
unauthorized: 2 * 60 * 1000, // 401 → 30 min
|
||||||
paymentRequired: 2 * 60 * 1000, // 402/403 → 30 min
|
paymentRequired: 2 * 60 * 1000, // 402/403 → 30 min
|
||||||
notFound: 2 * 60 * 60 * 1000, // 404 → 12 hours
|
notFound: 2 * 60 * 1000, // 404 → 2 minutes
|
||||||
transient: 30 * 1000, // 408/500/502/503/504 → 1 min
|
transient: 30 * 1000, // 408/500/502/503/504 → 1 min
|
||||||
requestNotAllowed: 5 * 1000, // "Request not allowed" → 5 sec
|
requestNotAllowed: 5 * 1000, // "Request not allowed" → 5 sec
|
||||||
// Legacy aliases for backward compatibility
|
// Legacy aliases for backward compatibility
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
export const PROVIDER_MODELS = {
|
export const PROVIDER_MODELS = {
|
||||||
// OAuth Providers (using alias)
|
// OAuth Providers (using alias)
|
||||||
cc: [ // Claude Code
|
cc: [ // Claude Code
|
||||||
|
{ id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
||||||
{ id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus" },
|
{ id: "claude-opus-4-5-20251101", name: "Claude 4.5 Opus" },
|
||||||
{ id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" },
|
{ id: "claude-sonnet-4-5-20250929", name: "Claude 4.5 Sonnet" },
|
||||||
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
||||||
|
|
@ -52,8 +53,8 @@ export const PROVIDER_MODELS = {
|
||||||
{ id: "glm-4.7", name: "GLM 4.7" },
|
{ id: "glm-4.7", name: "GLM 4.7" },
|
||||||
],
|
],
|
||||||
ag: [ // Antigravity - special case: models call different backends
|
ag: [ // Antigravity - special case: models call different backends
|
||||||
|
// { id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
||||||
{ id: "claude-opus-4-5-thinking", name: "Claude Opus 4.5 Thinking" },
|
{ id: "claude-opus-4-5-thinking", name: "Claude Opus 4.5 Thinking" },
|
||||||
{ id: "claude-opus-4-5", name: "Claude Opus 4.5" },
|
|
||||||
{ id: "claude-sonnet-4-5-thinking", name: "Claude Sonnet 4.5 Thinking" },
|
{ id: "claude-sonnet-4-5-thinking", name: "Claude Sonnet 4.5 Thinking" },
|
||||||
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
|
{ id: "claude-sonnet-4-5", name: "Claude Sonnet 4.5" },
|
||||||
{ id: "gemini-3-pro-high", name: "Gemini 3 Pro High" },
|
{ id: "gemini-3-pro-high", name: "Gemini 3 Pro High" },
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,36 @@ export async function getUsageForProvider(connection) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse reset date/time to ISO string
|
||||||
|
* Handles multiple formats: Unix timestamp (ms), ISO date string, etc.
|
||||||
|
*/
|
||||||
|
function parseResetTime(resetValue) {
|
||||||
|
if (!resetValue) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// If it's already a Date object
|
||||||
|
if (resetValue instanceof Date) {
|
||||||
|
return resetValue.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a number (Unix timestamp in milliseconds)
|
||||||
|
if (typeof resetValue === 'number') {
|
||||||
|
return new Date(resetValue).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a string (ISO date or any parseable date string)
|
||||||
|
if (typeof resetValue === 'string') {
|
||||||
|
return new Date(resetValue).toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Failed to parse reset time: ${resetValue}`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GitHub Copilot Usage
|
* GitHub Copilot Usage
|
||||||
* Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API
|
* Uses GitHub accessToken (not copilotToken) to call copilot_internal/user API
|
||||||
|
|
@ -92,19 +122,22 @@ async function getGitHubUsage(accessToken, providerSpecificData) {
|
||||||
if (data.quota_snapshots) {
|
if (data.quota_snapshots) {
|
||||||
// Paid plan format
|
// Paid plan format
|
||||||
const snapshots = data.quota_snapshots;
|
const snapshots = data.quota_snapshots;
|
||||||
|
const resetAt = parseResetTime(data.quota_reset_date);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plan: data.copilot_plan,
|
plan: data.copilot_plan,
|
||||||
resetDate: data.quota_reset_date,
|
resetDate: data.quota_reset_date,
|
||||||
quotas: {
|
quotas: {
|
||||||
chat: formatGitHubQuotaSnapshot(snapshots.chat),
|
chat: { ...formatGitHubQuotaSnapshot(snapshots.chat), resetAt },
|
||||||
completions: formatGitHubQuotaSnapshot(snapshots.completions),
|
completions: { ...formatGitHubQuotaSnapshot(snapshots.completions), resetAt },
|
||||||
premium_interactions: formatGitHubQuotaSnapshot(snapshots.premium_interactions),
|
premium_interactions: { ...formatGitHubQuotaSnapshot(snapshots.premium_interactions), resetAt },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else if (data.monthly_quotas || data.limited_user_quotas) {
|
} else if (data.monthly_quotas || data.limited_user_quotas) {
|
||||||
// Free/limited plan format
|
// Free/limited plan format
|
||||||
const monthlyQuotas = data.monthly_quotas || {};
|
const monthlyQuotas = data.monthly_quotas || {};
|
||||||
const usedQuotas = data.limited_user_quotas || {};
|
const usedQuotas = data.limited_user_quotas || {};
|
||||||
|
const resetAt = parseResetTime(data.limited_user_reset_date);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plan: data.copilot_plan || data.access_type_sku,
|
plan: data.copilot_plan || data.access_type_sku,
|
||||||
|
|
@ -114,11 +147,13 @@ async function getGitHubUsage(accessToken, providerSpecificData) {
|
||||||
used: usedQuotas.chat || 0,
|
used: usedQuotas.chat || 0,
|
||||||
total: monthlyQuotas.chat || 0,
|
total: monthlyQuotas.chat || 0,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
|
resetAt,
|
||||||
},
|
},
|
||||||
completions: {
|
completions: {
|
||||||
used: usedQuotas.completions || 0,
|
used: usedQuotas.completions || 0,
|
||||||
total: monthlyQuotas.completions || 0,
|
total: monthlyQuotas.completions || 0,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
|
resetAt,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -236,7 +271,7 @@ async function getAntigravityUsage(accessToken, providerSpecificData) {
|
||||||
quotas[modelKey] = {
|
quotas[modelKey] = {
|
||||||
used,
|
used,
|
||||||
total,
|
total,
|
||||||
resetAt: info.quotaInfo.resetTime || null,
|
resetAt: parseResetTime(info.quotaInfo.resetTime),
|
||||||
remainingPercentage,
|
remainingPercentage,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
displayName: info.displayName || modelKey,
|
displayName: info.displayName || modelKey,
|
||||||
|
|
@ -372,13 +407,9 @@ async function getCodexUsage(accessToken) {
|
||||||
const primaryWindow = rateLimit.primary_window || {};
|
const primaryWindow = rateLimit.primary_window || {};
|
||||||
const secondaryWindow = rateLimit.secondary_window || {};
|
const secondaryWindow = rateLimit.secondary_window || {};
|
||||||
|
|
||||||
// Calculate reset dates
|
// Parse reset dates (reset_at is Unix timestamp in seconds, multiply by 1000 for ms)
|
||||||
const sessionResetAt = primaryWindow.reset_at
|
const sessionResetAt = parseResetTime(primaryWindow.reset_at ? primaryWindow.reset_at * 1000 : null);
|
||||||
? new Date(primaryWindow.reset_at * 1000).toISOString()
|
const weeklyResetAt = parseResetTime(secondaryWindow.reset_at ? secondaryWindow.reset_at * 1000 : null);
|
||||||
: null;
|
|
||||||
const weeklyResetAt = secondaryWindow.reset_at
|
|
||||||
? new Date(secondaryWindow.reset_at * 1000).toISOString()
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plan: data.plan_type || "unknown",
|
plan: data.plan_type || "unknown",
|
||||||
|
|
@ -388,14 +419,14 @@ async function getCodexUsage(accessToken) {
|
||||||
used: primaryWindow.used_percent || 0,
|
used: primaryWindow.used_percent || 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
remaining: 100 - (primaryWindow.used_percent || 0),
|
remaining: 100 - (primaryWindow.used_percent || 0),
|
||||||
resetTime: sessionResetAt,
|
resetAt: sessionResetAt,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
},
|
},
|
||||||
weekly: {
|
weekly: {
|
||||||
used: secondaryWindow.used_percent || 0,
|
used: secondaryWindow.used_percent || 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
remaining: 100 - (secondaryWindow.used_percent || 0),
|
remaining: 100 - (secondaryWindow.used_percent || 0),
|
||||||
resetTime: weeklyResetAt,
|
resetAt: weeklyResetAt,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -439,10 +470,21 @@ async function getKiroUsage(accessToken, providerSpecificData) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
|
console.log("[Kiro Usage] API Response:", JSON.stringify(data, null, 2));
|
||||||
|
|
||||||
// Parse usage data from usageBreakdownList
|
// Parse usage data from usageBreakdownList
|
||||||
const usageList = data.usageBreakdownList || [];
|
const usageList = data.usageBreakdownList || [];
|
||||||
const quotaInfo = {};
|
const quotaInfo = {};
|
||||||
|
|
||||||
|
// Parse reset time - supports multiple formats (nextDateReset, resetDate, etc.)
|
||||||
|
const resetAt = parseResetTime(data.nextDateReset || data.resetDate);
|
||||||
|
|
||||||
|
console.log("[Kiro Usage] Reset time:", {
|
||||||
|
nextDateReset: data.nextDateReset,
|
||||||
|
resetDate: data.resetDate,
|
||||||
|
parsedResetAt: resetAt
|
||||||
|
});
|
||||||
|
|
||||||
usageList.forEach((breakdown) => {
|
usageList.forEach((breakdown) => {
|
||||||
const resourceType = breakdown.resourceType?.toLowerCase() || "unknown";
|
const resourceType = breakdown.resourceType?.toLowerCase() || "unknown";
|
||||||
|
|
@ -453,7 +495,7 @@ async function getKiroUsage(accessToken, providerSpecificData) {
|
||||||
used,
|
used,
|
||||||
total,
|
total,
|
||||||
remaining: total - used,
|
remaining: total - used,
|
||||||
resetTime: data.nextDateReset ? new Date(data.nextDateReset).toISOString() : null,
|
resetAt,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -466,7 +508,7 @@ async function getKiroUsage(accessToken, providerSpecificData) {
|
||||||
used: freeUsed,
|
used: freeUsed,
|
||||||
total: freeTotal,
|
total: freeTotal,
|
||||||
remaining: freeTotal - freeUsed,
|
remaining: freeTotal - freeUsed,
|
||||||
resetTime: data.nextDateReset ? new Date(data.nextDateReset).toISOString() : null,
|
resetAt,
|
||||||
unlimited: false,
|
unlimited: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,6 @@ import { formatResetTime } from "./utils";
|
||||||
|
|
||||||
// Calculate color based on remaining percentage
|
// Calculate color based on remaining percentage
|
||||||
const getColorClasses = (remainingPercentage) => {
|
const getColorClasses = (remainingPercentage) => {
|
||||||
if (remainingPercentage === 0) {
|
|
||||||
return {
|
|
||||||
text: "text-gray-400",
|
|
||||||
bg: "bg-gray-400",
|
|
||||||
bgLight: "bg-gray-400/10",
|
|
||||||
emoji: "⚫"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingPercentage > 70) {
|
if (remainingPercentage > 70) {
|
||||||
return {
|
return {
|
||||||
text: "text-green-500",
|
text: "text-green-500",
|
||||||
|
|
@ -32,6 +23,7 @@ const getColorClasses = (remainingPercentage) => {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0-29% including 0% (out of quota) - show red
|
||||||
return {
|
return {
|
||||||
text: "text-red-500",
|
text: "text-red-500",
|
||||||
bg: "bg-red-500",
|
bg: "bg-red-500",
|
||||||
|
|
|
||||||
|
|
@ -40,15 +40,6 @@ function formatResetTimeDisplay(resetTime) {
|
||||||
* Get color classes based on remaining percentage
|
* Get color classes based on remaining percentage
|
||||||
*/
|
*/
|
||||||
function getColorClasses(remainingPercentage) {
|
function getColorClasses(remainingPercentage) {
|
||||||
if (remainingPercentage === 0) {
|
|
||||||
return {
|
|
||||||
text: "text-text-muted",
|
|
||||||
bg: "bg-bg-muted",
|
|
||||||
bgLight: "bg-bg-muted/20",
|
|
||||||
emoji: "⚫"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingPercentage > 70) {
|
if (remainingPercentage > 70) {
|
||||||
return {
|
return {
|
||||||
text: "text-green-600 dark:text-green-400",
|
text: "text-green-600 dark:text-green-400",
|
||||||
|
|
@ -67,6 +58,7 @@ function getColorClasses(remainingPercentage) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 0-29% including 0% (out of quota) - show red
|
||||||
return {
|
return {
|
||||||
text: "text-red-600 dark:text-red-400",
|
text: "text-red-600 dark:text-red-400",
|
||||||
bg: "bg-red-500",
|
bg: "bg-red-500",
|
||||||
|
|
@ -85,7 +77,12 @@ export default function QuotaTable({ quotas = [] }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full">
|
<table className="w-full table-fixed">
|
||||||
|
<colgroup>
|
||||||
|
<col className="w-[30%]" /> {/* Model Name */}
|
||||||
|
<col className="w-[45%]" /> {/* Limit Progress */}
|
||||||
|
<col className="w-[25%]" /> {/* Reset Time */}
|
||||||
|
</colgroup>
|
||||||
<tbody>
|
<tbody>
|
||||||
{quotas.map((quota, index) => {
|
{quotas.map((quota, index) => {
|
||||||
const remaining = quota.remainingPercentage !== undefined
|
const remaining = quota.remainingPercentage !== undefined
|
||||||
|
|
@ -136,18 +133,22 @@ export default function QuotaTable({ quotas = [] }) {
|
||||||
|
|
||||||
{/* Reset Time */}
|
{/* Reset Time */}
|
||||||
<td className="py-2 px-3">
|
<td className="py-2 px-3">
|
||||||
<div className="space-y-0.5">
|
{countdown !== "-" || resetDisplay ? (
|
||||||
{countdown !== "-" && (
|
<div className="space-y-0.5">
|
||||||
<div className="text-sm text-text-primary font-medium">
|
{countdown !== "-" && (
|
||||||
in {countdown}
|
<div className="text-sm text-text-primary font-medium">
|
||||||
</div>
|
in {countdown}
|
||||||
)}
|
</div>
|
||||||
{resetDisplay && (
|
)}
|
||||||
<div className="text-xs text-text-muted">
|
{resetDisplay && (
|
||||||
{resetDisplay}
|
<div className="text-xs text-text-muted">
|
||||||
</div>
|
{resetDisplay}
|
||||||
)}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-text-muted italic">N/A</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -358,85 +358,68 @@ export default function ProviderLimits() {
|
||||||
const isLoading = loading[conn.id];
|
const isLoading = loading[conn.id];
|
||||||
const error = errors[conn.id];
|
const error = errors[conn.id];
|
||||||
|
|
||||||
// Use table layout for Antigravity and Kiro, card layout for others
|
// Use table layout for all providers
|
||||||
if (conn.provider === "antigravity" || conn.provider === "kiro") {
|
|
||||||
return (
|
|
||||||
<Card key={conn.id} padding="none">
|
|
||||||
<div className="p-6 border-b border-black/10 dark:border-white/10">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-10 h-10 rounded-lg flex items-center justify-center overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={`/providers/${conn.provider}.png`}
|
|
||||||
alt={conn.provider}
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
className="object-contain"
|
|
||||||
sizes="40px"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="text-base font-semibold text-text-primary capitalize">
|
|
||||||
{conn.provider}
|
|
||||||
</h3>
|
|
||||||
{conn.name && (
|
|
||||||
<p className="text-sm text-text-muted">{conn.name}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => refreshProvider(conn.id, conn.provider)}
|
|
||||||
disabled={isLoading}
|
|
||||||
className="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
|
|
||||||
title="Refresh quota"
|
|
||||||
>
|
|
||||||
<span className={`material-symbols-outlined text-[20px] text-text-muted ${isLoading ? "animate-spin" : ""}`}>
|
|
||||||
refresh
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6">
|
|
||||||
{isLoading ? (
|
|
||||||
<div className="text-center py-8 text-text-muted">
|
|
||||||
<span className="material-symbols-outlined text-[32px] animate-spin">
|
|
||||||
progress_activity
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<span className="material-symbols-outlined text-[32px] text-red-500">
|
|
||||||
error
|
|
||||||
</span>
|
|
||||||
<p className="mt-2 text-sm text-text-muted">{error}</p>
|
|
||||||
</div>
|
|
||||||
) : quota?.message ? (
|
|
||||||
<div className="text-center py-8">
|
|
||||||
<p className="text-sm text-text-muted">{quota.message}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<QuotaTable quotas={quota?.quotas} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use card layout for other providers
|
|
||||||
return (
|
return (
|
||||||
<ProviderLimitCard
|
<Card key={conn.id} padding="none">
|
||||||
key={conn.id}
|
<div className="p-6 border-b border-black/10 dark:border-white/10">
|
||||||
provider={conn.provider}
|
<div className="flex items-center justify-between">
|
||||||
name={conn.name}
|
<div className="flex items-center gap-3">
|
||||||
plan={quota?.plan}
|
<div className="w-10 h-10 rounded-lg flex items-center justify-center overflow-hidden">
|
||||||
quotas={quota?.quotas}
|
<Image
|
||||||
message={quota?.message}
|
src={`/providers/${conn.provider}.png`}
|
||||||
loading={isLoading}
|
alt={conn.provider}
|
||||||
error={error}
|
width={40}
|
||||||
onRefresh={() => refreshProvider(conn.id, conn.provider)}
|
height={40}
|
||||||
/>
|
className="object-contain"
|
||||||
|
sizes="40px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-base font-semibold text-text-primary capitalize">
|
||||||
|
{conn.provider}
|
||||||
|
</h3>
|
||||||
|
{conn.name && (
|
||||||
|
<p className="text-sm text-text-muted">{conn.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => refreshProvider(conn.id, conn.provider)}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="p-2 rounded-lg hover:bg-black/5 dark:hover:bg-white/5 transition-colors disabled:opacity-50"
|
||||||
|
title="Refresh quota"
|
||||||
|
>
|
||||||
|
<span className={`material-symbols-outlined text-[20px] text-text-muted ${isLoading ? "animate-spin" : ""}`}>
|
||||||
|
refresh
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="text-center py-8 text-text-muted">
|
||||||
|
<span className="material-symbols-outlined text-[32px] animate-spin">
|
||||||
|
progress_activity
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : error ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<span className="material-symbols-outlined text-[32px] text-red-500">
|
||||||
|
error
|
||||||
|
</span>
|
||||||
|
<p className="mt-2 text-sm text-text-muted">{error}</p>
|
||||||
|
</div>
|
||||||
|
) : quota?.message ? (
|
||||||
|
<div className="text-center py-8">
|
||||||
|
<p className="text-sm text-text-muted">{quota.message}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<QuotaTable quotas={quota?.quotas} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -42,31 +42,23 @@ export function formatResetTime(date) {
|
||||||
/**
|
/**
|
||||||
* Get Tailwind color class based on percentage
|
* Get Tailwind color class based on percentage
|
||||||
* @param {number} percentage - Remaining percentage (0-100)
|
* @param {number} percentage - Remaining percentage (0-100)
|
||||||
* @returns {string} Color name: "green" | "yellow" | "red" | "gray"
|
* @returns {string} Color name: "green" | "yellow" | "red"
|
||||||
*/
|
*/
|
||||||
export function getStatusColor(percentage) {
|
export function getStatusColor(percentage) {
|
||||||
if (percentage === 0 || percentage === null || percentage === undefined) {
|
|
||||||
return "gray";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (percentage > 70) return "green";
|
if (percentage > 70) return "green";
|
||||||
if (percentage >= 30) return "yellow";
|
if (percentage >= 30) return "yellow";
|
||||||
return "red";
|
return "red"; // 0-29% including 0% (out of quota) - show red
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get status emoji based on percentage
|
* Get status emoji based on percentage
|
||||||
* @param {number} percentage - Remaining percentage (0-100)
|
* @param {number} percentage - Remaining percentage (0-100)
|
||||||
* @returns {string} Emoji: "🟢" | "🟡" | "🔴" | "⚫"
|
* @returns {string} Emoji: "🟢" | "🟡" | "🔴"
|
||||||
*/
|
*/
|
||||||
export function getStatusEmoji(percentage) {
|
export function getStatusEmoji(percentage) {
|
||||||
if (percentage === 0 || percentage === null || percentage === undefined) {
|
|
||||||
return "⚫";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (percentage > 70) return "🟢";
|
if (percentage > 70) return "🟢";
|
||||||
if (percentage >= 30) return "🟡";
|
if (percentage >= 30) return "🟡";
|
||||||
return "🔴";
|
return "🔴"; // 0-29% including 0% (out of quota) - show red
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -76,9 +76,12 @@ export default function Sidebar({ onClose }) {
|
||||||
<div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#f97815] to-[#c2590a]">
|
<div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#f97815] to-[#c2590a]">
|
||||||
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
|
<span className="material-symbols-outlined text-white text-[20px]">hub</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-lg font-semibold tracking-tight text-text-main">
|
<div className="flex flex-col">
|
||||||
{APP_CONFIG.name}
|
<h1 className="text-lg font-semibold tracking-tight text-text-main">
|
||||||
</h1>
|
{APP_CONFIG.name}
|
||||||
|
</h1>
|
||||||
|
<span className="text-xs text-text-muted">v{APP_CONFIG.version}</span>
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
import pkg from "../../../../package.json" with { type: "json" };
|
||||||
|
|
||||||
// App configuration
|
// App configuration
|
||||||
export const APP_CONFIG = {
|
export const APP_CONFIG = {
|
||||||
name: "Endpoint Proxy",
|
name: "Endpoint Proxy",
|
||||||
description: "AI Infrastructure Management",
|
description: "AI Infrastructure Management",
|
||||||
version: "1.0.0",
|
version: pkg.version,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Theme configuration
|
// Theme configuration
|
||||||
|
|
|
||||||
|
|
@ -78,4 +78,4 @@ export const ID_TO_ALIAS = Object.values(AI_PROVIDERS).reduce((acc, p) => {
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
// Providers that support usage/quota API
|
// Providers that support usage/quota API
|
||||||
export const USAGE_SUPPORTED_PROVIDERS = ["antigravity", "kiro", "github", "claude"];
|
export const USAGE_SUPPORTED_PROVIDERS = ["antigravity", "kiro", "github"];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue