diff --git a/litellm/llms/anthropic/pass_through/responses_adapters/handler.py b/litellm/llms/anthropic/pass_through/responses_adapters/handler.py index abe0f2f419d..41411503a3d 100644 --- a/litellm/llms/anthropic/pass_through/responses_adapters/handler.py +++ b/litellm/llms/anthropic/pass_through/responses_adapters/handler.py @@ -16,7 +16,7 @@ from litellm.integrations.anthropic_cache_control_hook import ( ) from litellm.litellm_core_utils.core_helpers import as_str_mapping, iter_items, normalize_drop_params from litellm.litellm_core_utils.prompt_templates.common_utils import cache_control_as_prompt_cache_breakpoint -from litellm.litellm_core_utils.tool_fields import drops_mcp_servers, tools_without_mcp_servers +from litellm.litellm_core_utils.tool_fields import drop_policy, drops_mcp_servers, tools_without_mcp_servers from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator from litellm.types.integrations.custom_logger import CONVERTED_STREAM_KEYS from litellm.types.llms.anthropic import ( @@ -60,6 +60,33 @@ def _forwarded_kwargs(extra_kwargs: Mapping[str, object] | None) -> Mapping[str, return extra_kwargs or {} +def _requests_background(forwarded_kwargs: Mapping[str, object]) -> bool: + extra_body: Final = as_str_mapping(forwarded_kwargs.get("extra_body")) or {} + return any( + value is not None and value is not False + for value in (forwarded_kwargs.get("background"), extra_body.get("background")) + ) + + +def _without_background(extra_body: Mapping[str, object]) -> Mapping[str, object]: + return {key: value for key, value in extra_body.items() if key != "background"} + + +def _reject_background_mode(model: str, forwarded_kwargs: Mapping[str, object]) -> None: + if not _requests_background(forwarded_kwargs) or drop_policy(forwarded_kwargs).drops("background"): + return + provider: Final = forwarded_kwargs.get("custom_llm_provider") + raise litellm.UnsupportedParamsError( + model=model, + llm_provider=provider if isinstance(provider, str) else None, + message=( + f"/v1/messages does not support parameters: ['background'], for model={model}. " + "A background run answers with a response id the Anthropic Messages API has no way to retrieve. " + "To drop it, set `litellm.drop_params=True` or for proxy:\n\n`litellm_settings:\n drop_params: true`\n" + ), + ) + + def _provider_returns_encrypted_reasoning(model: str, custom_llm_provider: object) -> bool: provider: Final = ( custom_llm_provider if isinstance(custom_llm_provider, str) else litellm.get_llm_provider(model=model)[1] @@ -147,6 +174,7 @@ def _build_responses_kwargs( Build the kwargs dict to pass directly to litellm.responses() / litellm.aresponses(). """ forwarded_kwargs: Final = _forwarded_kwargs(extra_kwargs) + _reject_background_mode(model, forwarded_kwargs) target_provider: Final = forwarded_kwargs.get("custom_llm_provider") request_tools: Final = tools_without_mcp_servers( forwarded_kwargs.get("mcp_servers"), @@ -227,7 +255,7 @@ def _build_responses_kwargs( responses_kwargs["stream"] = True # Forward litellm-specific kwargs (api_key, api_base, logging obj, etc.) - excluded: Final = CONVERTED_STREAM_KEYS | {"anthropic_messages", "mcp_servers"} + excluded: Final = CONVERTED_STREAM_KEYS | {"anthropic_messages", "background", "mcp_servers"} for key, value in forwarded_kwargs.items(): if key == "litellm_logging_obj" and value is not None: from litellm.litellm_core_utils.litellm_logging import ( @@ -241,6 +269,8 @@ def _build_responses_kwargs( # Responses API result back to a ModelResponse for the row. setattr(value, "call_type", CallTypes.anthropic_messages.value) responses_kwargs[key] = value + elif key == "extra_body" and (extra_body := as_str_mapping(value)) is not None: + responses_kwargs[key] = _without_background(extra_body) elif key not in excluded and key not in responses_kwargs and value is not None: responses_kwargs[key] = value diff --git a/tests/unit/llms/anthropic/pass_through/responses_adapters/test_responses_adapters_handler.py b/tests/unit/llms/anthropic/pass_through/responses_adapters/test_responses_adapters_handler.py index 526ead24d26..e64a59a3884 100644 --- a/tests/unit/llms/anthropic/pass_through/responses_adapters/test_responses_adapters_handler.py +++ b/tests/unit/llms/anthropic/pass_through/responses_adapters/test_responses_adapters_handler.py @@ -562,6 +562,69 @@ def test_client_cache_control_changes_nothing_for_a_deployment_that_does_not_rea assert marked == unmarked +@pytest.mark.parametrize( + "background_request", + [ + pytest.param({"background": True}, id="background"), + pytest.param({"extra_body": {"background": True}}, id="extra-body-background"), + pytest.param({"extra_body": {"background": "true"}}, id="extra-body-background-string"), + pytest.param({"background": 0}, id="background-zero"), + ], +) +def test_background_answers_400_without_drop_params(background_request: Mapping[str, object]) -> None: + with pytest.raises(litellm.UnsupportedParamsError) as rejected: + _build_responses_kwargs( + model="openai/gpt-5.6", + extra_kwargs={**OPENAI_API, **background_request}, + max_tokens=1024, + messages=MESSAGES, + ) + + assert rejected.value.status_code == 400 + assert "['background']" in rejected.value.message + + +@pytest.mark.parametrize( + ("drop_setting", "global_drop_params"), + [ + pytest.param({"drop_params": True}, False, id="drop_params"), + pytest.param({"additional_drop_params": ["background"]}, False, id="additional_drop_params"), + pytest.param({}, True, id="litellm.drop_params"), + ], +) +def test_dropped_background_never_reaches_the_responses_request( + drop_setting: Mapping[str, object], global_drop_params: bool, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(litellm, "drop_params", global_drop_params) + responses_kwargs: Final = _build_responses_kwargs( + model="openai/gpt-5.6", + extra_kwargs={ + **OPENAI_API, + **drop_setting, + "background": True, + "extra_body": {"background": True, "service_tier": "priority"}, + }, + max_tokens=1024, + messages=MESSAGES, + ) + + assert "background" not in responses_kwargs + assert responses_kwargs["extra_body"] == {"service_tier": "priority"} + assert responses_kwargs["max_output_tokens"] == 1024 + + +def test_background_false_still_succeeds_without_reaching_the_responses_request() -> None: + responses_kwargs: Final = _build_responses_kwargs( + model="openai/gpt-5.6", + extra_kwargs={**OPENAI_API, "background": False, "extra_body": {"background": False}}, + max_tokens=1024, + messages=MESSAGES, + ) + + assert "background" not in responses_kwargs + assert responses_kwargs["extra_body"] == {} + + REASONING_MODEL: Final = "gpt-bridge-reasoning-cost" REASONING_RATES: Final = { "input_cost_per_token": 2e-06, diff --git a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py index 56272436857..fe2b6f69e7d 100644 --- a/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py +++ b/tests/unit/llms/bedrock/messages/invoke_transformations/test_anthropic_claude3_transformation.py @@ -4171,17 +4171,6 @@ _THINKING_HISTORY: Final = ( ) -@dataclass(slots=True) -class ScriptedBedrockInvoke: - replies: tuple[tuple[int, Mapping[str, object]], ...] - bodies: tuple[dict[str, object], ...] = () - - def __call__(self, request: httpx.Request) -> httpx.Response: - self.bodies = (*self.bodies, REASONING_BODY_ADAPTER.validate_json(request.content)) - status_code, body = self.replies[len(self.bodies) - 1] - return httpx.Response(status_code, json=body) - - async def _send_bedrock_messages( model: str, model_info: Mapping[str, object] | None, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 58ed951540e..a9e3aa7da5d 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1288,7 +1288,6 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "supports_native_streaming": {"type": "boolean"}, "supports_image_size": {"type": "boolean"}, "supports_native_structured_output": {"type": "boolean"}, - "supports_bedrock_native_structured_output": {"type": "boolean"}, "use_openai_responses_path": {"type": "boolean"}, "off_peak_pricing": { "type": "object",