| import { NextResponse } from 'next/server'; |
| import { requireAdmin } from '@/lib/auth-middleware'; |
| import { loadConfig } from '@/lib/server-config'; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| export const runtime = 'nodejs'; |
| export const dynamic = 'force-dynamic'; |
|
|
| interface ModelInfo { |
| id: string; |
| name: string; |
| ownedBy?: string; |
| context?: number; |
| pricing?: 'free' | 'paid' | 'cheap' | 'local'; |
| } |
|
|
| interface ProviderBlock { |
| provider: string; |
| label: string; |
| configured: boolean; |
| ok: boolean; |
| error?: string; |
| pricing: 'free' | 'paid' | 'cheap' | 'local'; |
| models: ModelInfo[]; |
| } |
|
|
| |
| function withTimeout(ms = 10000) { |
| return AbortSignal.timeout(ms); |
| } |
|
|
| async function safeJson(res: Response): Promise<any> { |
| try { |
| return await res.json(); |
| } catch { |
| return null; |
| } |
| } |
|
|
| |
|
|
| async function fetchOllaBridge( |
| url: string, |
| apiKey: string, |
| ): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'ollabridge', |
| label: 'OllaBridge Cloud', |
| configured: !!url, |
| ok: false, |
| pricing: 'free', |
| models: [], |
| }; |
| if (!url) { |
| block.error = 'Not configured — set OllaBridge URL in Server tab'; |
| return block; |
| } |
| try { |
| const cleanBase = url.replace(/\/+$/, ''); |
| const res = await fetch(`${cleanBase}/v1/models`, { |
| headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {}, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| return block; |
| } |
| const data = await safeJson(res); |
| const list = Array.isArray(data?.data) ? data.data : []; |
| block.models = list.map((m: any) => ({ |
| id: String(m.id ?? 'unknown'), |
| name: String(m.id ?? 'unknown'), |
| ownedBy: m.owned_by || 'ollabridge', |
| pricing: |
| String(m.id ?? '').startsWith('free-') |
| ? 'free' |
| : String(m.id ?? '').startsWith('cheap-') |
| ? 'cheap' |
| : 'local', |
| })); |
| block.ok = true; |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message?.slice(0, 100) || 'Request failed'; |
| } |
| return block; |
| } |
|
|
| async function fetchHuggingFace(token: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'huggingface', |
| label: 'HuggingFace Inference', |
| configured: !!token, |
| ok: false, |
| pricing: 'free', |
| models: [], |
| }; |
| if (!token) { |
| block.error = 'Not configured — set HF token in Server tab'; |
| |
| |
| block.models = CURATED_HF_MODELS.map((id) => ({ |
| id, |
| name: id.split('/').pop() || id, |
| ownedBy: id.split('/')[0], |
| pricing: 'free', |
| })); |
| return block; |
| } |
| try { |
| const res = await fetch('https://router.huggingface.co/v1/models', { |
| headers: { Authorization: `Bearer ${token}` }, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| |
| block.models = CURATED_HF_MODELS.map((id) => ({ |
| id, |
| name: id.split('/').pop() || id, |
| ownedBy: id.split('/')[0], |
| pricing: 'free', |
| })); |
| return block; |
| } |
| const data = await safeJson(res); |
| const list = Array.isArray(data?.data) ? data.data : []; |
| block.models = list |
| .filter((m: any) => typeof m?.id === 'string') |
| .map((m: any) => ({ |
| id: String(m.id), |
| name: String(m.id).split('/').pop() || String(m.id), |
| ownedBy: m.owned_by || String(m.id).split('/')[0], |
| pricing: 'free' as const, |
| })); |
| block.ok = true; |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message?.slice(0, 100) || 'Request failed'; |
| block.models = CURATED_HF_MODELS.map((id) => ({ |
| id, |
| name: id.split('/').pop() || id, |
| ownedBy: id.split('/')[0], |
| pricing: 'free', |
| })); |
| } |
| return block; |
| } |
|
|
| |
| const CURATED_HF_MODELS = [ |
| 'meta-llama/Llama-3.3-70B-Instruct', |
| 'Qwen/Qwen2.5-72B-Instruct', |
| 'Qwen/Qwen3-235B-A22B', |
| 'google/gemma-3-27b-it', |
| 'meta-llama/Llama-3.1-70B-Instruct', |
| 'deepseek-ai/DeepSeek-V3-0324', |
| ]; |
|
|
| async function fetchGroq(apiKey: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'groq', |
| label: 'Groq (Free tier)', |
| configured: !!apiKey, |
| ok: false, |
| pricing: 'free', |
| models: [], |
| }; |
| if (!apiKey) { |
| block.error = 'Not configured — add Groq API key in Server tab'; |
| return block; |
| } |
| try { |
| const res = await fetch('https://api.groq.com/openai/v1/models', { |
| headers: { Authorization: `Bearer ${apiKey}` }, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| return block; |
| } |
| const data = await safeJson(res); |
| const list = Array.isArray(data?.data) ? data.data : []; |
| block.models = list.map((m: any) => ({ |
| id: String(m.id ?? 'unknown'), |
| name: String(m.id ?? 'unknown'), |
| ownedBy: m.owned_by || 'groq', |
| context: typeof m.context_window === 'number' ? m.context_window : undefined, |
| pricing: 'free', |
| })); |
| block.ok = true; |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message?.slice(0, 100) || 'Request failed'; |
| } |
| return block; |
| } |
|
|
| async function fetchOpenAI(apiKey: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'openai', |
| label: 'OpenAI (Paid)', |
| configured: !!apiKey, |
| ok: false, |
| pricing: 'paid', |
| models: [], |
| }; |
| if (!apiKey) { |
| block.error = 'Not configured — add OpenAI API key in Server tab'; |
| return block; |
| } |
| try { |
| const res = await fetch('https://api.openai.com/v1/models', { |
| headers: { Authorization: `Bearer ${apiKey}` }, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| return block; |
| } |
| const data = await safeJson(res); |
| const list = Array.isArray(data?.data) ? data.data : []; |
| |
| block.models = list |
| .filter((m: any) => { |
| const id = String(m?.id || ''); |
| return /^(gpt-|o1-|o3-|chatgpt)/i.test(id); |
| }) |
| .map((m: any) => ({ |
| id: String(m.id), |
| name: String(m.id), |
| ownedBy: m.owned_by || 'openai', |
| pricing: 'paid' as const, |
| })); |
| block.ok = true; |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message?.slice(0, 100) || 'Request failed'; |
| } |
| return block; |
| } |
|
|
| async function fetchAnthropic(apiKey: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'anthropic', |
| label: 'Anthropic Claude (Paid)', |
| configured: !!apiKey, |
| ok: false, |
| pricing: 'paid', |
| models: [], |
| }; |
| if (!apiKey) { |
| block.error = 'Not configured — add Anthropic API key in Server tab'; |
| |
| block.models = CURATED_ANTHROPIC_MODELS; |
| return block; |
| } |
| try { |
| const res = await fetch('https://api.anthropic.com/v1/models', { |
| headers: { |
| 'x-api-key': apiKey, |
| 'anthropic-version': '2023-06-01', |
| }, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| block.models = CURATED_ANTHROPIC_MODELS; |
| return block; |
| } |
| const data = await safeJson(res); |
| const list = Array.isArray(data?.data) ? data.data : []; |
| block.models = list.map((m: any) => ({ |
| id: String(m.id ?? 'unknown'), |
| name: String(m.display_name || m.id || 'unknown'), |
| ownedBy: 'anthropic', |
| pricing: 'paid' as const, |
| })); |
| block.ok = true; |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message?.slice(0, 100) || 'Request failed'; |
| block.models = CURATED_ANTHROPIC_MODELS; |
| } |
| return block; |
| } |
|
|
| |
| const CURATED_ANTHROPIC_MODELS: ModelInfo[] = [ |
| { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', ownedBy: 'anthropic', pricing: 'paid' }, |
| { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6', ownedBy: 'anthropic', pricing: 'paid' }, |
| { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5', ownedBy: 'anthropic', pricing: 'paid' }, |
| ]; |
|
|
| async function fetchWatsonx( |
| apiKey: string, |
| projectId: string, |
| baseUrl: string, |
| ): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'watsonx', |
| label: 'IBM WatsonX (Paid)', |
| configured: !!(apiKey && projectId), |
| ok: false, |
| pricing: 'paid', |
| models: [], |
| }; |
| if (!apiKey || !projectId) { |
| block.error = 'Not configured — add WatsonX API key and project ID in Server tab'; |
| block.models = CURATED_WATSONX_MODELS; |
| return block; |
| } |
| try { |
| |
| const iamRes = await fetch('https://iam.cloud.ibm.com/identity/token', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
| body: new URLSearchParams({ |
| grant_type: 'urn:ibm:params:oauth:grant-type:apikey', |
| apikey: apiKey, |
| }), |
| signal: withTimeout(), |
| }); |
| if (!iamRes.ok) { |
| block.error = `IAM token failed: HTTP ${iamRes.status}`; |
| block.models = CURATED_WATSONX_MODELS; |
| return block; |
| } |
| const iamData = await safeJson(iamRes); |
| const bearer = iamData?.access_token; |
| if (!bearer) { |
| block.error = 'IAM response missing access_token'; |
| block.models = CURATED_WATSONX_MODELS; |
| return block; |
| } |
|
|
| |
| const cleanBase = baseUrl.replace(/\/+$/, ''); |
| const modelsRes = await fetch( |
| `${cleanBase}/ml/v1/foundation_model_specs?version=2024-05-01&limit=200`, |
| { |
| headers: { Authorization: `Bearer ${bearer}` }, |
| signal: withTimeout(), |
| }, |
| ); |
| if (!modelsRes.ok) { |
| block.error = `HTTP ${modelsRes.status}`; |
| block.models = CURATED_WATSONX_MODELS; |
| return block; |
| } |
| const data = await safeJson(modelsRes); |
| const list = Array.isArray(data?.resources) ? data.resources : []; |
| block.models = list.map((m: any) => ({ |
| id: String(m.model_id ?? 'unknown'), |
| name: String(m.label || m.model_id || 'unknown'), |
| ownedBy: m.provider || 'ibm', |
| pricing: 'paid' as const, |
| })); |
| block.ok = true; |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message?.slice(0, 100) || 'Request failed'; |
| block.models = CURATED_WATSONX_MODELS; |
| } |
| return block; |
| } |
|
|
| |
| const CURATED_WATSONX_MODELS: ModelInfo[] = [ |
| { id: 'ibm/granite-3-8b-instruct', name: 'Granite 3 8B Instruct', ownedBy: 'ibm', pricing: 'paid' }, |
| { id: 'meta-llama/llama-3-3-70b-instruct', name: 'Llama 3.3 70B', ownedBy: 'meta', pricing: 'paid' }, |
| { id: 'mistralai/mistral-large', name: 'Mistral Large', ownedBy: 'mistralai', pricing: 'paid' }, |
| ]; |
|
|
| |
|
|
| |
| |
| |
| |
| async function fetchGemini(apiKey: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'gemini', |
| label: 'Google Gemini', |
| configured: !!apiKey, |
| ok: false, |
| pricing: 'paid', |
| models: [], |
| }; |
| if (!apiKey) { |
| block.error = 'API key not configured'; |
| return block; |
| } |
| try { |
| const res = await fetch( |
| `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`, |
| { signal: withTimeout() }, |
| ); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| return block; |
| } |
| const data = await res.json().catch(() => ({})); |
| const arr = Array.isArray(data?.models) ? data.models : []; |
| block.ok = true; |
| block.models = arr |
| .filter((m: any) => typeof m?.name === 'string') |
| |
| |
| .map((m: any) => ({ |
| id: String(m.name).replace(/^models\//, ''), |
| name: m.displayName || String(m.name).replace(/^models\//, ''), |
| ownedBy: 'google', |
| context: typeof m.inputTokenLimit === 'number' ? m.inputTokenLimit : undefined, |
| pricing: 'paid' as const, |
| })); |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message || 'Request failed'; |
| } |
| return block; |
| } |
|
|
| |
| async function fetchOpenRouter(apiKey: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'openrouter', |
| label: 'OpenRouter', |
| configured: !!apiKey, |
| ok: false, |
| pricing: 'paid', |
| models: [], |
| }; |
| if (!apiKey) { |
| block.error = 'API key not configured'; |
| return block; |
| } |
| try { |
| const res = await fetch('https://openrouter.ai/api/v1/models', { |
| headers: { Authorization: `Bearer ${apiKey}` }, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| return block; |
| } |
| const data = await res.json().catch(() => ({})); |
| const arr = Array.isArray(data?.data) ? data.data : []; |
| block.ok = true; |
| block.models = arr.slice(0, 200).map((m: any) => ({ |
| id: String(m.id), |
| name: m.name || String(m.id), |
| ownedBy: (String(m.id).split('/')[0] as string) || undefined, |
| context: typeof m.context_length === 'number' ? m.context_length : undefined, |
| pricing: m?.pricing?.prompt === '0' ? 'free' : 'paid', |
| })); |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message || 'Request failed'; |
| } |
| return block; |
| } |
|
|
| |
| async function fetchTogether(apiKey: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'together', |
| label: 'Together AI', |
| configured: !!apiKey, |
| ok: false, |
| pricing: 'paid', |
| models: [], |
| }; |
| if (!apiKey) { |
| block.error = 'API key not configured'; |
| return block; |
| } |
| try { |
| const res = await fetch('https://api.together.xyz/v1/models', { |
| headers: { Authorization: `Bearer ${apiKey}` }, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| return block; |
| } |
| const data = await res.json().catch(() => []); |
| |
| const arr = Array.isArray(data) ? data : Array.isArray(data?.data) ? data.data : []; |
| block.ok = true; |
| block.models = arr.slice(0, 200).map((m: any) => ({ |
| id: String(m.id || m.name), |
| name: m.display_name || m.id || m.name, |
| ownedBy: m.organization || undefined, |
| context: typeof m.context_length === 'number' ? m.context_length : undefined, |
| pricing: 'paid' as const, |
| })); |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message || 'Request failed'; |
| } |
| return block; |
| } |
|
|
| |
| async function fetchMistral(apiKey: string): Promise<ProviderBlock> { |
| const block: ProviderBlock = { |
| provider: 'mistral', |
| label: 'Mistral AI', |
| configured: !!apiKey, |
| ok: false, |
| pricing: 'paid', |
| models: [], |
| }; |
| if (!apiKey) { |
| block.error = 'API key not configured'; |
| return block; |
| } |
| try { |
| const res = await fetch('https://api.mistral.ai/v1/models', { |
| headers: { Authorization: `Bearer ${apiKey}` }, |
| signal: withTimeout(), |
| }); |
| if (!res.ok) { |
| block.error = `HTTP ${res.status}`; |
| return block; |
| } |
| const data = await res.json().catch(() => ({})); |
| const arr = Array.isArray(data?.data) ? data.data : []; |
| block.ok = true; |
| block.models = arr.map((m: any) => ({ |
| id: String(m.id), |
| name: m.name || m.id, |
| ownedBy: m.owned_by || 'mistralai', |
| context: |
| typeof m.max_context_length === 'number' |
| ? m.max_context_length |
| : typeof m.context_length === 'number' |
| ? m.context_length |
| : undefined, |
| pricing: 'paid' as const, |
| })); |
| } catch (e: any) { |
| block.error = e?.name === 'TimeoutError' ? 'Timeout (10s)' : e?.message || 'Request failed'; |
| } |
| return block; |
| } |
|
|
| |
|
|
| export async function GET(req: Request) { |
| const admin = requireAdmin(req); |
| if (!admin) return NextResponse.json({ error: 'Admin access required' }, { status: 403 }); |
|
|
| const config = loadConfig(); |
| const { llm } = config; |
|
|
| |
| |
| const [ |
| ollabridge, |
| huggingface, |
| groq, |
| openai, |
| anthropic, |
| watsonx, |
| gemini, |
| openrouter, |
| together, |
| mistral, |
| ] = await Promise.all([ |
| fetchOllaBridge(llm.ollabridgeUrl, llm.ollabridgeApiKey), |
| fetchHuggingFace(llm.hfToken), |
| fetchGroq(llm.groqApiKey), |
| fetchOpenAI(llm.openaiApiKey), |
| fetchAnthropic(llm.anthropicApiKey), |
| fetchWatsonx(llm.watsonxApiKey, llm.watsonxProjectId, llm.watsonxUrl), |
| fetchGemini(llm.geminiApiKey), |
| fetchOpenRouter(llm.openrouterApiKey), |
| fetchTogether(llm.togetherApiKey), |
| fetchMistral(llm.mistralApiKey), |
| ]); |
|
|
| const providers = [ |
| ollabridge, |
| huggingface, |
| groq, |
| openai, |
| anthropic, |
| watsonx, |
| gemini, |
| openrouter, |
| together, |
| mistral, |
| ]; |
| const totalModels = providers.reduce((sum, p) => sum + p.models.length, 0); |
| const okCount = providers.filter((p) => p.ok).length; |
|
|
| return NextResponse.json({ |
| providers, |
| summary: { |
| providers: providers.length, |
| providersOk: okCount, |
| totalModels, |
| fetchedAt: new Date().toISOString(), |
| }, |
| }); |
| } |
|
|