diff --git a/hermes_state.py b/hermes_state.py index 13802967879..adee0d9f1ed 100644 --- a/hermes_state.py +++ b/hermes_state.py @@ -2579,6 +2579,33 @@ class SessionDB: ) def _do(conn): + row = conn.execute( + "SELECT model, billing_provider, api_call_count FROM sessions WHERE id = ?", + (session_id,), + ).fetchone() + existing_model = row["model"] if row is not None else None + existing_provider = row["billing_provider"] if row is not None else None + existing_api_calls = int((row["api_call_count"] if row is not None else 0) or 0) + + # Session creation records the requested primary route before any API + # call. If it fails and fallback succeeds, the first accounted usage + # event is the first authoritative route. After that, preserve the + # legacy row: one row cannot represent mixed-provider usage. + first_accounted_route = ( + existing_api_calls == 0 + and bool(model) + and bool(billing_provider) + and (existing_model != model or existing_provider != billing_provider) + ) + if first_accounted_route: + conn.execute( + """UPDATE sessions + SET model = ?, billing_provider = ?, + billing_base_url = COALESCE(?, billing_base_url), + billing_mode = COALESCE(?, billing_mode) + WHERE id = ?""", + (model, billing_provider, billing_base_url, billing_mode, session_id), + ) conn.execute(sql, params) if record_model_usage: self._record_model_usage( diff --git a/tests/test_hermes_state.py b/tests/test_hermes_state.py index 18c7e695029..eaa6dae3514 100644 --- a/tests/test_hermes_state.py +++ b/tests/test_hermes_state.py @@ -272,6 +272,43 @@ class TestSessionLifecycle: session = db.get_session("s1") assert session["model"] == "openai/gpt-5.4" + def test_first_accounted_fallback_replaces_requested_primary_route(self, db): + """First successful fallback usage must persist one coherent route pair.""" + db.create_session(session_id="s1", source="cli", model="gpt-5.6-sol") + + db.update_token_counts( + "s1", + input_tokens=10, + output_tokens=5, + model="glm-5.2", + billing_provider="custom:zai", + billing_base_url="https://api.z.ai/api/coding/paas/v4/", + api_call_count=1, + ) + + session = db.get_session("s1") + assert session["model"] == "glm-5.2" + assert session["billing_provider"] == "custom:zai" + assert session["billing_base_url"] == "https://api.z.ai/api/coding/paas/v4/" + assert session["api_call_count"] == 1 + + def test_accounted_primary_route_is_not_rewritten_by_later_fallback(self, db): + """A mixed-provider session keeps its first accounted route in the legacy row.""" + db.create_session(session_id="s1", source="cli", model="gpt-5.6-sol") + db.update_token_counts( + "s1", input_tokens=10, output_tokens=5, model="gpt-5.6-sol", + billing_provider="openai-codex", api_call_count=1, + ) + db.update_token_counts( + "s1", input_tokens=10, output_tokens=5, model="glm-5.2", + billing_provider="custom:zai", api_call_count=1, + ) + + session = db.get_session("s1") + assert session["model"] == "gpt-5.6-sol" + assert session["billing_provider"] == "openai-codex" + assert session["api_call_count"] == 2 + def test_update_token_counts_preserves_existing_model(self, db): db.create_session(session_id="s1", source="cli", model="anthropic/claude-opus-4.6") db.update_token_counts("s1", input_tokens=10, output_tokens=5, model="openai/gpt-5.4")