fix(cookbook): default Ollama serve to loopback (#872)

This commit is contained in:
lolwuttav
2026-06-01 21:27:04 -06:00
committed by GitHub
parent ffb77d7ff2
commit c99193041a
6 changed files with 95 additions and 9 deletions

View File

@@ -318,6 +318,38 @@ _SERVE_CMD_ALLOWLIST = {
_GGUF_PRELUDE_RE = re.compile(
r'^MODEL_FILE=\$\([^\n]*?\)\s*&&\s*\{[^{}]*\}\s*\|\|\s*\{[^{}]*\}\s*&&\s*'
)
_OLLAMA_HOST_ASSIGNMENT_RE = re.compile(r"(?:^|\s)OLLAMA_HOST=([^\s]+)")
_OLLAMA_BIND_RE = re.compile(r"^\[([^\]]+)\]:(\d+)$|^([^:]+):(\d+)$")
_OLLAMA_BIND_HOST_RE = re.compile(r"^[A-Za-z0-9._:-]+$")
def _ollama_bind_from_cmd(cmd: str | None, *, default_host: str = "127.0.0.1") -> tuple[str, str]:
"""Return the Ollama bind host/port requested by a serve command.
Plain local `ollama serve` defaults to loopback. Remote callers can pass a
wider default host so the resulting API is reachable by Odysseus.
"""
if not cmd:
return default_host, "11434"
match = _OLLAMA_HOST_ASSIGNMENT_RE.search(cmd)
if not match:
return default_host, "11434"
value = match.group(1).strip("'\"")
bind_match = _OLLAMA_BIND_RE.match(value)
if not bind_match:
return "127.0.0.1", "11434"
bracketed_host = bind_match.group(1)
host = bracketed_host or bind_match.group(3) or "127.0.0.1"
port = bind_match.group(2) or bind_match.group(4) or "11434"
if not _OLLAMA_BIND_HOST_RE.match(host):
return "127.0.0.1", "11434"
try:
port_num = int(port, 10)
except ValueError:
return "127.0.0.1", "11434"
if port_num < 1 or port_num > 65535:
return "127.0.0.1", "11434"
return f"[{host}]" if bracketed_host else host, port
def _check_serve_binary(seg: str) -> None:

View File

@@ -37,7 +37,7 @@ from routes.cookbook_helpers import (
_validate_local_dir, _validate_ssh_port, _validate_gpus, _shell_path,
_ps_squote, _bash_squote, _validate_serve_cmd, _parse_serve_phase,
_safe_env_prefix, _local_tooling_path_export, _append_serve_preflight_exit_lines,
_append_serve_exit_code_lines, _cached_model_scan_script,
_append_serve_exit_code_lines, _cached_model_scan_script, _ollama_bind_from_cmd,
_pip_install_fallback_chain, ModelDownloadRequest, ServeRequest,
)
@@ -999,13 +999,15 @@ def setup_cookbook_routes() -> APIRouter:
runner_lines.append('fi')
elif "ollama" in req.cmd:
handled_ollama_serve = True
_ollama_port = "11434"
_ollama_match = re.search(r"OLLAMA_HOST=[^\s:]+:(\d+)", req.cmd)
if _ollama_match:
_ollama_port = _ollama_match.group(1)
_ollama_default_host = "0.0.0.0" if remote else "127.0.0.1"
_ollama_host, _ollama_port = _ollama_bind_from_cmd(
req.cmd,
default_host=_ollama_default_host,
)
# Ollama can be a host binary, a system service, or a Docker
# container. If the HTTP API is already reachable, the model is
# already served and we should not require a host `ollama` CLI.
runner_lines.append(f'ODYSSEUS_OLLAMA_HOST={_bash_squote(_ollama_host)}')
runner_lines.append(f'ODYSSEUS_OLLAMA_PORT="{_ollama_port}"')
runner_lines.append('ODYSSEUS_OLLAMA_URL=""')
runner_lines.append('for _ody_ollama_port in "$ODYSSEUS_OLLAMA_PORT" 11434; do')
@@ -1034,8 +1036,12 @@ def setup_cookbook_routes() -> APIRouter:
runner_lines.append(' echo "=== Process exited with code 127 ==="')
runner_lines.append(' exec bash -i')
runner_lines.append('fi')
runner_lines.append('echo "Starting ollama server on 0.0.0.0:${ODYSSEUS_OLLAMA_PORT}..."')
runner_lines.append('OLLAMA_HOST="0.0.0.0:${ODYSSEUS_OLLAMA_PORT}" ollama serve')
runner_lines.append('ODYSSEUS_OLLAMA_URL="http://${ODYSSEUS_OLLAMA_HOST}:${ODYSSEUS_OLLAMA_PORT}"')
if remote and _ollama_host in ("0.0.0.0", "::"):
runner_lines.append('echo "[odysseus] WARNING: remote Ollama will bind to ${ODYSSEUS_OLLAMA_HOST}:${ODYSSEUS_OLLAMA_PORT} so Odysseus can reach it from this host."')
runner_lines.append('echo "[odysseus] Ollama has no built-in authentication; expose this only on a trusted LAN/VPN or provide an explicit OLLAMA_HOST with your own access controls."')
runner_lines.append('echo "Starting ollama server on ${ODYSSEUS_OLLAMA_HOST}:${ODYSSEUS_OLLAMA_PORT}..."')
runner_lines.append('OLLAMA_HOST="${ODYSSEUS_OLLAMA_HOST}:${ODYSSEUS_OLLAMA_PORT}" ollama serve')
runner_lines.append('_ody_exit=$?')
runner_lines.append('echo')
runner_lines.append('echo "=== Process exited with code ${_ody_exit} ==="')

View File

@@ -968,7 +968,7 @@ function initEndpointForm() {
const data = await res.json();
const items = data.items || [];
if (!items.length) {
msg.textContent = 'No model servers found. Make sure vLLM, llama.cpp, SGLang, or Ollama is running. Docker users may need OLLAMA_HOST=0.0.0.0:11434.';
msg.textContent = 'No model servers found. Make sure vLLM, llama.cpp, SGLang, or Ollama is running. Docker users may need Ollama bound to a trusted reachable interface.';
msg.className = 'admin-error';
} else {
// Auto-add each discovered endpoint. Server dedupes on base_url

View File

@@ -426,7 +426,8 @@ export function _buildServeCmd(f, modelName, backend) {
}
} else if (backend === 'ollama') {
const ollamaPort = f.port || '11434';
const hostEnv = ollamaPort !== '11434' ? `OLLAMA_HOST=0.0.0.0:${ollamaPort} ` : '';
const bindHost = _envState.remoteHost ? '0.0.0.0' : '127.0.0.1';
const hostEnv = ollamaPort !== '11434' ? `OLLAMA_HOST=${bindHost}:${ollamaPort} ` : '';
cmd = `${hostEnv}ollama serve`;
} else if (backend === 'diffusers') {
const gpuStr = f.gpus?.trim();

View File

@@ -11,6 +11,7 @@ from routes.cookbook_helpers import (
_append_serve_preflight_exit_lines,
_local_tooling_path_export,
_pip_install_fallback_chain,
_ollama_bind_from_cmd,
_safe_env_prefix,
_validate_gpus,
_validate_repo_id,
@@ -130,6 +131,40 @@ def test_serve_runner_preserves_command_exit_code():
assert 'echo "=== Process exited with code $? ==="' not in script
def test_ollama_serve_defaults_to_loopback_bind():
assert _ollama_bind_from_cmd("ollama serve") == ("127.0.0.1", "11434")
assert _ollama_bind_from_cmd("ollama run qwen2.5:0.5b") == ("127.0.0.1", "11434")
def test_ollama_serve_accepts_remote_reachable_default_bind():
assert (
_ollama_bind_from_cmd("ollama serve", default_host="0.0.0.0")
== ("0.0.0.0", "11434")
)
def test_ollama_serve_preserves_explicit_bind_opt_in():
assert (
_ollama_bind_from_cmd("OLLAMA_HOST=0.0.0.0:12345 ollama serve")
== ("0.0.0.0", "12345")
)
assert (
_ollama_bind_from_cmd("OLLAMA_HOST=[::1]:11435 ollama serve")
== ("[::1]", "11435")
)
def test_ollama_serve_rejects_unsafe_bind_values():
assert (
_ollama_bind_from_cmd("OLLAMA_HOST='$HOST:11434' ollama serve")
== ("127.0.0.1", "11434")
)
assert (
_ollama_bind_from_cmd("OLLAMA_HOST=127.0.0.1:99999 ollama serve")
== ("127.0.0.1", "11434")
)
def test_cached_model_scan_reports_plain_dir_gguf(tmp_path):
"""Custom download dirs may sit inside the HF hub cache and contain plain
per-model folders. They must show up in Serve and keep the GGUF signal."""

View File

@@ -125,6 +125,18 @@ def test_readme_native_quickstart_uses_loopback():
assert "Use `--host 0.0.0.0` only when you intentionally want" in readme
def test_ollama_cookbook_runner_does_not_force_public_bind():
route = Path("routes/cookbook_routes.py").read_text(encoding="utf-8")
cookbook_js = Path("static/js/cookbook.js").read_text(encoding="utf-8")
assert 'OLLAMA_HOST="0.0.0.0:${ODYSSEUS_OLLAMA_PORT}" ollama serve' not in route
assert 'OLLAMA_HOST="${ODYSSEUS_OLLAMA_HOST}:${ODYSSEUS_OLLAMA_PORT}" ollama serve' in route
assert '_ollama_default_host = "0.0.0.0" if remote else "127.0.0.1"' in route
assert "WARNING: remote Ollama will bind" in route
assert "OLLAMA_HOST=0.0.0.0:${ollamaPort}" not in cookbook_js
assert "const bindHost = _envState.remoteHost ? '0.0.0.0' : '127.0.0.1';" in cookbook_js
assert "OLLAMA_HOST=${bindHost}:${ollamaPort}" in cookbook_js
def _import_integrations(tmp_path, monkeypatch):
"""Import src.integrations with data + encryption key redirected to tmp."""
_import_secret_storage(tmp_path, monkeypatch)