diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 97f584d073208..0f7f0ca6d36b2 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -4537,6 +4537,51 @@ def get_session_title(self, key): assert info["title"] == "Dashboard title" +def test_session_info_prefers_session_model_override_provider_for_custom_runtime(): + agent = types.SimpleNamespace( + tools=[], + model="shared-model", + provider="custom", + base_url="https://provider-a.example/v1", + api_mode="codex_responses", + ) + session = { + "session_key": "session-key", + "history": [], + "model_override": { + "model": "shared-model", + "provider": "custom-provider-a", + "base_url": "https://provider-a.example/v1", + "api_mode": "codex_responses", + }, + } + + info = server._session_info(agent, session) + + assert info["model"] == "shared-model" + assert info["provider"] == "custom-provider-a" + + +def test_session_info_ignores_stale_model_override_provider(): + agent = types.SimpleNamespace( + tools=[], + model="current-model", + provider="openai-codex", + ) + session = { + "session_key": "session-key", + "history": [], + "model_override": { + "model": "old-model", + "provider": "custom-provider-a", + }, + } + + info = server._session_info(agent, session) + + assert info["provider"] == "openai-codex" + + # --------------------------------------------------------------------------- # History-mutating commands must reject while session.running is True. # Without these guards, prompt.submit's post-run history write either diff --git a/tui_gateway/server.py b/tui_gateway/server.py index ec61aed6d57ee..f73fe01b16814 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3028,6 +3028,31 @@ def _current_profile_name() -> str: DESKTOP_BACKEND_CONTRACT = 2 +def _session_info_provider(agent, session: dict | None = None) -> str: + """Return the user-facing provider identity for ``session.info``. + + ``agent.provider`` is the resolved runtime/billing class. For named custom + providers that value can be the literal ``custom`` even though the user chose + a routable provider slug from the desktop picker. If Desktop persists that + internal value into its sticky composer state, the next ``session.create`` + can pair the right model with the wrong provider group. + + The per-session ``model_override`` is the source of truth for picker and + live ``/model`` switches, so prefer it while it still describes the agent's + current model. Fall back to the runtime provider for older sessions and + built-in providers. + """ + provider = str(getattr(agent, "provider", "") or "").strip() + override = (session or {}).get("model_override") if isinstance(session, dict) else None + if isinstance(override, dict): + override_provider = str(override.get("provider") or "").strip() + override_model = str(override.get("model") or "").strip() + agent_model = str(getattr(agent, "model", "") or "").strip() + if override_provider and (not override_model or override_model == agent_model): + return override_provider + return provider + + def _session_info(agent, session: dict | None = None) -> dict: if session is None: for candidate in _sessions.values(): @@ -3070,7 +3095,7 @@ def _session_info(agent, session: dict | None = None) -> dict: yolo = False info: dict = { "model": getattr(agent, "model", ""), - "provider": getattr(agent, "provider", ""), + "provider": _session_info_provider(agent, session), "reasoning_effort": reasoning_effort, "service_tier": service_tier, "fast": service_tier == "priority",