feat(discovery): detect llama.cpp servers and label local providers (#4729)

* feat(discovery): detect llama.cpp servers and label local providers

Scan port 8080 (llama-server) and 11435 (APFEL) during discovery, fingerprint
llama.cpp via its native /props endpoint, and label well-known local serving
ports (8080 llama.cpp, 8000 vLLM, 1234 LM Studio, 11434 Ollama) consistently
in both the Python provider helper and the JS endpoint UI. Adds a llama.cpp
hint to the /setup slash command.

* fix(discovery): don't infer the serving tool from the port alone

Per review: vLLM, SGLang, llama.cpp and plain OpenAI-compatible servers all
share 8000/8080, so labeling by port mislabels real setups (a vLLM box on 8080
shown as llama.cpp). Drop the port->tool assertions from _provider_label and
providerLabel; the authoritative signal is the /props fingerprint done during
discovery, which is unchanged. Loopback now reads a neutral 'local endpoint' /
'Local'. Tests updated to assert the neutral labels.
This commit is contained in:
Joel Alejandro Escareño Fernández
2026-06-23 23:39:56 +02:00
committed by GitHub
parent 72c0bde8a9
commit e0ccf250a4
9 changed files with 330 additions and 15 deletions
+20 -3
View File
@@ -1603,8 +1603,8 @@ function initEndpointForm() {
wrap.style.cssText = 'display:flex;align-items:center;padding:8px 0;';
wrap.appendChild(wp.element);
const txt = document.createElement('span');
txt.textContent = 'Scanning ports 8000-8020 and 11434 for model servers...';
txt.style.cssText = 'opacity:0.7;';
txt.textContent = 'Scanning ports 8000-8020, 8080, 1234, 11434, and 11435 for model servers...';
txt.style.cssText = 'font-size:12px;opacity:0.7;';
wrap.appendChild(txt);
msg.appendChild(wrap);
discoverBtn._wp = wp;
@@ -1619,12 +1619,24 @@ function initEndpointForm() {
} else {
// Auto-add each discovered endpoint. Server dedupes on base_url
// and returns `existing: true` for already-registered ones.
// Map fingerprinted provider IDs to friendly display names.
const _PROVIDER_DISPLAY = {
llamacpp: 'llama.cpp', lmstudio: 'LM Studio', vllm: 'vLLM',
ollama: 'Ollama',
};
let added = 0;
let skipped = 0;
for (const item of items) {
const base = item.url.replace('/chat/completions', '').replace(/\/$/, '');
const providerDisplay = _PROVIDER_DISPLAY[item.provider] || null;
const fd = new FormData();
fd.append('base_url', base);
if (providerDisplay) {
// Use "Provider (host:port)" so the endpoint is immediately
// identifiable in the list, e.g. "llama.cpp (localhost:8080)".
const hostPart = base.replace(/^https?:\/\//, '').split('/')[0];
fd.append('name', `${providerDisplay} (${hostPart})`);
}
fd.append('endpoint_kind', 'local');
fd.append('model_refresh_mode', 'auto');
fd.append('skip_probe', 'false');
@@ -1638,7 +1650,12 @@ function initEndpointForm() {
}
}
const totalModels = items.reduce((n, i) => n + (i.models ? i.models.length : 0), 0);
const parts = [`Found ${items.length} server${items.length !== 1 ? 's' : ''} with ${totalModels} model${totalModels !== 1 ? 's' : ''}`];
const serverNames = items.map(i =>
(_PROVIDER_DISPLAY[i.provider] || i.url.replace(/^https?:\/\//, '').split('/')[0])
);
const parts = [
`Found ${items.length} server${items.length !== 1 ? 's' : ''} (${serverNames.join(', ')}) with ${totalModels} model${totalModels !== 1 ? 's' : ''}`,
];
if (added) parts.push(`added ${added} new`);
if (skipped) parts.push(`${skipped} already added`);
msg.innerHTML = parts.join(' — ');
+12 -3
View File
@@ -133,11 +133,20 @@ export function providerLabel(endpointUrl) {
try {
host = new URL(endpointUrl).hostname;
} catch (_) {
// Not a full URL (e.g. bare host[:port]) — strip scheme/path/port best-effort.
host = endpointUrl.replace(/^[a-z]+:\/\//i, "").split("/")[0].split(":")[0];
// Not a full URL (e.g. bare host[:port]) — strip scheme/path best-effort.
const stripped = endpointUrl.replace(/^[a-z]+:\/\//i, "").split("/")[0];
const colonIdx = stripped.lastIndexOf(":");
host = colonIdx >= 0 ? stripped.slice(0, colonIdx) : stripped;
}
if (!host) return null;
if (/^(localhost|127\.|0\.0\.0\.0|::1|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(host)) {
const isLoopback = /^(localhost|127\.|0\.0\.0\.0|::1)/.test(host);
if (isLoopback) {
// Don't name the serving tool from the port — it isn't authoritative
// (vLLM/SGLang/llama.cpp share 8000/8080). Discovery identifies the tool by
// probing /props and stores the result as the endpoint's name instead.
return "Local";
}
if (/^(192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(host)) {
return "Local";
}
for (const [re, label] of _ENDPOINT_LABELS) {
+8
View File
@@ -208,6 +208,8 @@ function _showSetupEndpointChoices() {
'<pre style="margin:4px 0 0;"><code class="setup-clickable-code" style="cursor:pointer;text-decoration:underline;" title="Click to fill in chat">http://localhost:11434/v1</code></pre>' +
'<div style="margin-top:4px;">or</div>' +
'<pre style="margin:2px 0 0;"><code class="setup-clickable-code" style="cursor:pointer;text-decoration:underline;" title="Click to fill in chat">http://llm-host.local:8000/v1</code></pre>' +
'<div style="margin-top:4px;">or llama.cpp (llama-server):</div>' +
'<pre style="margin:2px 0 0;"><code class="setup-clickable-code" style="cursor:pointer;text-decoration:underline;" title="Click to fill in chat">http://localhost:8080/v1</code></pre>' +
'</div>' +
'<div style="border:1px solid var(--border);border-radius:8px;padding:10px 12px;background:color-mix(in srgb,var(--bg) 88%,var(--fg) 12%);">' +
'<div style="font-weight:700;margin-bottom:6px;">' + SETUP_API_ICON + 'API setup</div>' +
@@ -238,6 +240,12 @@ function _showSetupEndpointChoicesStreamed(options = {}) {
text: 'http://llm-host.local:8000/v1',
copyText: 'http://llm-host.local:8000/v1',
},
{ kind: 'p', text: 'or llama.cpp (llama-server):' },
{
kind: 'code',
text: 'http://localhost:8080/v1',
copyText: 'http://localhost:8080/v1',
},
{ kind: 'heading', html: SETUP_API_ICON + 'API setup' },
{ kind: 'p', text: 'Paste provider name then API key (example):' },
{