mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-12 08:42:18 +00:00
fix(context_management): keep tool calls beside a client compaction block clearable
This commit is contained in:
parent
eed5157c63
commit
a85a5575f9
3 changed files with 32 additions and 31 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue