diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index 5316c7f833ccc..feacbba98f304 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -3497,11 +3497,11 @@ def fake_switch_model(**kwargs): "Model: anthropic/claude-sonnet-4.6\nProvider: anthropic" ) assert agent._cached_system_prompt == db.system_prompt - assert session["history"][-1]["role"] == "system" + assert session["history"][-1]["role"] == "user" assert "changed to anthropic/claude-sonnet-4.6" in session["history"][-1]["content"] assert db.messages[-1] == { "session_id": "session-key", - "role": "system", + "role": "user", "content": session["history"][-1]["content"], } # ...and the shared process env was NOT touched. diff --git a/tests/tui_gateway/test_model_switch_marker.py b/tests/tui_gateway/test_model_switch_marker.py new file mode 100644 index 0000000000000..f67ef94ece875 --- /dev/null +++ b/tests/tui_gateway/test_model_switch_marker.py @@ -0,0 +1,40 @@ +"""Regression tests for gateway model-switch history markers.""" + +from types import SimpleNamespace +from unittest.mock import MagicMock + +from tui_gateway.server import _append_model_switch_marker + + +def test_model_switch_marker_is_user_message_not_mid_history_system(): + """Model switches must not inject a system role after conversation turns. + + Strict OpenAI-compatible providers such as vLLM reject message arrays that + contain system messages after the beginning of the conversation. + """ + db = MagicMock() + session = { + "session_key": "sess-1", + "history": [ + {"role": "user", "content": "hello"}, + {"role": "assistant", "content": "hi"}, + ], + "history_version": 7, + "agent": SimpleNamespace(_session_db=db), + } + + _append_model_switch_marker( + session, + model="qwen3.6-35b", + provider="api.lucasnicolas.dev", + ) + + marker = session["history"][-1] + assert marker["role"] == "user" + assert "active model for this chat has changed to qwen3.6-35b" in marker["content"] + assert session["history_version"] == 8 + db.append_message.assert_called_once_with( + session_id="sess-1", + role="user", + content=marker["content"], + ) diff --git a/tui_gateway/server.py b/tui_gateway/server.py index cc9399a7c2e77..988c9b55165b7 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1721,7 +1721,11 @@ def _append_model_switch_marker(session: dict | None, *, model: str, provider: s f"{model}{provider_part}. From this point forward, use this runtime " "metadata when answering questions about what model/provider is active.]" ) - entry = {"role": "system", "content": marker} + # Persist as a user message, not a system message. The gateway appends + # this marker after prior conversation turns, and strict OpenAI-compatible + # providers such as vLLM reject system messages that are not at the + # beginning of the API message list. + entry = {"role": "user", "content": marker} lock = session.get("history_lock") if lock is not None: @@ -1736,14 +1740,14 @@ def _append_model_switch_marker(session: dict | None, *, model: str, provider: s agent = session.get("agent") db = getattr(agent, "_session_db", None) if agent is not None else None if db is not None: - db.append_message(session_id=session_key, role="system", content=marker) + db.append_message(session_id=session_key, role="user", content=marker) return _ensure_session_db_row(session) with _session_db(session) as scoped_db: if scoped_db is not None: scoped_db.append_message( - session_id=session_key, role="system", content=marker + session_id=session_key, role="user", content=marker ) except Exception: logger.debug("failed to persist model switch marker", exc_info=True)