# NousResearch/hermes-agent · Stale system prompt identity after mid-session model switch

## Open now

- 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.

## TL;DR

- 14 member(s): 0 open issue(s), 4 closed issue(s), 0 open PR(s), 1 merged PR(s), 9 PR(s) closed without merging.
- A fix was merged to main in PR #52666. Verify it addresses your case.
- **Recommended:** read PR #52666 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Closed / duplicate issues

- #6380: CLI session displays stale primary model name after fallback activation
- #24215: \[Bug\] System prompt injects stale model/provider after /model switch with custom_providers
- #40026: System prompt 'Model:' / 'Provider:' header is stale after mid-session model switches
- #48173: Mid-session model switch leaves stale Model/Provider in persisted system prompt

## Prior & competing PRs

- **#6381: fix(run_agent): invalidate cached system prompt on fallback switch and restore**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `c884ff64eaab0b5002e9bb703a9d3075b8dd8387`, `110cdd573a4e`, `4d1b98807016`, `019c11d07e08`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#24629: fix(agent): use resolved model/provider in system prompt instead of config-level values**. closed without merge · fix tier `suggested`
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#24634`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#52666: fix(hermes_state): persist billing route after mid-session /model switch (#48248)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `agent/agent_runtime_helpers.py`, `hermes_state.py`, `tests/test_hermes_state.py`
  - fix hunks:
  - `agent/agent_runtime_helpers.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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")
    ```
- **#24221: fix: inject base_url into system prompt volatile metadata**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `#3353`
- **#24634: fix(agent): use resolved model/provider in system prompt instead of config-level values**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#24629`, `#24215`
- **#40034: fix(agent): rebuild stale runtime prompt headers**. closed without merge
  - maintainer direction: category `uncategorized`; implication `needs_llm`; refs `7d938cc5c9c7beff22e7cb48886cba33753a5376`
- **#40049: fix(system_prompt): resolve Model/Provider header from live config at build time**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `7d938cc5c9c7beff22e7cb48886cba33753a5376`
- **#48196: fix(hermes_state): clear cached system_prompt on mid-session model switch**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#48173`, `#52666`
- **#48197: fix(state): clear stale system_prompt on mid-session model switch**. closed without merge
  - maintainer direction: category `duplicate_of`; implication `duplicate`; refs `#48196`
- **#48256: fix(agent): clear persisted system prompt on mid-session model switch (#48173)**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `#48173`, `#48248`, `#52666`

## Fix status

- Merged to main in PR #52666 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-25`.
- Subsystem: `agent`
