This commit is contained in:
HX 2026-09-28 12:54:14 -04:00 • committed by GitHub
commit c50582dfc4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 190 additions and 5 deletions

View file

@ -100,9 +100,11 @@ from ..common_utils import (
AnthropicError,
AnthropicModelInfo,
eager_input_streaming_flag,
has_context_1m_suffix,
process_anthropic_headers,
requires_native_compaction_beta,
strip_advisor_blocks_from_messages,
strip_context_1m_suffix,
)
if TYPE_CHECKING:
@ -1904,6 +1906,15 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
"""
Translate messages to anthropic format.
"""
if has_context_1m_suffix(model):
return AnthropicConfig.transform_request(
self,
model=strip_context_1m_suffix(model),
messages=messages,
optional_params=optional_params,
litellm_params=litellm_params,
headers=headers,
)
## VALIDATE REQUEST
"""Anthropic requires ``tools`` when messages include tool blocks; LiteLLM injects a dummy tool if omitted (no ``modify_params`` needed)."""
from litellm.litellm_core_utils.prompt_templates.factory import (

View file

@ -30,6 +30,7 @@ from litellm.litellm_core_utils.prompt_templates.factory import (
from litellm.llms.base_llm.base_utils import BaseLLMModelInfo, BaseTokenCounter
from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.types.llms.anthropic import (
ANTHROPIC_BETA_HEADER_VALUES,
ANTHROPIC_HOSTED_TOOLS,
ANTHROPIC_OAUTH_BETA_HEADER,
ANTHROPIC_OAUTH_TOKEN_PREFIX,
@ -71,6 +72,17 @@ _BEDROCK_VERSION_SUFFIX_RE: Final = re.compile(r"-v\d+(?::\d+)?$")
_INFERENCE_PROFILE_MINOR_RE: Final = re.compile(r":\d+$")
_DATED_RELEASE_SUFFIX_RE: Final = re.compile(r"-\d{8}$")
_DOTTED_VERSION_RE: Final = re.compile(r"(\d)\.(\d)")
_CONTEXT_1M_SUFFIX: Final = re.compile(r"\[1m\]$", flags=re.IGNORECASE)
def has_context_1m_suffix(model: str) -> bool:
return _CONTEXT_1M_SUFFIX.search(model) is not None
def strip_context_1m_suffix(model: str) -> str:
return _CONTEXT_1M_SUFFIX.sub("", model)
_CLAUDE_CODE_BILLING_HEADER_PREFIX: Final = "x-anthropic-billing-header:"
_CLAUDE_CODE_OBJECT_MAPPING_ADAPTER: Final = TypeAdapter(dict[object, object])
_CLAUDE_CODE_OBJECT_LIST_ADAPTER: Final = TypeAdapter(list[object])
@ -915,8 +927,11 @@ class AnthropicModelInfo(BaseLLMModelInfo):
container_with_skills_used: bool = False,
api_base: str | None = None,
use_bearer_for_custom_base: bool = False,
context_1m_requested: bool = False,
) -> dict:
betas: Final = set()
betas: Final[set[str]] = set()
if context_1m_requested:
betas.add(ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_1M_2025_08_07.value)
# Anthropic no longer requires the prompt-caching beta header
# Prompt caching now works automatically when cache_control is used in messages
# Reference: https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching
@ -972,8 +987,6 @@ class AnthropicModelInfo(BaseLLMModelInfo):
if is_vertex_request is True:
# Vertex AI requires web search beta header for web search to work
if web_search_tool_used:
from litellm.types.llms.anthropic import ANTHROPIC_BETA_HEADER_VALUES
headers["anthropic-beta"] = ANTHROPIC_BETA_HEADER_VALUES.WEB_SEARCH_2025_03_05.value
elif len(betas) > 0:
headers["anthropic-beta"] = ",".join(betas)
@ -1027,6 +1040,7 @@ class AnthropicModelInfo(BaseLLMModelInfo):
)
anthropic_headers: Final = self.get_anthropic_headers(
computer_tool_used=computer_tool_used,
context_1m_requested=has_context_1m_suffix(model),
prompt_caching_set=prompt_caching_set,
pdf_used=pdf_used,
api_key=api_key,

View file

@ -23,9 +23,11 @@ from litellm.types.router import GenericLiteLLMParams
from ...common_utils import (
AnthropicError,
AnthropicModelInfo,
has_context_1m_suffix,
optionally_handle_anthropic_oauth,
requires_native_compaction_beta,
strip_advisor_blocks_from_messages,
strip_context_1m_suffix,
strip_encrypted_reasoning_blocks_from_anthropic_messages,
)
from .mid_conversation_system import (
@ -277,6 +279,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
headers=headers,
optional_params=optional_params,
messages=messages,
model=model,
)
return headers, api_base
@ -562,7 +565,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
anthropic_messages_request: Final[AnthropicMessagesRequest] = AnthropicMessagesRequest(
messages=strip_encrypted_reasoning_blocks_from_anthropic_messages(messages),
max_tokens=max_tokens,
model=model,
model=strip_context_1m_suffix(model),
**anthropic_messages_optional_request_params,
)
return dict(anthropic_messages_request)
@ -611,6 +614,7 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
optional_params: dict,
custom_llm_provider: str = "anthropic",
messages: Sequence[object] = (),
model: str = "",
) -> dict:
"""
Auto-inject anthropic-beta headers based on features used.
@ -621,14 +625,16 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
- output_format: adds 'structured-outputs-2025-11-13'
- speed: adds 'fast-mode-2026-02-01'
- a message carrying output_config: adds 'per-turn-control-2026-07-01'
- [1m] model suffix: adds 'context-1m-2025-08-07'
Args:
headers: Request headers dict
optional_params: Optional parameters including tools, context_management, output_format, speed
custom_llm_provider: Provider name for looking up correct tool search header
messages: Request messages, scanned for per-message output_config
model: Requested model name, checked for the [1m] suffix
"""
beta_values: Final[set] = set()
beta_values: Final[set[str]] = set()
existing_beta: Final = tuple(
piece.strip()
@ -642,6 +648,9 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
if requires_native_compaction_beta(custom_llm_provider, optional_params, messages):
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.COMPACT_2026_09_04.value)
if has_context_1m_suffix(model):
beta_values.add(ANTHROPIC_BETA_HEADER_VALUES.CONTEXT_1M_2025_08_07.value)
# Check for context management
context_management_param: Final = optional_params.get("context_management")
if context_management_param is not None:

View file

@ -754,6 +754,7 @@ class ANTHROPIC_BETA_HEADER_VALUES(str, Enum):
WEB_FETCH_2025_09_10 = "web-fetch-2025-09-10"
WEB_SEARCH_2025_03_05 = "web-search-2025-03-05"
CONTEXT_MANAGEMENT_2025_06_27 = "context-management-2025-06-27"
CONTEXT_1M_2025_08_07 = "context-1m-2025-08-07"
COMPACT_2026_01_12 = "compact-2026-01-12"
COMPACT_2026_09_04 = "compact-2026-09-04"
STRUCTURED_OUTPUT_2025_09_25 = "structured-outputs-2025-11-13"

View file

@ -6798,3 +6798,58 @@ def test_chat_dummy_tool_result_for_an_orphaned_tool_call_replays_a_byte_identic
_assert_prefix_stable(requests)
assert [m["role"] for m in requests[0]["messages"]] == ["user", "assistant", "user"]
assert requests[0]["messages"][2]["content"][0]["type"] == "tool_result"
@pytest.mark.parametrize("optional_params", [{}, {"thinking": {"type": "enabled", "budget_tokens": 2048}}])
def test_transform_request_treats_context_1m_model_as_its_base_model(optional_params: dict[str, object]) -> None:
def transform(model: str) -> dict[str, object]:
return AnthropicConfig().transform_request(
model=model,
messages=[{"role": "user", "content": "hi"}],
optional_params=copy.deepcopy(optional_params),
litellm_params={},
headers={},
)
assert transform("claude-sonnet-4-6[1m]") == transform("claude-sonnet-4-6")
@pytest.mark.parametrize(
"model,expected_betas",
[
("anthropic/claude-sonnet-4-6[1m]", {"context-1m-2025-08-07", "interleaved-thinking-2025-05-14"}),
("anthropic/claude-sonnet-4-6", {"interleaved-thinking-2025-05-14"}),
],
)
async def test_acompletion_context_1m_suffix_sends_base_model_with_beta(
model: str,
expected_betas: set[str],
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True")
monkeypatch.setattr(litellm.anthropic_beta_headers_manager, "_BETA_HEADERS_CONFIG", None)
route: Final = respx_mock.post("https://api.anthropic.com/v1/messages").respond(
200,
json={
"id": "msg_context_1m",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-6",
"content": [{"type": "text", "text": "hi"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1},
},
)
await litellm.acompletion(
model=model,
messages=[{"role": "user", "content": "hi"}],
api_key="test",
extra_headers={"anthropic-beta": "interleaved-thinking-2025-05-14"},
)
request: Final = route.calls.last.request
assert json.loads(request.content)["model"] == "claude-sonnet-4-6"
assert {beta.strip() for beta in request.headers["anthropic-beta"].split(",")} == expected_betas

View file

@ -0,0 +1,57 @@
import json
from typing import Final
import pytest
import respx
import litellm
from litellm import Router
@pytest.mark.parametrize(
"deployment_model,expected_betas",
[
("anthropic/claude-sonnet-4-6[1m]", {"context-1m-2025-08-07", "interleaved-thinking-2025-05-14"}),
("anthropic/claude-sonnet-4-6", {"interleaved-thinking-2025-05-14"}),
],
)
async def test_router_messages_context_1m_deployment_sends_base_model_with_beta(
deployment_model: str,
expected_betas: set[str],
monkeypatch: pytest.MonkeyPatch,
respx_mock: respx.MockRouter,
) -> None:
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
monkeypatch.setenv("LITELLM_LOCAL_ANTHROPIC_BETA_HEADERS", "True")
monkeypatch.setattr(litellm.anthropic_beta_headers_manager, "_BETA_HEADERS_CONFIG", None)
route: Final = respx_mock.post("https://api.anthropic.com/v1/messages").respond(
200,
json={
"id": "msg_context_1m",
"type": "message",
"role": "assistant",
"model": "claude-sonnet-4-6",
"content": [{"type": "text", "text": "hi"}],
"stop_reason": "end_turn",
"usage": {"input_tokens": 1, "output_tokens": 1},
},
)
router: Final = Router(
model_list=[
{
"model_name": "claude-sonnet-4-6[1m]",
"litellm_params": {"model": deployment_model, "api_key": "test"},
}
]
)
await router.aanthropic_messages(
model="claude-sonnet-4-6[1m]",
messages=[{"role": "user", "content": "hi"}],
max_tokens=16,
extra_headers={"anthropic-beta": "interleaved-thinking-2025-05-14"},
)
request: Final = route.calls.last.request
assert json.loads(request.content)["model"] == "claude-sonnet-4-6"
assert {beta.strip() for beta in request.headers["anthropic-beta"].split(",")} == expected_betas

View file

@ -2345,3 +2345,41 @@ class TestMalformedContentListItems:
api_key=FAKE_REGULAR_KEY,
max_tokens=5,
)
@pytest.mark.parametrize(
"model,base_model",
[
("claude-sonnet-4-6[1m]", "claude-sonnet-4-6"),
("claude-sonnet-4-6[1M]", "claude-sonnet-4-6"),
("claude-sonnet-4-6", "claude-sonnet-4-6"),
("claude-[1m]-sonnet-4-6", "claude-[1m]-sonnet-4-6"),
("claude-sonnet-4-6[1m][1m]", "claude-sonnet-4-6[1m]"),
("claude-sonnet-4-6[200k]", "claude-sonnet-4-6[200k]"),
],
)
def test_context_1m_suffix_is_only_a_trailing_marker(model: str, base_model: str) -> None:
from litellm.llms.anthropic.common_utils import has_context_1m_suffix, strip_context_1m_suffix
assert strip_context_1m_suffix(model) == base_model
assert has_context_1m_suffix(model) is (model != base_model)
@pytest.mark.parametrize("model,expects_context_1m", [("claude-sonnet-4-6[1m]", True), ("claude-sonnet-4-6", False)])
def test_validate_environment_adds_context_1m_beta_only_for_suffixed_model(
model: str, expects_context_1m: bool
) -> None:
from litellm.llms.anthropic.common_utils import AnthropicModelInfo
headers: Final = AnthropicModelInfo().validate_environment(
headers={"anthropic-beta": "interleaved-thinking-2025-05-14"},
model=model,
messages=[{"role": "user", "content": "hi"}],
optional_params={},
litellm_params={},
api_key=FAKE_REGULAR_KEY,
)
betas: Final = {beta.strip() for beta in headers["anthropic-beta"].split(",")}
assert ("context-1m-2025-08-07" in betas) is expects_context_1m
assert "interleaved-thinking-2025-05-14" in betas