- Added new model "Claude Opus 4.6" to the provider models.

This commit is contained in:
decolua
2026-02-06 11:23:08 +07:00
parent 39c555ca7e
commit e8aa3e21fe
10 changed files with 159 additions and 143 deletions

View File

@@ -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

View File

@@ -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" },

View File

@@ -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,
}, },
}, },
@@ -440,10 +471,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";
const used = breakdown.currentUsageWithPrecision || 0; const used = breakdown.currentUsageWithPrecision || 0;
@@ -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,
}; };
} }

View File

@@ -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",

View File

@@ -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,6 +133,7 @@ export default function QuotaTable({ quotas = [] }) {
{/* Reset Time */} {/* Reset Time */}
<td className="py-2 px-3"> <td className="py-2 px-3">
{countdown !== "-" || resetDisplay ? (
<div className="space-y-0.5"> <div className="space-y-0.5">
{countdown !== "-" && ( {countdown !== "-" && (
<div className="text-sm text-text-primary font-medium"> <div className="text-sm text-text-primary font-medium">
@@ -148,6 +146,9 @@ export default function QuotaTable({ quotas = [] }) {
</div> </div>
)} )}
</div> </div>
) : (
<div className="text-sm text-text-muted italic">N/A</div>
)}
</td> </td>
</tr> </tr>
); );

View File

@@ -358,8 +358,7 @@ 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 ( return (
<Card key={conn.id} padding="none"> <Card key={conn.id} padding="none">
<div className="p-6 border-b border-black/10 dark:border-white/10"> <div className="p-6 border-b border-black/10 dark:border-white/10">
@@ -422,22 +421,6 @@ export default function ProviderLimits() {
</div> </div>
</Card> </Card>
); );
}
// Use card layout for other providers
return (
<ProviderLimitCard
key={conn.id}
provider={conn.provider}
name={conn.name}
plan={quota?.plan}
quotas={quota?.quotas}
message={quota?.message}
loading={isLoading}
error={error}
onRefresh={() => refreshProvider(conn.id, conn.provider)}
/>
);
})} })}
</div> </div>
</div> </div>

View File

@@ -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
} }
/** /**

View File

@@ -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>
<div className="flex flex-col">
<h1 className="text-lg font-semibold tracking-tight text-text-main"> <h1 className="text-lg font-semibold tracking-tight text-text-main">
{APP_CONFIG.name} {APP_CONFIG.name}
</h1> </h1>
<span className="text-xs text-text-muted">v{APP_CONFIG.version}</span>
</div>
</Link> </Link>
</div> </div>

View File

@@ -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

View File

@@ -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"];