Files
Momento/memento-note/app/api/admin/models/route.ts
Sepehr Ramezani dbd49d6fcb
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 1m25s
feat: 8 AI providers, rich text editor, agent notifications, UI contrast & font settings
- Add DeepSeek, OpenRouter, Mistral, Z.AI, LM Studio as AI providers
  with editable model names via Combobox in admin settings
- Fix OpenRouter broken by normalizeProvider bug in config.ts
- Convert agent-created notes from Markdown to HTML (TipTap rich text)
- Add Notification model + in-app notifications for agent results
- Agent notification click opens the created note directly
- Add note count display on notebook and inbox headers
- Fix checklist toggle in card view (persist state via localCheckItems)
- Add checklist creation option in tabs/list view (dropdown on + button)
- Fix image description ENOENT error with HTTP fallback
- Improve UI contrast across all themes (input, border, checkbox visibility)
- Add font family setting (Inter vs System Default) in Appearance settings
- Fix CSS font-sans variable conflict (removed dead Geist references)
- Update README with new features and 8 providers

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-05-01 16:14:07 +02:00

127 lines
4.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/auth'
async function requireAdmin() {
const session = await auth()
if (!session?.user?.id || (session.user as any).role !== 'ADMIN') return null
return session
}
/**
* GET /api/admin/models?type=ollama&url=<base_url>
* GET /api/admin/models?type=custom&url=<base_url>&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=deepseek&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=openrouter&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=mistral&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=zai&key=<api_key>&kind=tags|embeddings
* GET /api/admin/models?type=lmstudio&url=<base_url>
*
* Route API for fetching AI model lists from providers.
*/
export async function GET(request: NextRequest) {
if (!(await requireAdmin())) {
return NextResponse.json({ success: false, error: 'Unauthorized' }, { status: 401 })
}
const { searchParams } = request.nextUrl
const type = searchParams.get('type')
const rawUrl = searchParams.get('url') ?? ''
const apiKey = searchParams.get('key') ?? undefined
const kind = searchParams.get('kind') ?? 'tags'
// Provider-specific base URLs (used when url param is empty)
const PROVIDER_URLS: Record<string, string> = {
deepseek: 'https://api.deepseek.com/v1',
openrouter: 'https://openrouter.ai/api/v1',
mistral: 'https://api.mistral.ai/v1',
zai: 'https://api.zukijourney.com/v1',
lmstudio: 'http://localhost:1234/v1',
}
try {
// Ollama: uses native /api/tags endpoint
if (type === 'ollama') {
if (!rawUrl) {
return NextResponse.json({ success: false, models: [], error: 'url parameter is required for Ollama' })
}
const baseUrl = rawUrl.replace(/\/$/, '').replace(/\/api$/, '')
const res = await fetch(`${baseUrl}/api/tags`, {
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(5000),
})
if (!res.ok) {
return NextResponse.json({ success: false, models: [], error: `Ollama ${res.status}` })
}
const data = await res.json()
const models: string[] = (data.models ?? []).map((m: { name: string }) => m.name)
return NextResponse.json({ success: true, models })
}
// All other providers: use OpenAI-compatible /v1/models endpoint
const baseUrl = rawUrl
? rawUrl.replace(/\/$/, '')
: (PROVIDER_URLS[type || ''] || '')
if (!baseUrl) {
return NextResponse.json({ success: false, models: [], error: 'url parameter is required' })
}
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`
// For OpenRouter, add required headers
if (type === 'openrouter') {
headers['HTTP-Referer'] = 'https://localhost:3000'
headers['X-Title'] = 'Memento AI'
}
// Try provider-specific embeddings endpoint first for embeddings kind
if (kind === 'embeddings') {
try {
const embRes = await fetch(`${baseUrl}/embeddings/models`, {
headers,
signal: AbortSignal.timeout(8000),
})
if (embRes.ok) {
const embData = await embRes.json()
const embModels: string[] = (embData.data ?? [])
.map((m: { id: string }) => m.id)
.filter(Boolean)
.sort()
if (embModels.length > 0) {
return NextResponse.json({ success: true, models: embModels })
}
}
} catch {
// Fall through to /v1/models with keyword filter
}
}
const res = await fetch(`${baseUrl}/models`, {
headers,
signal: AbortSignal.timeout(8000),
})
if (!res.ok) {
return NextResponse.json({ success: false, models: [], error: `Provider ${res.status}` })
}
const data = await res.json()
let models: string[] = (data.data ?? [])
.map((m: { id: string }) => m.id)
.filter(Boolean)
.sort()
if (kind === 'embeddings') {
const keywords = ['embed', 'embedding', 'ada', 'e5', 'bge', 'gte', 'minilm']
const filtered = models.filter((id) =>
keywords.some((kw) => id.toLowerCase().includes(kw))
)
if (filtered.length > 0) models = filtered
}
return NextResponse.json({ success: true, models })
} catch (err: any) {
return NextResponse.json({ success: false, models: [], error: err.message ?? 'Request failed' })
}
}