| import { NextRequest, NextResponse } from 'next/server'; |
| import { ProviderId } from '@/lib/llm/providers/types'; |
| import { getProvider } from '@/lib/llm/providers/registry'; |
| import { assertPublicHttpUrl } from '@/lib/llm/providers/url-safety'; |
| import { logger } from '@/lib/utils'; |
|
|
| export async function POST(request: NextRequest) { |
| try { |
| const { apiKey, provider, baseUrl: requestBaseUrl } = await request.json(); |
| |
| if (!provider) { |
| return NextResponse.json( |
| { error: 'Provider is required' }, |
| { status: 400 } |
| ); |
| } |
|
|
| const providerConfig = getProvider(provider as ProviderId); |
| const customBaseUrl = requestBaseUrl; |
| const effectiveBaseUrl = customBaseUrl || providerConfig.baseUrl; |
|
|
| |
| |
| |
| if (customBaseUrl && !providerConfig.isLocal) assertPublicHttpUrl(customBaseUrl); |
|
|
| |
| if (providerConfig.apiKeyRequired && !apiKey && !providerConfig.usesOAuth) { |
| return NextResponse.json({ models: [] }); |
| } |
|
|
| let models: Array<string | { id: string; contextLength?: number; inputModalities?: string[] }> = []; |
|
|
| try { |
| switch (provider) { |
| case 'openrouter': |
| const orResponse = await fetch('https://openrouter.ai/api/v1/models', { |
| headers: { |
| 'Authorization': `Bearer ${apiKey}`, |
| 'HTTP-Referer': request.headers.get('referer') || 'http://localhost:3000', |
| 'X-Title': 'OSW-Studio' |
| } |
| }); |
| if (orResponse.ok) { |
| const orData = await orResponse.json(); |
| models = orData.data |
| ?.filter((model: { id: string }) => |
| model.id.includes('deepseek') || |
| model.id.includes('qwen') || |
| model.id.includes('claude') || |
| model.id.includes('gpt') || |
| model.id.includes('llama') |
| ) |
| ?.map((model: { id: string; context_length: number; architecture?: { input_modalities?: string[] } }) => ({ |
| id: model.id, |
| contextLength: model.context_length, |
| inputModalities: model.architecture?.input_modalities, |
| })) || []; |
| } |
| break; |
|
|
| case 'anthropic': |
| const anthropicResponse = await fetch('https://api.anthropic.com/v1/models', { |
| headers: { |
| 'x-api-key': apiKey, |
| 'anthropic-version': '2023-06-01' |
| } |
| }); |
| if (anthropicResponse.ok) { |
| const anthropicData = await anthropicResponse.json(); |
| models = anthropicData.data?.map((model: { id: string; capabilities?: Record<string, { supported?: boolean }> }) => { |
| const modalities: string[] = ['text']; |
| if (model.capabilities?.image_input?.supported) modalities.push('image'); |
| return { id: model.id, inputModalities: modalities }; |
| }) || []; |
| } |
| break; |
|
|
| case 'openai': |
| case 'openai-codex': |
| const openaiResponse = await fetch('https://api.openai.com/v1/models', { |
| headers: { |
| 'Authorization': `Bearer ${apiKey}` |
| } |
| }); |
| if (openaiResponse.ok) { |
| const openaiData = await openaiResponse.json(); |
| models = openaiData.data?.map((model: { id: string }) => model.id) || []; |
| } |
| break; |
|
|
| case 'groq': |
| const groqResponse = await fetch('https://api.groq.com/openai/v1/models', { |
| headers: { |
| 'Authorization': `Bearer ${apiKey}` |
| } |
| }); |
| if (groqResponse.ok) { |
| const groqData = await groqResponse.json(); |
| models = groqData.data?.map((model: { id: string }) => model.id) || []; |
| } |
| break; |
|
|
| case 'ollama': |
| try { |
| |
| |
| |
| const ollamaResponse = await fetch(`http://127.0.0.1:11434/api/tags`); |
| if (ollamaResponse.ok) { |
| const ollamaData = await ollamaResponse.json(); |
| |
| models = ollamaData.models?.map((m: any) => m.name) || []; |
| } |
| } catch (error) { |
| logger.debug('Ollama models fetch failed (server not running?):', error); |
| } |
| break; |
|
|
| case 'lmstudio': |
| try { |
| |
| |
| const lmsRestUrl = (providerConfig.baseUrl?.replace('/v1', '') || 'http://localhost:1234').replace('localhost', '127.0.0.1'); |
| const lmsResponse = await fetch(`${lmsRestUrl}/api/v1/models`); |
| if (lmsResponse.ok) { |
| const lmsData = await lmsResponse.json(); |
| |
| |
| const lmsModels = Array.isArray(lmsData) ? lmsData : (lmsData.models || lmsData.data || []); |
| models = lmsModels |
| .filter((m: any) => !String(m.type ?? '').startsWith('embed')) |
| .map((m: any) => { |
| const modalities: string[] = ['text']; |
| if (m.capabilities?.vision) modalities.push('image'); |
| return { id: m.key || m.id, inputModalities: modalities }; |
| }); |
| } |
| } catch (error) { |
| logger.debug('LM Studio models fetch failed (server not running?):', error); |
| } |
| break; |
|
|
| case 'llamacpp': |
| case 'meshllm': |
| try { |
| const lmResponse = await fetch(`${providerConfig.baseUrl?.replace('localhost', '127.0.0.1')}/models`); |
| if (lmResponse.ok) { |
| const lmData = await lmResponse.json(); |
| models = lmData.data?.map((m: any) => m.id) || []; |
| } |
| } catch (error) { |
| logger.debug(`${provider} models fetch failed (server not running?):`, error); |
| } |
| break; |
|
|
| case 'gemini': |
| const geminiResponse = await fetch( |
| `https://generativelanguage.googleapis.com/v1beta/models?pageSize=100&key=${apiKey}` |
| ); |
| if (geminiResponse.ok) { |
| const geminiData = await geminiResponse.json(); |
| models = (geminiData.models || []) |
| .filter((m: any) => |
| m.supportedGenerationMethods?.includes('generateContent') && |
| /gemini/i.test(m.name) |
| ) |
| .map((m: any) => ({ |
| id: m.name.replace('models/', ''), |
| contextLength: m.inputTokenLimit, |
| inputModalities: m.supportedGenerationMethods?.includes('generateContent') |
| ? ['text', 'image'] : ['text'], |
| })); |
| } |
| break; |
|
|
| case 'huggingface': |
| try { |
| const hfHeaders: Record<string, string> = {}; |
| if (apiKey) { |
| hfHeaders['Authorization'] = `Bearer ${apiKey}`; |
| } |
| const hfResponse = await fetch('https://router.huggingface.co/v1/models', { |
| headers: hfHeaders, |
| }); |
| if (hfResponse.ok) { |
| const hfData = await hfResponse.json(); |
| models = hfData.data?.map((m: any) => ({ |
| id: m.id, |
| inputModalities: m.architecture?.input_modalities, |
| })) || []; |
| } |
| } catch (error) { |
| logger.error('HuggingFace models fetch error:', error); |
| } |
| break; |
|
|
| case 'opencode-go': { |
| const mdResp = await fetch('https://models.dev/api.json'); |
| if (mdResp.ok) { |
| const mdData = await mdResp.json(); |
| const ocg = (mdData['opencode-go']?.models ?? {}) as Record<string, any>; |
| models = Object.values(ocg) |
| .filter((m: any) => m.status !== 'deprecated') |
| .map((m: any) => ({ |
| id: m.id, |
| contextLength: m.limit?.context, |
| inputModalities: m.modalities?.input, |
| })); |
| } |
| break; |
| } |
|
|
| default: |
| |
| if (effectiveBaseUrl) { |
| const headers: Record<string, string> = {}; |
| if (apiKey) { |
| headers['Authorization'] = `Bearer ${apiKey}`; |
| } |
| const defaultResponse = await fetch(`${effectiveBaseUrl}/models`, { headers }); |
| const responseText = await defaultResponse.text(); |
| if (defaultResponse.ok) { |
| try { |
| const defaultData = JSON.parse(responseText); |
| |
| |
| const list = Array.isArray(defaultData) |
| ? defaultData |
| : (defaultData.data || defaultData.models || defaultData.list || []); |
| models = list |
| .map((m: any) => (typeof m === 'string' ? m : (m.id || m.model || m.name || m.model_id || m.slug))) |
| .filter(Boolean); |
| if (models.length === 0) { |
| logger.warn(`[models] ${provider} /models returned no recognizable models`); |
| logger.debug(`[models] ${provider} raw response:`, responseText.slice(0, 300)); |
| } |
| } catch { |
| logger.warn(`[models] ${provider} /models returned non-JSON`); |
| logger.debug(`[models] ${provider} raw response:`, responseText.slice(0, 300)); |
| } |
| } else { |
| logger.warn(`[models] ${provider} /models returned HTTP ${defaultResponse.status}`); |
| logger.debug(`[models] ${provider} raw response:`, responseText.slice(0, 300)); |
| } |
| } |
| break; |
| } |
| } catch (error) { |
| logger.error(`Error fetching models for ${provider}:`, error); |
| |
| if (providerConfig.models) { |
| models = providerConfig.models.map(m => m.id); |
| } |
| } |
|
|
| return NextResponse.json({ models }); |
|
|
| } catch (error) { |
| logger.error('Models API error:', error); |
| return NextResponse.json({ models: [] }); |
| } |
| } |
|
|