diff --git a/litellm/compression/compress.py b/litellm/compression/compress.py index 604e216a832..4c4ce74fbbd 100644 --- a/litellm/compression/compress.py +++ b/litellm/compression/compress.py @@ -48,24 +48,28 @@ def _content_to_text(content: Any) -> str: Text extraction policy: - Include text-bearing fields only (`text` blocks + string values). - - For `tool_result`, recurse into nested `content`. + - For `tool_result`, expand into nested `content` items. - Ignore non-textual blocks (images/documents/tool metadata/thinking metadata). + + Implemented iteratively (stack-based) to avoid unbounded recursion. """ - if isinstance(content, str): - return content - if isinstance(content, list): - parts: List[str] = [] - for part in content: - if isinstance(part, dict): - part_type = part.get("type") - if part_type == "text": - parts.append(str(part.get("text", ""))) - elif part_type == "tool_result": - parts.append(_content_to_text(part.get("content", ""))) - elif isinstance(part, str): - parts.append(part) - return " ".join(parts) - return "" + parts: List[str] = [] + stack: List[Any] = [content] + while stack: + item = stack.pop() + if isinstance(item, str): + parts.append(item) + elif isinstance(item, list): + # Push list items in reverse order so they are processed left-to-right. + for element in reversed(item): + stack.append(element) + elif isinstance(item, dict): + item_type = item.get("type") + if item_type == "text": + parts.append(str(item.get("text", ""))) + elif item_type == "tool_result": + stack.append(item.get("content", "")) + return " ".join(parts) def _normalize_messages_for_compression( diff --git a/litellm/integrations/compression_interception/handler.py b/litellm/integrations/compression_interception/handler.py index af55a084c9f..8d211a77129 100644 --- a/litellm/integrations/compression_interception/handler.py +++ b/litellm/integrations/compression_interception/handler.py @@ -9,8 +9,8 @@ import time import uuid from typing import Any, Dict, List, Optional, Tuple, cast -import litellm from litellm._logging import verbose_logger +from litellm.compression import compress from litellm.integrations.custom_logger import CustomLogger from litellm.types.integrations.compression_interception import ( CompressionInterceptionConfig, @@ -96,7 +96,7 @@ class CompressionInterceptionLogger(CustomLogger): self._prune_expired_cache() - compressed = litellm.compress( + compressed = compress( messages=messages, model=model, input_type="anthropic_messages", diff --git a/litellm/types/compression.py b/litellm/types/compression.py index cc6e53819be..7b14db70942 100644 --- a/litellm/types/compression.py +++ b/litellm/types/compression.py @@ -2,7 +2,14 @@ Type definitions for litellm.compress(). """ -from typing import Dict, List, Literal, NotRequired, TypedDict +import sys + +if sys.version_info >= (3, 11): + from typing import Dict, List, Literal, NotRequired, TypedDict +else: + from typing import Dict, List, Literal, TypedDict + + from typing_extensions import NotRequired CompressionInputType = Literal["anthropic_messages", "openai_chat_completions"]