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

0open issues
0open PRs
14items grouped
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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #52666 fix(hermes_state): persist billing route after mid-session /model switch (#48248)

    merged to main

    files: agent/agent_runtime_helpers.py, hermes_state.py, tests/test_hermes_state.py

    Show 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.00 means 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 Lab

At a glance

Subsystem
agent
Merged PRs
#52666
Closed issues
4
Attempts
9 PRs closed without merging
Fix link checked
25 Jun 2026

For your agent

The same facts, typed.