From b352d1434c7ba96c4538cb869f4852ef2b0dfbf8 Mon Sep 17 00:00:00 2001 From: NW Date: Sat, 8 Aug 2026 14:01:29 +0100 Subject: [PATCH] feat(admin): load LLM models from provider /models endpoint (OpenAI-compatible) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New API /api/chatbot/models: fetches model list from configured endpoint (/models, OpenAI/Ollama format) - Uses saved site_settings endpoint+key, or accepts endpoint/apiKey query params (unsaved custom provider) - UI: 'Загрузить модели' button in Provider tab — loads models into select - Manual model input always available for Custom providers - No more hardcoded Ollama-only model list --- .../src/app/api/chatbot/models/route.ts | 61 ++++++++++ .../chatbot/chatbot-settings-page.tsx | 105 +++++++++++++----- 2 files changed, 138 insertions(+), 28 deletions(-) create mode 100644 admin-next/src/app/api/chatbot/models/route.ts diff --git a/admin-next/src/app/api/chatbot/models/route.ts b/admin-next/src/app/api/chatbot/models/route.ts new file mode 100644 index 0000000..f8110b4 --- /dev/null +++ b/admin-next/src/app/api/chatbot/models/route.ts @@ -0,0 +1,61 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { db } from '@/lib/db'; +import { getAuth } from '@/lib/auth-middleware'; + +// Загрузка списка доступных моделей от провайдера через OpenAI-совместимый /models +// GET /api/chatbot/models?endpoint=...&apiKey=... +// GET /api/chatbot/models — использует сохранённые настройки из site_settings +export async function GET(request: NextRequest) { + const auth = getAuth(request); + if ('status' in auth) return auth; + + try { + const { searchParams } = request.nextUrl; + let endpoint = searchParams.get('endpoint') || ''; + let apiKey = searchParams.get('apiKey') || ''; + + // Если endpoint не передан — берём из сохранённых настроек + if (!endpoint) { + const rows = await db.siteSetting.findMany({ + where: { key: { in: ['chatbot_api_endpoint', 'chatbot_api_key'] } }, + }); + const settings: Record = {}; + for (const r of rows) settings[r.key] = r.value; + endpoint = settings.chatbot_api_endpoint || ''; + apiKey = settings.chatbot_api_key || ''; + } + + if (!endpoint) { + return NextResponse.json({ error: 'API endpoint is not configured' }, { status: 400 }); + } + + // OpenAI-совместимый /models: берём базовый URL и добавляем /models + let base = endpoint.replace(/\/chat\/completions$/, '').replace(/\/+$/, ''); + // Если в endpoint уже есть /models — используем его как есть + const modelsUrl = /\/models$/i.test(base) ? base : `${base}/models`; + + const headers: Record = { 'Content-Type': 'application/json' }; + if (apiKey) headers.Authorization = `Bearer ${apiKey}`; + + const res = await fetch(modelsUrl, { headers, signal: AbortSignal.timeout(15000) }); + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`Provider ${res.status}: ${text.slice(0, 200)}`); + } + + const data = await res.json(); + // OpenAI: { data: [{ id, object, owned_by, ... }] } + // Ollama: { models: [{ name, model, ... }] } + const rawModels = data?.data || data?.models || []; + const models = rawModels + .map((m: { id?: string; name?: string; model?: string }) => m.id || m.name || m.model || '') + .filter((id: string) => typeof id === 'string' && id.trim().length > 0) + .sort(); + + return NextResponse.json({ models, source: modelsUrl }); + } catch (error) { + const msg = error instanceof Error ? error.message : 'Failed to load models'; + return NextResponse.json({ error: msg }, { status: 500 }); + } +} diff --git a/admin-next/src/components/chatbot/chatbot-settings-page.tsx b/admin-next/src/components/chatbot/chatbot-settings-page.tsx index 559b0f4..0e43651 100755 --- a/admin-next/src/components/chatbot/chatbot-settings-page.tsx +++ b/admin-next/src/components/chatbot/chatbot-settings-page.tsx @@ -95,6 +95,8 @@ export function ChatbotSettingsPage() { const [settings, setSettings] = useState(DEFAULTS); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); + const [models, setModels] = useState([]); + const [loadingModels, setLoadingModels] = useState(false); const loadSettings = useCallback(async () => { try { @@ -159,6 +161,31 @@ export function ChatbotSettingsPage() { } }; + const loadModels = async () => { + setLoadingModels(true); + try { + const qs = new URLSearchParams(); + if (settings.chatbot_api_endpoint) qs.set("endpoint", settings.chatbot_api_endpoint); + if (settings.chatbot_api_key) qs.set("apiKey", settings.chatbot_api_key); + const res = await fetch(`/api/chatbot/models?${qs.toString()}`); + const data = await res.json(); + if (res.ok && Array.isArray(data.models)) { + setModels(data.models); + if (data.models.length > 0) { + toast.success(`Загружено моделей: ${data.models.length}`); + } else { + toast.info("Провайдер не вернул список моделей"); + } + } else { + toast.error(data.error || "Ошибка загрузки моделей"); + } + } catch { + toast.error("Ошибка соединения с провайдером"); + } finally { + setLoadingModels(false); + } + }; + const SaveButton = () => (
+
+ update("chatbot_model", v)} - > - - - - - DeepSeek V4 Flash - DeepSeek V4 Pro - DeepSeek V4 Flash 0731 - Kimi K3 - Kimi K2.6 - Kimi K2.7 Code - Gemma 4 31B - GPT-OSS 120B - GPT-OSS 20B - Mistral Large 3 675B - Nemotron 3 Ultra - Nemotron 3 Super - MiniMax M3 - MiniMax M2.7 - Qwen 3.5 397B - GLM 5.2 - GLM 5.1 - Nemotron 3 Nano 30B - - + onChange={(e) => update("chatbot_model", e.target.value)} + placeholder="Введите модель вручную, напр. deepseek-chat / gpt-4o / llama3.1:8b" + className="font-mono" + />

- Доступные модели Ollama Cloud. Для Custom — введите вручную. + Кнопка загружает список моделей из настроенного API (OpenAI-совместимый + /models). Для Custom-провайдера введите название модели вручную.