feat(chat): explain why a reply is stalled when the endpoint isn't ready - #108
Conversation
refresh_managed_service_runtime_liveness() ran the real HTTP model-ready probe but only used a passing result to skip demotion, never to persist the running->ready transition. Its twin in providers.rs::ready_local_services() already promotes correctly; mirror that here so load_managed_services() (and therefore the "services" MCP tool and chat's pick_managed_chat_endpoint, which requires exact status "ready") see the true state instead of a manifest stuck at "running" forever. Relates to EAI-7352 Signed-off-by: Michael Roy <michael.roy@amd.com>
Sending a message to a local endpoint whose model was still coming up showed only a generic "waiting for the agent…" spinner, indistinguishable from a slow reply or a hang. The chat input never consulted the readiness of the instance it was pointed at. Match the chat endpoint to its daemon-surfaced instance by port and, when that instance is not answering, replace the generic spinner with a specific reason: the model is still starting up, the endpoint has stopped, or it reported an error. A live instance, a remote gateway (no local instance to inspect), or an unknown state falls back to the plain spinner unchanged. Parsing and the reason mapping are pure, unit-tested helpers (port_from_base_url, chat_backend_wait_reason). Stacked on #101 (EAI-7352). Relates to EAI-7348 Signed-off-by: Michael Roy <michael.roy@amd.com>
… alone chat_backend_wait_reason matched the chat endpoint to a daemon-surfaced instance by TCP port only. Daemon-tracked instances are always co-located (scraped over loopback), but a remote gateway URL that happens to share a port number with a local managed service -- e.g. both on 8000 -- would false-match that unrelated local instance and could wrongly show a "still starting up" reason for a perfectly healthy remote endpoint. Add host_from_base_url and only attempt the match when the endpoint's host is loopback (reusing llm::is_loopback_host, now pub(crate)); a non-loopback host always falls back to the generic spinner regardless of port. Added host_from_base_url unit tests and a backend_wait_reason_ignores_remote_endpoint_sharing_a_local_port_number test exercising the exact false-match scenario (remote host, same port as a Starting local instance) to prove the generic spinner is kept. Regenerated THIRD_PARTY_NOTICES.txt (pre-existing ordering drift, unrelated to this change). Relates to EAI-7348 Signed-off-by: Michael Roy <michael.roy@amd.com>
rominf
left a comment
There was a problem hiding this comment.
The core logic is correct and well-tested — the readiness reason is recomputed live each frame (no staleness once the endpoint becomes ready), the match over InstanceStatus is exhaustive (no wildcard), and Running/Unknown return None so a slow-but-live model is never falsely labeled "stalled". Two quality issues worth fixing:
-
Misattached doc comment —
crates/rocm-dash-tui/src/ui/tabs/chat.rs:276. The inserted functions pusheddraw_input's doc comment (/// Render the single-row input line…) ontoport_from_base_url, socargo doc/hover now describes the port parser as an input renderer, anddraw_inputis left undocumented. Move the comment back down todraw_input. -
IPv6 regression from reimplemented URL parsing —
chat.rs:281-303.host_from_base_url/port_from_base_urlduplicatellm::parse_host_portbut drop the IPv6 bracket-stripping it already does:http://[::1]:8000/v1→ host"[::1]"→is_loopback_hostreturnsfalse→ the whole function bails toNone, so a user on an IPv6-loopback local instance never gets the readiness reason (the exact case this PR targets).is_loopback_host's own doc says it expects a bracket-stripped host fromparse_host_port. Reuseparse_host_port(with an explicit-port check layered on top) instead of the two new parsers.
Neither blocks merge if IPv6 chat targets are out of scope, but both are easy fixes.
|
🔴 Automated review · pr-review-watcher · a068711 Summary
Blocking1. Feature is a no-op in its motivating case;
|
rominf
left a comment
There was a problem hiding this comment.
Please have a look at automated review by Eugene above.
The readiness-reason helper hand-rolled two authority parsers
(port_from_base_url / host_from_base_url) with rsplit(':'), which mangles
a bracketed IPv6 loopback: host_from_base_url("http://[::1]:8000") kept
the brackets so is_loopback_host("[::1]") was false, and
port_from_base_url("http://[::1]/v1") parsed "1]" and failed. Either
path silently disabled chat_backend_wait_reason for a legitimate local
IPv6 endpoint.
Replace both helpers with the crate's existing parse_host_port, which
already strips IPv6 brackets, defaults the port from the scheme, and is
pinned by parse_host_port_handles_bracketed_ipv6. Drop the now-redundant
per-helper unit tests (covered by the llm.rs suite) and add a
bracketed-IPv6 regression test against chat_backend_wait_reason.
Also tighten the doc comment: Running is not a hard HTTP-readiness
guarantee, and record the #106/#107 status-signal dependency in-tree so
the Starting/Stopped/Error arms are discoverable as pending until that
work lands beneath this change.
Addresses pr-review-watcher blocking finding #2 (IPv6) and the doc /
test-coverage non-blocking notes on #108.
Signed-off-by: Michael Roy <michael.roy@amd.com>
rominf
left a comment
There was a problem hiding this comment.
Automated review: re-reviewed at head 72391ae6, covering the commit added since my previous review. Both requested fixes are resolved.
- The
draw_inputdocumentation is attached to the correct function again. chat_backend_wait_reasonnow uses the sharedparse_host_portpath, which correctly strips brackets from IPv6 authorities before loopback detection. The new bracketed-IPv6 regression test exercises the previously broken input and passes.
The relevant rocm-dash-tui tests pass. I found no remaining code defect in this follow-up. The PR is still conflicting with main, so it must be rebased and the mechanical conflicts resolved before merge.
Signed-off-by: Michael Roy <michael.roy@amd.com>
Summary
Sending a message to a local endpoint whose model was still coming up showed only a generic
⠿ waiting for the agent…spinner — indistinguishable from a slow reply or an outright hang. The chat input never consulted the readiness of the instance it was pointed at.Root Cause
crates/rocm-dash-tui/src/ui/tabs/chat.rs::draw_input()rendered a fixed "waiting for the agent…" line wheneverchat_sendingwas set, regardless of whether the backing endpoint was actually up. The dashboard already tracks each managed instance's status inAppState.instances, but chat made no use of it.Changes
draw_input()now matches the chat endpoint to its daemon-surfaced instance (by port parsed from the configuredbase_url) and, when that instance is not answering, replaces the generic spinner with a specific reason:Starting→ "the model is still starting up — hang tight"Stopped→ "the endpoint has stopped — restart the service to chat"Error→ "the endpoint reported an error — check the service logs"Running) instance, a remote gateway URL (no local instance to inspect), an unmatched port, or anUnknownstate all fall back to the existing plain spinner — unchanged behavior.port_from_base_url,chat_backend_wait_reason), so the render path stays thin.Scope / Notes
InstanceStatusenum. The phase-aware startup detail (DOWNLOADING/LOADING/WARMUP from EAI-7355 / feat(serve): surface a coarse startup phase while a service comes up #107) lives in the other stack; if both land, the reason text here naturally benefits from the richerStartingstate without further changes to this file.Test Plan
cargo build -p rocm-dash-tuicargo clippy -p rocm-dash-tui --all-targets --all-features -- -D warnings(clean)cargo test -p rocm-dash-tui -- --test-threads=1— 547 + 16 + 5 green, incl. new tests:port_from_base_url_parses_local_endpoints,backend_wait_reason_reflects_instance_status,sending_input_surfaces_startup_reason_not_generic_spinner,sending_input_falls_back_to_generic_spinner_for_remote_endpointRelates to EAI-7348