diff --git a/strix/llm/compaction.py b/strix/llm/compaction.py index cd6126c4..59eb1bf2 100644 --- a/strix/llm/compaction.py +++ b/strix/llm/compaction.py @@ -13,6 +13,7 @@ import logging from typing import TYPE_CHECKING, Any import litellm +from litellm.exceptions import ContextWindowExceededError from strix.config import load_settings from strix.core.sessions import replace_session_items, session_write_lock @@ -30,30 +31,16 @@ _TOOL_OUTPUT_MAX_CHARS = 2_000 _MIN_ITEMS_TO_COMPACT = 6 _HEAD_TRUNCATED_MARKER = "\n\n[... older conversation omitted to fit the summary request ...]\n\n" -# Substrings that identify a context-window-overflow error across providers. -# Deliberately excludes rate-limit/throttle wording, which must not trigger -# compaction. -_OVERFLOW_MARKERS = ( - "context length", - "context window", - "maximum context", - "context_length_exceeded", - "too many tokens", - "reduce the length", - "input is too long", - "prompt is too long", - "exceeds the maximum", - "string too long", -) - def is_context_overflow(exc: BaseException) -> bool: - """Whether ``exc`` looks like a model context-window-overflow error.""" - overflow_error = getattr(litellm, "ContextWindowExceededError", None) - if overflow_error is not None and isinstance(exc, overflow_error): - return True - message = str(exc).lower() - return any(marker in message for marker in _OVERFLOW_MARKERS) + """Whether ``exc`` is a model context-window-overflow error. + + LiteLLM normalises every provider's overflow error to + ``ContextWindowExceededError`` (a ``BadRequestError`` subclass) and keeps + the provider-specific detection upstream, so we rely on that type rather + than matching error message strings ourselves. + """ + return isinstance(exc, ContextWindowExceededError) _SUMMARY_INSTRUCTIONS = """\ diff --git a/tests/test_compaction.py b/tests/test_compaction.py index 9e56825e..01d5d5ac 100644 --- a/tests/test_compaction.py +++ b/tests/test_compaction.py @@ -6,6 +6,7 @@ from types import SimpleNamespace from typing import TYPE_CHECKING, Any import pytest +from litellm.exceptions import ContextWindowExceededError, RateLimitError from strix.config import ContextSettings from strix.llm import compaction @@ -70,12 +71,17 @@ def _has_orphan_tool_output(items: list[Any]) -> bool: return any(i["call_id"] not in call_ids for i in items if compaction._is_tool_output(i)) -def test_is_context_overflow_matches_and_excludes() -> None: - assert compaction.is_context_overflow( - RuntimeError("This model's maximum context length is 8192") +def test_is_context_overflow_uses_litellm_typed_error() -> None: + # LiteLLM maps every provider's overflow error to this type; anything else + # (including a rate-limit error) must not trigger compaction. + overflow = ContextWindowExceededError( + message="context length exceeded", model="m", llm_provider="openai" ) - assert compaction.is_context_overflow(ValueError("input is too long for the model")) - assert not compaction.is_context_overflow(RuntimeError("rate limit exceeded, retry later")) + assert compaction.is_context_overflow(overflow) + assert not compaction.is_context_overflow( + RateLimitError(message="slow down", model="m", llm_provider="openai") + ) + assert not compaction.is_context_overflow(RuntimeError("maximum context length is 8192")) def test_select_split_never_orphans_tool_output(monkeypatch: pytest.MonkeyPatch) -> None: