From e256f4aae493cac7d591a7de9034ecc0e0fa307d Mon Sep 17 00:00:00 2001 From: Haozhe Zhang Date: Wed, 10 Jun 2026 23:32:04 -0700 Subject: [PATCH] fix(gateway): don't restore a bare billing provider as the resumed session's provider `_stored_session_runtime_overrides` restored the session provider from `billing_provider` when `model_config` had no explicit provider. For a `custom:` endpoint that only ran normal turns (no `/model` switch), the persisted `billing_provider` is the bare billing bucket `"custom"`, which `agent_init` treats as non-routable, so `session.resume` failed with "No LLM provider configured" even though new chats and CLI `--resume` work. Only restore an explicit `model_config.provider`; skip a bare billing bucket (`auto`/`openrouter`/`custom`) so resume falls back to the configured default, matching the CLI path. Fixes #44022 --- tests/test_tui_gateway_server.py | 29 +++++++++++++++++++++++++++++ tui_gateway/server.py | 21 ++++++++++++++++----- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index baa8b7f79e6..d29f5b12adb 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -971,6 +971,35 @@ def test_session_resume_passes_stored_runtime_to_agent(monkeypatch): assert server._sessions[runtime_sid]["model_override"] == captured["model_override"] +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 + `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 + `model_config.provider`, is still restored. + """ + # Bare "custom" bucket, no explicit model_config.provider: no provider override restored. + ov = server._stored_session_runtime_overrides({"model": "my-model", "billing_provider": "custom"}) + assert "provider_override" not in ov + assert ov["model_override"]["provider"] is None + + for bare in ("auto", "openrouter", "custom"): + ov = server._stored_session_runtime_overrides({"model": "m", "billing_provider": bare}) + assert "provider_override" not in ov + + # A real provider in billing_provider is still restored. + ov = server._stored_session_runtime_overrides({"model": "m", "billing_provider": "anthropic"}) + assert ov["provider_override"] == "anthropic" + assert ov["model_override"]["provider"] == "anthropic" + + # An explicit routable provider in model_config wins over the bare billing bucket. + ov = server._stored_session_runtime_overrides( + {"model": "m", "billing_provider": "custom", "model_config": {"provider": "custom:myendpoint"}} + ) + assert ov["provider_override"] == "custom:myendpoint" + assert ov["model_override"]["provider"] == "custom:myendpoint" + + def test_persist_live_session_runtime_preserves_resume_metadata(monkeypatch): updates = {} diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 1228f0d9be0..283e38f069a 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1477,6 +1477,11 @@ 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"} + + def _stored_session_runtime_overrides(row: dict | None) -> dict: """Return runtime fields persisted with a stored session. @@ -1503,12 +1508,18 @@ def _stored_session_runtime_overrides(row: dict | None) -> dict: overrides: dict = {} model = str(row.get("model") or model_config.get("model") or "").strip() - provider = str( - model_config.get("provider") - or model_config.get("billing_provider") - or row.get("billing_provider") - or "" + # ``billing_provider`` is only the billing bucket — for a custom endpoint it is the + # bare class ``"custom"``, which agent_init treats as non-routable, so restoring it as + # the provider override makes ``session.resume`` fail with "No LLM provider configured". + # Only restore an explicit provider; otherwise leave it unset so resume falls back to + # the configured default, matching the working CLI path. + explicit_provider = str(model_config.get("provider") or "").strip() + billing_provider = str( + model_config.get("billing_provider") or row.get("billing_provider") or "" ).strip() + provider = explicit_provider + if not provider and billing_provider.lower() not in _BARE_BILLING_PROVIDERS: + provider = billing_provider base_url = str(model_config.get("base_url") or "").strip() api_mode = str(model_config.get("api_mode") or "").strip() reasoning_config = model_config.get("reasoning_config")