diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 1407c9586fb6a..8799be8afefc0 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -3125,10 +3125,11 @@ def update_session_cwd(self, *_args): def test_stored_session_runtime_overrides_skips_bare_billing_provider(): - """A bare billing bucket ("custom"/"auto"/"openrouter") must not be restored as the - provider identity on resume. A custom endpoint that never used `/model` persists only + """A bare billing bucket ("custom"/"auto") must not be restored as the provider + identity on resume. A custom endpoint that never used `/model` persists only `billing_provider="custom"`; restoring that broke `session.resume` with "No LLM provider - configured" (agent_init treats it as non-routable). A real provider, or an explicit + configured" (agent_init treats it as non-routable). ``"openrouter"`` is NOT a bare bucket + — it is a fully routable provider; see #57588. A real provider, or an explicit `model_config.provider`, is still restored. """ # Bare "custom" bucket, no explicit model_config.provider: no provider override restored. @@ -3136,7 +3137,7 @@ def test_stored_session_runtime_overrides_skips_bare_billing_provider(): assert "provider_override" not in ov assert ov["model_override"]["provider"] is None - for bare in ("auto", "openrouter", "custom"): + for bare in ("auto", "custom"): ov = server._stored_session_runtime_overrides({"model": "m", "billing_provider": bare}) assert "provider_override" not in ov @@ -3165,6 +3166,33 @@ def test_stored_session_runtime_overrides_restores_explicit_normal_tier(): assert overrides["service_tier_override"] == "" +def test_openrouter_session_resume_restores_provider(): + """OpenRouter is a fully routable provider — sessions that used OpenRouter must + restore the "openrouter" provider override on resume, not fall through to whatever + the current global model is. (#57588) + """ + # OpenRouter session with no explicit model_config.provider (the common case + # for sessions that never used /model): billing_provider="openrouter" should + # be restored as the provider override. + ov = server._stored_session_runtime_overrides( + {"model": "anthropic/claude-opus-4.8", "billing_provider": "openrouter"} + ) + assert ov["provider_override"] == "openrouter" + assert ov["model_override"]["provider"] == "openrouter" + assert ov["model_override"]["model"] == "anthropic/claude-opus-4.8" + + # When an explicit model_config.provider exists, it takes precedence over + # billing_provider (this path was already correct). + ov = server._stored_session_runtime_overrides( + { + "model": "anthropic/claude-opus-4.8", + "billing_provider": "openrouter", + "model_config": {"provider": "openrouter", "base_url": "https://openrouter.ai/api/v1"}, + } + ) + assert ov["provider_override"] == "openrouter" + + def test_persist_live_session_runtime_preserves_resume_metadata(monkeypatch): updates = {} diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 9c00bc2062eca..eb7e388cbde5c 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -3766,9 +3766,16 @@ def _resolve_startup_runtime() -> tuple[str, str | None]: return model, None -# Bare billing buckets are not routable provider identities (kept in parity with the -# provider gate in agent_init). Restoring one as a session provider override breaks resume. -_BARE_BILLING_PROVIDERS = {"auto", "openrouter", "custom"} +# Bare billing buckets are not routable provider identities; restoring one as a +# session provider override breaks resume. (agent_init's fail-fast gate is a +# DIFFERENT set that also skips "openrouter" — there it means "default route, +# don't fail fast", not "unroutable".) +# ``openrouter`` is deliberately excluded here — it is a fully routable provider +# with its own API key and base_url. Sessions that used OpenRouter store +# ``billing_provider="openrouter"``; dropping it forces resume to the current +# global model (e.g. a custom endpoint), which is the wrong provider for the +# stored model. See #57588. +_BARE_BILLING_PROVIDERS = {"auto", "custom"} def _stored_session_runtime_overrides(row: dict | None) -> dict: