{"repo":"NousResearch/hermes-agent","public_id":"37794","display_label":"Provider/route persistence after mid-session /model switches","summary":"The three members appear related: one issue reports the dashboard showing the wrong provider name after a model is chosen via /model in Discord, and two pull requests concern persisting the billing or provider route after a mid-session or gateway model switch. The titles alone do not confirm the exact cause or whether the changes address the dashboard report.","subsystem":"dashboard","counts":{"members":3,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":1},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":52666,"merged_at":"2026-06-25T21:44:01+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-06-25T21:44:01+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.6666666666666666},"fix_edges":[{"issue_number":37794,"pr_number":52666,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T21:44:01+00:00"},{"issue_number":48173,"pr_number":52666,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-06-25T21:44:01+00:00"},{"issue_number":48248,"pr_number":52666,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true,"pr_merged_at":"2026-06-25T21:44:01+00:00"}],"members":[{"kind":"issue","number":37794,"title":"Dashboard shows wrong provider name for model selected via /model in Discord","url":null,"state":"closed"},{"kind":"pr","number":37828,"title":"fix: persist gateway model-switch provider route","url":null,"state":"closed","close_reason":{"refs":[{"kind":"commit","value":"c7e934a5b","number":0},{"kind":"pr","value":"#52666","number":52666}],"category":"implemented_elsewhere","implication":"superseded"},"merged":false},{"kind":"pr","number":52666,"title":"fix(hermes_state): persist billing route after mid-session /model switch (#48248)","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["agent/agent_runtime_helpers.py","hermes_state.py","tests/test_hermes_state.py"],"fix_hunks":[{"file":"agent/agent_runtime_helpers.py","hunks":["@@ -1697,6 +1697,27 @@ def switch_model(agent, new_model, new_provider, api_key='', base_url='', api_mo\n         old_model, old_provider, new_model, new_provider,\n     )\n \n+    # ── Persist billing route to session DB ──\n+    # The agent's _session_db / session_id may not be set in all contexts\n+    # (tests, bare agents without a session DB, etc.).  This ensures the\n+    # dashboard Model cards show the actual provider after a mid-session\n+    # /model switch instead of the stale session-creation provider.\n+    # See #48248 for the full bug description.\n+    _session_db = getattr(agent, \"_session_db\", None)\n+    _session_id = getattr(agent, \"session_id\", None)\n+    if _session_db is not None and _session_id:\n+        try:\n+            _session_db.update_session_billing_route(\n+                _session_id,\n+                provider=agent.provider,\n+                base_url=agent.base_url,\n+                billing_mode=getattr(agent, \"api_mode\", None),\n+            )\n+        except Exception:\n+            logger.warning(\n+                \"Failed to persist billing route after model switch\",\n+                exc_info=True,\n+            )\n \n \n def invoke_tool(agent, function_name: str, function_args: dict, effective_task_id: str,"]},{"file":"hermes_state.py","hunks":["@@ -1584,6 +1584,36 @@ def _do(conn):\n             )\n         self._execute_write(_do)\n \n+    def update_session_billing_route(\n+        self,\n+        session_id: str,\n+        *,\n+        provider: str,\n+        base_url: str,\n+        billing_mode: Optional[str] = None,\n+    ) -> None:\n+        \"\"\"Unconditionally update the billing provider/base_url for a session.\n+\n+        Unlike ``update_token_counts`` which uses ``COALESCE(billing_provider, ?)``\n+        (only filling in NULL), this unconditionally sets the billing fields so\n+        that the dashboard reflects the user's latest /model switch.\n+\n+        Also nulls ``system_prompt`` so the cached snapshot (which embeds a\n+        stale ``Model:`` / ``Provider:`` header) is rebuilt — matching the\n+        behavior of ``update_session_model`` (see #48173, #48248).\n+        \"\"\"\n+        def _do(conn):\n+            conn.execute(\n+                \"\"\"UPDATE sessions SET\n+                   billing_provider = ?,\n+                   billing_base_url = ?,\n+                   billing_mode = COALESCE(?, billing_mode),\n+                   system_prompt = NULL\n+                   WHERE id = ?\"\"\",\n+                (provider, base_url, billing_mode, session_id),\n+            )\n+        self._execute_write(_do)\n+\n     def update_token_counts(\n         self,\n         session_id: str,"]},{"file":"tests/test_hermes_state.py","hunks":["@@ -210,6 +210,54 @@ def test_update_session_model_overwrites_existing(self, db):\n                                model=\"xiaomi/mimo-v2.5-pro\")\n         assert db.get_session(\"s1\")[\"model\"] == \"xiaomi/mimo-v2.5\"\n \n+    def test_update_session_billing_route_overwrites_after_switch(self, db):\n+        \"\"\"A mid-session provider switch must overwrite the billing route.\n+\n+        update_token_counts writes billing fields with\n+        COALESCE(billing_provider, ?) (first-writer-wins), so after a\n+        provider switch the dashboard kept attributing cost to the original\n+        provider (#48248). update_session_billing_route sets them\n+        unconditionally and nulls system_prompt so the next turn rebuilds\n+        the Model:/Provider: header (#48173).\n+        \"\"\"\n+        db.create_session(session_id=\"s1\", source=\"telegram\")\n+        # First token update seeds the billing route.\n+        db.update_token_counts(\"s1\", input_tokens=10, output_tokens=5,\n+                               billing_provider=\"openrouter\",\n+                               billing_base_url=\"https://openrouter.ai/api/v1\",\n+                               billing_mode=\"api_key\")\n+        sess = db.get_session(\"s1\")\n+        assert sess[\"billing_provider\"] == \"openrouter\"\n+        # A later token update never changes it (COALESCE first-writer-wins).\n+        db.update_token_counts(\"s1\", input_tokens=10, output_tokens=5,\n+                               billing_provider=\"ollama\",\n+                               billing_base_url=\"http://localhost:11434/v1\",\n+                               billing_mode=\"local\")\n+        assert db.get_session(\"s1\")[\"billing_provider\"] == \"openrouter\"\n+\n+        # Seed a stale prompt snapshot, then switch the billing route.\n+        db.update_system_prompt(\"s1\", \"Model: x/old\\nProvider: openrouter\")\n+        assert db.get_session(\"s1\")[\"system_prompt\"] is not None\n+        db.update_session_billing_route(\n+            \"s1\", provider=\"ollama\",\n+            base_url=\"http://localhost:11434/v1\", billing_mode=\"local\",\n+        )\n+        sess = db.get_session(\"s1\")\n+        assert sess[\"billing_provider\"] == \"ollama\"\n+        assert sess[\"billing_base_url\"] == \"http://localhost:11434/v1\"\n+        assert sess[\"billing_mode\"] == \"local\"\n+        assert sess[\"system_prompt\"] is None, \\\n+            \"system_prompt must be nulled so the next turn rebuilds Model:/Provider:\"\n+\n+        # billing_mode defaults to COALESCE — omitting it preserves the value.\n+        db.update_session_billing_route(\n+            \"s1\", provider=\"openai\",\n+            base_url=\"https://api.openai.com/v1\",\n+        )\n+        sess = db.get_session(\"s1\")\n+        assert sess[\"billing_provider\"] == \"openai\"\n+        assert sess[\"billing_mode\"] == \"local\"  # preserved (COALESCE on None)\n+\n     def test_parent_session(self, db):\n         db.create_session(session_id=\"parent\", source=\"cli\")\n         db.create_session(session_id=\"child\", source=\"cli\", parent_session_id=\"parent\")\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}