fix(tui-gateway): heal or fall back when a resumed session's provider is stale
A session row persists the provider identity a chat actually used. When that provider is later renamed or removed (e.g. a custom_providers:/providers: entry deleted, or a provider renamed oldone->newone), Desktop/TUI resume restores the stale name into agent init and dies with: agent init failed: Unknown provider '<name>' while the CLI resumes the same session fine with the configured default. - runtime_provider: add is_routable_provider() (full resolution chain: built-in -> providers: -> custom_providers: -> models.dev) - _stored_session_runtime_overrides: heal a non-routable provider via canonical_custom_identity (base_url -> model -> configured provider), drop to the configured default when unrecoverable, and clear the stale base_url after healing so a dead endpoint cannot override the registry URL - _start_agent_build: gate deferred-resume overrides on provider routability; when the stored provider is gone, prefer the model the user picked for THIS session, else the configured default - tests: is_routable_provider cases, heal/fallback round-trips, gate checks Refs #75128
This commit is contained in:
@@ -1104,6 +1104,45 @@ def canonical_custom_identity(
|
||||
return None
|
||||
|
||||
|
||||
def is_routable_provider(provider: Optional[str]) -> bool:
|
||||
"""Whether a provider name currently resolves to a routable route.
|
||||
|
||||
Empty/None is vacuously routable: agent build falls back to the
|
||||
configured default instead of failing. A name that resolves through
|
||||
the full chain (built-in -> user ``providers:`` -> ``custom_providers:``
|
||||
-> models.dev) is routable; anything else would fail agent init with
|
||||
"Unknown provider '<name>'".
|
||||
|
||||
Session resume uses this to detect a stale/renamed/removed provider
|
||||
persisted in an older session snapshot, so recovery can fall back to
|
||||
the configured default or the model the user picked instead of letting
|
||||
the agent build die.
|
||||
"""
|
||||
name = str(provider or "").strip()
|
||||
if not name or name.lower() == "auto":
|
||||
return True
|
||||
if name.lower() == "custom":
|
||||
# The bare string is the resolved billing class shared by every
|
||||
# named custom entry — not a routable identity. restore paths must
|
||||
# heal it (canonical_custom_identity) or fall back, never hand it
|
||||
# straight to agent init.
|
||||
return False
|
||||
try:
|
||||
from hermes_cli.providers import resolve_provider_full
|
||||
|
||||
config = load_config()
|
||||
return (
|
||||
resolve_provider_full(
|
||||
name,
|
||||
config.get("providers"),
|
||||
get_compatible_custom_providers(config),
|
||||
)
|
||||
is not None
|
||||
)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _normalize_base_url_for_match(value) -> str:
|
||||
return str(value or "").strip().rstrip("/").lower()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user