fix(chat): revalidate a stale persisted chat_url and fall back to a live endpoint#105
fix(chat): revalidate a stale persisted chat_url and fall back to a live endpoint#105michaelroy-amd wants to merge 5 commits into
Conversation
A configured chat URL (OPENAI_BASE_URL / --chat-url) with no explicit model resolved to the "local-model" placeholder, which 404s on servers that register the model under its real id. Query /v1/models on startup to adopt the served model, mirroring the managed-service path. Gated on a reachable probe so an unreachable endpoint adds no fetch timeout to launch, and an explicit --chat-model/config value still wins. Signed-off-by: Eugene Volen <Eugene.Volen@amd.com>
…ive endpoint A persisted tui.chat_url survives across dash launches, but the server it points at may have been stopped or replaced since it was written (e.g. a managed vLLM/Lemonade instance was restarted on a different port). Previously the startup path passed a stale chat_url straight into resolve_llm_config regardless of reachability, so the agent was built against a dead endpoint with no indication anything had changed. Add stale_chat_url_replacement (app/chat.rs): a pure decision function that, only when the persisted chat_url's own TCP probe fails and no managed-services entry matched it, checks whether detect_local_chat finds a live, differently-addressed endpoint. If so, the startup path in event_loop() substitutes that live LlmConfig before calling resolve_llm_config and pushes a "server changed" ChatTurn::system notice into the transcript pointing at `/detect save` to persist it. This only ever revalidates the persisted-config tier: chat_env_url is untouched (it's excluded from the managed-registry check already), an explicit --chat-url wired through the same field would be revalidated identically today since this crate never distinguishes CLI from config at this layer, and resolve_llm_config's own CLI>config>env>probe precedence is not modified at all — the substitution happens purely at the call site, and its existing unit tests (cli_wins_over_every_other_tier, config_wins_when_no_cli, env_url_wins_when_no_cli_or_config, probed_default_used_when_nothing_configured_but_reachable) all remain green untouched. Complements unmerged PR #97, which addresses the chat_model half of this gap. Relates to EAI-7360. Signed-off-by: Michael Roy <michael.roy@amd.com>
…ormatting Two refinements to the stale persisted-chat_url revalidation: 1. Guard the substitution on chat_api_key.is_none() && chat_auth_header.is_none(). The replacement is a keyless detected_llm_config, and ResolvedArgs cannot distinguish an explicit --chat-url from persisted config, so a remote gateway URL plus OPENAI_API_KEY whose TCP connect merely times out (the 300ms budget is tight) could silently swap to a local keyless engine and drop the credential. Requiring no configured auth confines the swap to the true EAI-7360 target — a dead persisted local endpoint with no auth. 2. Normalize base_url before the change-detection compare (trim a trailing `/` and a trailing `/v1`) so a formatting-only difference (e.g. http://h:8000 vs http://h:8000/v1/) is not mistaken for a server change and does not emit a spurious "server changed" notice. Relates to EAI-7360. Signed-off-by: Michael Roy <michael.roy@amd.com>
rominf
left a comment
There was a problem hiding this comment.
LGTM. The stale-persisted-URL revalidation is well-guarded: the swap only fires on !probe_ok and a genuinely-different (trailing-/ and /v1 normalized) URL, and is gated on chat_api_key.is_none() && chat_auth_header.is_none() so a configured remote gateway that merely times out won't silently fall back to a keyless local engine. No loop (re-detecting the same URL returns None, so no spurious "server changed" notice), no panic (unwrap_or(false) / .ok()), and correct precedence (managed.or_else(stale).or_else(resolve_llm_config) — the CLI>config>env>probe contract is untouched). Dropping chat_model on swap is correct since it's a different server. Good test coverage including the normalization cases.
Note: on the degraded path (persisted URL dead) startup now does a bounded one-time extra probe (~300ms + detect_local_chat) before the first frame — worth a sentence in the PR body.
Approving.
|
🔴 Automated review · pr-review-watcher · ed0b00e Summary
Verified as correct (the load-bearing claims)
Non-blocking suggestions
Tradeoffs (deliberate choices worth confirming, not change requests)
Positive signals
|
Reconcile PR #97 (configured-endpoint served-model discovery) with main's PR #100 startup local-first detection (EAI-7347). Conflicts in crates/rocm-dash-tui/src/app/{chat.rs,mod.rs} resolved as a union that keeps both behaviors: - Preserve main's local-engine detection: should_detect_local_chat, StartupChatOutcome/startup_chat_outcome, detect_local_chat_with_probe, and the #89 Press-only overlay key gate (is_actionable_key). - Port PR #97's /v1/models discovery for a *configured* chat endpoint into a new helper, chat::discover_configured_chat_model, wired only on the StartupChatOutcome::Configured path. Startup matrix (verified by unit tests in app::chat::tests): - Configured URL + no explicit model + reachable -> adopt served /v1/models id. - Configured URL + explicit model -> config precedence, never overridden. - Configured URL + unreachable -> never probed, never replaced (no fetch timeout; an unreachable explicit CLI/env URL is left untouched). - Local detected / OAuth paths unchanged from main. RED before the port: configured_endpoint_adopts_served_model_when_no_model_set failed against the merged-main stub (got "local-model", expected served id); GREEN after wiring the helper. Full workspace tests, clippy -D warnings, and scripts/smoke_local.py all pass. Signed-off-by: Michael Roy <michael.roy@amd.com>
…ale chat_url recovery) Layer PR #105's stale persisted-URL recovery onto PR #97's configured-endpoint model discovery (which already includes current main). Manually resolve the chat.rs/mod.rs conflicts, preserving both behaviors: - PR #97: discover_configured_chat_model() adopts a served /v1/models id for a reachable configured endpoint that has no explicit model (probe_ok path). - PR #105 (EAI-7360): stale_chat_url_replacement() swaps a dead persisted tui.chat_url for a genuinely-different live local engine on the Configured + !probe_ok path, emits a "server changed" notice pointing at `/detect save`, and normalize_base_url() treats trailing slash and /v1 formatting as the same endpoint. Gated on chat_url.is_some() + no api_key/auth_header so env URLs and authenticated remote gateways are never silently replaced. The two paths are mutually exclusive on the Configured branch's probe_ok, so they compose without interference; resolve_llm_config's CLI>config>env>probe precedence is untouched. Matrix verified: no URL/model -> local detection; reachable configured -> /v1/models discovery; stale persisted -> live replacement + notice; unreachable env/explicit URL -> fail honestly; explicit auth -> no silent fallback. Signed-off-by: Michael Roy <michael.roy@amd.com>
Summary
tui.chat_urlat startup: if it's unreachableand not matched by the live managed-services registry, the dash now
falls back to a freshly-detected local engine instead of silently
building an agent against a dead endpoint.
the new
/detect savecommand (PR feat(chat): add /detect slash command for mid-session re-detection #102) to persist the replacement.Root cause
tui.chat_urlsurvives across dash launches once written (e.g. via thein-TUI "use & save" flow). If the server behind it is later stopped or
replaced — most commonly a managed vLLM/Lemonade instance relaunched on
a different port — the startup path passed the stale URL straight into
resolve_llm_configregardless of reachability. The chat backend wasthen built against a dead endpoint with no on-screen indication
anything had changed; the user would only discover it from a failed
completion request.
Fix
app/chat.rs: new pure decision functionstale_chat_url_replacement— given the configured base URL, whether its own probe succeeded, and
an already-detected
liveLlmConfig(orNone), returnsSome(live)only when the configured endpoint is unreachable and adifferently-addressed live endpoint exists. No I/O; fully
unit-testable.
app/mod.rs(startup block inevent_loop()): whenchat_urlisconfigured, wasn't matched by the managed-services registry, and its
TCP probe fails, calls
detect_local_chatand feeds the resultthrough
stale_chat_url_replacement. A replacement, if found, issubstituted before
resolve_llm_configruns and aChatTurn::system(...)notice is pushed into the transcript.Precedence preserved
This only ever revalidates the persisted-config tier:
chat_env_url(the env tier) is untouched — it's already excludedfrom the managed-registry/stale-check path (both only run when
chat_urlitself is set).resolve_llm_config's own CLI>config>env>probe precedence is notmodified at all; the substitution happens purely at the call site by
swapping in a different
LlmConfigbefore that function runs. Itsexisting unit tests —
cli_wins_over_every_other_tier,config_wins_when_no_cli,env_url_wins_when_no_cli_or_config,probed_default_used_when_nothing_configured_but_reachable— areunmodified and pass unchanged.
(Note: this crate's
ResolvedArgs::chat_urlis always sourced fromtui.chat_urlat the one call site inapps/rocm/src/dash.rs— thereis no separate explicit-CLI flag wired through a different field today
— so "persisted-config tier" is precisely what this revalidates.)
Coordination notes
Complements unmerged PR #97, which addresses the
chat_modelhalf ofthe same "server changed" gap. No overlapping hunks expected (this PR
touches
app/chat.rs+ the startup block'schat_url/probe handlingin
app/mod.rs).Relates to EAI-7360.
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(all passing, precedence tests unaffected)stale_chat_url_replacement_none_when_configured_endpoint_reachablestale_chat_url_replacement_none_when_nothing_live_foundstale_chat_url_replacement_none_when_live_matches_configuredstale_chat_url_replacement_swaps_to_a_different_live_endpoint