Files
hermes-agent/tests/agent/test_compression_auto_summary_fallback.py
Anthony Ruiz 71448afc55 fix(compression): name the main model when aux summary falls back
`_fallback_to_main_for_compression` clears `summary_model` to mean "use the
main model". But `call_llm(task="compression", ...)` resolves an unspecified
route from `auxiliary.compression`, i.e. the model that just failed. The
fallback therefore re-calls the failing auxiliary model; since
`_summary_model_fallen_back` is already set, no second fallback is allowed,
`_generate_summary` returns None, and with
`compression.abort_on_summary_failure` the whole compression is skipped.

Observed with an auxiliary model whose weekly quota was exhausted: two
identical 429s, `failure_class=summary_generation_aborted`,
`fallback_used=false`, and a session that kept growing uncompressed.

Add `_apply_summary_route()` as the single place that pins the summary wire
route, and use it from both compression entry points (`_generate_summary`
and `_micro_summarize_one`, which had the same defect).

Three existing tests asserted the buggy contract (`assert "model" not in
kwargs`, commented "no model kwarg -> call_llm uses main"); they now assert
the main model is named explicitly.

Salvaged from #88247 (slimmed: docstring, 2 invariant tests; dropped the redundant
_last_summary_auth_failure reset — a main-model success already clears terminal flags).
Fixes #123362.
2026-10-05 15:37:17 -07:00

58 lines
2.5 KiB
Python

"""#116472: an ``auto``-resolved compression summary model that fails (e.g. a proxy channel
answering HTTP 200 with empty content) must fall back to the main model. ``auto`` resolves a
model per call without setting ``summary_model``, so the retry gate previously saw "no separate
model" and re-hit the same bad route forever; the resolved model is also recorded so the
user-visible warning names it instead of only appearing in errors.log.
"""
from unittest.mock import MagicMock, patch
from agent.context_compressor import ContextCompressor
def _msgs():
return [
{"role": "user", "content": "do something"},
{"role": "assistant", "content": "ok"},
]
def test_auto_resolved_summary_model_falls_back_to_main_on_empty_content():
calls = {"n": 0}
def _route(**kwargs):
calls["n"] += 1
if calls["n"] == 1:
# First (auto) attempt: a proxy channel answers 200 with empty content.
kwargs["route_info"].update(provider="openrouter", model="z-ai/glm-5.3")
return {"choices": [{"message": {"content": " "}}]}
# Main-model retry succeeds.
kwargs["route_info"].update(provider="openrouter", model="main-model")
ok = MagicMock()
ok.choices = [MagicMock()]
ok.choices[0].message.content = "summary via main model"
return ok
with patch("agent.context_compressor.get_model_context_length", return_value=100000):
c = ContextCompressor(model="main-model", quiet_mode=True) # no override → provider: auto
with patch("agent.context_compressor.call_llm", side_effect=_route) as mock_call:
result = c._generate_summary(_msgs())
assert mock_call.call_count == 2 # first auto route failed → retried on main
assert mock_call.call_args_list[1].kwargs.get("model") == "main-model"
assert result is not None and "summary via main model" in result
# The model that actually failed (the auto-resolved one) is recorded for the user warning.
assert c._last_aux_model_failure_model == "z-ai/glm-5.3"
def test_fallback_records_the_explicit_failed_model():
with patch("agent.context_compressor.get_model_context_length", return_value=100000):
c = ContextCompressor(model="main-model", quiet_mode=True)
c._fallback_to_main_for_compression(Exception("boom"), "failed", failed_model="bad/route")
assert c.summary_model == "" # empty = use the main model
assert c._summary_model_fallen_back is True
assert c._last_aux_model_failure_model == "bad/route"