Summary
tui_gateway/server.py::_runtime_model_config() persists the live agent's resolved provider name into the session row's model_config JSON. For any named providers: / custom_providers: entry (e.g. one called mimo-v2.5-pro), agent.provider is the literal string "custom" — the entry name is lost, and the api_key is (deliberately) never persisted.
On session.resume or _reset_session_agent (/new), _stored_session_runtime_overrides() feeds that stored provider="custom" back into _make_agent() → resolve_runtime_provider(requested="custom", target_model=...). _get_named_custom_provider("custom") cannot match an entry named mimo-v2.5-pro, so depending on config the rebuild either:
- raises
No LLM provider configured. Run hermes model... (resume fails outright), or
- silently resolves placeholder credentials (
"no-key-required") against the patched-back base_url — every subsequent turn then 401s against the user's real endpoint.
Distinct from #44022 (sibling bug)
#44022 covers sessions where model_config was never persisted and only the bare billing_provider="custom" row column exists. This issue is the complementary path: model_config is persisted (any /model switch, reasoning change, etc. triggers _persist_live_session_runtime), but with the lossy resolved "custom" instead of the entry identity. Fixing #44022's restore-path heuristic does not help here, because these rows do carry a base_url — the override is restored verbatim, with unusable credentials.
Same resolved-vs-requested identity-loss family as the investigation in #44099.
Reproduction (dynamic)
With a config like:
custom_providers:
- name: mimo-v2.5-pro
base_url: https://token-plan-cn.xiaomimimo.com/v1
api_key: sk-...
resolve_runtime_provider(requested="mimo-v2.5-pro", ...) → resolves fine, provider="custom", entry api_key present. Agent works.
- Round-trip the resulting agent through
_runtime_model_config() → stored model_config has provider: "custom", base_url: ..., no api_key.
_stored_session_runtime_overrides() on that row → model_override.provider == "custom".
_make_agent() → resolve_runtime_provider(requested="custom") → api_key resolves empty → AIAgent.__init__ raises RuntimeError: No LLM provider configured.
Suggested fix
Persist the requested/entry identity, not the resolved one: when agent.provider == "custom" and the base_url matches a providers:/custom_providers: entry, store the canonical custom:<normalized-name> menu key (the same form a live /model switch already records via pdef.id, and which _get_named_custom_provider resolves). For rows persisted before the fix, _make_agent can perform the same base_url → entry recovery, falling back to passing the stored base_url as explicit_base_url so the direct-alias branch still targets the session's endpoint.
PR with fix + regression tests incoming.
Summary
tui_gateway/server.py::_runtime_model_config()persists the live agent's resolved provider name into the session row'smodel_configJSON. For any namedproviders:/custom_providers:entry (e.g. one calledmimo-v2.5-pro),agent.provideris the literal string"custom"— the entry name is lost, and the api_key is (deliberately) never persisted.On
session.resumeor_reset_session_agent(/new),_stored_session_runtime_overrides()feeds that storedprovider="custom"back into_make_agent()→resolve_runtime_provider(requested="custom", target_model=...)._get_named_custom_provider("custom")cannot match an entry namedmimo-v2.5-pro, so depending on config the rebuild either:No LLM provider configured. Runhermes model...(resume fails outright), or"no-key-required") against the patched-backbase_url— every subsequent turn then 401s against the user's real endpoint.Distinct from #44022 (sibling bug)
#44022 covers sessions where
model_configwas never persisted and only the barebilling_provider="custom"row column exists. This issue is the complementary path:model_configis persisted (any/modelswitch, reasoning change, etc. triggers_persist_live_session_runtime), but with the lossy resolved"custom"instead of the entry identity. Fixing #44022's restore-path heuristic does not help here, because these rows do carry abase_url— the override is restored verbatim, with unusable credentials.Same resolved-vs-requested identity-loss family as the investigation in #44099.
Reproduction (dynamic)
With a config like:
resolve_runtime_provider(requested="mimo-v2.5-pro", ...)→ resolves fine,provider="custom", entry api_key present. Agent works._runtime_model_config()→ storedmodel_confighasprovider: "custom",base_url: ..., no api_key._stored_session_runtime_overrides()on that row →model_override.provider == "custom"._make_agent()→resolve_runtime_provider(requested="custom")→ api_key resolves empty →AIAgent.__init__raisesRuntimeError: No LLM provider configured.Suggested fix
Persist the requested/entry identity, not the resolved one: when
agent.provider == "custom"and the base_url matches aproviders:/custom_providers:entry, store the canonicalcustom:<normalized-name>menu key (the same form a live/modelswitch already records viapdef.id, and which_get_named_custom_providerresolves). For rows persisted before the fix,_make_agentcan perform the same base_url → entry recovery, falling back to passing the stored base_url asexplicit_base_urlso the direct-alias branch still targets the session's endpoint.PR with fix + regression tests incoming.