odysseus-dev/odysseus · error · HTTPException

ssh_port requires host

Error message

ssh_port requires host

What it means

Raised by _validate_detection_target in the hardware-fit routes when a caller supplies ssh_port without host. The validator normalizes host via validate_remote_host and port via validate_ssh_port; a port only makes sense as part of a host:port SSH target, so a bare port is rejected with 400 before any fitting logic runs.

Source

Thrown at routes/hwfit_routes.py:25

from fastapi import APIRouter, HTTPException

from core.platform_compat import run_ssh_command
from routes._validators import validate_remote_host, validate_ssh_port


# Backends the manual hardware simulator accepts. Must stay a subset of what
# services.hwfit.fit understands so a simulated box ranks like a real one:
# "metal" routes through the Apple-Silicon path (GGUF-only, llama.cpp/Ollama),
# the CPU backends through the RAM/offload path, cuda/rocm through vLLM.
_MANUAL_BACKENDS = {"cuda", "rocm", "metal", "cpu_x86", "cpu_arm"}


def _validate_detection_target(host: str = "", ssh_port: str = "") -> tuple[str, str]:
    host_value = validate_remote_host(host) or ""
    port_value = validate_ssh_port(ssh_port) or ""
    if port_value and not host_value:
        raise HTTPException(400, "ssh_port requires host")
    return host_value, port_value


def _apply_manual_hardware(system, manual_mode="", manual_gpu_count="", manual_vram_gb="", manual_ram_gb="", manual_backend=""):
    """Manual hardware is a "what if I had this setup" simulator —
    REPLACES the detected hardware entirely instead of adding to it.

    The previous additive behavior averaged the manual VRAM across
    all GPUs (base + manual), which meant adding "1× 400 GB" on top
    of "2× 70 GB" only nudged the per-GPU cap from 70 to 180 GB
    (= 540 / 3), so GGUF models bigger than that still didn't surface
    — exactly the "cap stuck at detected level" bug the user hit.
    """
    manual_mode = (manual_mode or "").lower()
    if manual_mode not in {"gpu", "ram"}:
        return system

    try:

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Include the host field whenever ssh_port is sent (e.g. host="192.168.1.10" plus ssh_port="22").
  2. If you do not want remote detection, omit ssh_port entirely and run local detection.
  3. Check the client payload right before submit and log it — verify host is non-empty after your form-binding step, since validate_remote_host returning empty is what trips the check.

Example fix

# before
payload = {"ssh_port": "22"}

# after
payload = {"host": "192.168.1.10", "ssh_port": "22"}
Defensive patterns

Strategy: validation

Validate before calling

def hwfit_params_valid(host: str, ssh_port: str) -> bool:
    return bool(ssh_port.strip()) <= bool(host.strip())  # port implies host

Try / catch

Catch HTTPException/400 responses from the hwfit endpoint and re-prompt for the host field; do not retry unchanged.

Prevention

When it happens

Trigger: Calling a /hwfit detection/fit endpoint with ssh_port="22" (or any port) in the form/query while the host field is empty, or a UI that remembers the port but not the hostname from a previous submission.

Common situations: Frontend form where the host input failed to bind or was cleared; scripts that template the port but leave host blank; copy-pasting a config where host key name differs (e.g. hostname vs host).

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/c91dbfcbdbb48a10. Report an issue: GitHub.