mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
feat(executors): Improved UI components for displaying provider limits and usage statistics in the dashboard.
This commit is contained in:
@@ -155,7 +155,10 @@ export default function ProviderLimitCard({
|
||||
{!loading && !error && !message && quotas?.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{quotas.map((quota, index) => {
|
||||
const percentage = calculatePercentage(quota.used, quota.total);
|
||||
// For Antigravity, use remainingPercentage if available, otherwise calculate
|
||||
const percentage = quota.remainingPercentage !== undefined
|
||||
? Math.round((quota.total - quota.used) / quota.total * 100)
|
||||
: calculatePercentage(quota.used, quota.total);
|
||||
const unlimited = quota.total === 0 || quota.total === null;
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,61 +1,76 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/shared/utils/cn";
|
||||
|
||||
// Helper function to calculate time until reset
|
||||
const getResetTimeText = (resetTime) => {
|
||||
if (!resetTime) return null;
|
||||
|
||||
const now = new Date();
|
||||
const reset = new Date(resetTime);
|
||||
const diffMs = reset - now;
|
||||
|
||||
if (diffMs <= 0) return "Reset now";
|
||||
|
||||
const hours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (hours > 0) {
|
||||
return `Reset in ${hours}h`;
|
||||
}
|
||||
return `Reset in ${minutes}m`;
|
||||
};
|
||||
import { formatResetTime } from "./utils";
|
||||
|
||||
// Calculate color based on remaining percentage
|
||||
const getColorClasses = (percentage) => {
|
||||
if (percentage === 0) {
|
||||
const getColorClasses = (remainingPercentage) => {
|
||||
if (remainingPercentage === 0) {
|
||||
return {
|
||||
text: "text-gray-400",
|
||||
bg: "bg-gray-400",
|
||||
bgLight: "bg-gray-400/10"
|
||||
bgLight: "bg-gray-400/10",
|
||||
emoji: "⚫"
|
||||
};
|
||||
}
|
||||
|
||||
const remaining = 100 - percentage;
|
||||
|
||||
if (remaining > 70) {
|
||||
if (remainingPercentage > 70) {
|
||||
return {
|
||||
text: "text-green-500",
|
||||
bg: "bg-green-500",
|
||||
bgLight: "bg-green-500/10"
|
||||
bgLight: "bg-green-500/10",
|
||||
emoji: "🟢"
|
||||
};
|
||||
}
|
||||
|
||||
if (remaining >= 30) {
|
||||
if (remainingPercentage >= 30) {
|
||||
return {
|
||||
text: "text-yellow-500",
|
||||
bg: "bg-yellow-500",
|
||||
bgLight: "bg-yellow-500/10"
|
||||
bgLight: "bg-yellow-500/10",
|
||||
emoji: "🟡"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: "text-red-500",
|
||||
bg: "bg-red-500",
|
||||
bgLight: "bg-red-500/10"
|
||||
bgLight: "bg-red-500/10",
|
||||
emoji: "🔴"
|
||||
};
|
||||
};
|
||||
|
||||
// Format reset time display
|
||||
const formatResetTimeDisplay = (resetTime) => {
|
||||
if (!resetTime) return null;
|
||||
|
||||
try {
|
||||
const resetDate = new Date(resetTime);
|
||||
const now = new Date();
|
||||
const isToday = resetDate.toDateString() === now.toDateString();
|
||||
const isTomorrow = resetDate.toDateString() === new Date(now.getTime() + 86400000).toDateString();
|
||||
|
||||
const timeStr = resetDate.toLocaleTimeString(undefined, {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
|
||||
if (isToday) return `Today, ${timeStr}`;
|
||||
if (isTomorrow) return `Tomorrow, ${timeStr}`;
|
||||
|
||||
return resetDate.toLocaleString(undefined, {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default function QuotaProgressBar({
|
||||
percentage = 0,
|
||||
label = "",
|
||||
@@ -65,29 +80,24 @@ export default function QuotaProgressBar({
|
||||
resetTime = null
|
||||
}) {
|
||||
const colors = getColorClasses(percentage);
|
||||
const resetText = getResetTimeText(resetTime);
|
||||
const countdown = formatResetTime(resetTime);
|
||||
const resetDisplay = formatResetTimeDisplay(resetTime);
|
||||
|
||||
// percentage is already remaining percentage (from ProviderLimitCard)
|
||||
const remaining = percentage;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* Label and usage info */}
|
||||
{/* Label and percentage */}
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium text-text-primary dark:text-white">
|
||||
<span className="font-semibold text-text-primary">
|
||||
{label}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
{unlimited ? (
|
||||
<span>Unlimited</span>
|
||||
) : (
|
||||
<span>
|
||||
{used.toLocaleString()}/{total.toLocaleString()} ({percentage}%)
|
||||
</span>
|
||||
)}
|
||||
{resetText && (
|
||||
<>
|
||||
<span>•</span>
|
||||
<span className="text-xs">{resetText}</span>
|
||||
</>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs">{colors.emoji}</span>
|
||||
<span className={cn("font-medium", colors.text)}>
|
||||
{remaining}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,10 +106,30 @@ export default function QuotaProgressBar({
|
||||
<div className={cn("h-2 rounded-full overflow-hidden", colors.bgLight)}>
|
||||
<div
|
||||
className={cn("h-full transition-all duration-300", colors.bg)}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
style={{ width: `${Math.min(remaining, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Usage details and countdown */}
|
||||
<div className="flex items-center justify-between text-xs text-text-muted">
|
||||
<span>
|
||||
{used.toLocaleString()} / {total.toLocaleString()} requests
|
||||
</span>
|
||||
{countdown !== "-" && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span>•</span>
|
||||
<span className="font-medium">Reset in {countdown}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Reset time display */}
|
||||
{resetDisplay && (
|
||||
<div className="text-xs text-text-muted/70">
|
||||
Reset at {resetDisplay}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import { formatResetTime, calculatePercentage } from "./utils";
|
||||
|
||||
/**
|
||||
* Format reset time display (Today, 12:00 PM)
|
||||
*/
|
||||
function formatResetTimeDisplay(resetTime) {
|
||||
if (!resetTime) return null;
|
||||
|
||||
try {
|
||||
const date = new Date(resetTime);
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const tomorrow = new Date(today);
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
let dayStr = "";
|
||||
if (date >= today && date < tomorrow) {
|
||||
dayStr = "Today";
|
||||
} else if (date >= tomorrow && date < new Date(tomorrow.getTime() + 24 * 60 * 60 * 1000)) {
|
||||
dayStr = "Tomorrow";
|
||||
} else {
|
||||
dayStr = date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
const timeStr = date.toLocaleTimeString("en-US", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true
|
||||
});
|
||||
|
||||
return `${dayStr}, ${timeStr}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color classes based on remaining percentage
|
||||
*/
|
||||
function getColorClasses(remainingPercentage) {
|
||||
if (remainingPercentage === 0) {
|
||||
return {
|
||||
text: "text-text-muted",
|
||||
bg: "bg-bg-muted",
|
||||
bgLight: "bg-bg-muted/20",
|
||||
emoji: "⚫"
|
||||
};
|
||||
}
|
||||
|
||||
if (remainingPercentage > 70) {
|
||||
return {
|
||||
text: "text-green-600 dark:text-green-400",
|
||||
bg: "bg-green-500",
|
||||
bgLight: "bg-green-500/10",
|
||||
emoji: "🟢"
|
||||
};
|
||||
}
|
||||
|
||||
if (remainingPercentage >= 30) {
|
||||
return {
|
||||
text: "text-yellow-600 dark:text-yellow-400",
|
||||
bg: "bg-yellow-500",
|
||||
bgLight: "bg-yellow-500/10",
|
||||
emoji: "🟡"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
text: "text-red-600 dark:text-red-400",
|
||||
bg: "bg-red-500",
|
||||
bgLight: "bg-red-500/10",
|
||||
emoji: "🔴"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Quota Table Component - Table-based display for quota data
|
||||
*/
|
||||
export default function QuotaTable({ quotas = [] }) {
|
||||
if (!quotas || quotas.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<tbody>
|
||||
{quotas.map((quota, index) => {
|
||||
const remaining = quota.remainingPercentage !== undefined
|
||||
? Math.round(quota.remainingPercentage)
|
||||
: calculatePercentage(quota.used, quota.total);
|
||||
|
||||
const colors = getColorClasses(remaining);
|
||||
const countdown = formatResetTime(quota.resetAt);
|
||||
const resetDisplay = formatResetTimeDisplay(quota.resetAt);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={index}
|
||||
className="border-b border-black/5 dark:border-white/5 hover:bg-black/[0.02] dark:hover:bg-white/[0.02] transition-colors"
|
||||
>
|
||||
{/* Model Name with Status Emoji */}
|
||||
<td className="py-2 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs">{colors.emoji}</span>
|
||||
<span className="text-sm font-medium text-text-primary">{quota.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Limit (Progress + Numbers) */}
|
||||
<td className="py-2 px-3">
|
||||
<div className="space-y-1.5">
|
||||
{/* Progress bar - always show with border for visibility */}
|
||||
<div className={`h-1.5 rounded-full overflow-hidden border ${colors.bgLight} ${
|
||||
remaining === 0 ? 'border-black/10 dark:border-white/10' : 'border-transparent'
|
||||
}`}>
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${colors.bg}`}
|
||||
style={{ width: `${Math.min(remaining, 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Numbers */}
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="text-text-muted">
|
||||
{quota.used.toLocaleString()} / {quota.total > 0 ? quota.total.toLocaleString() : "∞"}
|
||||
</span>
|
||||
<span className={`font-medium ${colors.text}`}>
|
||||
{remaining}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Reset Time */}
|
||||
<td className="py-2 px-3">
|
||||
<div className="space-y-0.5">
|
||||
{countdown !== "-" && (
|
||||
<div className="text-sm text-text-primary font-medium">
|
||||
in {countdown}
|
||||
</div>
|
||||
)}
|
||||
{resetDisplay && (
|
||||
<div className="text-xs text-text-muted">
|
||||
{resetDisplay}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import Image from "next/image";
|
||||
import ProviderLimitCard from "./ProviderLimitCard";
|
||||
import QuotaTable from "./QuotaTable";
|
||||
import { parseQuotaData, calculatePercentage } from "./utils";
|
||||
import Card from "@/shared/components/Card";
|
||||
import Button from "@/shared/components/Button";
|
||||
import { CardSkeleton } from "@/shared/components/Loading";
|
||||
import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
||||
|
||||
const REFRESH_INTERVAL_MS = 60000; // 60 seconds
|
||||
|
||||
@@ -48,8 +51,32 @@ export default function ProviderLimits() {
|
||||
try {
|
||||
console.log(`[ProviderLimits] Fetching quota for ${provider} (${connectionId})`);
|
||||
const response = await fetch(`/api/usage/${connectionId}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
const errorMsg = errorData.error || response.statusText;
|
||||
|
||||
// Handle different error types gracefully
|
||||
if (response.status === 404) {
|
||||
// Connection not found - skip silently
|
||||
console.warn(`[ProviderLimits] Connection not found for ${provider}, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
// Auth error - show message instead of throwing
|
||||
console.warn(`[ProviderLimits] Auth error for ${provider}:`, errorMsg);
|
||||
setQuotaData((prev) => ({
|
||||
...prev,
|
||||
[connectionId]: {
|
||||
quotas: [],
|
||||
message: errorMsg,
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`HTTP ${response.status}: ${errorMsg}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -97,9 +124,14 @@ export default function ProviderLimits() {
|
||||
try {
|
||||
const conns = await fetchConnections();
|
||||
|
||||
// Fetch quota for all connections (filter by provider support in parseQuotaData)
|
||||
// Filter only supported OAuth providers
|
||||
const oauthConnections = conns.filter(
|
||||
(conn) => USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth"
|
||||
);
|
||||
|
||||
// Fetch quota for supported OAuth connections only
|
||||
await Promise.all(
|
||||
conns.map((conn) => fetchQuota(conn.id, conn.provider))
|
||||
oauthConnections.map((conn) => fetchQuota(conn.id, conn.provider))
|
||||
);
|
||||
|
||||
setLastUpdated(new Date());
|
||||
@@ -198,13 +230,31 @@ export default function ProviderLimits() {
|
||||
}, [lastUpdated]);
|
||||
|
||||
// Filter only supported providers
|
||||
const supportedProviders = ["antigravity", "kiro", "github", "claude"];
|
||||
const filteredConnections = connections.filter((conn) =>
|
||||
supportedProviders.includes(conn.provider)
|
||||
USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth"
|
||||
);
|
||||
|
||||
// Sort providers: antigravity first, then kiro, then others alphabetically
|
||||
const sortedConnections = [...filteredConnections].sort((a, b) => {
|
||||
const getProviderPriority = (provider) => {
|
||||
if (provider === "antigravity") return 1;
|
||||
if (provider === "kiro") return 2;
|
||||
return 3;
|
||||
};
|
||||
|
||||
const priorityA = getProviderPriority(a.provider);
|
||||
const priorityB = getProviderPriority(b.provider);
|
||||
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA - priorityB;
|
||||
}
|
||||
|
||||
// Same priority: sort alphabetically
|
||||
return a.provider.localeCompare(b.provider);
|
||||
});
|
||||
|
||||
// Calculate summary stats
|
||||
const totalProviders = filteredConnections.length;
|
||||
const totalProviders = sortedConnections.length;
|
||||
const activeWithLimits = Object.values(quotaData).filter(
|
||||
(data) => data?.quotas?.length > 0
|
||||
).length;
|
||||
@@ -236,7 +286,7 @@ export default function ProviderLimits() {
|
||||
}
|
||||
|
||||
// Empty state
|
||||
if (filteredConnections.length === 0) {
|
||||
if (sortedConnections.length === 0) {
|
||||
return (
|
||||
<Card padding="lg">
|
||||
<div className="text-center py-12">
|
||||
@@ -303,11 +353,78 @@ export default function ProviderLimits() {
|
||||
|
||||
{/* Provider Cards Grid */}
|
||||
<div className="flex flex-col gap-4">
|
||||
{filteredConnections.map((conn) => {
|
||||
{sortedConnections.map((conn) => {
|
||||
const quota = quotaData[conn.id];
|
||||
const isLoading = loading[conn.id];
|
||||
const error = errors[conn.id];
|
||||
|
||||
// Use table layout for Antigravity and Kiro, card layout for others
|
||||
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 (
|
||||
<ProviderLimitCard
|
||||
key={conn.id}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { getModelsByProviderId } from "open-sse/config/providerModels.js";
|
||||
|
||||
/**
|
||||
* Format ISO date string to countdown format
|
||||
* Format ISO date string to countdown format (inspired by vscode-antigravity-cockpit)
|
||||
* @param {string|Date} date - ISO date string or Date object
|
||||
* @returns {string} Formatted countdown (e.g., "5d 12h", "2h 30m", "15m") or "-"
|
||||
* @returns {string} Formatted countdown (e.g., "2d 5h 30m", "4h 40m", "15m") or "-"
|
||||
*/
|
||||
export function formatResetTime(date) {
|
||||
if (!date) return "-";
|
||||
@@ -13,23 +15,25 @@ export function formatResetTime(date) {
|
||||
|
||||
if (diffMs <= 0) return "-";
|
||||
|
||||
const totalMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
const totalMinutes = Math.ceil(diffMs / (1000 * 60));
|
||||
|
||||
// < 60 minutes: show only minutes
|
||||
if (totalMinutes < 60) {
|
||||
return `${totalMinutes}m`;
|
||||
}
|
||||
|
||||
const totalHours = Math.floor(totalMinutes / 60);
|
||||
const totalDays = Math.floor(totalHours / 24);
|
||||
|
||||
const days = totalDays;
|
||||
const hours = totalHours % 24;
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
if (days > 0) {
|
||||
return `${days}d ${hours}h`;
|
||||
const remainingMinutes = totalMinutes % 60;
|
||||
|
||||
// < 24 hours: show hours and minutes
|
||||
if (totalHours < 24) {
|
||||
return `${totalHours}h ${remainingMinutes}m`;
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
|
||||
return `${minutes}m`;
|
||||
|
||||
// >= 24 hours: show days, hours, and minutes
|
||||
const days = Math.floor(totalHours / 24);
|
||||
const remainingHours = totalHours % 24;
|
||||
return `${days}d ${remainingHours}h ${remainingMinutes}m`;
|
||||
} catch (error) {
|
||||
return "-";
|
||||
}
|
||||
@@ -107,12 +111,14 @@ export function parseQuotaData(provider, data) {
|
||||
|
||||
case "antigravity":
|
||||
if (data.quotas) {
|
||||
Object.entries(data.quotas).forEach(([modelName, quota]) => {
|
||||
Object.entries(data.quotas).forEach(([modelKey, quota]) => {
|
||||
normalizedQuotas.push({
|
||||
name: modelName,
|
||||
name: quota.displayName || modelKey,
|
||||
modelKey: modelKey, // Keep modelKey for sorting
|
||||
used: quota.used || 0,
|
||||
total: quota.total || 0,
|
||||
resetAt: quota.resetAt || null,
|
||||
remainingPercentage: quota.remainingPercentage,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -184,5 +190,20 @@ export function parseQuotaData(provider, data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Sort quotas according to PROVIDER_MODELS order
|
||||
const modelOrder = getModelsByProviderId(provider);
|
||||
if (modelOrder.length > 0) {
|
||||
const orderMap = new Map(modelOrder.map((m, i) => [m.id, i]));
|
||||
|
||||
normalizedQuotas.sort((a, b) => {
|
||||
// Use modelKey for antigravity, otherwise use name
|
||||
const keyA = a.modelKey || a.name;
|
||||
const keyB = b.modelKey || b.name;
|
||||
const orderA = orderMap.get(keyA) ?? 999;
|
||||
const orderB = orderMap.get(keyB) ?? 999;
|
||||
return orderA - orderB;
|
||||
});
|
||||
}
|
||||
|
||||
return normalizedQuotas;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user