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:
@@ -8,6 +8,7 @@
|
||||
var IN = 'padding:8px 11px;border:1px solid var(--border,#e2e8f0);border-radius:9px;font:inherit;font-size:.85rem;width:100%;box-sizing:border-box;background:var(--surface,#fff);color:var(--text,#0f172a)';
|
||||
var SPARK = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/></svg>';
|
||||
function fmtTok(n) { if (!n) return '—'; if (n >= 1000000) { var m = n / 1000000; return (m >= 10 ? Math.round(m) : m.toFixed(1).replace(/\.0$/, '')) + 'M'; } if (n >= 1000) return Math.round(n / 1000) + 'K'; return String(n); }
|
||||
function fmtLimits(L) { var s = 'контекст <b>' + fmtTok(L.ctx) + '</b> · ответ до <b>' + fmtTok(L.out) + '</b> токенов'; if (L.free === true) s += ' · <b>бесплатно</b>'; else if (L.free === false) s += ' · платно'; return s; }
|
||||
|
||||
function ensureStyle() {
|
||||
if (document.getElementById('asst-adm-style')) return;
|
||||
@@ -100,6 +101,8 @@
|
||||
'<div class="asst-flabel">URL (chat/completions)</div><input id="asst-url" placeholder="https://…/chat/completions" style="' + IN + '" />' +
|
||||
'<div id="asst-kbox" style="display:none"><div class="asst-flabel">Модель Kilo (бесплатные)</div><select id="asst-kmodels" style="' + IN + '"></select></div>' +
|
||||
'<div class="asst-flabel">Модель</div><input id="asst-model" placeholder="model-id" style="' + IN + '" />' +
|
||||
'<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap"><button id="asst-fetch" type="button" class="asst-ib">Загрузить модели провайдера</button><span id="asst-fetch-st" style="font-size:.74rem;color:#8a94a6"></span></div>' +
|
||||
'<select id="asst-fetched" style="display:none;' + IN + '"></select>' +
|
||||
'<div class="asst-flabel">API-ключ</div><input id="asst-key" type="password" autocomplete="off" placeholder="ключ" style="' + IN + '" />' +
|
||||
'<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:4px"><button id="asst-save" class="asst-ib primary" style="padding:8px 16px">Сохранить провайдера</button><button id="asst-cancel" class="asst-ib" style="padding:8px 16px">Отмена</button></div>' +
|
||||
'</div>';
|
||||
@@ -127,14 +130,18 @@
|
||||
else listEl.innerHTML = providers.map(function (p) {
|
||||
var isKilo = /kilocode\.ai/.test(p.url || '');
|
||||
var act = p.id === activeId;
|
||||
var ksel = '', lim = '';
|
||||
var ksel = '';
|
||||
if (isKilo) {
|
||||
var opts = kiloModels.slice();
|
||||
if (!opts.some(function (m) { return m.id === p.model; })) opts = [{ id: p.model, label: p.model }].concat(opts);
|
||||
ksel = '<select class="asst-ksel" data-ksel="' + p.id + '">' + opts.map(function (m) { return '<option value="' + esc(m.id) + '"' + (m.id === p.model ? ' selected' : '') + '>' + esc(m.label) + '</option>'; }).join('') + '</select>';
|
||||
var km = kiloModels.find(function (m) { return m.id === p.model; });
|
||||
if (km) lim = '<div class="asst-pclim">контекст <b>' + fmtTok(km.ctx) + '</b> · ответ до <b>' + fmtTok(km.out) + '</b> токенов · <b>бесплатно</b></div>';
|
||||
}
|
||||
// лимиты: сохранённые на провайдере → для Kilo из списка → иначе авто-подгрузка
|
||||
var L = p.ctx ? { ctx: p.ctx, out: p.out, free: p.free } : null;
|
||||
if (!L && isKilo) { var km = kiloModels.find(function (m) { return m.id === p.model; }); if (km) L = { ctx: km.ctx, out: km.out, free: true }; }
|
||||
var lim = L
|
||||
? '<div class="asst-pclim" data-lim="' + p.id + '">' + fmtLimits(L) + '</div>'
|
||||
: '<div class="asst-pclim" data-lim="' + p.id + '" style="opacity:.6">лимиты: загрузка…</div>';
|
||||
return '<div class="asst-pcard' + (act ? ' active' : '') + '">' +
|
||||
'<div class="asst-pcic">' + SPARK + '</div>' +
|
||||
'<div class="asst-pcb"><div class="asst-pcn">' + esc(p.name || 'Провайдер') +
|
||||
@@ -149,8 +156,20 @@
|
||||
'</div></div>';
|
||||
}).join('');
|
||||
|
||||
// авто-подгрузка лимитов для провайдеров без сохранённых (Gemini, новые модели) — фоном, сервер кэширует
|
||||
var _pickedLimits = null;
|
||||
providers.forEach(function (p) {
|
||||
if (p.ctx) return;
|
||||
if (/kilocode\.ai/.test(p.url || '') && kiloModels.some(function (m) { return m.id === p.model; })) return;
|
||||
LS.adminAssistantModels({ id: p.id }).then(function (r) {
|
||||
var el = host.querySelector('[data-lim="' + p.id + '"]'); if (!el) return;
|
||||
if (r && !r.error && r.current) { el.style.opacity = ''; el.innerHTML = fmtLimits(r.current); }
|
||||
else { el.style.display = 'none'; }
|
||||
}).catch(function () { var el = host.querySelector('[data-lim="' + p.id + '"]'); if (el) el.style.display = 'none'; });
|
||||
});
|
||||
|
||||
function openForm(show) { Q('#asst-form').style.display = show ? 'flex' : 'none'; }
|
||||
function clearForm() { editingId = null; Q('#asst-fhead').textContent = 'Новый провайдер'; Q('#asst-preset').value = ''; Q('#asst-name').value = ''; Q('#asst-url').value = ''; Q('#asst-model').value = ''; Q('#asst-key').value = ''; Q('#asst-key').placeholder = 'ключ'; toggleKilo(); openForm(false); }
|
||||
function clearForm() { editingId = null; _pickedLimits = null; Q('#asst-fhead').textContent = 'Новый провайдер'; Q('#asst-preset').value = ''; Q('#asst-name').value = ''; Q('#asst-url').value = ''; Q('#asst-model').value = ''; Q('#asst-key').value = ''; Q('#asst-key').placeholder = 'ключ'; Q('#asst-fetched').style.display = 'none'; Q('#asst-fetched').innerHTML = ''; Q('#asst-fetch-st').textContent = ''; toggleKilo(); openForm(false); }
|
||||
function toggleKilo() {
|
||||
var isKilo = /kilocode\.ai/.test(Q('#asst-url').value || '');
|
||||
Q('#asst-kbox').style.display = isKilo ? '' : 'none';
|
||||
@@ -163,7 +182,10 @@
|
||||
// карточные действия
|
||||
listEl.querySelectorAll('[data-ksel]').forEach(function (sel) {
|
||||
sel.addEventListener('change', function () {
|
||||
LS.adminSaveProvider({ id: sel.getAttribute('data-ksel'), model: sel.value }).then(function () { LS.toast('Модель обновлена', 'success'); render(); }).catch(function () { LS.toast('Ошибка', 'error'); });
|
||||
var km = kiloModels.find(function (m) { return m.id === sel.value; });
|
||||
var body = { id: sel.getAttribute('data-ksel'), model: sel.value };
|
||||
if (km) { body.ctx = km.ctx; body.out = km.out; body.free = true; }
|
||||
LS.adminSaveProvider(body).then(function () { LS.toast('Модель обновлена', 'success'); render(); }).catch(function () { LS.toast('Ошибка', 'error'); });
|
||||
});
|
||||
});
|
||||
listEl.querySelectorAll('[data-act]').forEach(function (b) {
|
||||
@@ -186,14 +208,38 @@
|
||||
|
||||
// форма
|
||||
Q('#asst-ftoggle').addEventListener('click', function () { var open = Q('#asst-form').style.display !== 'none'; if (open) { clearForm(); } else { clearForm(); openForm(true); Q('#asst-name').focus(); } });
|
||||
Q('#asst-preset').addEventListener('change', function () { var p = presets[Number(this.value)]; if (p) { Q('#asst-url').value = p.url; Q('#asst-model').value = p.model; if (!Q('#asst-name').value) Q('#asst-name').value = p.name; toggleKilo(); } });
|
||||
Q('#asst-url').addEventListener('input', toggleKilo);
|
||||
Q('#asst-kmodels').addEventListener('change', function () { Q('#asst-model').value = this.value; });
|
||||
Q('#asst-preset').addEventListener('change', function () { var p = presets[Number(this.value)]; if (p) { Q('#asst-url').value = p.url; Q('#asst-model').value = p.model; if (!Q('#asst-name').value) Q('#asst-name').value = p.name; _pickedLimits = null; Q('#asst-fetched').style.display = 'none'; Q('#asst-fetch-st').textContent = ''; toggleKilo(); } });
|
||||
Q('#asst-url').addEventListener('input', function () { toggleKilo(); _pickedLimits = null; Q('#asst-fetched').style.display = 'none'; Q('#asst-fetch-st').textContent = ''; });
|
||||
Q('#asst-kmodels').addEventListener('change', function () { Q('#asst-model').value = this.value; var km = kiloModels.find(function (m) { return m.id === Q('#asst-model').value; }); _pickedLimits = km ? { ctx: km.ctx, out: km.out, free: true } : null; });
|
||||
Q('#asst-cancel').addEventListener('click', clearForm);
|
||||
Q('#asst-fetch').addEventListener('click', async function () {
|
||||
var st = Q('#asst-fetch-st'), btn = this;
|
||||
var params = editingId ? { id: editingId } : {};
|
||||
if (!editingId) { params.url = Q('#asst-url').value.trim(); params.key = Q('#asst-key').value.trim(); if (!params.url) { st.textContent = 'Сначала введите URL'; return; } }
|
||||
btn.disabled = true; st.textContent = 'Загружаю…';
|
||||
try {
|
||||
var r = await LS.adminAssistantModels(params);
|
||||
if (!r || r.error) { st.textContent = 'Ошибка: ' + ((r && (r.error || ('HTTP ' + r.status))) || 'нет данных'); return; }
|
||||
var models = r.models || [];
|
||||
if (!models.length) { st.textContent = 'Моделей не найдено'; return; }
|
||||
var sel = Q('#asst-fetched');
|
||||
sel.innerHTML = '<option value="">— выбрать из ' + models.length + ' моделей —</option>' + models.map(function (m) {
|
||||
return '<option value="' + esc(m.id) + '" data-ctx="' + (m.ctx || '') + '" data-out="' + (m.out || '') + '" data-free="' + (m.free === null || m.free === undefined ? '' : m.free) + '">' + esc(m.id) + ' (' + fmtTok(m.ctx) + '/' + fmtTok(m.out) + (m.free === true ? ', free' : '') + ')</option>';
|
||||
}).join('');
|
||||
sel.style.display = ''; st.textContent = 'Загружено: ' + models.length + ' — выберите модель';
|
||||
} catch (e) { st.textContent = 'Ошибка'; } finally { btn.disabled = false; }
|
||||
});
|
||||
Q('#asst-fetched').addEventListener('change', function () {
|
||||
var o = this.options[this.selectedIndex]; if (!o || !o.value) { _pickedLimits = null; return; }
|
||||
Q('#asst-model').value = o.value;
|
||||
var fr = o.getAttribute('data-free');
|
||||
_pickedLimits = { ctx: Number(o.getAttribute('data-ctx')) || null, out: Number(o.getAttribute('data-out')) || null, free: fr === '' ? null : fr === 'true' };
|
||||
});
|
||||
Q('#asst-save').addEventListener('click', async function () {
|
||||
var body = { name: Q('#asst-name').value, url: Q('#asst-url').value, model: Q('#asst-model').value };
|
||||
if (editingId) body.id = editingId;
|
||||
var k = Q('#asst-key').value.trim(); if (k) body.key = k;
|
||||
if (_pickedLimits) { body.ctx = _pickedLimits.ctx; body.out = _pickedLimits.out; body.free = _pickedLimits.free; }
|
||||
if (!body.url || !body.model) { LS.toast('Заполни URL и модель', 'warn'); return; }
|
||||
try { await LS.adminSaveProvider(body); LS.toast('Сохранено', 'success'); clearForm(); render(); } catch (e) { LS.toast('Ошибка: ' + (e.message || ''), 'error'); }
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user