fix(proxy): run pre-call hooks for Anthropic messages passthrough

This commit is contained in:
JerryLee 2026-05-11 16:37:27 +10:00
parent aabc45ac53
commit d6abae7e6d
2 changed files with 130 additions and 48 deletions

View file

@ -56,16 +56,26 @@ base_llm_http_handler = BaseLLMHTTPHandler()
async def _execute_pre_request_hooks(
model: str,
messages: List[Dict],
max_tokens: int,
metadata: Optional[Dict],
stop_sequences: Optional[List[str]],
tools: Optional[List[Dict]],
stream: Optional[bool],
system: Optional[str],
temperature: Optional[float],
thinking: Optional[Dict],
tool_choice: Optional[Dict],
top_k: Optional[int],
top_p: Optional[float],
custom_llm_provider: Optional[str],
**kwargs,
) -> Dict:
"""
Execute pre-request hooks from CustomLogger callbacks.
Execute pre-call and pre-request hooks from CustomLogger callbacks.
Allows CustomLoggers to modify request parameters before the API call.
Used for WebSearch tool conversion, stream modification, etc.
Used for proxy guardrails, WebSearch tool conversion, stream
modification, etc.
Args:
model: Model name
@ -88,8 +98,19 @@ async def _execute_pre_request_hooks(
# Build complete request kwargs dict
request_kwargs = {
"model": model,
"messages": messages,
"max_tokens": max_tokens,
"metadata": metadata,
"stop_sequences": stop_sequences,
"tools": tools,
"stream": stream,
"system": system,
"temperature": temperature,
"thinking": thinking,
"tool_choice": tool_choice,
"top_k": top_k,
"top_p": top_p,
"litellm_params": {
"custom_llm_provider": custom_llm_provider,
},
@ -105,9 +126,31 @@ async def _execute_pre_request_hooks(
if not isinstance(callback, _CustomLogger):
continue
if (
"async_pre_call_hook" in vars(callback.__class__)
and callback.__class__.async_pre_call_hook != _CustomLogger.async_pre_call_hook
):
# Keep the experimental Messages path in parity with the proxy
# pre-call hook contract used by the standard /v1/messages path.
modified_kwargs = await callback.async_pre_call_hook(
user_api_key_dict=request_kwargs.get("user_api_key_dict"),
cache=request_kwargs.get("cache"),
data=request_kwargs,
call_type="anthropic_messages",
)
if isinstance(modified_kwargs, Exception):
raise modified_kwargs
if isinstance(modified_kwargs, str):
raise ValueError(modified_kwargs)
if modified_kwargs is not None:
request_kwargs = modified_kwargs
# Call the pre-request hook
modified_kwargs = await callback.async_pre_request_hook(
model, messages, request_kwargs
request_kwargs.get("model", model),
request_kwargs.get("messages", messages),
request_kwargs,
)
# If hook returned modified kwargs, use them
@ -190,30 +233,46 @@ async def anthropic_messages(
"""
Async: Make llm api request in Anthropic /messages API spec
"""
original_stream = stream or kwargs.get(
"_websearch_interception_converted_stream", False
)
original_stream = stream or kwargs.get("_websearch_interception_converted_stream", False)
# Execute pre-request hooks to allow CustomLoggers to modify request
request_kwargs = await _execute_pre_request_hooks(
model=model,
messages=messages,
max_tokens=max_tokens,
metadata=metadata,
stop_sequences=stop_sequences,
tools=tools,
stream=stream,
system=system,
temperature=temperature,
thinking=thinking,
tool_choice=tool_choice,
top_k=top_k,
top_p=top_p,
custom_llm_provider=custom_llm_provider,
**kwargs,
)
# Extract modified parameters
model = request_kwargs.pop("model", model)
messages = request_kwargs.pop("messages", messages)
max_tokens = request_kwargs.pop("max_tokens", max_tokens)
metadata = request_kwargs.pop("metadata", metadata)
stop_sequences = request_kwargs.pop("stop_sequences", stop_sequences)
tools = request_kwargs.pop("tools", tools)
stream = request_kwargs.pop("stream", stream)
system = request_kwargs.pop("system", system)
temperature = request_kwargs.pop("temperature", temperature)
thinking = request_kwargs.pop("thinking", thinking)
tool_choice = request_kwargs.pop("tool_choice", tool_choice)
top_k = request_kwargs.pop("top_k", top_k)
top_p = request_kwargs.pop("top_p", top_p)
# Propagate the provider derived inside pre-request hooks, if not already set.
# The litellm_params dict may have been overwritten by **kwargs in
# _execute_pre_request_hooks, so fall back to get_llm_provider() if needed.
if not custom_llm_provider:
custom_llm_provider = request_kwargs.get("litellm_params", {}).get(
"custom_llm_provider"
)
custom_llm_provider = request_kwargs.get("litellm_params", {}).get("custom_llm_provider")
if not custom_llm_provider:
try:
_, custom_llm_provider, _, _ = litellm.get_llm_provider(model=model)
@ -376,9 +435,7 @@ def anthropic_messages_handler(
# Check if stream was converted for WebSearch interception
# This is set in the async wrapper above when stream=True is converted to stream=False
if kwargs.get("_websearch_interception_converted_stream", False):
litellm_logging_obj.model_call_details[
"websearch_interception_converted_stream"
] = True
litellm_logging_obj.model_call_details["websearch_interception_converted_stream"] = True
if litellm_params.mock_response and isinstance(litellm_params.mock_response, str):
return mock_response(
@ -390,14 +447,10 @@ def anthropic_messages_handler(
anthropic_messages_provider_config: Optional[BaseAnthropicMessagesConfig] = None
if custom_llm_provider is not None and custom_llm_provider in [
provider.value for provider in LlmProviders
]:
anthropic_messages_provider_config = (
ProviderConfigManager.get_provider_anthropic_messages_config(
model=model,
provider=litellm.LlmProviders(custom_llm_provider),
)
if custom_llm_provider is not None and custom_llm_provider in [provider.value for provider in LlmProviders]:
anthropic_messages_provider_config = ProviderConfigManager.get_provider_anthropic_messages_config(
model=model,
provider=litellm.LlmProviders(custom_llm_provider),
)
if anthropic_messages_provider_config is None:
# Route to Responses API for OpenAI / Azure, chat/completions for everything else.
@ -423,14 +476,8 @@ def anthropic_messages_handler(
**kwargs,
)
if _should_route_to_responses_api(custom_llm_provider):
return LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler(
**_shared_kwargs
)
return (
LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler(
**_shared_kwargs
)
)
return LiteLLMMessagesToResponsesAPIHandler.anthropic_messages_handler(**_shared_kwargs)
return LiteLLMMessagesToCompletionTransformationHandler.anthropic_messages_handler(**_shared_kwargs)
if custom_llm_provider is None:
raise ValueError(
@ -439,16 +486,11 @@ def anthropic_messages_handler(
local_vars.update(kwargs)
anthropic_messages_optional_request_params = (
AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param(
params=local_vars
)
AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param(params=local_vars)
)
if is_reasoning_auto_summary_enabled():
thinking_param = anthropic_messages_optional_request_params.get("thinking")
if (
isinstance(thinking_param, dict)
and thinking_param.get("type") != "disabled"
):
if isinstance(thinking_param, dict) and thinking_param.get("type") != "disabled":
anthropic_messages_optional_request_params["thinking"] = {
**thinking_param,
"display": "summarized",
@ -458,9 +500,7 @@ def anthropic_messages_handler(
model=model,
messages=messages,
anthropic_messages_provider_config=anthropic_messages_provider_config,
anthropic_messages_optional_request_params=dict(
anthropic_messages_optional_request_params
),
anthropic_messages_optional_request_params=dict(anthropic_messages_optional_request_params),
_is_async=is_async,
client=client,
custom_llm_provider=custom_llm_provider,

View file

@ -50,15 +50,11 @@ def client_no_auth(fake_env_vars):
@mock_patch_anthropic_messages()
def test_anthropic_messages_runs_proxy_async_pre_call_hook(
mock_anthropic_messages, client_no_auth
):
def test_anthropic_messages_runs_proxy_async_pre_call_hook(mock_anthropic_messages, client_no_auth):
hook_calls = []
class AnthropicMessagesPreCallHook(CustomLogger):
async def async_pre_call_hook(
self, user_api_key_dict, cache, data, call_type, **kwargs
):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type, **kwargs):
hook_calls.append(call_type)
data["metadata"] = {**(data.get("metadata") or {}), "source": "unit-test"}
return data
@ -80,9 +76,55 @@ def test_anthropic_messages_runs_proxy_async_pre_call_hook(
assert response.json()["content"][0]["text"] == "Hello from LiteLLM"
assert hook_calls == ["anthropic_messages"]
mock_anthropic_messages.assert_called_once()
assert (
mock_anthropic_messages.call_args.kwargs["metadata"]["source"]
== "unit-test"
)
assert mock_anthropic_messages.call_args.kwargs["metadata"]["source"] == "unit-test"
finally:
litellm.callbacks = original_callbacks
@pytest.mark.asyncio
async def test_experimental_anthropic_messages_runs_proxy_async_pre_call_hook():
from litellm.llms.anthropic.experimental_pass_through.messages.handler import (
anthropic_messages,
)
hook_calls = []
class AnthropicMessagesPreCallHook(CustomLogger):
async def async_pre_call_hook(self, user_api_key_dict, cache, data, call_type, **kwargs):
hook_calls.append((call_type, data["model"]))
data["metadata"] = {
**(data.get("metadata") or {}),
"source": "experimental-unit-test",
}
data["temperature"] = 0.2
return data
original_callbacks = litellm.callbacks
original_flag = litellm.use_chat_completions_url_for_anthropic_messages
litellm.callbacks = [AnthropicMessagesPreCallHook()]
litellm.use_chat_completions_url_for_anthropic_messages = True
try:
with mock.patch(
"litellm.llms.anthropic.experimental_pass_through.messages.handler.anthropic_messages_handler",
return_value=EXAMPLE_ANTHROPIC_MESSAGES_RESULT,
) as mock_handler:
response = await anthropic_messages(
model="openai/gpt-4o-mini",
max_tokens=100,
messages=[{"role": "user", "content": "hi"}],
metadata={"existing": "keep"},
custom_llm_provider="openai",
)
assert response == EXAMPLE_ANTHROPIC_MESSAGES_RESULT
assert hook_calls == [("anthropic_messages", "openai/gpt-4o-mini")]
mock_handler.assert_called_once()
assert mock_handler.call_args.kwargs["metadata"] == {
"existing": "keep",
"source": "experimental-unit-test",
}
assert mock_handler.call_args.kwargs["temperature"] == 0.2
finally:
litellm.callbacks = original_callbacks
litellm.use_chat_completions_url_for_anthropic_messages = original_flag