Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/my-website/docs/providers/anthropic.md
Original file line number Diff line number Diff line change
Expand Up @@ -953,6 +953,30 @@ except Exception as e:

s/o @[Shekhar Patnaik](https://www.linkedin.com/in/patnaikshekhar) for requesting this!

### Context Management (Beta)

Anthropic’s [context editing](https://docs.claude.com/en/docs/build-with-claude/context-editing) API lets you automatically clear older tool results or thinking blocks. LiteLLM now forwards the native `context_management` payload when you call Anthropic models, and automatically attaches the required `context-management-2025-06-27` beta header.

```python
from litellm import completion

response = completion(
model="anthropic/claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "Summarize the latest tool results"}],
context_management={
"edits": [
{
"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 30000},
"keep": {"type": "tool_uses", "value": 3},
"clear_at_least": {"type": "input_tokens", "value": 5000},
"exclude_tools": ["web_search"],
}
]
},
)
```

### Anthropic Hosted Tools (Computer, Text Editor, Web Search, Memory)


Expand Down
34 changes: 30 additions & 4 deletions litellm/llms/anthropic/chat/transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ def get_supported_openai_params(self, model: str):
"response_format",
"user",
"web_search_options",
"context_management",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not an openai param

]

if "claude-3-7-sonnet" in model or supports_reasoning(
Expand Down Expand Up @@ -661,6 +662,13 @@ def update_headers_with_optional_anthropic_beta(
headers[
"anthropic-beta"
] = ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value
if optional_params.get("context_management") is not None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this a helper method

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

existing_beta = headers.get("anthropic-beta")
beta_value = ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value
if existing_beta is None:
headers["anthropic-beta"] = beta_value
elif beta_value not in [beta.strip() for beta in existing_beta.split(",")]:
headers["anthropic-beta"] = f"{existing_beta}, {beta_value}"
return headers

def transform_request(
Expand Down Expand Up @@ -972,13 +980,21 @@ def transform_parsed_response(
):
text_content = prefix_prompt + text_content

context_management: Optional[Dict] = completion_response.get(
"context_management"
)

provider_specific_fields: Dict[str, Any] = {
"citations": citations,
"thinking_blocks": thinking_blocks,
}
if context_management is not None:
provider_specific_fields["context_management"] = context_management

_message = litellm.Message(
tool_calls=tool_calls,
content=text_content or None,
provider_specific_fields={
"citations": citations,
"thinking_blocks": thinking_blocks,
},
provider_specific_fields=provider_specific_fields,
thinking_blocks=thinking_blocks,
reasoning_content=reasoning_content,
)
Expand Down Expand Up @@ -1011,6 +1027,16 @@ def transform_parsed_response(
model_response.created = int(time.time())
model_response.model = completion_response["model"]

context_management_response = completion_response.get("context_management")
if context_management_response is not None:
_hidden_params["context_management"] = context_management_response
try:
model_response.__dict__["context_management"] = (
context_management_response
)
except Exception:
pass

model_response._hidden_params = _hidden_params

return model_response
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from litellm.llms.base_llm.anthropic_messages.transformation import (
BaseAnthropicMessagesConfig,
)
from litellm.types.llms.anthropic import AnthropicMessagesRequest
from litellm.types.llms.anthropic import (
ANTHROPIC_BETA_HEADER_VALUES,
AnthropicMessagesRequest,
)
from litellm.types.llms.anthropic_messages.anthropic_response import (
AnthropicMessagesResponse,
)
Expand All @@ -32,6 +35,7 @@ def get_supported_anthropic_messages_params(self, model: str) -> list:
"tools",
"tool_choice",
"thinking",
"context_management",
# TODO: Add Anthropic `metadata` support
# "metadata",
]
Expand Down Expand Up @@ -71,6 +75,11 @@ def validate_anthropic_messages_environment(
if "content-type" not in headers:
headers["content-type"] = "application/json"

headers = self._update_headers_with_optional_anthropic_beta(
headers=headers,
context_management=optional_params.get("context_management"),
)

return headers, api_base

def transform_anthropic_messages_request(
Expand Down Expand Up @@ -142,3 +151,18 @@ def get_async_streaming_response_iterator(
request_body=request_body,
litellm_logging_obj=litellm_logging_obj,
)

@staticmethod
def _update_headers_with_optional_anthropic_beta(
headers: dict, context_management: Optional[Dict]
) -> dict:
if context_management is None:
return headers

existing_beta = headers.get("anthropic-beta")
beta_value = ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_MANAGEMENT_2025_06_27.value
if existing_beta is None:
headers["anthropic-beta"] = beta_value
elif beta_value not in [beta.strip() for beta in existing_beta.split(",")]:
headers["anthropic-beta"] = f"{existing_beta}, {beta_value}"
return headers
3 changes: 2 additions & 1 deletion litellm/types/llms/anthropic.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from enum import Enum
from typing import Dict, Iterable, List, Optional, Union
from typing import Any, Dict, Iterable, List, Optional, Union

from pydantic import BaseModel
from typing_extensions import Literal, Required, TypedDict
Expand Down Expand Up @@ -276,6 +276,7 @@ class AnthropicMessagesRequestOptionalParams(TypedDict, total=False):
top_k: Optional[int]
top_p: Optional[float]
mcp_servers: Optional[List[AnthropicMcpServerTool]]
context_management: Optional[Dict[str, Any]]


class AnthropicMessagesRequest(AnthropicMessagesRequestOptionalParams, total=False):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from unittest.mock import MagicMock, patch

from litellm.llms.anthropic.chat.transformation import AnthropicConfig
from litellm.llms.anthropic.experimental_pass_through.messages.transformation import (
AnthropicMessagesConfig,
)
from litellm.types.utils import PromptTokensDetailsWrapper, ServerToolUse


Expand Down Expand Up @@ -389,3 +392,125 @@ def test_anthropic_memory_tool_auto_adds_beta_header():

assert "anthropic-beta" in headers
assert headers["anthropic-beta"] == "context-management-2025-06-27"


def _sample_context_management_payload():
return {
"edits": [
{
"type": "clear_tool_uses_20250919",
"trigger": {"type": "input_tokens", "value": 30000},
"keep": {"type": "tool_uses", "value": 3},
"clear_at_least": {"type": "input_tokens", "value": 5000},
"exclude_tools": ["web_search"],
"clear_tool_inputs": False,
}
]
}


def test_anthropic_messages_validate_adds_beta_header():
config = AnthropicMessagesConfig()
headers, _ = config.validate_anthropic_messages_environment(
headers={},
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": [{"type": "text", "text": "Hi"}]}],
optional_params={"context_management": _sample_context_management_payload()},
litellm_params={},
)
assert headers["anthropic-beta"] == "context-management-2025-06-27"


def test_anthropic_messages_transform_includes_context_management():
config = AnthropicMessagesConfig()
payload = _sample_context_management_payload()
headers = {
"x-api-key": "test",
"anthropic-version": "2023-06-01",
"content-type": "application/json",
}
result = config.transform_anthropic_messages_request(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": [{"type": "text", "text": "Hi"}]}],
anthropic_messages_optional_request_params={
"max_tokens": 512,
"context_management": payload,
},
litellm_params={},
headers=headers,
)
assert result["context_management"] == payload


def test_anthropic_chat_headers_add_context_management_beta():
config = AnthropicConfig()
headers = config.update_headers_with_optional_anthropic_beta(
headers={},
optional_params={"context_management": _sample_context_management_payload()},
)
assert headers["anthropic-beta"] == "context-management-2025-06-27"


def test_anthropic_chat_transform_request_includes_context_management():
config = AnthropicConfig()
headers = {}
result = config.transform_request(
model="claude-sonnet-4-20250514",
messages=[{"role": "user", "content": "Hello"}],
optional_params={
"context_management": _sample_context_management_payload(),
"max_tokens": 256,
},
litellm_params={},
headers=headers,
)
assert result["context_management"] == _sample_context_management_payload()


def test_transform_parsed_response_includes_context_management_metadata():
import httpx
from litellm.types.utils import ModelResponse

config = AnthropicConfig()
context_management_payload = {
"applied_edits": [
{
"type": "clear_tool_uses_20250919",
"cleared_tool_uses": 2,
"cleared_input_tokens": 5000,
}
]
}
completion_response = {
"id": "msg_context_management_test",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-20250514",
"content": [{"type": "text", "text": "Done."}],
"stop_reason": "end_turn",
"stop_sequence": None,
"usage": {
"input_tokens": 10,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 0,
"output_tokens": 5,
},
"context_management": context_management_payload,
}
raw_response = httpx.Response(
status_code=200,
headers={},
)
model_response = ModelResponse()

result = config.transform_parsed_response(
completion_response=completion_response,
raw_response=raw_response,
model_response=model_response,
json_mode=False,
prefix_prompt=None,
)

assert result.__dict__.get("context_management") == context_management_payload
provider_fields = result.choices[0].message.provider_specific_fields
assert provider_fields and provider_fields["context_management"] == context_management_payload
Loading