mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
Discover LM Studio via host/port scanning and native-API fingerprint (#1126)
Scan port 1234 and any custom port from LM_STUDIO_URL, add the LM_STUDIO_URL host to the discovery sweep alongside the Ollama env vars, and tag each discovered endpoint with its provider by fingerprinting the native /api/v1/models response (entries carrying key + architecture). Documents LM_STUDIO_URL in .env.example.
This commit is contained in:
+41
-13
@@ -74,15 +74,33 @@ class ModelDiscovery:
|
||||
self.default_host = default_host
|
||||
self.openai_api_key = openai_api_key
|
||||
self.openai_compat_path = "/v1/chat/completions"
|
||||
# Custom ports from env vars, merged into the scan list by discover_models.
|
||||
self._extra_ports: set = set()
|
||||
|
||||
def _get_hosts(self) -> List[str]:
|
||||
"""Get all hosts to scan, using env override, Tailscale, or default."""
|
||||
self._extra_ports = set()
|
||||
|
||||
def _append_host(out: List[str], host: str) -> None:
|
||||
host = (host or "").strip()
|
||||
if not host or host in out:
|
||||
return
|
||||
out.append(host)
|
||||
|
||||
def _append_env_hosts(out: List[str]) -> None:
|
||||
"""Add hosts (and any custom ports) from provider-specific env vars."""
|
||||
for env_name in ("OLLAMA_BASE_URL", "OLLAMA_URL", "LM_STUDIO_URL"):
|
||||
raw = os.getenv(env_name, "").strip()
|
||||
if not raw:
|
||||
continue
|
||||
try:
|
||||
parsed = urlparse(raw if "://" in raw else "http://" + raw)
|
||||
_append_host(out, parsed.hostname or "")
|
||||
if parsed.port:
|
||||
self._extra_ports.add(parsed.port)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Manual override takes priority
|
||||
extra = os.getenv("LLM_HOSTS", "").strip()
|
||||
if extra:
|
||||
@@ -91,6 +109,7 @@ class ModelDiscovery:
|
||||
if self.default_host not in hosts:
|
||||
hosts.insert(0, self.default_host)
|
||||
_append_host(hosts, "host.docker.internal")
|
||||
_append_env_hosts(hosts)
|
||||
return hosts
|
||||
|
||||
# Try Tailscale discovery
|
||||
@@ -100,23 +119,30 @@ class ModelDiscovery:
|
||||
if self.default_host not in ts_hosts:
|
||||
ts_hosts.insert(0, self.default_host)
|
||||
_append_host(ts_hosts, "host.docker.internal")
|
||||
_append_env_hosts(ts_hosts)
|
||||
return ts_hosts
|
||||
|
||||
hosts = [self.default_host]
|
||||
# Docker desktop/Linux compose maps this to the host machine. That is
|
||||
# the common "I started Ollama normally on this computer" case.
|
||||
_append_host(hosts, "host.docker.internal")
|
||||
for env_name in ("OLLAMA_BASE_URL", "OLLAMA_URL"):
|
||||
raw = os.getenv(env_name, "").strip()
|
||||
if not raw:
|
||||
continue
|
||||
try:
|
||||
parsed = urlparse(raw if "://" in raw else "http://" + raw)
|
||||
_append_host(hosts, parsed.hostname or "")
|
||||
except Exception:
|
||||
pass
|
||||
_append_env_hosts(hosts)
|
||||
return hosts
|
||||
|
||||
def _fingerprint_provider(self, host: str, port: int) -> Optional[str]:
|
||||
"""Identify the server software via its native API, independent of port."""
|
||||
try:
|
||||
r = httpx.get(f"http://{host}:{port}/api/v1/models", timeout=1.5)
|
||||
if r.is_success:
|
||||
models = (r.json() or {}).get("models")
|
||||
if (isinstance(models, list) and models
|
||||
and isinstance(models[0], dict)
|
||||
and "key" in models[0] and "architecture" in models[0]):
|
||||
return "lmstudio"
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
def _check_port(self, host: str, port: int) -> Optional[Dict[str, Any]]:
|
||||
"""Check a single host:port for models."""
|
||||
base = f"http://{host}:{port}/v1"
|
||||
@@ -132,7 +158,8 @@ class ModelDiscovery:
|
||||
"port": port,
|
||||
"url": f"http://{host}:{port}{self.openai_compat_path}",
|
||||
"models": ids,
|
||||
"models_display": [i.lstrip("/") for i in ids]
|
||||
"models_display": [i.lstrip("/") for i in ids],
|
||||
"provider": self._fingerprint_provider(host, port),
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
@@ -145,9 +172,10 @@ class ModelDiscovery:
|
||||
|
||||
logger.info(f"Scanning {len(hosts)} hosts for models: {hosts}")
|
||||
|
||||
# Build list of (host, port) to check. 8000-8020 catches vLLM,
|
||||
# llama.cpp, SGLang, and Cookbook serves; 11434 catches Ollama.
|
||||
ports = list(range(8000, 8021)) + [11434]
|
||||
# Well-known ports: 8000-8020 (vLLM, llama.cpp, SGLang, Cookbook),
|
||||
# 1234 (LM Studio), 11434 (Ollama)
|
||||
ports = list(range(8000, 8021)) + [1234, 11434]
|
||||
ports += [p for p in sorted(self._extra_ports) if p not in ports]
|
||||
targets = [(h, p) for h in hosts for p in ports]
|
||||
|
||||
seen_models = set() # dedupe by (port, model_ids) to avoid same machine via different IPs
|
||||
|
||||
Reference in New Issue
Block a user