Stale system prompt identity after mid-session model switch
NousResearch/hermes-agent · cluster 6380 · agent
Data as of
Nothing open on GitHub. Fix merged in #52666. Check it covers your case.
The short answer
- Known?
- Yes. 14 related items grouped here, 4 issues already closed.
- Fix event
- A fix was merged to main in PR #52666. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
unresolved. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and fixes center on system prompts retaining outdated model, provider, or base_url information when the active model changes mid-session, including via fallback, /model switch, or custom providers. One entry covers a desktop composer model selector feature with per-model presets.
Open issues0
Open PRs0
Fixes on record
-
#52666 fix(hermes_state): persist billing route after mid-session /model switch (#48248)
files:
agent/agent_runtime_helpers.py,hermes_state.py,tests/test_hermes_state.pyShow the diff
-
agent/agent_runtime_helpers.py@@ -1697,6 +1697,27 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo old_model, old_provider, new_model, new_provider, ) + # ── Persist billing route to session DB ── + # The agent's _session_db / session_id may not be set in all contexts + # (tests, bare agents without a session DB, etc.). This ensures the + # dashboard Model cards show the actual provider after a mid-session + # /model switch instead of the stale session-creation provider. + # See #48248 for the full bug description. + _session_db = getattr(agent, "_session_db", None) + _session_id = getattr(agent, "session_id", None) + if _session_db is not None and _session_id: + try: + _session_db.update_session_billing_route( + _session_id, + provider=agent.provider, + base_url=agent.base_url, + billing_mode=getattr(agent, "api_mode", None), + ) + except Exception: + logger.warning( + "Failed to persist billing route after model switch", + exc_info=True, + ) def invoke_tool(agent, function_name: str, function_args: dict, effective_task_id: str, -
hermes_state.py@@ -1584,6 +1584,36 @@ def _do(conn): ) self._execute_write(_do) + def update_session_billing_route( + self, + session_id: str, + *, + provider: str, + base_url: str, + billing_mode: Optional[str] = None, + ) -> None: + """Unconditionally update the billing provider/base_url for a session. + + Unlike ``update_token_counts`` which uses ``COALESCE(billing_provider, ?)`` + (only filling in NULL), this unconditionally sets the billing fields so + that the dashboard reflects the user's latest /model switch. + + Also nulls ``system_prompt`` so the cached snapshot (which embeds a + stale ``Model:`` / ``Provider:`` header) is rebuilt — matching the + behavior of ``update_session_model`` (see #48173, #48248). + """ + def _do(conn): + conn.execute( + """UPDATE sessions SET + billing_provider = ?, + billing_base_url = ?, + billing_mode = COALESCE(?, billing_mode), + system_prompt = NULL + WHERE id = ?""", + (provider, base_url, billing_mode, session_id), + ) + self._execute_write(_do) + def update_token_counts( self, session_id: str, -
tests/test_hermes_state.py@@ -210,6 +210,54 @@ def test_update_session_model_overwrites_existing(self, db): model="xiaomi/mimo-v2.5-pro") assert db.get_session("s1")["model"] == "xiaomi/mimo-v2.5" + def test_update_session_billing_route_overwrites_after_switch(self, db): + """A mid-session provider switch must overwrite the billing route. + + update_token_counts writes billing fields with + COALESCE(billing_provider, ?) (first-writer-wins), so after a + provider switch the dashboard kept attributing cost to the original + provider (#48248). update_session_billing_route sets them + unconditionally and nulls system_prompt so the next turn rebuilds + the Model:/Provider: header (#48173). + """ + db.create_session(session_id="s1", source="telegram") + # First token update seeds the billing route. + db.update_token_counts("s1", input_tokens=10, output_tokens=5, + billing_provider="openrouter", + billing_base_url="https://openrouter.ai/api/v1", + billing_mode="api_key") + sess = db.get_session("s1") + assert sess["billing_provider"] == "openrouter" + # A later token update never changes it (COALESCE first-writer-wins). + db.update_token_counts("s1", input_tokens=10, output_tokens=5, + billing_provider="ollama", + billing_base_url="http://localhost:11434/v1", + billing_mode="local") + assert db.get_session("s1")["billing_provider"] == "openrouter" + + # Seed a stale prompt snapshot, then switch the billing route. + db.update_system_prompt("s1", "Model: x/old\nProvider: openrouter") + assert db.get_session("s1")["system_prompt"] is not None + db.update_session_billing_route( + "s1", provider="ollama", + base_url="http://localhost:11434/v1", billing_mode="local", + ) + sess = db.get_session("s1") + assert sess["billing_provider"] == "ollama" + assert sess["billing_base_url"] == "http://localhost:11434/v1" + assert sess["billing_mode"] == "local" + assert sess["system_prompt"] is None, \ + "system_prompt must be nulled so the next turn rebuilds Model:/Provider:" + + # billing_mode defaults to COALESCE — omitting it preserves the value. + db.update_session_billing_route( + "s1", provider="openai", + base_url="https://api.openai.com/v1", + ) + sess = db.get_session("s1") + assert sess["billing_provider"] == "openai" + assert sess["billing_mode"] == "local" # preserved (COALESCE on None) + def test_parent_session(self, db): db.create_session(session_id="parent", source="cli") db.create_session(session_id="child", source="cli", parent_session_id="parent")
-
9 PRs closed without merging
From the close comments: done elsewhere 4 · duplicate 2 · superseded by another PR 1 · other 2
-
#6381 fix(run_agent): invalidate cached system prompt on fallback switch and restore · done elsewhere →
c884ff6,110cdd5,4d1b988,019c11d -
#24221 fix: inject base_url into system prompt volatile metadata · other →
#3353 -
#24629 fix(agent): use resolved model/provider in system prompt instead of config-level values · superseded by another PR →
#24634 -
#24634 fix(agent): use resolved model/provider in system prompt instead of config-level values · duplicate →
#24629,#24215 -
#40034 fix(agent): rebuild stale runtime prompt headers · other →
7d938cc
Show 4 more
-
#40049 fix(system_prompt): resolve Model/Provider header from live config at build time · done elsewhere →
7d938cc -
#48196 fix(hermes_state): clear cached system_prompt on mid-session model switch · done elsewhere →
#48173,#52666 -
#48197 fix(state): clear stale system_prompt on mid-session model switch · duplicate →
#48196 -
#48256 fix(agent): clear persisted system prompt on mid-session model switch (#48173) · done elsewhere →
#48173,#48248,#52666
Closed issues · 4
- Closed#6380CLI session displays stale primary model name after fallback activation
- Closed#24215[Bug] System prompt injects stale model/provider after /model switch with custom_providers
- Closed#40026System prompt 'Model:' / 'Provider:' header is stale after mid-session model switches
- Closed#48173Mid-session model switch leaves stale Model/Provider in persisted system prompt
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.12: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- agent
- Merged PRs
- #52666
- Closed issues
- 4
- Attempts
- 9 PRs closed without merging
- Fix link checked
- 25 Jun 2026