fix: normalize stale session models after provider switch — v0.50.99 (#751)
## Summary Rebased-on-behalf of @likawa3b (originally PR #748 — stale base). Sessions can outlive provider changes. When an old session still points to a model from a previous provider (e.g. `gemini-3.1-pro-preview` after switching the agent to OpenAI Codex), starting a chat hits the wrong backend and fails silently. This PR adds a lightweight normalization pass: - `_normalize_provider_id()` maps common prefixes to canonical provider IDs - `_resolve_compatible_session_model()` checks the session model's provider against `active_provider` and returns the default model if they differ - `_normalize_session_model_in_place()` is called at GET `/api/session` — corrects and persists stale models once - Chat start also normalizes via `_resolve_compatible_session_model()` and returns `effective_model` in the response - `messages.js` applies `effective_model` back to the UI/localStorage/dropdown if set Closes #748 ## Tests 1498 passed (2 pre-existing ordering failures unrelated to this PR; 5 new tests added in `test_provider_mismatch.py`). **Original author:** @likawa3b
This commit is contained in:
@@ -278,6 +278,99 @@ def test_api_models_includes_active_provider():
|
||||
)
|
||||
|
||||
|
||||
def test_bare_gemini_session_model_normalizes_to_active_provider_default(monkeypatch):
|
||||
"""Persisted bare Gemini IDs must not survive a provider switch."""
|
||||
import api.routes as routes
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"get_available_models",
|
||||
lambda: {
|
||||
"active_provider": "openai-codex",
|
||||
"default_model": "gpt-5.4-mini",
|
||||
},
|
||||
)
|
||||
|
||||
effective, changed = routes._resolve_compatible_session_model(
|
||||
"gemini-3.1-pro-preview"
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
assert effective == "gpt-5.4-mini"
|
||||
|
||||
|
||||
def test_prefixed_google_session_model_normalizes_to_active_provider_default(monkeypatch):
|
||||
"""Persisted provider-prefixed Gemini IDs must normalize too."""
|
||||
import api.routes as routes
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"get_available_models",
|
||||
lambda: {
|
||||
"active_provider": "openai-codex",
|
||||
"default_model": "gpt-5.4-mini",
|
||||
},
|
||||
)
|
||||
|
||||
effective, changed = routes._resolve_compatible_session_model(
|
||||
"google/gemini-3.1-pro-preview"
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
assert effective == "gpt-5.4-mini"
|
||||
|
||||
|
||||
def test_google_active_provider_keeps_valid_gemini_session_model(monkeypatch):
|
||||
"""A Google-configured session must keep its Gemini model."""
|
||||
import api.routes as routes
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"get_available_models",
|
||||
lambda: {
|
||||
"active_provider": "google",
|
||||
"default_model": "gemini-3.1-pro-preview",
|
||||
},
|
||||
)
|
||||
|
||||
effective, changed = routes._resolve_compatible_session_model(
|
||||
"gemini-3.1-pro-preview"
|
||||
)
|
||||
|
||||
assert changed is False
|
||||
assert effective == "gemini-3.1-pro-preview"
|
||||
|
||||
|
||||
def test_session_model_normalizer_persists_corrected_model(monkeypatch):
|
||||
"""GET /api/session should persist the corrected model back to disk/state."""
|
||||
import api.routes as routes
|
||||
|
||||
monkeypatch.setattr(
|
||||
routes,
|
||||
"get_available_models",
|
||||
lambda: {
|
||||
"active_provider": "openai-codex",
|
||||
"default_model": "gpt-5.4-mini",
|
||||
},
|
||||
)
|
||||
|
||||
save_calls = []
|
||||
|
||||
class DummySession:
|
||||
def __init__(self):
|
||||
self.model = "gemini-3.1-pro-preview"
|
||||
|
||||
def save(self, touch_updated_at=True):
|
||||
save_calls.append(touch_updated_at)
|
||||
|
||||
session = DummySession()
|
||||
effective = routes._normalize_session_model_in_place(session)
|
||||
|
||||
assert effective == "gpt-5.4-mini"
|
||||
assert session.model == "gpt-5.4-mini"
|
||||
assert save_calls == [False]
|
||||
|
||||
|
||||
# ── Model switch toast (#419) ─────────────────────────────────────────────────
|
||||
|
||||
class TestModelSwitchToast:
|
||||
@@ -323,3 +416,17 @@ class TestModelSwitchToast:
|
||||
assert "typeof showToast" in surrounding, (
|
||||
"showToast call must be guarded with typeof check"
|
||||
)
|
||||
|
||||
|
||||
class TestChatStartEffectiveModelRecovery:
|
||||
"""messages.js must accept an effective_model correction from the backend."""
|
||||
|
||||
def test_send_applies_effective_model_from_chat_start(self):
|
||||
src = _read("static/messages.js")
|
||||
assert "startData.effective_model" in src, (
|
||||
"send() must read effective_model from /api/chat/start so the UI can "
|
||||
"recover from stale persisted session models"
|
||||
)
|
||||
assert "localStorage.setItem('hermes-webui-model', startData.effective_model)" in src, (
|
||||
"effective_model correction must update the saved model preference"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user