diff --git a/open-sse/config/providerModels.js b/open-sse/config/providerModels.js index ae8d8d1..288cbd2 100644 --- a/open-sse/config/providerModels.js +++ b/open-sse/config/providerModels.js @@ -39,7 +39,6 @@ export const PROVIDER_MODELS = { { id: "deepseek-v3.2-chat", name: "DeepSeek V3.2 Chat" }, { id: "deepseek-v3.2-reasoner", name: "DeepSeek V3.2 Reasoner" }, { id: "minimax-m2", name: "MiniMax M2" }, - { id: "glm-4.6", name: "GLM 4.6" }, { id: "glm-4.7", name: "GLM 4.7" }, ], ag: [ // Antigravity - special case: models call different backends @@ -70,7 +69,7 @@ export const PROVIDER_MODELS = { { id: "grok-code-fast-1", name: "Grok Code Fast 1" }, ], kr: [ // Kiro AI - { id: "claude-opus-4.5", name: "Claude Opus 4.5" }, + // { id: "claude-opus-4.5", name: "Claude Opus 4.5" }, { id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5" }, { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" }, ], @@ -99,7 +98,6 @@ export const PROVIDER_MODELS = { ], glm: [ { id: "glm-4.7", name: "GLM 4.7" }, - { id: "glm-4.6", name: "GLM 4.6" }, { id: "glm-4.6v", name: "GLM 4.6V (Vision)" }, ], kimi: [ diff --git a/open-sse/translator/index.js b/open-sse/translator/index.js index 0674c16..7555f5f 100644 --- a/open-sse/translator/index.js +++ b/open-sse/translator/index.js @@ -8,6 +8,9 @@ import { normalizeThinkingConfig } from "../services/provider.js"; const requestRegistry = new Map(); const responseRegistry = new Map(); +// Track initialization state +let initialized = false; + // Register translator export function register(from, to, requestFn, responseFn) { const key = `${from}:${to}`; @@ -19,8 +22,30 @@ export function register(from, to, requestFn, responseFn) { } } +// Lazy load translators (called once on first use) +function ensureInitialized() { + if (initialized) return; + initialized = true; + + // Request translators - sync require pattern for bundler + require("./request/claude-to-openai.js"); + require("./request/openai-to-claude.js"); + require("./request/gemini-to-openai.js"); + require("./request/openai-to-gemini.js"); + require("./request/openai-responses.js"); + require("./request/openai-to-kiro.js"); + + // Response translators + require("./response/claude-to-openai.js"); + require("./response/openai-to-claude.js"); + require("./response/gemini-to-openai.js"); + require("./response/openai-responses.js"); + require("./response/kiro-to-openai.js"); +} + // Translate request: source -> openai -> target export function translateRequest(sourceFormat, targetFormat, model, body, stream = true, credentials = null, provider = null) { + ensureInitialized(); let result = body; // Normalize thinking config: remove if lastMessage is not user @@ -66,6 +91,7 @@ export function translateRequest(sourceFormat, targetFormat, model, body, stream // Translate response chunk: target -> openai -> source export function translateResponse(targetFormat, sourceFormat, chunk, state) { + ensureInitialized(); // If same format, return as-is if (sourceFormat === targetFormat) { return [chunk]; @@ -155,20 +181,7 @@ export function initState(sourceFormat) { return base; } -// Initialize all translators -export async function initTranslators() { - // Request translators - await import("./request/claude-to-openai.js"); - await import("./request/openai-to-claude.js"); - await import("./request/gemini-to-openai.js"); - await import("./request/openai-to-gemini.js"); - await import("./request/openai-responses.js"); - await import("./request/openai-to-kiro.js"); - - // Response translators - await import("./response/claude-to-openai.js"); - await import("./response/openai-to-claude.js"); - await import("./response/gemini-to-openai.js"); - await import("./response/openai-responses.js"); - await import("./response/kiro-to-openai.js"); +// Initialize all translators (kept for backward compatibility) +export function initTranslators() { + ensureInitialized(); } diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js index a6f8018..7ed642c 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js @@ -185,8 +185,8 @@ export default function ClaudeToolCard({
-
- {tool.name} { e.target.style.display = "none"; }} /> +
+ {tool.name} { e.target.style.display = "none"; }} />
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js index faaa396..5a5688a 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js @@ -158,8 +158,8 @@ wire_api = "responses"
-
- {tool.name} { e.target.style.display = "none"; }} /> +
+ {tool.name} { e.target.style.display = "none"; }} />
diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js index cf220b8..8f945e4 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/DefaultToolCard.js @@ -227,23 +227,23 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba {tool.name} { e.target.style.display = "none"; }} /> ); } if (tool.icon) { - return {tool.icon}; + return {tool.icon}; } return ( {tool.name} { e.target.style.display = "none"; }} /> ); @@ -253,7 +253,7 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
-
+
{renderIcon()}
diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js index 32a23bb..8daa968 100644 --- a/src/app/(dashboard)/dashboard/providers/page.js +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -1,14 +1,12 @@ "use client"; import { useState, useEffect } from "react"; -import { Card, CardSkeleton, Badge, UsageStats, RequestLogger } from "@/shared/components"; +import { Card, CardSkeleton, Badge } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; import Link from "next/link"; import { getErrorCode, getRelativeTime } from "@/shared/utils"; export default function ProvidersPage() { - const [activeTab, setActiveTab] = useState("connections"); - const [usageSubTab, setUsageSubTab] = useState("overview"); const [connections, setConnections] = useState([]); const [loading, setLoading] = useState(true); @@ -73,85 +71,35 @@ export default function ProvidersPage() { return (
- {/* Tabs */} -
- - + {/* OAuth Providers */} +
+

OAuth Providers

+
+ {Object.entries(OAUTH_PROVIDERS).map(([key, info]) => ( + + ))} +
- {activeTab === "usage" ? ( -
-
- - -
- {usageSubTab === "overview" ? : } + {/* API Key Providers */} +
+

API Key Providers

+
+ {Object.entries(APIKEY_PROVIDERS).map(([key, info]) => ( + + ))}
- ) : ( - <> - {/* OAuth Providers */} -
-

OAuth Providers

-
- {Object.entries(OAUTH_PROVIDERS).map(([key, info]) => ( - - ))} -
-
- - {/* API Key Providers */} -
-

API Key Providers

-
- {Object.entries(APIKEY_PROVIDERS).map(([key, info]) => ( - - ))} -
-
- - )} +
); } diff --git a/src/app/(dashboard)/dashboard/usage/page.js b/src/app/(dashboard)/dashboard/usage/page.js new file mode 100644 index 0000000..e74897c --- /dev/null +++ b/src/app/(dashboard)/dashboard/usage/page.js @@ -0,0 +1,39 @@ +"use client"; + +import { useState } from "react"; +import { UsageStats, RequestLogger } from "@/shared/components"; + +export default function UsagePage() { + const [activeTab, setActiveTab] = useState("overview"); + + return ( +
+ {/* Tabs */} +
+ + +
+ + {/* Content */} + {activeTab === "overview" ? : } +
+ ); +} diff --git a/src/shared/components/Header.js b/src/shared/components/Header.js index 29a1ba8..f08ad5b 100644 --- a/src/shared/components/Header.js +++ b/src/shared/components/Header.js @@ -28,6 +28,7 @@ const getPageInfo = (pathname) => { if (pathname.includes("/providers")) return { title: "Providers", description: "Manage your AI provider connections", breadcrumbs: [] }; if (pathname.includes("/combos")) return { title: "Combos", description: "Model combos with fallback", breadcrumbs: [] }; + if (pathname.includes("/usage")) return { title: "Usage & Analytics", description: "Monitor your API usage, token consumption, and request logs", breadcrumbs: [] }; if (pathname.includes("/cli-tools")) return { title: "CLI Tools", description: "Configure CLI tools", breadcrumbs: [] }; if (pathname.includes("/endpoint")) return { title: "Endpoint", description: "API endpoint configuration", breadcrumbs: [] }; if (pathname.includes("/profile")) return { title: "Settings", description: "Manage your preferences", breadcrumbs: [] }; diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js index 25e06da..99a208e 100644 --- a/src/shared/components/ModelSelectModal.js +++ b/src/shared/components/ModelSelectModal.js @@ -1,9 +1,15 @@ "use client"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import Modal from "./Modal"; import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; -import { AI_PROVIDERS } from "@/shared/constants/providers"; +import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers"; + +// Provider order: OAuth first, then API Key (matches dashboard/providers) +const PROVIDER_ORDER = [ + ...Object.keys(OAUTH_PROVIDERS), + ...Object.keys(APIKEY_PROVIDERS), +]; export default function ModelSelectModal({ isOpen, @@ -15,19 +21,39 @@ export default function ModelSelectModal({ modelAliases = {}, }) { const [searchQuery, setSearchQuery] = useState(""); + const [combos, setCombos] = useState([]); - // Group models by provider + // Fetch combos when modal opens + useEffect(() => { + if (isOpen) { + fetch("/api/combos") + .then(res => res.json()) + .then(data => setCombos(data.combos || [])) + .catch(() => setCombos([])); + } + }, [isOpen]); + + const allProviders = { ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS }; + + // Group models by provider with priority order const groupedModels = useMemo(() => { const groups = {}; // Get active provider IDs const activeProviderIds = activeProviders.length > 0 ? activeProviders.map(p => p.provider) - : Object.keys(AI_PROVIDERS); + : PROVIDER_ORDER; - activeProviderIds.forEach((providerId) => { + // Sort by PROVIDER_ORDER + const sortedProviderIds = [...activeProviderIds].sort((a, b) => { + const indexA = PROVIDER_ORDER.indexOf(a); + const indexB = PROVIDER_ORDER.indexOf(b); + return (indexA === -1 ? 999 : indexA) - (indexB === -1 ? 999 : indexB); + }); + + sortedProviderIds.forEach((providerId) => { const alias = PROVIDER_ID_TO_ALIAS[providerId] || providerId; - const providerInfo = AI_PROVIDERS[providerId] || { name: providerId, color: "#666" }; + const providerInfo = allProviders[providerId] || { name: providerId, color: "#666" }; // For passthrough providers, get models from aliases if (providerInfo.passthroughModels) { @@ -65,7 +91,14 @@ export default function ModelSelectModal({ }); return groups; - }, [activeProviders, modelAliases]); + }, [activeProviders, modelAliases, allProviders]); + + // Filter combos by search query + const filteredCombos = useMemo(() => { + if (!searchQuery.trim()) return combos; + const query = searchQuery.toLowerCase(); + return combos.filter(c => c.name.toLowerCase().includes(query)); + }, [combos, searchQuery]); // Filter models by search query const filteredGroups = useMemo(() => { @@ -128,6 +161,38 @@ export default function ModelSelectModal({ {/* Models grouped by provider - compact */}
+ {/* Combos section - always first */} + {filteredCombos.length > 0 && ( +
+
+ layers + Combos + ({filteredCombos.length}) +
+
+ {filteredCombos.map((combo) => { + const isSelected = selectedModel === combo.name; + return ( + + ); + })} +
+
+ )} + + {/* Provider models */} {Object.entries(filteredGroups).map(([providerId, group]) => (
{/* Provider header */} @@ -168,7 +233,7 @@ export default function ModelSelectModal({
))} - {Object.keys(filteredGroups).length === 0 && ( + {Object.keys(filteredGroups).length === 0 && filteredCombos.length === 0 && (
search_off diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index 445c8e5..2ef629a 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -12,6 +12,7 @@ const navItems = [ { href: "/dashboard/endpoint", label: "Endpoint", icon: "api" }, { href: "/dashboard/providers", label: "Providers", icon: "dns" }, { href: "/dashboard/combos", label: "Combos", icon: "layers" }, + { href: "/dashboard/usage", label: "Usage", icon: "bar_chart" }, { href: "/dashboard/cli-tools", label: "CLI Tools", icon: "terminal" }, ]; diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index d2a8525..da405e7 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -1,6 +1,6 @@ "use client"; -import { useState, useEffect, useMemo } from "react"; +import { useState, useEffect, useMemo, useCallback } from "react"; import { useSearchParams, useRouter } from "next/navigation"; import Card from "./Card"; import Badge from "./Badge"; @@ -38,6 +38,8 @@ export default function UsageStats() { const [loading, setLoading] = useState(true); const [autoRefresh, setAutoRefresh] = useState(true); const [viewMode, setViewMode] = useState("tokens"); // 'tokens' or 'costs' + const [refreshInterval, setRefreshInterval] = useState(5000); // Start with 5s + const [prevTotalRequests, setPrevTotalRequests] = useState(0); const toggleSort = (field) => { const params = new URLSearchParams(searchParams.toString()); @@ -114,34 +116,66 @@ export default function UsageStats() { return sortData(stats?.byAccount, accountPendingMap); }, [stats?.byAccount, stats?.pending?.byAccount, sortBy, sortOrder]); - useEffect(() => { - fetchStats(); - }, []); - - useEffect(() => { - let interval; - if (autoRefresh) { - interval = setInterval(() => { - fetchStats(false); // fetch without loading skeleton - }, 1000); - } - return () => clearInterval(interval); - }, [autoRefresh]); - - const fetchStats = async (showLoading = true) => { + const fetchStats = useCallback(async (showLoading = true) => { if (showLoading) setLoading(true); try { const res = await fetch("/api/usage/history"); if (res.ok) { const data = await res.json(); setStats(data); + + // Smart polling: adjust interval based on activity + const currentTotal = data.totalRequests || 0; + if (currentTotal > prevTotalRequests) { + // New requests detected - reset to fast polling + setRefreshInterval(5000); + } else { + // No change - increase interval (exponential backoff) + setRefreshInterval((prev) => Math.min(prev * 2, 60000)); // Max 60s + } + setPrevTotalRequests(currentTotal); } } catch (error) { console.error("Failed to fetch usage stats:", error); } finally { if (showLoading) setLoading(false); } - }; + }, [prevTotalRequests]); + + useEffect(() => { + fetchStats(); + }, [fetchStats]); + + useEffect(() => { + let intervalId; + let isPageVisible = true; + + // Page Visibility API - pause when tab is hidden + const handleVisibilityChange = () => { + isPageVisible = !document.hidden; + if (isPageVisible && autoRefresh) { + fetchStats(false); // Fetch immediately when tab becomes visible + } + }; + + document.addEventListener("visibilitychange", handleVisibilityChange); + + if (autoRefresh) { + // Clear any existing interval first + if (intervalId) clearInterval(intervalId); + + intervalId = setInterval(() => { + if (isPageVisible) { + fetchStats(false); // fetch without loading skeleton + } + }, refreshInterval); + } + + return () => { + if (intervalId) clearInterval(intervalId); + document.removeEventListener("visibilitychange", handleVisibilityChange); + }; + }, [autoRefresh, refreshInterval, fetchStats]); if (loading) return ; @@ -202,7 +236,7 @@ export default function UsageStats() { {/* Auto Refresh Toggle */}