diff --git a/litellm/llms/anthropic/messages/context_management/editors/clear_tool_uses.py b/litellm/llms/anthropic/messages/context_management/editors/clear_tool_uses.py index 88a4640633c..c4f7966f429 100644 --- a/litellm/llms/anthropic/messages/context_management/editors/clear_tool_uses.py +++ b/litellm/llms/anthropic/messages/context_management/editors/clear_tool_uses.py @@ -31,7 +31,7 @@ from ..constants import ( ) from ..errors import AnthropicContextManagementError from ..placeholders import build_cleared_tool_result_content -from .compact import after_client_compaction, count_system_tokens +from .compact import count_system_tokens, history_after_client_compaction if TYPE_CHECKING: from litellm.types.llms.openai import ChatCompletionToolParam @@ -242,7 +242,7 @@ def _resolve_keep_count(keep: dict[str, object]) -> int: def _last_completed_tool_use_id( - messages: list[dict[str, object]], + messages: Sequence[Mapping[str, object]], ) -> str | None: """Latest completed tool_result id; never cleared.""" last_id: str | None = None @@ -258,7 +258,7 @@ def _last_completed_tool_use_id( def _clear_tool_results( - messages: list[dict[str, object]], ids_to_clear: set[str] + messages: list[dict[str, object]], ids_to_clear: Set[str] ) -> tuple[list[dict[str, object]], int]: """Clear matching tool_result content; return (messages, cleared_count).""" cleared = 0 @@ -318,10 +318,9 @@ def apply_clear_tool_uses_20250919( ) -> tuple[list[dict[str, object]], AppliedEdit | None]: """Apply clear_tool_uses to an edit ``check_clear_tool_uses_spec`` returned; return (messages, AppliedEdit or None). - Like the API, only the history after a client compaction block is edited, with its summary counted as system. + Like the API, it reads only the history after a client compaction block and counts the block's summary as system. """ - tail_start, counted_system = after_client_compaction(messages, system) or (0, system) - tail: Final = messages[tail_start:] + kept, counted_system = history_after_client_compaction(messages, system) fields: Final = _ClearToolUsesFields.model_validate(edit_spec) clear_at_least: Final = 0 if fields.clear_at_least is None else fields.clear_at_least.value exclude_tools: Final = frozenset(fields.exclude_tools or ()) @@ -336,7 +335,7 @@ def apply_clear_tool_uses_20250919( "value": DEFAULT_KEEP_TOOL_USES, } - blocks: Final = _all_blocks(tail) + blocks: Final = _all_blocks(kept) results: Final = _blocks_by_id(blocks, "tool_result", "tool_use_id") uses: Final = _blocks_by_id(blocks, "tool_use", "id") tiny: Final = _tiny_result_ids(results) @@ -349,11 +348,11 @@ def apply_clear_tool_uses_20250919( if len(clearable) <= keep_count: return messages, None - met, counted_tokens = _trigger_met(trigger, model, tail, tools, counted_system, len(clearable)) + met, counted_tokens = _trigger_met(trigger, model, kept, tools, counted_system, len(clearable)) if not met: return messages, None - last_completed_id: Final = _last_completed_tool_use_id(tail) + last_completed_id: Final = _last_completed_tool_use_id(kept) candidates: Final = tuple( (tool_use_id, name) for tool_use_id, name in clearable[: len(clearable) - keep_count] @@ -368,7 +367,7 @@ def apply_clear_tool_uses_20250919( _select_in_batches(candidate_ids, tokens, clear_at_least) if clear_at_least > 0 else candidate_ids ) - edited, cleared_count = _clear_tool_results(tail, set(ids_to_clear)) + edited, cleared_count = _clear_tool_results(messages, ids_to_clear) verbose_logger.debug("context_management polyfill: edited: %s", edited) if cleared_count == 0: return messages, None @@ -380,15 +379,16 @@ def apply_clear_tool_uses_20250919( counted_tokens if counted_tokens is not None else litellm.token_counter( - model=model, messages=tail, tools=cast("list[ChatCompletionToolParam] | None", tools) + model=model, messages=kept, tools=cast("list[ChatCompletionToolParam] | None", tools) ) ) + kept_after, _ = history_after_client_compaction(final_messages, system) tokens_after: Final = litellm.token_counter( - model=model, messages=final_messages, tools=cast("list[ChatCompletionToolParam] | None", tools) + model=model, messages=kept_after, tools=cast("list[ChatCompletionToolParam] | None", tools) ) cleared_input_tokens: Final = max(tokens_before - tokens_after, 0) - return [*messages[:tail_start], *final_messages], { + return final_messages, { "type": CLEAR_TOOL_USES_EDIT_TYPE, "cleared_tool_uses": cleared_count, "cleared_input_tokens": cleared_input_tokens, diff --git a/litellm/llms/anthropic/messages/context_management/editors/compact.py b/litellm/llms/anthropic/messages/context_management/editors/compact.py index 0fe9516bcb2..0bf8cac7344 100644 --- a/litellm/llms/anthropic/messages/context_management/editors/compact.py +++ b/litellm/llms/anthropic/messages/context_management/editors/compact.py @@ -167,21 +167,6 @@ def _slice_around_compaction_block( return sliced_messages, compaction_block -def after_client_compaction( - messages: Sequence[Mapping[str, object]], - system: str | Sequence[Mapping[str, object]] | None, -) -> tuple[int, str | Sequence[Mapping[str, object]] | None] | None: - """Return where the history after the latest compaction block starts and the system text with its summary.""" - sliced, block = _slice_around_compaction_block(messages) - if block is None: - return None - tail_start: Final = len(messages) - len(sliced) + 1 - summary: Final = block.get("content") - if not isinstance(summary, str) or not summary: - return tail_start, system - return tail_start, _augment_system_with_summary(_system_to_text(system) or None, summary) - - def _strip_compaction_blocks( messages: Sequence[dict[str, object]], ) -> list[dict[str, object]]: @@ -204,6 +189,21 @@ def _strip_compaction_blocks( return cleaned +def history_after_client_compaction( + messages: Sequence[dict[str, object]], + system: str | Sequence[Mapping[str, object]] | None, +) -> tuple[list[dict[str, object]], str | Sequence[Mapping[str, object]] | None]: + """Return the history the API keeps after the latest compaction block, without it, and the system with its summary.""" + sliced, block = _slice_around_compaction_block(messages) + if block is None: + return list(messages), system + tail: Final = _strip_compaction_blocks(sliced) + summary: Final = block.get("content") + if not isinstance(summary, str) or not summary: + return tail, system + return tail, _augment_system_with_summary(_system_to_text(system) or None, summary) + + def _augment_system_with_summary( system: str | list[dict[str, object]] | None, summary_text: str, diff --git a/tests/unit/llms/anthropic/messages/context_management/test_clear_tool_uses.py b/tests/unit/llms/anthropic/messages/context_management/test_clear_tool_uses.py index 175f82a6715..f03ffb25b2b 100644 --- a/tests/unit/llms/anthropic/messages/context_management/test_clear_tool_uses.py +++ b/tests/unit/llms/anthropic/messages/context_management/test_clear_tool_uses.py @@ -726,14 +726,15 @@ def test_input_tokens_trigger_counts_the_system_prompt(system: str | list[dict[s @pytest.mark.parametrize("compact_position", [None, "after", "before"], ids=["alone", "before_compact", "after_compact"]) async def test_input_tokens_trigger_counts_a_client_compaction_summary(compact_position: str | None) -> None: - """A client compaction summary moves into the system prompt, so the trigger counts it too, in any edit order.""" + """A client compaction summary counts toward the trigger and calls beside it stay clearable, in any edit order.""" tail: Final = _make_history(n_pairs=3) edit_spec: Final = { "type": "clear_tool_uses_20250919", "trigger": {"type": "input_tokens", "value": litellm.token_counter(model=MODEL, messages=tail) + 100}, "keep": {"type": "tool_uses", "value": 1}, } - compacted_turn: Final = {"role": "assistant", "content": [{"type": "compaction", "content": "Earlier work. " * 400}]} + compaction: Final = {"type": "compaction", "content": "Earlier work. " * 400} + compacted_history: Final = [tail[0], {**tail[1], "content": [compaction, *tail[1]["content"]]}, *tail[2:]] compact_spec: Final = {"type": "compact_20260112", "trigger": {"type": "input_tokens", "value": 50_000}} edits: Final = {None: [edit_spec], "after": [edit_spec, compact_spec], "before": [compact_spec, edit_spec]}[ compact_position @@ -756,7 +757,7 @@ async def test_input_tokens_trigger_counts_a_client_compaction_summary(compact_p return [edit.get("cleared_tool_uses") for edit in applied if edit.get("type") == "clear_tool_uses_20250919"] without_summary: Final = await prepare(deepcopy(tail)) - with_summary: Final = await prepare([compacted_turn, *deepcopy(tail)]) + with_summary: Final = await prepare(deepcopy(compacted_history)) assert cleared(without_summary) == [] assert cleared(with_summary) == [2]