feat(assistant): авто-получение лимитов моделей для любого провайдера
Новый GET /admin/assistant/models: тянет список моделей провайдера с лимитами (OpenAI-совместимый /models: context_length+max_completion_tokens+pricing; нативный Google generativelanguage: inputTokenLimit/outputTokenLimit) и кэширует лимиты текущей модели на провайдере. Карточка показывает лимиты у ВСЕХ провайдеров (не только Kilo), для отсутствующих — фоновая авто-подгрузка. В форме — кнопка «Загрузить модели провайдера» с выбором модели и её лимитами. Так Gemini и любые новые модели получают лимиты автоматически. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -916,7 +916,7 @@ function getAssistant(_req, res) {
|
||||
}
|
||||
}
|
||||
const list = _aProviders();
|
||||
const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key }));
|
||||
const providers = list.map(p => ({ id: p.id, name: p.name, url: p.url, model: p.model, hasKey: !!p.key, ctx: p.ctx || null, out: p.out || null, free: (typeof p.free === 'boolean' ? p.free : null) }));
|
||||
const activeId = _aset('assistant_active') || (providers[0] && providers[0].id) || null;
|
||||
const ap = list.find(p => p.id === activeId);
|
||||
const active = !!(ap && (ap.key || _aIsLocal(ap.url)));
|
||||
@@ -962,11 +962,20 @@ function saveProvider(req, res) {
|
||||
let p;
|
||||
if (b.id) { p = arr.find(x => x.id === b.id); if (!p) { p = { id: b.id }; arr.push(p); } }
|
||||
else { p = { id: 'p' + Date.now().toString(36) + Math.floor(Math.random() * 1000) }; arr.push(p); }
|
||||
const prevModel = p.model;
|
||||
if (typeof b.name === 'string') p.name = b.name.trim().slice(0, 40) || p.name || 'Провайдер';
|
||||
if (typeof b.url === 'string') p.url = b.url.trim().slice(0, 300);
|
||||
if (typeof b.model === 'string') p.model = b.model.trim().slice(0, 120);
|
||||
if (typeof b.key === 'string' && b.key.trim()) p.key = b.key.trim().slice(0, 400);
|
||||
if (!p.name) p.name = 'Провайдер';
|
||||
// Лимиты модели (ctx/out/free): из тела или сброс при смене модели (перезапросятся авто)
|
||||
if (b.ctx !== undefined || b.out !== undefined || b.free !== undefined) {
|
||||
if (b.ctx !== undefined) p.ctx = (b.ctx === null || b.ctx === '') ? null : (Number(b.ctx) || null);
|
||||
if (b.out !== undefined) p.out = (b.out === null || b.out === '') ? null : (Number(b.out) || null);
|
||||
if (b.free !== undefined) p.free = (typeof b.free === 'boolean') ? b.free : null;
|
||||
} else if (typeof b.model === 'string' && p.model !== prevModel) {
|
||||
p.ctx = null; p.out = null; p.free = null;
|
||||
}
|
||||
_aSetProviders(arr);
|
||||
if (b.makeActive || arr.length === 1) db.prepare("INSERT OR REPLACE INTO app_settings (key, value) VALUES ('assistant_active', ?)").run(p.id);
|
||||
audit(req, 'assistant.provider', p.id, 'сохранён');
|
||||
@@ -983,6 +992,63 @@ function deleteProvider(req, res) {
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
/* Запрос списка моделей провайдера с лимитами. Понимает OpenAI-совместимый /models
|
||||
* (context_length + max_completion_tokens + pricing) и нативный Google generativelanguage
|
||||
* (inputTokenLimit / outputTokenLimit). */
|
||||
async function _fetchModels(url, key) {
|
||||
if (typeof fetch !== 'function') return { error: 'fetch недоступен' };
|
||||
url = String(url || '');
|
||||
const isGoogle = /generativelanguage\.googleapis\.com/.test(url);
|
||||
let ep; const headers = {};
|
||||
if (isGoogle) {
|
||||
const base = url.replace(/\/openai\/chat\/completions.*$/, '').replace(/\/chat\/completions.*$/, '');
|
||||
ep = base + '/models?pageSize=200' + (key ? '&key=' + encodeURIComponent(key) : '');
|
||||
} else {
|
||||
ep = url.replace(/\/chat\/completions.*$/, '/models');
|
||||
if (key) headers.Authorization = 'Bearer ' + key;
|
||||
}
|
||||
const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 15000);
|
||||
try {
|
||||
const r = await fetch(ep, { headers, signal: ctrl.signal });
|
||||
if (!r.ok) return { error: 'HTTP ' + r.status, status: r.status };
|
||||
const j = await r.json();
|
||||
const raw = j.data || j.models || [];
|
||||
const models = [];
|
||||
for (const m of raw) {
|
||||
const methods = m.supportedGenerationMethods;
|
||||
if (methods && methods.indexOf('generateContent') === -1) continue; // Google: только генеративные
|
||||
const id = String(m.id || m.name || '').replace(/^models\//, '');
|
||||
if (!id) continue;
|
||||
const tp = m.top_provider || {};
|
||||
const pr = m.pricing || {};
|
||||
const ctx = m.context_length || tp.context_length || m.inputTokenLimit || null;
|
||||
const out = tp.max_completion_tokens || m.outputTokenLimit || null;
|
||||
let free = null;
|
||||
if (pr && (pr.prompt != null || pr.completion != null)) free = Number(pr.prompt) === 0 && Number(pr.completion) === 0;
|
||||
models.push({ id, ctx: ctx || null, out: out || null, free });
|
||||
}
|
||||
return { models };
|
||||
} catch (e) { return { error: e.name === 'AbortError' ? 'timeout' : 'network' }; }
|
||||
finally { clearTimeout(timer); }
|
||||
}
|
||||
|
||||
/* GET /api/admin/assistant/models?id=&url=&key= — модели провайдера с лимитами.
|
||||
* С id: берём сохранённого провайдера и кэшируем лимиты его текущей модели. */
|
||||
async function getProviderModels(req, res) {
|
||||
const id = req.query.id ? String(req.query.id) : '';
|
||||
let url = req.query.url, key = req.query.key, prov = null;
|
||||
if (id) { prov = _aProviders().find(x => x.id === id); if (!prov) return res.json({ error: 'провайдер не найден' }); url = prov.url; key = prov.key; }
|
||||
const r = await _fetchModels(url, key);
|
||||
if (r.error) return res.json({ error: r.error, status: r.status });
|
||||
let current = null;
|
||||
if (prov) {
|
||||
const arr = _aProviders(); const p2 = arr.find(x => x.id === id);
|
||||
const m = p2 && r.models.find(x => x.id === p2.model);
|
||||
if (p2 && m) { p2.ctx = m.ctx; p2.out = m.out; p2.free = m.free; _aSetProviders(arr); current = { ctx: m.ctx, out: m.out, free: m.free }; }
|
||||
}
|
||||
res.json({ models: r.models, current });
|
||||
}
|
||||
|
||||
/* POST /api/admin/assistant/active { id } — выбрать активного провайдера */
|
||||
function setActiveProvider(req, res) {
|
||||
const id = String((req.body && req.body.id) || '');
|
||||
@@ -1036,5 +1102,5 @@ module.exports = {
|
||||
getSecurityLog, clearSecurityLog,
|
||||
getTopics, createTopic, updateTopic, deleteTopic,
|
||||
broadcast,
|
||||
getAssistant, saveAssistant, testAssistant, reindexTextbooks, saveProvider, deleteProvider, setActiveProvider,
|
||||
getAssistant, saveAssistant, testAssistant, reindexTextbooks, saveProvider, deleteProvider, setActiveProvider, getProviderModels,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user