mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-22 14:38:12 +02:00
Repris d'AJEAN 38919238 (#121 amont) et de la partie moteur de 6db3f3d0 : STRATA_SPLIT=layers dans le preset fait lister les deux cartes dans la clé « gpu » de la config (principale d'abord, numéros nvidia-smi) ; le serveur de Strata en déduit --layer-split. Rien du mode d'aide ne subsiste alors : ni --expert-cache-device1 / --remote-expert-opt, ni source d'aide à la lecture des prompts, ni encodeur d'images déplacé ; la réserve de VRAM de l'installeur reste. Sans la clé, ou la seconde carte coupée, rien ne change. Propre à Loki : le placement des experts selon la RAM disponible ne choisit jamais le mode hors RAM en partage des couches (la seconde carte n'y garde pas d'experts, la marge calculée pour elle serait fausse). Réglages : « split » (helper ou layers ; absent = inchangé pour un ancien client), détail split_layers. Fenêtre Strata : la case « Carte d'appoint » devient un choix « Seconde carte » (non utilisée, aide aux experts, partage des couches) ; la carte de configuration du moteur actif l'affiche. Façade Rust alignée (strata/mod.rs, strata/api.rs, tests portés) ; journal de parité régénéré sous Linux avec les réglages et l'affichage du partage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
384 lines
20 KiB
JavaScript
384 lines
20 KiB
JavaScript
let LAST_PRESET=null; // id du preset actif vu au dernier poll (détecte une bascule faite ailleurs)
|
|
async function loadStatus(){
|
|
const s=await jget('/api/status');
|
|
// Preset actif changé alors qu'on n'a rien touché ici : un AUTRE appareil a
|
|
// basculé de preset/modèle. On rafraîchit la liste (et le libellé du preset)
|
|
// pour rester en phase sans recharger l'app. Premier poll (LAST_PRESET null) :
|
|
// on note seulement la valeur, loadPresets a déjà tourné au démarrage.
|
|
if(typeof s.preset!=='undefined'){
|
|
if(LAST_PRESET!==null && s.preset!==LAST_PRESET && typeof loadPresets==='function') loadPresets();
|
|
LAST_PRESET=s.preset;
|
|
}
|
|
const el=document.getElementById('status-svc');
|
|
// Trois états : service coupé (err) · service actif mais modèle pas encore
|
|
// chargé (loading, llama-server renvoie 503) · modèle prêt (ok).
|
|
// Pastille COURTE et honnête. Le détail (cause exacte) va dans #model-err
|
|
// dessous : la pastille ne doit pas affirmer « incompatible » quand l'échec
|
|
// peut être tout autre chose.
|
|
let cls='err', txt='arrêté';
|
|
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
|
else if(s.load_error){ cls='err'; txt='erreur'; }
|
|
else if(s.active){
|
|
cls='loading'; txt='chargement…';
|
|
// Pourcentage estimé (octets du modèle déjà lus) : voir « ça avance »
|
|
// plutôt qu'un « chargement… » figé qui ressemble à un plantage.
|
|
if(typeof s.load_pct==='number' && s.load_pct>=0) txt='chargement '+s.load_pct+' %';
|
|
}
|
|
el.className='statuspill '+cls;
|
|
el.innerHTML='<span class="dot"></span>'+txt;
|
|
MODEL_READY = !!(s.active && s.health);
|
|
// Le bouton « Libérer la VRAM » suit l'état RÉEL du moteur (et pas un drapeau
|
|
// local) : un arrêt venu d'un autre onglet, du terminal ou d'un job doit se
|
|
// lire ici aussi, sinon le bouton propose de libérer une carte déjà libre.
|
|
ENGINE_ACTIVE = !!s.active;
|
|
// Un moteur qui a PLANTÉ au chargement n'est pas « déchargé » : sans ce
|
|
// drapeau, syncSendBtn proposerait de recharger un modèle qui va replanter.
|
|
LOAD_ERROR = s.load_error || '';
|
|
paintVramBtn();
|
|
// Le bouton d'envoi suit l'état du moteur : inutile de pouvoir envoyer un
|
|
// message à un modèle qui n'est pas encore chargé (voir syncSendBtn).
|
|
STATUS_SEEN = true;
|
|
if(typeof syncSendBtn==='function') syncSendBtn();
|
|
if(s.ctx){ CTX_MAX=s.ctx; updateCtxMeter(); }
|
|
if(s.version){
|
|
document.getElementById('ver').textContent='v'+s.version;
|
|
}
|
|
// Avertissement de lancement (App Translocation macOS) : rare, mais il explique
|
|
// des symptomes tres deroutants, donc on l'affiche en permanence tant qu'il dure.
|
|
const wb=document.getElementById('app-warn');
|
|
if(wb){
|
|
if(s.warn){ wb.textContent='⚠ '+s.warn; wb.style.display=''; }
|
|
else { wb.style.display='none'; }
|
|
}
|
|
// Conseil de performance (stockage Unraid via fuse.shfs) : encart discret,
|
|
// pas le bandeau d'alerte — rien n'est perdu, seul le décodage ralentit.
|
|
// Il reste affiché tant que la cause dure : masquable, et seulement pour CE
|
|
// texte — un autre conseil (autre chemin concerné) réapparaît.
|
|
const hb=document.getElementById('app-hint');
|
|
if(hb){
|
|
let gone='';
|
|
try{ gone=localStorage.getItem('loki-hint-dismissed')||''; }catch(e){}
|
|
if(s.hint && s.hint!==gone){
|
|
document.getElementById('app-hint-text').textContent=s.hint;
|
|
hb.dataset.hint=s.hint; hb.style.display='';
|
|
}
|
|
else { hb.style.display='none'; }
|
|
}
|
|
// Modèle qui ne charge pas (souvent un moteur incompatible) : message explicite
|
|
// plutôt qu'un « chargement… » perpétuel ou un crash-loop muet.
|
|
const me=document.getElementById('model-err');
|
|
if(me){
|
|
if(s.load_error){ me.textContent='⚠ '+s.load_error; me.style.display=''; }
|
|
else { me.style.display='none'; }
|
|
}
|
|
}
|
|
// Masque le conseil de stockage pour ce navigateur (simple confort : sans
|
|
// localStorage, il revient au prochain rafraîchissement, rien de plus).
|
|
function dismissHint(){
|
|
const hb=document.getElementById('app-hint');
|
|
if(!hb) return;
|
|
try{ localStorage.setItem('loki-hint-dismissed', hb.dataset.hint||''); }catch(e){}
|
|
hb.style.display='none';
|
|
}
|
|
// Journal du moteur — section repliable comme les autres. La pastille d'état
|
|
// reste un raccourci ; le chargement du journal est déclenché par l'événement
|
|
// `toggle` du <details>, qu'on l'ouvre par la pastille ou par son chevron.
|
|
function toggleSvcLog(){
|
|
const box=document.getElementById('svc-log-box');
|
|
if(!box) return;
|
|
// openDetails déplie aussi « Réglages » : sans lui, la section s'ouvrait à
|
|
// l'intérieur d'un repli fermé — donc nulle part, du point de vue de l'écran.
|
|
if(box.open) box.open = false; else openDetails(box);
|
|
}
|
|
async function loadSvcLog(){
|
|
const el=document.getElementById('svc-log');
|
|
if(!el) return;
|
|
el.textContent='chargement du journal…';
|
|
try{
|
|
const r=await jget('/api/service/log?n=120');
|
|
el.textContent = (r && r.log && r.log.trim()) ? r.log : 'journal vide — le moteur n\'a encore rien écrit.';
|
|
el.scrollTop = el.scrollHeight;
|
|
}catch(e){ el.textContent='journal indisponible : '+e; }
|
|
}
|
|
async function checkUpdate(){
|
|
const b=document.getElementById('upd-check'), msg=document.getElementById('upd-msg');
|
|
b.disabled=true; msg.textContent='Vérification…';
|
|
try{
|
|
const r=await jget('/api/update');
|
|
// En conteneur, la mise à jour passe par l'image : on l'explique au lieu de
|
|
// proposer un remplacement de binaire impossible ici.
|
|
if(r.note){ msg.textContent=r.note; }
|
|
else if(r.error){ msg.textContent='Erreur : '+r.error; }
|
|
else if(r.available){
|
|
msg.innerHTML='Nouvelle version <b>v'+r.latest+'</b> disponible. ';
|
|
const btn=document.createElement('button'); btn.textContent='Mettre à jour'; btn.onclick=applyUpdate;
|
|
msg.appendChild(btn);
|
|
} else { msg.textContent='Loki est à jour ✓'; }
|
|
}catch(e){ msg.textContent='Erreur réseau'; }
|
|
b.disabled=false;
|
|
}
|
|
// Emplacements — affichés avec le journal du moteur : c'est le panneau qu'on
|
|
// ouvre quand on cherche à comprendre l'état de son installation.
|
|
async function showPaths(){
|
|
const el=document.getElementById('paths-msg');
|
|
if(!el) return;
|
|
el.textContent='…';
|
|
try{
|
|
const p=await jget('/api/paths');
|
|
const rows=[['Données',p.home],['Base (config, préférences, conversation)',p.database],['Modèles',p.models],['Presets',p.presets],['Mémoire',p.memory],['Fichiers créés par l\'IA',p.workspace],['Moteur llama.cpp',p.backends],['Programme',p.exe]];
|
|
el.innerHTML=rows.map(r=>'<div style="margin-bottom:4px">'+r[0]+'<br><code style="word-break:break-all">'+escHtml(r[1]||'')+'</code></div>').join('');
|
|
}catch(e){ el.textContent='Erreur'; }
|
|
}
|
|
async function applyUpdate(){
|
|
const msg=document.getElementById('upd-msg');
|
|
msg.textContent='Téléchargement et installation…';
|
|
try{
|
|
// Signal dédié : le timeout par défaut (30 s) coupe le téléchargement du
|
|
// binaire sur une connexion lente et fait croire à un échec alors que la
|
|
// mise à jour aboutit côté serveur.
|
|
const ac=new AbortController(); const t=setTimeout(()=>ac.abort(), 10*60*1000);
|
|
let r;
|
|
try{ r=await (await jfetch('/api/update/apply',{method:'POST',headers:{'Content-Type':'application/json'},body:'{}',signal:ac.signal})).json(); }
|
|
finally{ clearTimeout(t); }
|
|
if(r.ok){
|
|
msg.innerHTML='✓ Installé en <b>v'+r.version+'</b>.<br>'+(r.restart||'');
|
|
// Redémarrage auto du service côté serveur : le flux va se couper puis
|
|
// reconnecter tout seul (connectStream boucle). On rafraîchit l'état après.
|
|
if(r.restarting){ toast('mise à jour appliquée — reconnexion…'); setTimeout(loadAll, 6000); }
|
|
}
|
|
else { msg.textContent='Échec : '+(r.error||'inconnu'); }
|
|
}catch(e){ msg.textContent='Erreur pendant la mise à jour (réessaie).'; }
|
|
}
|
|
// Compteur de contexte : CTX_USED estimé via les stats serveur (prefill+decode
|
|
// du dernier tour ≈ taille du prochain prompt). À 90% on propose de compacter.
|
|
let CTX_MAX=0, CTX_USED=0, MODEL_READY=false;
|
|
// STATUS_SEEN : /api/status a répondu au moins une fois. Avant ça (ou s'il ne
|
|
// répond pas), on ne verrouille RIEN — mieux vaut un envoi qui échoue qu'un chat
|
|
// bloqué par un état inconnu.
|
|
let STATUS_SEEN=false;
|
|
function setCtxUsed(n){ CTX_USED=n||0; updateCtxMeter(); }
|
|
function updateCtxMeter(){
|
|
if(!CTX_MAX) return;
|
|
const pct=Math.min(100, Math.round(CTX_USED*100/CTX_MAX));
|
|
const fill=document.getElementById('ctx-fill');
|
|
fill.style.width=pct+'%';
|
|
fill.style.background = pct>=90 ? 'var(--err,#c44)' : pct>=70 ? 'var(--warn,#c93)' : 'var(--ok,#3a7)';
|
|
// Les chiffres SANS le mot « contexte » : la jauge juste en dessous dit déjà de
|
|
// quoi on parle, et le pied de carte est étroit. Le libellé complet reste en
|
|
// infobulle.
|
|
const ct=document.getElementById('ctx-text');
|
|
ct.textContent=CTX_USED+' / '+CTX_MAX+' ('+pct+'%)';
|
|
ct.title='contexte utilisé';
|
|
// Bouton de compaction MANUELLE : visible dès la moitié du contexte pour qu'on
|
|
// puisse compacter à la demande avant que l'auto-compaction (75%) ne s'en charge.
|
|
document.getElementById('ctx-compact').style.display = (pct>=50 && CTX_USED>0) ? 'inline-block' : 'none';
|
|
}
|
|
// ─── Moniteur machine (pied de barre latérale) ───────────────────────────────
|
|
// Une jauge par ressource : nom, pourcentage, barre, puis le détail chiffré en
|
|
// dessous. C'est la hiérarchie de la maquette — on lit la charge d'un coup
|
|
// d'œil, et le détail (Gio, température) reste là pour qui le cherche.
|
|
//
|
|
// Construit EN DOM et non en innerHTML : les noms de GPU viennent du pilote,
|
|
// donc d'ailleurs. Même règle que pour les noms de fichiers et les titres de
|
|
// discussions.
|
|
function perfRow(label, pct, title){
|
|
pct = Math.max(0, Math.min(100, Math.round(pct||0)));
|
|
const row=document.createElement('div'); row.className='perf-row';
|
|
if(title) row.title=title;
|
|
const l=document.createElement('div'); l.className='perf-l';
|
|
const b=document.createElement('b'); b.textContent=label;
|
|
const v=document.createElement('span'); v.className='perf-pct'; v.textContent=pct+'%';
|
|
l.append(b,v);
|
|
const track=document.createElement('div'); track.className='perf-track';
|
|
const fill=document.createElement('div');
|
|
// Sauge jusqu'à 75 %, ambre au-delà, rouille à 90 : la couleur ne sert à rien
|
|
// tant que la machine respire, elle doit sauter aux yeux quand elle sature.
|
|
fill.className='perf-fill'+(pct>=90?' max':pct>=75?' hot':'');
|
|
fill.style.width=pct+'%';
|
|
track.appendChild(fill);
|
|
row.append(l,track);
|
|
return row;
|
|
}
|
|
async function loadVram(){
|
|
const gpus=await jget('/api/vram');
|
|
const box=document.getElementById('vram');
|
|
if(!box) return;
|
|
box.textContent='';
|
|
if(!gpus || !gpus.length){
|
|
const d=document.createElement('div'); d.className='perf-sub'; d.textContent='(pas de GPU détecté)';
|
|
box.appendChild(d);
|
|
return;
|
|
}
|
|
// Deux mesures par carte, deux jauges — une seule barre ne peut pas porter la
|
|
// charge du processeur ET le remplissage de la mémoire vidéo, et pour du LLM
|
|
// c'est la VRAM qui décide si un modèle tient. Sur une machine à UNE carte on
|
|
// retombe exactement sur les trois jauges de la maquette (GPU, VRAM, RAM).
|
|
const many = gpus.length > 1;
|
|
gpus.forEach((g,i)=>{
|
|
const vram=(g.used/1024).toFixed(1)+' / '+(g.total/1024).toFixed(1)+' Gio';
|
|
box.appendChild(perfRow('GPU '+i, g.util, g.name+' · '+vram+' · '+g.temp+' °C'));
|
|
box.appendChild(perfRow(many ? 'VRAM '+i : 'VRAM', g.used*100/g.total,
|
|
g.name+' · '+vram));
|
|
});
|
|
}
|
|
// ─── Libérer la mémoire vidéo ───────────────────────────────────────────────
|
|
// Tant que le moteur tourne, le modèle occupe la VRAM : une autre application
|
|
// qui réclame la carte (un jeu, un encodage, un autre serveur d'inférence) ne
|
|
// trouve plus rien à prendre. Ce bouton l'en sort, et le MÊME bouton la
|
|
// reprend ensuite — le geste existait déjà (« arrêter » le service, au fond des
|
|
// réglages), mais son nom ne disait pas qu'il rendait la carte.
|
|
//
|
|
// ENGINE_ACTIVE vient de /api/status (relu toutes les 5 s), jamais d'un drapeau
|
|
// posé au clic : sinon un second onglet, ou un arrêt venu du terminal,
|
|
// afficherait un bouton qui ment.
|
|
let ENGINE_ACTIVE = true, VRAM_BUSY = false, LOAD_ERROR = '';
|
|
function paintVramBtn(){
|
|
const b = document.getElementById('vram-btn');
|
|
if(!b || VRAM_BUSY) return; // pendant l'opération, le libellé appartient à vramBusy
|
|
b.disabled = false;
|
|
b.classList.toggle('reload', !ENGINE_ACTIVE);
|
|
b.textContent = ENGINE_ACTIVE ? 'Libérer la VRAM' : 'Recharger le modèle';
|
|
b.title = ENGINE_ACTIVE
|
|
? 'décharge le modèle et arrête le moteur — la mémoire vidéo repart à la machine'
|
|
: 'relance le moteur : le modèle revient en mémoire vidéo';
|
|
}
|
|
function vramBtnClick(){ return ENGINE_ACTIVE ? vramUnload(false) : vramReload(); }
|
|
function vramBusy(on, label){
|
|
VRAM_BUSY = on;
|
|
const b = document.getElementById('vram-btn');
|
|
if(b){ b.disabled = on; if(on && label) b.textContent = label; }
|
|
if(!on) paintVramBtn();
|
|
}
|
|
// force=true : on a déjà demandé confirmation ET accepté de couper une réponse
|
|
// en cours d'écriture (le serveur refuse sans ça, voir handleVramUnload).
|
|
// Les deux gestes sont exclusifs : un second clic (le bouton des réglages
|
|
// pendant que celui du moniteur travaille) lancerait un « stop » au milieu d'un
|
|
// démarrage, ou l'inverse. VRAM_BUSY tient lieu de verrou.
|
|
async function vramUnload(force){
|
|
if(VRAM_BUSY) return;
|
|
if(!force){
|
|
const ok = await askConfirm(
|
|
'Décharger le modèle ? Le moteur s\'arrête et rend la mémoire vidéo — le chat reste indisponible jusqu\'au rechargement.',
|
|
{title:'Libérer la VRAM', okText:'Libérer'});
|
|
if(!ok) return;
|
|
}
|
|
vramBusy(true, 'libération…');
|
|
let r = null;
|
|
try{ r = await jpost('/api/vram/unload', {force: !!force}); }catch(_){}
|
|
// Le serveur dit l'état RÉEL après coup : le repeindre depuis l'ancien
|
|
// ENGINE_ACTIVE (relu seulement toutes les 5 s) afficherait « Libérer » sur
|
|
// un moteur déjà arrêté.
|
|
if(r && typeof r.active === 'boolean') ENGINE_ACTIVE = r.active;
|
|
vramBusy(false);
|
|
if(r && r.generating){
|
|
const ok = await askConfirm('Une réponse est en cours d\'écriture : décharger maintenant l\'interrompt. Continuer ?',
|
|
{title:'Génération en cours', okText:'Décharger quand même'});
|
|
if(ok) return vramUnload(true);
|
|
return;
|
|
}
|
|
// La mesure d'abord : la dictée peut avoir rendu de la VRAM alors même que le
|
|
// moteur était déjà arrêté — annoncer « déjà arrêté » nierait ce qui vient
|
|
// d'être libéré. Sous quelques dizaines de Mo, c'est le bruit du bureau entre
|
|
// deux lectures, pas une libération : on ne l'annonce pas.
|
|
if(!r || !r.ok) toast('échec : ' + ((r && r.error) || 'réseau'));
|
|
else if(r.gpu && r.freed_mb >= 64) toast('VRAM libérée : ' + fmtMo(r.freed_mb));
|
|
else if(!r.was_active) toast('le moteur était déjà arrêté');
|
|
else toast('modèle déchargé');
|
|
loadAll();
|
|
}
|
|
function fmtMo(mb){ return mb >= 1024 ? (mb/1024).toFixed(1) + ' Gio' : mb + ' Mo'; }
|
|
async function vramReload(){
|
|
if(VRAM_BUSY) return;
|
|
vramBusy(true, 'démarrage…');
|
|
let r = null;
|
|
try{ r = await jpost('/api/vram/reload', {}); }catch(_){}
|
|
if(r && r.ok) ENGINE_ACTIVE = true;
|
|
vramBusy(false);
|
|
if(!r || !r.ok){ toast('échec : ' + ((r && r.error) || 'réseau')); return; }
|
|
toast('modèle en cours de chargement…');
|
|
loadAll();
|
|
}
|
|
async function loadRam(){
|
|
const m=await jget('/api/ram');
|
|
const box=document.getElementById('ram-details');
|
|
if(!box) return;
|
|
if(!m || !m.total){ box.style.display='none'; return; }
|
|
box.style.display='';
|
|
const host=document.getElementById('ram');
|
|
host.textContent='';
|
|
host.appendChild(perfRow('Mémoire vive', m.used*100/m.total,
|
|
(m.used/1024).toFixed(1)+' / '+(m.total/1024).toFixed(1)+' Gio utilisés'));
|
|
}
|
|
async function loadCfg(){
|
|
// /api/llamacpp en parallèle : il indique si le BIN de la config correspond au
|
|
// précompilé (prebuilt.in_use) ou compilé ici (in_use) — sinon c'est un fork perso.
|
|
const [c, lc] = await Promise.all([jget('/api/config'), jget('/api/llamacpp').catch(()=>null)]);
|
|
const row=(k,v,title)=>'<div class="kv"><span>'+k+'</span><span title="'+String(title!=null?title:v).replace(/"/g,'"')+'">'+String(v)+'</span></div>';
|
|
const rows=[];
|
|
// Strata (backend_strata.go) : la config réelle de lancement, calculée par le
|
|
// serveur comme au démarrage (/api/strata → details du modèle actif).
|
|
if(/^(strata|moe)$/i.test((c.ENGINE||'').trim())){
|
|
const s = await jget('/api/strata').catch(()=>null);
|
|
let fam=null, fit=null;
|
|
for(const f of (s&&s.families)||[]){ const x=f.fits.find(x=>x.active); if(x){ fam=f; fit=x; } }
|
|
const d=fit&&fit.details;
|
|
rows.push(row('MOTEUR', 'Strata', 'paquet AJEAN MoE '+((s&&s.version)||'')));
|
|
if(typeof setLoadedModel === 'function') setLoadedModel('');
|
|
if(fam) rows.push(row('MODEL', 'Qwen3.8 Flash Next '+fam.label+' '+fit.quant.id));
|
|
if(d){
|
|
const k=n=>(+n>=1024 ? Math.round(+n/1024)+'K' : n);
|
|
rows.push(row('CTX', d.ctx));
|
|
rows.push(row('KV', d.kv+(d.kv_resident ? ' · '+k(d.kv_resident)+' VRAM' : '')));
|
|
rows.push(row('MTP', d.spec));
|
|
if(d.helper_gpu) rows.push(row('GPU 2', d.split_layers ? 'couches' : 'aide', d.helper_gpu));
|
|
rows.push(row('EXPERTS', ({drop:'mlock (hors appoint)', ram:'RAM', arena:'experts.bin', disk:'mmap (disque)'})[d.mode] || (d.mmap ? 'mmap' : 'RAM'), d.mode_label||''));
|
|
if(d.vision_gpu) rows.push(row('VISION', 'on', d.vision_gpu));
|
|
}else if(c.CTX) rows.push(row('CTX', c.CTX));
|
|
document.getElementById('cfg').innerHTML = rows.join('') + await engineCmdlineRow();
|
|
return;
|
|
}
|
|
if(c.BIN){
|
|
// Moteur : précompilé / compilé / personnalisé (avec le chemin). Le title garde
|
|
// toujours le chemin complet, quel que soit le libellé.
|
|
let v;
|
|
// En conteneur, le moteur vient de l'image : le dire, plutôt que de le
|
|
// ranger dans « personnalisé » qui laisse croire à un bricolage.
|
|
if(lc && lc.provided && sameBinPath(c.BIN, lc.provided_bin)) v='llama.cpp de l\'image';
|
|
// Moteur mis à jour depuis l'image officielle (Réglages → Moteur) : c'est la
|
|
// voie normale en conteneur, pas un « personnalisé ».
|
|
else if(lc && lc.engine && lc.engine.in_use){
|
|
const b = (lc.engine.tag||'').match(/b\d+$/);
|
|
v = b ? 'llama.cpp '+b[0]+' (mis à jour)' : 'llama.cpp mis à jour';
|
|
}
|
|
else if(lc && lc.prebuilt && lc.prebuilt.in_use) v='llama.cpp précompilé';
|
|
else if(lc && lc.in_use) v='llama.cpp compilé';
|
|
else v='llama.cpp personnalisé : '+c.BIN;
|
|
rows.push(row('MOTEUR', v, c.BIN));
|
|
}
|
|
// Le modèle chargé alimente aussi le sélecteur de l'en-tête : c'est la seule
|
|
// source qui le connaisse quand aucun preset ne correspond à la configuration.
|
|
if(typeof setLoadedModel === 'function') setLoadedModel(c.MODEL || '');
|
|
// Nombre de couches du modèle (amont #43) : NGL au-delà de ngl_max ne change
|
|
// rien, en dessous on libère de la VRAM pour le contexte et le cache KV.
|
|
const ly=c.MODEL ? await jget('/api/model/layers').catch(()=>null) : null;
|
|
['MODEL','CTX','BATCH','UBATCH','NGL'].filter(k=>c[k]).forEach(k=>{
|
|
let v=c[k]; if(k==='MODEL') v=v.split(/[\\/]/).pop();
|
|
if(k==='NGL' && ly && ly.ok){ rows.push(row(k, v+' / '+ly.ngl_max, ly.ngl_max+' = tout le modèle sur le GPU ('+ly.layers+' couches + la sortie)')); return; }
|
|
rows.push(row(k, v));
|
|
});
|
|
// n-cpu-moe : affiché seulement s'il est réellement présent dans EXTRA_ARGS.
|
|
const m=(c.EXTRA_ARGS||'').match(/--n-cpu-moe\s+(\d+)/);
|
|
if(m) rows.push(row('N-CPU-MOE', m[1]));
|
|
document.getElementById('cfg').innerHTML = rows.join('') + await engineCmdlineRow();
|
|
}
|
|
// Ligne de commande exacte du dernier lancement du moteur (amont #108),
|
|
// copiable pour la vérifier ou la partager. Clé API masquée côté serveur.
|
|
let ENGINE_CMDLINE='';
|
|
async function engineCmdlineRow(){
|
|
const cl=await jget('/api/engine/cmdline').catch(()=>null);
|
|
ENGINE_CMDLINE=(cl&&cl.cmdline)||'';
|
|
if(!ENGINE_CMDLINE) return '';
|
|
return '<div class="kv"><span>COMMANDE</span><span><button class="pe-link" onclick="copyText(ENGINE_CMDLINE, \'commande copiée\')" title="'
|
|
+escHtml(ENGINE_CMDLINE)+'">copier la commande du moteur</button></span></div>';
|
|
}
|