fix(hermes_state): persist billing provider/base_url after mid-session /model switch

The session database records billing_provider and billing_base_url using
COALESCE(column, ?) in update_token_counts(), making them write-once.
When a user switches models mid-session via /model, the runtime (agent.provider,
agent.base_url) updates correctly, but the session row never reflects the new
provider. This causes the dashboard Models page to display a stale provider
badge and misattributes token usage / cost analytics.

Fix: add update_session_billing_route() that unconditionally sets
billing_provider, billing_base_url, and billing_mode (no COALESCE), and call
it from switch_model() in agent_runtime_helpers.py after the swap succeeds.

This follows the same pattern as update_session_model() which already
unconditionally updates the model column (added for the identical COALESCE
problem on the model field).

Closes #48248
This commit is contained in:
x7peeps 2026-06-18 13:49:15 +08:00 committed by Teknium
parent bf0513bca0
commit c7e934a5b4
2 changed files with 51 additions and 0 deletions

View file

@ -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,

View file

@ -1584,6 +1584,36 @@ class SessionDB:
)
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,