Skip to main content
Glama
OpenRouterInfo.tsx20.4 kB
import { useState, useEffect, useRef } from 'react'; import api from '../lib/api'; import { Zap, DollarSign, Activity, List, RefreshCw, AlertCircle, CheckCircle, Clock, Pause, Play, Plus } from 'lucide-react'; import ModelFormModal, { mapOpenRouterToModelForm, type ModelFormData } from '../components/ModelFormModal'; // Model interface matching OpenRouter API response interface Model { id: string; name: string; description?: string; context_length?: number; pricing?: { prompt: string; completion: string; }; } interface Limits { limit: number; usage: number; is_free_tier: boolean; rate_limit?: { requests: number; interval: string; }; } interface Credits { balance: number; limit: number; usage: number; } interface ActivityItem { id: string; created_at: string; model: string; total_cost: number; generations: number; } interface CachedData<T> { data: T; timestamp: number; expiresAt: number; } // Cache duration: 10 seconds const CACHE_DURATION = 10 * 1000; // Track which models have been added to Model Layers interface AddedModelInfo { id: string; layer: string; addedAt: number; } export default function OpenRouterInfo() { const [activeTab, setActiveTab] = useState<'models' | 'limits' | 'credits' | 'activity'>('models'); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); const [autoRefresh, setAutoRefresh] = useState(true); const [lastUpdate, setLastUpdate] = useState<Date | null>(null); const [models, setModels] = useState<Model[]>([]); const [limits, setLimits] = useState<Limits | null>(null); const [credits, setCredits] = useState<Credits | null>(null); const [activity, setActivity] = useState<ActivityItem[]>([]); const [searchTerm, setSearchTerm] = useState(''); // Modal state for "Add to Models" const [isModalOpen, setIsModalOpen] = useState(false); const [selectedModel, setSelectedModel] = useState<Model | null>(null); const [addedModels, setAddedModels] = useState<AddedModelInfo[]>([]); const [successMessage, setSuccessMessage] = useState<string | null>(null); // Cache for API responses const cacheRef = useRef<{ models: CachedData<Model[]> | null; limits: CachedData<Limits> | null; credits: CachedData<Credits> | null; activity: CachedData<ActivityItem[]> | null; }>({ models: null, limits: null, credits: null, activity: null }); // Auto-refresh timer const timerRef = useRef<number | null>(null); useEffect(() => { loadData(); // Set up auto-refresh if enabled if (autoRefresh) { startAutoRefresh(); } // Cleanup timer on unmount return () => { if (timerRef.current) { clearInterval(timerRef.current); } }; }, [activeTab, autoRefresh]); function startAutoRefresh() { if (timerRef.current) { clearInterval(timerRef.current); } timerRef.current = window.setInterval(() => { loadData(); }, 10000); // 10 seconds } function stopAutoRefresh() { if (timerRef.current) { clearInterval(timerRef.current); timerRef.current = null; } } function toggleAutoRefresh() { setAutoRefresh(!autoRefresh); if (!autoRefresh) { startAutoRefresh(); } else { stopAutoRefresh(); } } async function loadData() { setLoading(true); setError(null); try { switch (activeTab) { case 'models': await loadModels(); break; case 'limits': await loadLimits(); break; case 'credits': await loadCredits(); break; case 'activity': await loadActivity(); break; } setLastUpdate(new Date()); } catch (err: any) { setError(err.response?.data?.error?.message || err.message || 'Failed to load data'); } finally { setLoading(false); } } function isDataFresh<T>(cached: CachedData<T> | null): boolean { if (!cached) return false; return Date.now() < cached.expiresAt; } async function loadModels() { const cached = cacheRef.current.models; if (cached && isDataFresh(cached)) { setModels(cached.data); return; } const response = await api.get(`/v1/openrouter/models`); const data = response.data.models || []; // Cache the response cacheRef.current.models = { data, timestamp: Date.now(), expiresAt: Date.now() + CACHE_DURATION }; setModels(data); } async function loadLimits() { const cached = cacheRef.current.limits; if (cached && isDataFresh(cached)) { setLimits(cached.data); return; } const response = await api.get(`/v1/openrouter/limits`); const data = response.data.limits || null; // Cache the response cacheRef.current.limits = { data, timestamp: Date.now(), expiresAt: Date.now() + CACHE_DURATION }; setLimits(data); } async function loadCredits() { const cached = cacheRef.current.credits; if (cached && isDataFresh(cached)) { setCredits(cached.data); return; } const response = await api.get(`/v1/openrouter/credits`); const data = response.data.credits || null; // Cache the response cacheRef.current.credits = { data, timestamp: Date.now(), expiresAt: Date.now() + CACHE_DURATION }; setCredits(data); } async function loadActivity() { const cached = cacheRef.current.activity; if (cached && isDataFresh(cached)) { setActivity(cached.data); return; } const response = await api.get(`/v1/openrouter/activity`); const data = response.data.activity || []; // Cache the response cacheRef.current.activity = { data, timestamp: Date.now(), expiresAt: Date.now() + CACHE_DURATION }; setActivity(data); } // ===== Add to Models functionality ===== /** * Open the modal to add an OpenRouter model to Model Layers */ function handleAddToModels(model: Model) { setSelectedModel(model); setIsModalOpen(true); } /** * Check if a model has already been added */ function isModelAdded(modelId: string): AddedModelInfo | undefined { return addedModels.find(m => m.id === modelId); } /** * Handle save from the modal - creates a new model in Model Layers */ async function handleSaveModel(data: ModelFormData) { const modelId = `${data.provider}-${data.apiModelName.replace(/\//g, '-')}`; await api.post(`/v1/models`, { id: modelId, ...data, enabled: true, }); // Track that this model was added setAddedModels(prev => [ ...prev, { id: selectedModel?.id || '', layer: data.layer, addedAt: Date.now() } ]); // Show success message setSuccessMessage(`Đã thêm model "${selectedModel?.name || selectedModel?.id}" vào layer ${data.layer}`); setTimeout(() => setSuccessMessage(null), 5000); } function closeModal() { setIsModalOpen(false); setSelectedModel(null); } const filteredModels = models.filter(m => m.id.toLowerCase().includes(searchTerm.toLowerCase()) || m.name?.toLowerCase().includes(searchTerm.toLowerCase()) ); return ( <div className="space-y-6"> <div className="flex items-center justify-between"> <h1 className="text-3xl font-bold text-white">OpenRouter Info</h1> <div className="flex items-center gap-3"> {lastUpdate && ( <div className="text-xs text-slate-400 flex items-center gap-1"> <Clock className="w-3 h-3" /> Last updated: {lastUpdate.toLocaleTimeString()} </div> )} <button onClick={toggleAutoRefresh} className={`btn-secondary flex items-center gap-2 ${autoRefresh ? 'text-green-400' : 'text-slate-400'}`} title={autoRefresh ? 'Auto-refresh enabled (10s)' : 'Auto-refresh disabled'} > {autoRefresh ? <Pause className="w-4 h-4" /> : <Play className="w-4 h-4" />} {autoRefresh ? 'Auto' : 'Manual'} </button> <button onClick={loadData} className="btn-secondary flex items-center gap-2" disabled={loading} > <RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} /> Refresh </button> </div> </div> {error && ( <div className="card p-4 border-red-500/50 bg-red-500/10"> <div className="flex items-center gap-2 text-red-400"> <AlertCircle className="w-5 h-5" /> <span>{error}</span> </div> </div> )} {/* Cache Status */} {autoRefresh && ( <div className="card p-3 border-blue-500/30 bg-blue-500/10"> <div className="flex items-center gap-2 text-blue-400 text-sm"> <CheckCircle className="w-4 h-4" /> <span>Auto-refresh enabled (10 second intervals) • Data cached for performance</span> </div> </div> )} {/* Tabs */} <div className="flex gap-2 border-b border-slate-700"> <button onClick={() => setActiveTab('models')} className={`px-4 py-2 font-semibold ${ activeTab === 'models' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-slate-400 hover:text-white' }`} > <div className="flex items-center gap-2"> <List className="w-4 h-4" /> Models </div> </button> <button onClick={() => setActiveTab('limits')} className={`px-4 py-2 font-semibold ${ activeTab === 'limits' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-slate-400 hover:text-white' }`} > <div className="flex items-center gap-2"> <Zap className="w-4 h-4" /> Limits </div> </button> <button onClick={() => setActiveTab('credits')} className={`px-4 py-2 font-semibold ${ activeTab === 'credits' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-slate-400 hover:text-white' }`} > <div className="flex items-center gap-2"> <DollarSign className="w-4 h-4" /> Credits </div> </button> <button onClick={() => setActiveTab('activity')} className={`px-4 py-2 font-semibold ${ activeTab === 'activity' ? 'text-blue-400 border-b-2 border-blue-400' : 'text-slate-400 hover:text-white' }`} > <div className="flex items-center gap-2"> <Activity className="w-4 h-4" /> Activity </div> </button> </div> {/* Content */} {loading && ( <div className="card p-12 text-center"> <RefreshCw className="w-8 h-8 animate-spin text-blue-400 mx-auto mb-4" /> <p className="text-slate-400">Loading...</p> </div> )} {/* Success notification */} {successMessage && ( <div className="card p-4 border-green-500/50 bg-green-500/10"> <div className="flex items-center gap-2 text-green-400"> <CheckCircle className="w-5 h-5" /> <span>{successMessage}</span> </div> </div> )} {!loading && activeTab === 'models' && ( <div className="space-y-4"> <input type="text" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} placeholder="Search models..." className="input w-full" /> <div className="card p-4"> <p className="text-sm text-slate-400 mb-4"> Found {filteredModels.length} models </p> <div className="space-y-2 max-h-[600px] overflow-y-auto"> {filteredModels.map((model) => { const addedInfo = isModelAdded(model.id); return ( <div key={model.id} className={`p-3 bg-slate-800/50 rounded border ${addedInfo ? 'border-green-500/50' : 'border-slate-700'} hover:border-blue-500/50 transition-colors`}> <div className="flex items-start justify-between"> <div className="flex-1"> <div className="flex items-center gap-2"> <h3 className="font-semibold text-white">{model.name || model.id}</h3> {addedInfo && ( <span className="badge badge-success text-xs"> Added ({addedInfo.layer}) </span> )} </div> <p className="text-xs text-slate-400 font-mono mt-1">{model.id}</p> {model.description && ( <p className="text-sm text-slate-300 mt-2 line-clamp-2">{model.description}</p> )} </div> <div className="flex items-start gap-3 ml-4"> {model.pricing && ( <div className="text-right"> <p className="text-xs text-slate-400">Pricing</p> <p className="text-xs text-green-400">Prompt: ${model.pricing.prompt}</p> <p className="text-xs text-yellow-400">Completion: ${model.pricing.completion}</p> </div> )} {/* Add to Models button */} <button onClick={() => handleAddToModels(model)} className={`btn-secondary flex items-center gap-1.5 text-sm ${ addedInfo ? 'bg-green-500/20 text-green-400 border-green-500/30' : 'bg-blue-500/20 text-blue-400 border-blue-500/30 hover:bg-blue-500/30' }`} title={addedInfo ? `Already added to ${addedInfo.layer}. Click to add again.` : 'Add to Model Layers'} > <Plus className="w-4 h-4" /> {addedInfo ? 'Add Again' : 'Add to Models'} </button> </div> </div> {model.context_length && ( <div className="mt-2 text-xs text-slate-400"> Context: {(typeof model.context_length === 'number' ? model.context_length : parseInt(model.context_length) || 0).toLocaleString()} tokens </div> )} </div> )})} </div> </div> </div> )} {!loading && activeTab === 'limits' && limits && ( <div className="card p-6"> <h2 className="text-xl font-bold text-white mb-4">API Key Limits</h2> <div className="space-y-4"> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded"> <span className="text-slate-400">Tier</span> <span className={`font-semibold ${limits.is_free_tier ? 'text-yellow-400' : 'text-green-400'}`}> {limits.is_free_tier ? 'Free' : 'Paid'} </span> </div> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded"> <span className="text-slate-400">Usage</span> <span className="text-white font-semibold">{(typeof limits.usage === 'number' ? limits.usage : 0).toLocaleString()}</span> </div> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded"> <span className="text-slate-400">Limit</span> <span className="text-white font-semibold">{(typeof limits.limit === 'number' ? limits.limit : 0).toLocaleString()}</span> </div> {limits.rate_limit && ( <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded"> <span className="text-slate-400">Rate Limit</span> <span className="text-white font-semibold"> {limits.rate_limit.requests} requests / {limits.rate_limit.interval} </span> </div> )} </div> </div> )} {!loading && activeTab === 'credits' && credits && ( <div className="card p-6"> <h2 className="text-xl font-bold text-white mb-4">Credits</h2> <div className="space-y-4"> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded"> <span className="text-slate-400">Balance</span> <span className="text-green-400 font-semibold text-xl">${(typeof credits.balance === 'number' ? credits.balance : 0).toFixed(2)}</span> </div> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded"> <span className="text-slate-400">Usage</span> <span className="text-red-400 font-semibold">${(typeof credits.usage === 'number' ? credits.usage : 0).toFixed(2)}</span> </div> <div className="flex items-center justify-between p-4 bg-slate-800/50 rounded"> <span className="text-slate-400">Limit</span> <span className="text-white font-semibold">${(typeof credits.limit === 'number' ? credits.limit : 0).toFixed(2)}</span> </div> <div className="mt-4"> <div className="w-full bg-slate-700 rounded-full h-3"> <div className="bg-gradient-to-r from-green-500 to-blue-500 h-3 rounded-full" style={{ width: `${Math.min(((typeof credits.usage === 'number' ? credits.usage : 0) / (typeof credits.limit === 'number' ? credits.limit : 1)) * 100, 100)}%` }} /> </div> <p className="text-xs text-slate-400 mt-2 text-center"> {(((typeof credits.usage === 'number' ? credits.usage : 0) / (typeof credits.limit === 'number' ? credits.limit : 1)) * 100).toFixed(1)}% used </p> </div> </div> </div> )} {!loading && activeTab === 'activity' && ( <div className="card p-6"> <h2 className="text-xl font-bold text-white mb-4">Recent Activity</h2> {activity.length === 0 ? ( <p className="text-slate-400 text-center py-8">No activity yet</p> ) : ( <div className="space-y-2"> {activity.map((item) => ( <div key={item.id} className="p-3 bg-slate-800/50 rounded border border-slate-700"> <div className="flex items-center justify-between"> <div> <p className="text-white font-semibold">{item.model}</p> <p className="text-xs text-slate-400 flex items-center gap-1 mt-1"> <Clock className="w-3 h-3" /> {new Date(item.created_at).toLocaleString()} </p> </div> <div className="text-right"> <p className="text-green-400 font-semibold">${(typeof item.total_cost === 'number' ? item.total_cost : 0).toFixed(4)}</p> <p className="text-xs text-slate-400">{item.generations || 0} generations</p> </div> </div> </div> ))} </div> )} </div> )} {/* Add to Models Modal */} <ModelFormModal isOpen={isModalOpen} onClose={closeModal} onSave={handleSaveModel} mode="create" initialData={selectedModel ? mapOpenRouterToModelForm(selectedModel) : undefined} sourceModelName={selectedModel?.name || selectedModel?.id || undefined} /> </div> ); }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/babasida246/ai-mcp-gateway'

If you have feedback or need assistance with the MCP directory API, please join our Discord server