mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(anthropic): strip beta-gated system fields before an older model's system turn becomes a user turn
Models without supports_mid_conversation_system send no mid-conversation beta, so tool_addition and tool_removal blocks are dropped (and logged) before the turn is converted instead of riding along as user content. The Invoke output_config regression test reads the real cost map instead of patching _bedrock_model_supports Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
a4950a1fcf
commit
409735806a
2 changed files with 73 additions and 18 deletions
|
|
@ -7,8 +7,10 @@ from pydantic import TypeAdapter
|
|||
import litellm
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.anthropic_beta_gated_fields import (
|
||||
StrippedMessages,
|
||||
log_beta_gated_strips,
|
||||
strip_beta_gated_native_request,
|
||||
strip_beta_gated_system_messages,
|
||||
unstripped_native_request,
|
||||
)
|
||||
from litellm.anthropic_beta_headers_manager import get_unsupported_headers, is_beta_header_supported_by_bundled_config
|
||||
|
|
@ -53,6 +55,8 @@ DEFAULT_ANTHROPIC_API_VERSION: Final = "2023-06-01"
|
|||
|
||||
_CALLER_CREDENTIAL_HEADERS: Final = frozenset({"x-api-key", "authorization"})
|
||||
|
||||
_MESSAGE_MAPPINGS: Final = TypeAdapter(tuple[Mapping[str, object], ...])
|
||||
|
||||
|
||||
def _carries_caller_credential(headers: Mapping[str, str]) -> bool:
|
||||
"""Whether the caller sent their own Anthropic credential, in which case this passthrough
|
||||
|
|
@ -265,14 +269,22 @@ class AnthropicMessagesConfig(BaseAnthropicMessagesConfig):
|
|||
len(messages),
|
||||
)
|
||||
hoisted: Final = messages[:leading_count]
|
||||
keeps_system_turns: Final = supports_factory(
|
||||
model=model,
|
||||
custom_llm_provider=self.custom_llm_provider,
|
||||
key="supports_mid_conversation_system",
|
||||
)
|
||||
trailing_messages: Final = _MESSAGE_MAPPINGS.validate_python(messages[leading_count:]) # pyright: ignore[reportUnknownArgumentType] # messages comes from the bare request dict
|
||||
gated: Final = (
|
||||
StrippedMessages(messages=trailing_messages, stripped=())
|
||||
if keeps_system_turns
|
||||
else strip_beta_gated_system_messages(trailing_messages, frozenset())
|
||||
)
|
||||
log_beta_gated_strips(gated.stripped, model=model, provider=self.custom_llm_provider or "anthropic")
|
||||
remaining: Final = (
|
||||
messages[leading_count:]
|
||||
if supports_factory(
|
||||
model=model,
|
||||
custom_llm_provider=self.custom_llm_provider,
|
||||
key="supports_mid_conversation_system",
|
||||
)
|
||||
else list(convert_mid_conversation_system_turns(messages[leading_count:]))
|
||||
if keeps_system_turns
|
||||
else list(convert_mid_conversation_system_turns(_MESSAGE_MAPPINGS.validate_python(gated.messages)))
|
||||
)
|
||||
if hoisted or remaining != messages:
|
||||
anthropic_messages_request["messages"] = remaining
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import httpx
|
|||
import pytest
|
||||
import requests_mock as requests_mock_lib
|
||||
import respx
|
||||
from pydantic import TypeAdapter
|
||||
from pydantic import BaseModel, ConfigDict, TypeAdapter
|
||||
|
||||
import litellm
|
||||
from litellm.constants import (
|
||||
|
|
@ -4200,10 +4200,9 @@ def test_bedrock_messages_without_guardrail_config_sends_no_guardrail_headers()
|
|||
assert not any(name.startswith("X-Amzn-Bedrock-Guardrail") for name in headers)
|
||||
|
||||
|
||||
def test_bedrock_messages_keeps_mid_conversation_output_config_its_auto_added_beta_gates():
|
||||
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
|
||||
def test_bedrock_messages_keeps_mid_conversation_output_config_its_auto_added_beta_gates() -> None:
|
||||
"""Invoke learns its body betas after the shared transform, so the gated field it supports must survive."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
messages: Final = [
|
||||
|
|
@ -4212,14 +4211,58 @@ def test_bedrock_messages_keeps_mid_conversation_output_config_its_auto_added_be
|
|||
{"role": "system", "content": "Answer briefly", "output_config": {"effort": "high"}},
|
||||
{"role": "user", "content": [{"type": "text", "text": "Go on"}]},
|
||||
]
|
||||
with patch("litellm.llms.bedrock.common_utils._bedrock_model_supports", return_value=True):
|
||||
result: Final = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request( # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType] # the base signature returns a bare dict
|
||||
model="anthropic.claude-opus-4-8-v1:0",
|
||||
messages=messages,
|
||||
anthropic_messages_optional_request_params={"max_tokens": 4096},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
result: Final = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request(
|
||||
model="anthropic.claude-opus-4-8-v1:0",
|
||||
messages=messages,
|
||||
anthropic_messages_optional_request_params={"max_tokens": 4096},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "mid-conversation-output-config-2026-07-01" in result["anthropic_beta"]
|
||||
assert {"role": "system", "content": "Answer briefly", "output_config": {"effort": "high"}} in result["messages"]
|
||||
|
||||
|
||||
class _SentMessage(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
role: str
|
||||
content: list[dict[str, object]]
|
||||
|
||||
|
||||
_SENT_MESSAGES: Final = TypeAdapter(list[_SentMessage])
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("local_model_cost_map", "local_beta_headers_config")
|
||||
def test_bedrock_messages_drops_tool_changes_from_a_system_turn_an_older_model_gets_as_a_user_turn() -> None:
|
||||
"""A model without mid-conversation system support sends no tool-changes beta, so the converted turn
|
||||
must not carry the blocks that beta gates."""
|
||||
from litellm.types.router import GenericLiteLLMParams
|
||||
|
||||
messages: Final = [
|
||||
{"role": "user", "content": [{"type": "text", "text": "Hello"}]},
|
||||
{"role": "assistant", "content": [{"type": "text", "text": "Hi"}]},
|
||||
{
|
||||
"role": "system",
|
||||
"content": [
|
||||
{"type": "text", "text": "A weather tool is now available"},
|
||||
{"type": "tool_addition", "tool": {"type": "tool_reference", "name": "get_weather"}},
|
||||
],
|
||||
},
|
||||
{"role": "user", "content": [{"type": "text", "text": "Go on"}]},
|
||||
]
|
||||
result: Final = AmazonAnthropicClaudeMessagesConfig().transform_anthropic_messages_request(
|
||||
model="anthropic.claude-3-7-sonnet-20250219-v1:0",
|
||||
messages=messages,
|
||||
anthropic_messages_optional_request_params={"max_tokens": 4096},
|
||||
litellm_params=GenericLiteLLMParams(),
|
||||
headers={},
|
||||
)
|
||||
|
||||
sent: Final = _SENT_MESSAGES.validate_python(result["messages"])
|
||||
blocks: Final = [
|
||||
block for message in sent for block in message.content
|
||||
] # comprehension-ok: flattening the sent content blocks
|
||||
assert "mid-conversation-tool-changes-2026-07-01" not in result.get("anthropic_beta", [])
|
||||
assert all(block["type"] != "tool_addition" for block in blocks)
|
||||
assert {"type": "text", "text": "A weather tool is now available"} in blocks
|
||||
assert all(message.role != "system" for message in sent)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue