From e78c6be0418e4c40c018bd5c59bccf39e6f6de6e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 02:23:10 +0000 Subject: [PATCH] fix(complexity_router): count structured tool results in context escalation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../complexity_router/complexity_router.py | 16 +++++++++++++++- .../router_strategy/test_complexity_router.py | 6 +++++- .../add_model/ClassificationMethodConfig.tsx | 2 ++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index e2587b14b83..becadd7756b 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -460,8 +460,22 @@ def _message_text(content: object) -> str: return content if isinstance(content, str) else "" +def _estimated_content_characters(content: object) -> int: + if isinstance(content, str): + return len(content) + if isinstance(content, list): + return sum(_estimated_content_characters(part) for part in content) + if isinstance(content, Mapping): + content_type: Final = content.get("type") + if content_type == "text": + return _estimated_content_characters(content.get("text")) + if content_type == "tool_result": + return _estimated_content_characters(content.get("content")) + return 0 + + def _estimated_conversation_tokens(messages: Sequence[Mapping[str, object]] | None) -> int: - return sum(len(_message_text(message.get("content"))) // 4 for message in messages or ()) + return sum(_estimated_content_characters(message.get("content")) // 4 for message in messages or ()) def _reminder_block_spans(lowered: str, open_marker: str, close_marker: str) -> Iterator[tuple[int, int]]: diff --git a/tests/test_litellm/router_strategy/test_complexity_router.py b/tests/test_litellm/router_strategy/test_complexity_router.py index b53d5e764b9..1b6dd9ac7f3 100644 --- a/tests/test_litellm/router_strategy/test_complexity_router.py +++ b/tests/test_litellm/router_strategy/test_complexity_router.py @@ -13538,9 +13538,13 @@ class TestHeuristicFirst: ], 4, ), + ( + [{"role": "user", "content": [{"type": "tool_result", "content": "abcdefghijklmnop"}]}], + 4, + ), ], ) - def test_estimated_conversation_tokens_counts_text_parts(self, messages, expected): + def test_estimated_conversation_tokens_counts_text_and_tool_result_parts(self, messages, expected): assert _estimated_conversation_tokens(messages) == expected @pytest.mark.asyncio diff --git a/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx b/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx index 97f0965c49a..47586a992a3 100644 --- a/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx +++ b/ui/litellm-dashboard/src/components/add_model/ClassificationMethodConfig.tsx @@ -278,7 +278,9 @@ const ClassificationMethodConfig: React.FC = ({ const parsed: number = Number(raw); if (Number.isFinite(parsed)) { onChange({ ...value, heuristic_first_max_context_tokens: Math.max(1, Math.round(parsed)) }); + return; } + onChange({ ...value, heuristic_first_max_context_tokens: undefined }); }; const handleHybridBoundaryMarginChange = (raw: string) => {