{"record":{"id":"c91dbfcbdbb48a10","repo":"odysseus-dev/odysseus","slug":"ssh-port-requires-host","errorCode":null,"errorMessage":"ssh_port requires host","messagePattern":"ssh_port requires host","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"routes/hwfit_routes.py","lineNumber":25,"sourceCode":"\nfrom fastapi import APIRouter, HTTPException\n\nfrom core.platform_compat import run_ssh_command\nfrom routes._validators import validate_remote_host, validate_ssh_port\n\n\n# Backends the manual hardware simulator accepts. Must stay a subset of what\n# services.hwfit.fit understands so a simulated box ranks like a real one:\n# \"metal\" routes through the Apple-Silicon path (GGUF-only, llama.cpp/Ollama),\n# the CPU backends through the RAM/offload path, cuda/rocm through vLLM.\n_MANUAL_BACKENDS = {\"cuda\", \"rocm\", \"metal\", \"cpu_x86\", \"cpu_arm\"}\n\n\ndef _validate_detection_target(host: str = \"\", ssh_port: str = \"\") -> tuple[str, str]:\n    host_value = validate_remote_host(host) or \"\"\n    port_value = validate_ssh_port(ssh_port) or \"\"\n    if port_value and not host_value:\n        raise HTTPException(400, \"ssh_port requires host\")\n    return host_value, port_value\n\n\ndef _apply_manual_hardware(system, manual_mode=\"\", manual_gpu_count=\"\", manual_vram_gb=\"\", manual_ram_gb=\"\", manual_backend=\"\"):\n    \"\"\"Manual hardware is a \"what if I had this setup\" simulator —\n    REPLACES the detected hardware entirely instead of adding to it.\n\n    The previous additive behavior averaged the manual VRAM across\n    all GPUs (base + manual), which meant adding \"1× 400 GB\" on top\n    of \"2× 70 GB\" only nudged the per-GPU cap from 70 to 180 GB\n    (= 540 / 3), so GGUF models bigger than that still didn't surface\n    — exactly the \"cap stuck at detected level\" bug the user hit.\n    \"\"\"\n    manual_mode = (manual_mode or \"\").lower()\n    if manual_mode not in {\"gpu\", \"ram\"}:\n        return system\n\n    try:","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/hwfit_routes.py#L7-L43","documentation":"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.","triggerScenarios":"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.","commonSituations":"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).","solutions":["Include the host field whenever ssh_port is sent (e.g. host=\"192.168.1.10\" plus ssh_port=\"22\").","If you do not want remote detection, omit ssh_port entirely and run local detection.","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."],"exampleFix":"# before\npayload = {\"ssh_port\": \"22\"}\n\n# after\npayload = {\"host\": \"192.168.1.10\", \"ssh_port\": \"22\"}","handlingStrategy":"validation","validationCode":"def hwfit_params_valid(host: str, ssh_port: str) -> bool:\n    return bool(ssh_port.strip()) <= bool(host.strip())  # port implies host","typeGuard":null,"tryCatchPattern":"Catch HTTPException/400 responses from the hwfit endpoint and re-prompt for the host field; do not retry unchanged.","preventionTips":["Make the host field required in the UI whenever ssh_port is non-empty.","Log the exact payload on 400 to catch form-binding mistakes early."],"tags":["validation","ssh","hardware-fit","fastapi"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}