feat(xtts): OpenAI-Endpunkt fuers lokale LLM + OpenHands-Profil
llama-swap ist bereits OpenAI-kompatibel, war aber nur im Compose-Netz erreichbar
(kein ports-Mapping). Jetzt nutzbar fuer OpenHands, Continue, Open WebUI & Co.
- compose: llama-swap veroeffentlicht 8080 als
${LLM_OPENAI_BIND:-127.0.0.1}:${LLM_OPENAI_PORT:-8081} — sicher per Default
(nur localhost), fuer LAN bewusst BIND=0.0.0.0 setzen.
- llm-adapter: API-Key wird in JEDEN llama-server-Befehl der generierten
llama-swap-Config eingehaengt (--api-key, shlex-gequotet). Quelle: .env
(Vorrang) sonst /models/openai_key.json (0600). Neuer Handler
llm_openai_config setzt ihn zur Laufzeit und lehnt bei .env-Vorrang ab.
worker_hello meldet Port/Bind/keySet/envLocked — den Key NIE (Broadcast!).
- Diagnostic: pro LLM-Box Endpunkt-Zeile mit Base-URL, Key-Status und Eingabe.
Bei .env-Vorrang ist das Feld schreibgeschuetzt (🔒). Warnung, wenn im LAN
offen ohne Key. Der PORT wird nur ANGEZEIGT: er ist ein Docker-Port-Mapping
und steht beim Container-Start fest — dafuer ist die .env zustaendig.
- rvs: llm_openai_config(_result) in ALLOWED_TYPES (sonst still verworfen).
- Neues Profil 'openhands' (COMPOSE_PROFILES=llm,openhands) mit Port/Bind/Modell
aus der .env, lokales LLM vorbelegt. Ohne Profil aendert sich nichts.
- .env.example + README: alle Variablen, Sicherheitshinweise (offener Endpunkt,
Docker-Socket bei OpenHands), curl-Test und wann der Key greift.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+60
-1
@@ -2278,6 +2278,7 @@
|
||||
if (msg.type === 'host_update') { hosts = msg.hosts || []; renderHosts(); return; }
|
||||
if (msg.type === 'rooms_info') { rooms = (msg.payload && msg.payload.rooms) || []; renderRooms(); return; }
|
||||
if (msg.type === 'sat_result') { onSatResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'llm_openai_config_result') { onOpenAiConfigResult(msg.payload || {}); return; }
|
||||
if (msg.type === 'agent_history_list') {
|
||||
if (agentHistoryHost && msg.payload && msg.payload.host === agentHistoryHost) {
|
||||
renderAgentHistory(msg.payload.events || []);
|
||||
@@ -4621,7 +4622,9 @@
|
||||
'<span style="color:#8888AA;">' + escapeHtml(modelText) + '</span>' +
|
||||
(w.gpus ? '<span style="color:#8888AA;">GPU ' + escapeHtml(w.gpus) + '</span>' : '') +
|
||||
'<span style="margin-left:auto;color:' + dot + ';">' + stat + '</span>' +
|
||||
'</div>';
|
||||
'</div>' +
|
||||
// Nur LLM-Boxen: OpenAI-Endpunkt fuer Fremd-Tools (OpenHands & Co.)
|
||||
(w.service === 'llm' ? renderOpenAiEndpoint(w) : '');
|
||||
}).join('');
|
||||
// Auslastungs-Button pro Node — Ziel bevorzugt die llm-Instanz (sieht alle
|
||||
// GPUs + Tokens), sonst irgendein online Worker des Node.
|
||||
@@ -4634,6 +4637,62 @@
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// ── OpenAI-Endpunkt einer LLM-Box (fuer OpenHands, Continue, ...) ──
|
||||
// Der PORT kommt immer aus der .env der Box (Docker-Port-Mapping, steht beim
|
||||
// Container-Start fest) und wird hier nur angezeigt. Der API-Key ist hier
|
||||
// setzbar — ausser er steht in der .env, dann hat die Vorrang und das Feld
|
||||
// ist schreibgeschuetzt.
|
||||
function renderOpenAiEndpoint(w) {
|
||||
const id = 'oa-' + (w.instanceId || '').replace(/[^a-zA-Z0-9]/g, '-');
|
||||
const port = w.openaiPort || '8081';
|
||||
const bind = w.openaiBind || '127.0.0.1';
|
||||
const lanOpen = bind === '0.0.0.0';
|
||||
const locked = !!w.openaiEnvLocked;
|
||||
const keyState = w.openaiKeySet
|
||||
? '<span style="color:#34C759;">Key gesetzt</span>'
|
||||
: '<span style="color:#FF9500;">kein Key — Endpunkt offen</span>';
|
||||
const warn = (lanOpen && !w.openaiKeySet)
|
||||
? '<div style="color:#FF6E6E;margin-top:4px;">⚠ Im LAN erreichbar OHNE Key — jeder im Netz kann das Modell nutzen.</div>'
|
||||
: '';
|
||||
const field = locked
|
||||
? '<input type="text" value="aus .env übernommen" disabled ' +
|
||||
'style="flex:1;background:#15151F;color:#666;border:1px solid #1E1E2E;border-radius:4px;padding:4px 8px;font-size:11px;">' +
|
||||
'<span style="color:#8888AA;font-size:10px;">🔒 .env hat Vorrang</span>'
|
||||
: '<input type="password" id="' + id + '" placeholder="API-Key setzen (leer = entfernen)" ' +
|
||||
'style="flex:1;background:#0D0D1A;color:#E0E0F0;border:1px solid #1E1E2E;border-radius:4px;padding:4px 8px;font-size:11px;">' +
|
||||
'<button class="btn secondary" onclick="saveOpenAiKey(\'' + escapeHtml(w.instanceId) + '\',\'' + id + '\')" ' +
|
||||
'style="padding:2px 8px;font-size:10px;">Speichern</button>';
|
||||
return '<div style="margin:2px 0 8px 18px;padding:6px 8px;background:#0D0D1A;border-radius:6px;font-size:11px;">' +
|
||||
'<div style="color:#8888AA;">OpenAI-Endpunkt (OpenHands & Co.): ' +
|
||||
'<code style="color:#FFD60A;">http://' + escapeHtml(bind === '0.0.0.0' ? '<box-ip>' : '127.0.0.1') +
|
||||
':' + escapeHtml(port) + '/v1</code> · ' + keyState + '</div>' +
|
||||
'<div style="display:flex;gap:6px;align-items:center;margin-top:5px;">' + field + '</div>' +
|
||||
'<div id="' + id + '-msg" style="color:#8888AA;margin-top:4px;"></div>' +
|
||||
'<div style="color:#666;margin-top:3px;">Port und Erreichbarkeit kommen aus der <code>.env</code> der Box ' +
|
||||
'(<code>LLM_OPENAI_PORT</code>, <code>LLM_OPENAI_BIND</code>) — sie stehen beim Container-Start fest.</div>' +
|
||||
warn + '</div>';
|
||||
}
|
||||
|
||||
function saveOpenAiKey(instanceId, fieldId) {
|
||||
const el = document.getElementById(fieldId);
|
||||
const msg = document.getElementById(fieldId + '-msg');
|
||||
if (!el) return;
|
||||
openAiReqId = 'oa_' + Date.now() + '_' + Math.floor(Math.random() * 100000);
|
||||
if (msg) msg.textContent = 'speichere …';
|
||||
send({ action: 'llm_openai_config', instanceId, apiKey: el.value, requestId: openAiReqId });
|
||||
}
|
||||
let openAiReqId = null;
|
||||
|
||||
function onOpenAiConfigResult(p) {
|
||||
if (!p || (openAiReqId && p.requestId && p.requestId !== openAiReqId)) return;
|
||||
const id = 'oa-' + String(p.instanceId || '').replace(/[^a-zA-Z0-9]/g, '-');
|
||||
const msg = document.getElementById(id + '-msg');
|
||||
if (!msg) return;
|
||||
msg.innerHTML = p.ok
|
||||
? '<span style="color:#34C759;">✓ ' + escapeHtml(p.message || 'gespeichert') + '</span>'
|
||||
: '<span style="color:#FF6E6E;">✗ ' + escapeHtml(p.error || 'fehlgeschlagen') + '</span>';
|
||||
}
|
||||
|
||||
// Sprung von den Einstellungen zur Compute-Flotte (dort sitzt der
|
||||
// Auslastung-Button pro Node → Live nvidia-smi + Graphen).
|
||||
function gotoComputeFleet() {
|
||||
|
||||
Reference in New Issue
Block a user