mirror of
https://github.com/tiennm99/litellm.git
synced 2026-08-09 18:25:53 +00:00
Merge pull request #26248 from BerriAI/litellm_anthropic_messages_call_type_fix
fix(proxy): preserve anthropic_messages call type for /v1/messages logging
This commit is contained in:
@@ -1534,12 +1534,16 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig):
|
||||
f"Invalid effort value: {effort}. Must be one of: "
|
||||
f"'high', 'medium', 'low', 'xhigh', 'max'"
|
||||
)
|
||||
# ``max`` is Claude Opus 4.6 only (not Sonnet 4.6, not Opus 4.5/4.7).
|
||||
# Keep this hardcoded so the error message is specific and stable.
|
||||
if effort == "max" and not self._is_opus_4_6_model(model):
|
||||
# ``max`` is for Opus 4.6+ output effort (not Sonnet 4.6, not Opus 4.5).
|
||||
# Accept known Opus 4.6/4.7 id patterns and/or ``supports_max_reasoning_effort``
|
||||
# in the model map (same pattern as ``xhigh`` below).
|
||||
if effort == "max" and not (
|
||||
self._is_opus_4_6_model(model)
|
||||
or self._is_opus_4_7_model(model)
|
||||
or self._supports_effort_level(model, "max")
|
||||
):
|
||||
raise ValueError(
|
||||
f"effort='max' is only supported by Claude Opus 4.6. "
|
||||
f"Got model: {model}"
|
||||
f"effort='max' is not supported by this model. Got model: {model}"
|
||||
)
|
||||
# ``xhigh`` is data-driven via ``supports_xhigh_reasoning_effort`` so
|
||||
# enabling it for a new model is a pure model-map change.
|
||||
|
||||
@@ -235,7 +235,7 @@ class LiteLLMMessagesToCompletionTransformationHandler:
|
||||
):
|
||||
from litellm.types.utils import CallTypes
|
||||
|
||||
setattr(value, "call_type", CallTypes.completion.value)
|
||||
setattr(value, "call_type", CallTypes.anthropic_messages.value)
|
||||
setattr(
|
||||
value, "stream_options", completion_kwargs.get("stream_options")
|
||||
)
|
||||
|
||||
@@ -105,7 +105,7 @@ def _build_responses_kwargs(
|
||||
# Reclassify as acompletion so the success handler doesn't try to
|
||||
# validate the Responses API event as an AnthropicResponse.
|
||||
# (Mirrors the pattern used in LiteLLMMessagesToCompletionTransformationHandler.)
|
||||
setattr(value, "call_type", CallTypes.acompletion.value)
|
||||
setattr(value, "call_type", CallTypes.anthropic_messages.value)
|
||||
responses_kwargs[key] = value
|
||||
elif key not in excluded and key not in responses_kwargs and value is not None:
|
||||
responses_kwargs[key] = value
|
||||
|
||||
@@ -9262,6 +9262,7 @@
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_max_reasoning_effort": true,
|
||||
"tool_use_system_prompt_tokens": 346,
|
||||
"provider_specific_entry": {
|
||||
"us": 1.1,
|
||||
@@ -9296,6 +9297,7 @@
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_max_reasoning_effort": true,
|
||||
"tool_use_system_prompt_tokens": 346,
|
||||
"provider_specific_entry": {
|
||||
"us": 1.1,
|
||||
|
||||
@@ -9276,6 +9276,7 @@
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_max_reasoning_effort": true,
|
||||
"tool_use_system_prompt_tokens": 346,
|
||||
"provider_specific_entry": {
|
||||
"us": 1.1,
|
||||
@@ -9310,6 +9311,7 @@
|
||||
"supports_tool_choice": true,
|
||||
"supports_vision": true,
|
||||
"supports_xhigh_reasoning_effort": true,
|
||||
"supports_max_reasoning_effort": true,
|
||||
"tool_use_system_prompt_tokens": 346,
|
||||
"provider_specific_entry": {
|
||||
"us": 1.1,
|
||||
|
||||
@@ -1654,7 +1654,7 @@ def test_max_effort_rejected_for_opus_45():
|
||||
messages = [{"role": "user", "content": "Test"}]
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="effort='max' is only supported by Claude Opus 4.6"
|
||||
ValueError, match="effort='max' is not supported by this model"
|
||||
):
|
||||
optional_params = {"output_config": {"effort": "max"}}
|
||||
config.transform_request(
|
||||
@@ -2213,12 +2213,12 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models():
|
||||
|
||||
|
||||
def test_max_effort_rejected_for_sonnet_46():
|
||||
"""Test that effort='max' is rejected for Sonnet 4.6 (only Opus 4.6 supports max)."""
|
||||
"""Test that effort='max' is rejected for Sonnet 4.6 (Opus-only effort level)."""
|
||||
config = AnthropicConfig()
|
||||
messages = [{"role": "user", "content": "Test"}]
|
||||
|
||||
with pytest.raises(
|
||||
ValueError, match="effort='max' is only supported by Claude Opus 4.6"
|
||||
ValueError, match="effort='max' is not supported by this model"
|
||||
):
|
||||
config.transform_request(
|
||||
model="claude-sonnet-4-6-20260219",
|
||||
@@ -2245,6 +2245,22 @@ def test_max_effort_accepted_for_opus_46():
|
||||
assert result["output_config"]["effort"] == "max"
|
||||
|
||||
|
||||
def test_max_effort_accepted_for_opus_47():
|
||||
"""Test that effort='max' works for Opus 4.7."""
|
||||
config = AnthropicConfig()
|
||||
messages = [{"role": "user", "content": "Test"}]
|
||||
|
||||
result = config.transform_request(
|
||||
model="claude-opus-4-7",
|
||||
messages=messages,
|
||||
optional_params={"output_config": {"effort": "max"}},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert result["output_config"]["effort"] == "max"
|
||||
|
||||
|
||||
def test_effort_beta_header_not_injected_for_46_models():
|
||||
"""
|
||||
Test that is_effort_used returns False for Claude 4.6 models.
|
||||
|
||||
@@ -1749,6 +1749,95 @@ async def test_add_litellm_metadata_from_request_headers():
|
||||
litellm.callbacks = original_callbacks
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_anthropic_messages_standard_logging_object_matches_fixture():
|
||||
"""
|
||||
Regression: /v1/messages calls routed to non-Anthropic providers should keep
|
||||
call_type=anthropic_messages in standard logging payloads.
|
||||
"""
|
||||
litellm._turn_on_debug()
|
||||
test_logger = TestCustomLogger()
|
||||
original_callbacks = litellm.callbacks
|
||||
litellm.callbacks = [test_logger]
|
||||
|
||||
try:
|
||||
data = {
|
||||
"model": "gemini/gemini-2.5-flash",
|
||||
"messages": [{"role": "user", "content": "Hi."}],
|
||||
"stream": False,
|
||||
"mock_response": "Hello! How can I help you today?",
|
||||
"api_key": "fake-key",
|
||||
"max_tokens": 4096,
|
||||
}
|
||||
|
||||
mock_request = MagicMock(spec=Request)
|
||||
mock_request.headers = {"user-agent": "PostmanRuntime/7.53.0"}
|
||||
mock_request.url.path = "/v1/messages"
|
||||
mock_request.url = MagicMock()
|
||||
mock_request.url.__str__.return_value = "http://localhost/v1/messages"
|
||||
mock_request.method = "POST"
|
||||
mock_request.query_params = {}
|
||||
mock_request.client = MagicMock()
|
||||
mock_request.client.host = "127.0.0.1"
|
||||
|
||||
mock_fastapi_response = MagicMock(spec=Response)
|
||||
mock_user_api_key_dict = UserAPIKeyAuth(
|
||||
api_key="test-key", user_id="default_user_id"
|
||||
)
|
||||
|
||||
mock_proxy_logging_obj = MagicMock(spec=ProxyLogging)
|
||||
|
||||
async def mock_during_call_hook(*args, **kwargs):
|
||||
return None
|
||||
|
||||
async def mock_pre_call_hook(*args, **kwargs):
|
||||
return data
|
||||
|
||||
async def mock_post_call_success_hook(*args, **kwargs):
|
||||
return kwargs.get("response", args[2] if len(args) > 2 else None)
|
||||
|
||||
mock_proxy_logging_obj.during_call_hook = mock_during_call_hook
|
||||
mock_proxy_logging_obj.pre_call_hook = mock_pre_call_hook
|
||||
mock_proxy_logging_obj.post_call_success_hook = mock_post_call_success_hook
|
||||
|
||||
processor = ProxyBaseLLMRequestProcessing(data=data)
|
||||
await processor.base_process_llm_request(
|
||||
request=mock_request,
|
||||
fastapi_response=mock_fastapi_response,
|
||||
user_api_key_dict=mock_user_api_key_dict,
|
||||
route_type="anthropic_messages",
|
||||
proxy_logging_obj=mock_proxy_logging_obj,
|
||||
general_settings={},
|
||||
proxy_config=MagicMock(),
|
||||
select_data_generator=None,
|
||||
llm_router=None,
|
||||
model="gemini/gemini-2.5-flash",
|
||||
is_streaming_request=False,
|
||||
)
|
||||
|
||||
await asyncio.sleep(3)
|
||||
|
||||
assert test_logger.standard_logging_object is not None
|
||||
actual = test_logger.standard_logging_object
|
||||
|
||||
expected = {
|
||||
"call_type": "anthropic_messages",
|
||||
"status": "success",
|
||||
"model": "gemini/gemini-2.5-flash",
|
||||
}
|
||||
|
||||
# Compare only stable fields from the saved proxy log snapshot.
|
||||
actual_projection = {
|
||||
"call_type": actual.get("call_type"),
|
||||
"status": actual.get("status"),
|
||||
"model": actual.get("model"),
|
||||
}
|
||||
assert actual_projection == expected
|
||||
assert actual.get("call_type") == "anthropic_messages"
|
||||
finally:
|
||||
litellm.callbacks = original_callbacks
|
||||
|
||||
|
||||
def test_add_litellm_metadata_from_request_headers_x_litellm_trace_id_sets_chain_id():
|
||||
"""x-litellm-trace-id sets both metadata and top-level litellm_session_id/litellm_trace_id for call chaining."""
|
||||
headers = {"x-litellm-trace-id": "foo"}
|
||||
|
||||
Reference in New Issue
Block a user