diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 36372e7b4e5..3770dc66f0e 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -20,7 +20,11 @@ from litellm.litellm_core_utils.prompt_templates.common_utils import ( from litellm.litellm_core_utils.prompt_templates.factory import ( THOUGHT_SIGNATURE_SEPARATOR, ) -from litellm.llms.anthropic.wif import aget_anthropic_wif_token, get_anthropic_wif_token +from litellm.llms.anthropic.wif import ( + aget_anthropic_wif_token, + anthropic_base_without_chat_suffix, + get_anthropic_wif_token, +) from litellm.llms.base_llm.base_utils import BaseLLMModelInfo, BaseTokenCounter from litellm.llms.base_llm.chat.transformation import BaseLLMException from litellm.proxy._types import SpecialHeaders @@ -62,6 +66,16 @@ _SERVER_OWNED_AUTH_HEADERS: Final = SpecialHeaders.litellm_credential_header_nam _WIF_ELIGIBILITY_ATTR: Final = "_workload_identity_eligible" +def without_caller_credential_headers(headers: Mapping[str, str]) -> dict[str, str]: + """``headers`` minus every header that authenticates the caller to litellm. + + The deployment's own credential is applied on top of the result, so a caller-supplied + credential must not survive into the upstream request: without this a minted federation + Bearer travels beside the caller's own ``x-api-key``, and Anthropic sees two credentials. + """ + return {name: value for name, value in headers.items() if name.lower() not in _SERVER_OWNED_AUTH_HEADERS} + + def config_allows_workload_identity(config: object) -> bool: """A federation token is an Anthropic-org credential and its exchange POSTs the workload's OIDC assertion to the deployment's own host, so eligibility is declared per class and read from that @@ -887,11 +901,7 @@ class AnthropicModelInfo(BaseLLMModelInfo): wif_minted=wif_minted, ) - caller_headers: Final = ( - {name: value for name, value in headers.items() if name.lower() not in _SERVER_OWNED_AUTH_HEADERS} - if wif_minted - else headers - ) + caller_headers: Final = without_caller_credential_headers(headers) if wif_minted else headers return {**caller_headers, **anthropic_headers} @@ -1032,8 +1042,13 @@ class AnthropicModelInfo(BaseLLMModelInfo): "Anthropic's `/models` endpoint." ) headers: Final = MappingProxyType({"anthropic-version": "2023-06-01", **auth_header}) + # /v1/models is appended below, so a base the operator already wrote as .../v1 or + # .../v1/messages would otherwise be asked for /v1/v1/models. model_ids: Final = _fetch_anthropic_model_ids( - resolved_api_base, headers, after_id=None, pages_left=_MODEL_LIST_PAGE_CAP + anthropic_base_without_chat_suffix(resolved_api_base), + headers, + after_id=None, + pages_left=_MODEL_LIST_PAGE_CAP, ) return [ # mutable-ok: matches get_models' list[str] contract shared by every provider override "anthropic/" + model_id for model_id in model_ids diff --git a/litellm/llms/anthropic/files/transformation.py b/litellm/llms/anthropic/files/transformation.py index f8ebccb7215..f8ce572bfc8 100644 --- a/litellm/llms/anthropic/files/transformation.py +++ b/litellm/llms/anthropic/files/transformation.py @@ -36,7 +36,12 @@ from litellm.types.llms.openai import ( ) from litellm.types.utils import LlmProviders -from ..common_utils import AnthropicError, AnthropicModelInfo, merge_anthropic_beta_headers +from ..common_utils import ( + AnthropicError, + AnthropicModelInfo, + merge_anthropic_beta_headers, + without_caller_credential_headers, +) ANTHROPIC_FILES_API_BASE: Final = "https://api.anthropic.com" ANTHROPIC_FILES_BETA_HEADER: Final = "files-api-2025-04-14" @@ -138,14 +143,12 @@ class AnthropicFilesConfig(BaseFilesConfig): merge_anthropic_beta_headers(headers.get("anthropic-beta"), auth_header.get("anthropic-beta")), ANTHROPIC_FILES_BETA_HEADER, ) - headers.update( - { - **auth_header, - "anthropic-version": "2023-06-01", - "anthropic-beta": merged_beta, - } - ) - return headers + return { + **without_caller_credential_headers(headers), + **auth_header, + "anthropic-version": "2023-06-01", + "anthropic-beta": merged_beta, + } def get_supported_openai_params(self, model: str) -> list[OpenAICreateFileRequestOptionalParams]: return ["purpose"] diff --git a/litellm/llms/anthropic/wif.py b/litellm/llms/anthropic/wif.py index 0342926a598..6b88bfd7f63 100644 --- a/litellm/llms/anthropic/wif.py +++ b/litellm/llms/anthropic/wif.py @@ -271,7 +271,7 @@ def _token_exchange_base(api_base: str | None) -> str: """Exchange base for any caller-supplied form of the deployment base: trailing slashes and chat-appended ``/v1/messages`` suffixes stripped, so every tier derives the same token URL (and cache key) for the same deployment.""" - return _strip_chat_suffix(api_base if api_base is not None else _resolve_default_api_base()) + return anthropic_base_without_chat_suffix(api_base if api_base is not None else _resolve_default_api_base()) def _trusted_exchange_hosts() -> frozenset[str]: @@ -315,7 +315,9 @@ def _resolve_default_api_base() -> str: return AnthropicModelInfo.get_api_base(None) or _DEFAULT_API_BASE -def _strip_chat_suffix(base: str) -> str: +def anthropic_base_without_chat_suffix(base: str) -> str: + """A deployment base with its chat-surface suffix removed, so the token URL and model + discovery both derive from the same value whatever form the operator configured.""" parts: Final = urlsplit(base) if not parts.scheme or not parts.netloc: return base.rstrip("/") diff --git a/tests/test_litellm/llms/anthropic/batches/test_handler.py b/tests/test_litellm/llms/anthropic/batches/test_handler.py index 398e0aeea0f..398c94a6798 100644 --- a/tests/test_litellm/llms/anthropic/batches/test_handler.py +++ b/tests/test_litellm/llms/anthropic/batches/test_handler.py @@ -37,9 +37,7 @@ def _ok_batch_response(): "ended_at": "2024-09-24T11:00:00Z", "request_counts": {"succeeded": 2, "errored": 0}, }, - request=httpx.Request( - "GET", "https://api.anthropic.com/v1/messages/batches/msgbatch_abc" - ), + request=httpx.Request("GET", "https://api.anthropic.com/v1/messages/batches/msgbatch_abc"), ) @@ -61,9 +59,7 @@ def patched_client(): @pytest.mark.asyncio -async def test_aretrieve_batch_fires_get_with_correct_url_and_headers( - handler, patched_client -): +async def test_aretrieve_batch_fires_get_with_correct_url_and_headers(handler, patched_client): fake_client, factory = patched_client batch = await handler.aretrieve_batch( @@ -78,9 +74,7 @@ async def test_aretrieve_batch_fires_get_with_correct_url_and_headers( fake_client.get.assert_awaited_once() _, call_kwargs = fake_client.get.call_args # Exact URL built by get_retrieve_batch_url. - assert call_kwargs["url"] == ( - "https://api.anthropic.com/v1/messages/batches/msgbatch_abc" - ) + assert call_kwargs["url"] == ("https://api.anthropic.com/v1/messages/batches/msgbatch_abc") # Auth + version + beta headers built by validate_environment. headers = call_kwargs["headers"] assert headers["x-api-key"] == "sk-ant-test" @@ -95,9 +89,7 @@ async def test_aretrieve_batch_fires_get_with_correct_url_and_headers( @pytest.mark.asyncio -async def test_aretrieve_batch_uses_anthropic_provider_for_client( - handler, patched_client -): +async def test_aretrieve_batch_uses_anthropic_provider_for_client(handler, patched_client): from litellm.types.utils import LlmProviders _, factory = patched_client @@ -113,14 +105,10 @@ async def test_aretrieve_batch_uses_anthropic_provider_for_client( @pytest.mark.asyncio -async def test_aretrieve_batch_resolves_api_key_from_model_info( - handler, patched_client -): +async def test_aretrieve_batch_resolves_api_key_from_model_info(handler, patched_client): fake_client, _ = patched_client # api_key=None -> handler falls back to AnthropicModelInfo.get_api_key(). - with patch.object( - handler.anthropic_model_info, "get_api_key", return_value="sk-from-env" - ): + with patch.object(handler.anthropic_model_info, "get_api_key", return_value="sk-from-env"): await handler.aretrieve_batch( batch_id="msgbatch_abc", api_base="https://api.anthropic.com", @@ -136,9 +124,7 @@ async def test_aretrieve_batch_resolves_api_key_from_model_info( async def test_aretrieve_batch_missing_api_key_raises(handler, patched_client): fake_client, _ = patched_client # No api_key and resolver yields None -> hard error before any network call. - with patch.object( - handler.anthropic_model_info, "get_api_key", return_value=None - ): + with patch.object(handler.anthropic_model_info, "get_api_key", return_value=None): with pytest.raises(ValueError, match="Missing Anthropic API Key"): await handler.aretrieve_batch( batch_id="msgbatch_abc", @@ -167,9 +153,7 @@ async def test_aretrieve_batch_resolves_default_api_base(handler, patched_client max_retries=0, ) _, call_kwargs = fake_client.get.call_args - assert call_kwargs["url"] == ( - "https://api.anthropic.com/v1/messages/batches/msgbatch_abc" - ) + assert call_kwargs["url"] == ("https://api.anthropic.com/v1/messages/batches/msgbatch_abc") @pytest.mark.asyncio @@ -178,9 +162,7 @@ async def test_aretrieve_batch_raises_for_status(handler): error_response = httpx.Response( status_code=404, json={"error": "not found"}, - request=httpx.Request( - "GET", "https://api.anthropic.com/v1/messages/batches/missing" - ), + request=httpx.Request("GET", "https://api.anthropic.com/v1/messages/batches/missing"), ) fake_client = MagicMock() fake_client.get = AsyncMock(return_value=error_response) @@ -215,21 +197,15 @@ async def test_aretrieve_batch_invokes_pre_call_logging(handler, patched_client) assert pre_kwargs["input"] == "msgbatch_abc" assert pre_kwargs["api_key"] == "sk-ant-test" # The logged api_base is the full retrieve URL, not the bare base. - assert pre_kwargs["additional_args"]["api_base"] == ( - "https://api.anthropic.com/v1/messages/batches/msgbatch_abc" - ) + assert pre_kwargs["additional_args"]["api_base"] == ("https://api.anthropic.com/v1/messages/batches/msgbatch_abc") @pytest.mark.asyncio -async def test_aretrieve_batch_builds_default_logging_obj_when_absent( - handler, patched_client -): +async def test_aretrieve_batch_builds_default_logging_obj_when_absent(handler, patched_client): # logging_obj=None -> handler constructs a real Logging object; the call # must still complete (no AttributeError on a missing logger). _, _ = patched_client - with patch( - "litellm.litellm_core_utils.litellm_logging.Logging" - ) as logging_cls: + with patch("litellm.litellm_core_utils.litellm_logging.Logging") as logging_cls: logging_cls.return_value = MagicMock() batch = await handler.aretrieve_batch( batch_id="msgbatch_abc", @@ -318,9 +294,7 @@ class _BlockingPoster: @pytest.mark.asyncio -async def test_aretrieve_batch_wif_exchange_does_not_block_event_loop( - handler, patched_client, monkeypatch -): +async def test_aretrieve_batch_wif_exchange_does_not_block_event_loop(handler, patched_client, monkeypatch): """Regression: aretrieve_batch called the synchronous validate_environment directly, so a cold WIF mint ran inline on the event loop and froze every other concurrent coroutine until the exchange finished.""" @@ -345,9 +319,7 @@ async def test_aretrieve_batch_wif_exchange_does_not_block_event_loop( def routed_through_injected_engine(litellm_params, api_base, model): return get_anthropic_wif_token(litellm_params, api_base, model, engine) - monkeypatch.setattr( - anthropic_common_utils, "get_anthropic_wif_token", routed_through_injected_engine - ) + monkeypatch.setattr(anthropic_common_utils, "get_anthropic_wif_token", routed_through_injected_engine) ticks = [] diff --git a/tests/test_litellm/llms/anthropic/batches/test_transformation.py b/tests/test_litellm/llms/anthropic/batches/test_transformation.py index 52b620edd3a..dbdb77db093 100644 --- a/tests/test_litellm/llms/anthropic/batches/test_transformation.py +++ b/tests/test_litellm/llms/anthropic/batches/test_transformation.py @@ -103,9 +103,7 @@ def test_validate_environment_oauth_key_uses_bearer(config): def test_validate_environment_missing_key_raises(config): # No api_key passed and no env credentials -> get_auth_header returns None. - with patch.object( - config.anthropic_model_info, "get_auth_header", return_value=None - ): + with patch.object(config.anthropic_model_info, "get_auth_header", return_value=None): with pytest.raises(ValueError, match="Missing Anthropic API Key"): config.validate_environment( headers={}, @@ -244,12 +242,7 @@ def test_get_retrieve_batch_url_uses_default_api_base(config): def test_transform_retrieve_batch_request_returns_empty_dict(config): - assert ( - config.transform_retrieve_batch_request( - batch_id="msgbatch_123", optional_params={}, litellm_params={} - ) - == {} - ) + assert config.transform_retrieve_batch_request(batch_id="msgbatch_123", optional_params={}, litellm_params={}) == {} # =========================================================================== # @@ -458,9 +451,7 @@ def test_transform_retrieve_response_unparseable_json_raises(config): def test_get_error_class_with_dict_headers(config): - err = config.get_error_class( - error_message="rate limited", status_code=429, headers={"x-ratelimit": "0"} - ) + err = config.get_error_class(error_message="rate limited", status_code=429, headers={"x-ratelimit": "0"}) from litellm.llms.anthropic.common_utils import AnthropicError assert isinstance(err, AnthropicError) @@ -470,9 +461,7 @@ def test_get_error_class_with_dict_headers(config): def test_get_error_class_with_httpx_headers(config): hdrs = httpx.Headers({"retry-after": "5"}) - err = config.get_error_class( - error_message="server error", status_code=500, headers=hdrs - ) + err = config.get_error_class(error_message="server error", status_code=500, headers=hdrs) assert err.status_code == 500 assert err.message == "server error" @@ -546,9 +535,7 @@ def test_transform_response_skips_malformed_lines(config): def fake_transform_parsed(*, completion_response, raw_response, model_response): mr = ModelResponse() - setattr( - mr, "usage", Usage(prompt_tokens=7, completion_tokens=3, total_tokens=10) - ) + setattr(mr, "usage", Usage(prompt_tokens=7, completion_tokens=3, total_tokens=10)) return mr with patch.object( @@ -591,13 +578,16 @@ def test_transform_response_reraises_unexpected_error(config): # A non-JSONDecodeError raised during usage aggregation must propagate # (the outer `except Exception: raise e`), not be swallowed. - with patch.object( - config.anthropic_chat_config, - "transform_parsed_response", - side_effect=fake_transform_parsed, - ), patch( - "litellm.cost_calculator.BaseTokenUsageProcessor.combine_usage_objects", - side_effect=RuntimeError("boom"), + with ( + patch.object( + config.anthropic_chat_config, + "transform_parsed_response", + side_effect=fake_transform_parsed, + ), + patch( + "litellm.cost_calculator.BaseTokenUsageProcessor.combine_usage_objects", + side_effect=RuntimeError("boom"), + ), ): with pytest.raises(RuntimeError, match="boom"): config.transform_response( diff --git a/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py b/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py index 2b392456763..7c508c5b00d 100644 --- a/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/guardrail_translation/test_anthropic_guardrail_handler.py @@ -47,9 +47,7 @@ class MockDynamicGuardrail(CustomGuardrail): input_type: Literal["request", "response"], logging_obj: Optional[Any] = None, ) -> GenericGuardrailAPIInputs: - self.dynamic_params = self.get_guardrail_dynamic_request_body_params( - request_data - ) + self.dynamic_params = self.get_guardrail_dynamic_request_body_params(request_data) return inputs @@ -196,9 +194,7 @@ class TestAnthropicMessagesHandlerStreamingRequestData: assert guardrail.request_data is not None assert guardrail.request_data["response"] is mock_response - assert ( - guardrail.request_data["litellm_metadata"]["user_api_key_user_id"] == "u-1" - ) + assert guardrail.request_data["litellm_metadata"]["user_api_key_user_id"] == "u-1" @pytest.mark.asyncio async def test_mid_stream_chunk_passes_responses_so_far_and_metadata(self): @@ -210,9 +206,7 @@ class TestAnthropicMessagesHandlerStreamingRequestData: with ( patch.object(handler, "_check_streaming_has_ended", return_value=False), - patch.object( - handler, "get_streaming_string_so_far", return_value="partial text" - ), + patch.object(handler, "get_streaming_string_so_far", return_value="partial text"), ): await handler.process_output_streaming_response( responses_so_far=responses_so_far, @@ -224,9 +218,7 @@ class TestAnthropicMessagesHandlerStreamingRequestData: assert guardrail.request_data is not None assert guardrail.request_data["responses"] is responses_so_far - assert ( - guardrail.request_data["litellm_metadata"]["user_api_key_user_id"] == "u-1" - ) + assert guardrail.request_data["litellm_metadata"]["user_api_key_user_id"] == "u-1" class TestAnthropicMessagesHandlerStreamingOutputProcessing: @@ -275,17 +267,11 @@ class TestAnthropicMessagesHandlerInputProcessing: data = { "model": "claude-3-5-sonnet-20241022", "messages": [{"role": "user", "content": "hello"}], - "litellm_metadata": { - "guardrails": [ - {"cygnal-monitor": {"extra_body": {"policy_id": "policy-123"}}} - ] - }, + "litellm_metadata": {"guardrails": [{"cygnal-monitor": {"extra_body": {"policy_id": "policy-123"}}}]}, } with patch("litellm.proxy.proxy_server.premium_user", True): - await handler.process_input_messages( - data=data, guardrail_to_apply=guardrail - ) + await handler.process_input_messages(data=data, guardrail_to_apply=guardrail) assert data.get("litellm_metadata", {}).get("guardrails") assert guardrail.dynamic_params == {"policy_id": "policy-123"} @@ -1164,9 +1150,7 @@ class TestAnthropicMessagesHandlerInputProcessing: # Mock _check_streaming_has_ended to return False (stream not ended) with ( patch.object(handler, "_check_streaming_has_ended", return_value=False), - patch.object( - handler, "get_streaming_string_so_far", return_value="partial text" - ), + patch.object(handler, "get_streaming_string_so_far", return_value="partial text"), ): responses_so_far = [b"data: some chunk"] @@ -1197,9 +1181,7 @@ class TestAnthropicMessagesHandlerInputProcessing: data = { "model": "claude-opus-4-6", - "messages": [ - {"role": "user", "content": "What is the weather in San Francisco?"} - ], + "messages": [{"role": "user", "content": "What is the weather in San Francisco?"}], "tools": [ { "type": "tool_search_tool_regex_20251119", @@ -1362,17 +1344,11 @@ class TestAnthropicMessagesIncrementalScan: ] with patch.object(guardrail, "make_bedrock_api_request", new_callable=AsyncMock) as mock_api: mock_api.return_value = {"action": "NONE", "output": [], "outputs": []} - await handler.process_input_messages( - data=self._data(turn1, sid), guardrail_to_apply=guardrail - ) + await handler.process_input_messages(data=self._data(turn1, sid), guardrail_to_apply=guardrail) assert mock_api.call_count == 1 - assert [m["content"] for m in mock_api.call_args.kwargs["messages"]] == [ - "What is the capital of France?" - ] + assert [m["content"] for m in mock_api.call_args.kwargs["messages"]] == ["What is the capital of France?"] mock_api.reset_mock() - await handler.process_input_messages( - data=self._data(turn2, sid), guardrail_to_apply=guardrail - ) + await handler.process_input_messages(data=self._data(turn2, sid), guardrail_to_apply=guardrail) assert mock_api.call_count == 1 assert [m["content"] for m in mock_api.call_args.kwargs["messages"]] == [ "Paris.", @@ -1725,9 +1701,7 @@ class TestAnthropicMessagesScanOnlyToolResults: await handler.process_input_messages(data=data, guardrail_to_apply=guardrail) - assert guardrail.seen_texts == ["fetched POISON page"], ( - "only the tool_result payload may reach the guardrail" - ) + assert guardrail.seen_texts == ["fetched POISON page"], "only the tool_result payload may reach the guardrail" assert guardrail.captured_inputs is not None assert guardrail.captured_inputs.get("tools") is None assert [m["role"] for m in guardrail.captured_inputs["structured_messages"]] == ["tool"] diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py index f6cd6ac6734..5506d8e4979 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -21,9 +21,7 @@ async def test_make_call_passes_logging_obj_to_client_post(): mock_client = AsyncMock() mock_response = MagicMock() mock_response.aiter_lines = MagicMock( - return_value=iter( - [b'data: {"type":"message_start"}\n', b'data: {"type":"message_delta"}\n'] - ) + return_value=iter([b'data: {"type":"message_start"}\n', b'data: {"type":"message_delta"}\n']) ) mock_client.post.return_value = mock_response @@ -55,9 +53,7 @@ def test_redacted_thinking_content_block_delta(): "data": "EuoBCoYBGAIiQJ/SxkPAgqxhKok29YrpJHRUJ0OT8ahCHKAwyhmRuUhtdmDX9+mn4gDzKNv3fVpQdB01zEPMzNY3QuTCd+1bdtEqQK6JuKHqdndbwpr81oVWb4wxd1GqF/7Jkw74IlQa27oobX+KuRkopr9Dllt/RDe7Se0sI1IkU7tJIAQCoP46OAwSDF51P09q67xhHlQ3ihoM2aOVlkghq/X0w8NlIjBMNvXYNbjhyrOcIg6kPFn2ed/KK7Cm5prYAtXCwkb4Wr5tUSoSHu9T5hKdJRbr6WsqEc7Lle7FULqMLZGkhqXyc3BA", }, } - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=False, json_mode=False - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=False, json_mode=False) model_response = model_response_iterator.chunk_parser(chunk=chunk) print(f"\n\nmodel_response: {model_response}\n\n") assert model_response.choices[0].delta.thinking_blocks is not None @@ -65,19 +61,14 @@ def test_redacted_thinking_content_block_delta(): print( f"\n\nmodel_response.choices[0].delta.thinking_blocks[0]: {model_response.choices[0].delta.thinking_blocks[0]}\n\n" ) - assert ( - model_response.choices[0].delta.thinking_blocks[0]["type"] - == "redacted_thinking" - ) + assert model_response.choices[0].delta.thinking_blocks[0]["type"] == "redacted_thinking" assert model_response.choices[0].delta.provider_specific_fields is not None assert "thinking_blocks" in model_response.choices[0].delta.provider_specific_fields def test_streaming_thinking_blocks_are_replayable_after_signature_delta(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) chunks = [ { "type": "content_block_start", @@ -101,17 +92,12 @@ def test_streaming_thinking_blocks_are_replayable_after_signature_delta(): }, ] - parsed_chunks = [ - model_response_iterator.chunk_parser(chunk=chunk) for chunk in chunks - ] + parsed_chunks = [model_response_iterator.chunk_parser(chunk=chunk) for chunk in chunks] reasoning_content = "".join( - getattr(chunk.choices[0].delta, "reasoning_content", None) or "" - for chunk in parsed_chunks + getattr(chunk.choices[0].delta, "reasoning_content", None) or "" for chunk in parsed_chunks ) thinking_blocks = tuple( - block - for chunk in parsed_chunks - for block in (getattr(chunk.choices[0].delta, "thinking_blocks", None) or []) + block for chunk in parsed_chunks for block in (getattr(chunk.choices[0].delta, "thinking_blocks", None) or []) ) expected_delta_blocks = ( {"type": "thinking", "thinking": "Step 1. "}, @@ -125,18 +111,12 @@ def test_streaming_thinking_blocks_are_replayable_after_signature_delta(): assert reasoning_content == "Step 1. Step 2." assert thinking_blocks == (*expected_delta_blocks, expected_thinking_block) - assert parsed_chunks[1].choices[0].delta.provider_specific_fields == { - "thinking_blocks": [expected_delta_blocks[0]] - } - assert parsed_chunks[-1].choices[0].delta.provider_specific_fields == { - "thinking_blocks": [expected_thinking_block] - } + assert parsed_chunks[1].choices[0].delta.provider_specific_fields == {"thinking_blocks": [expected_delta_blocks[0]]} + assert parsed_chunks[-1].choices[0].delta.provider_specific_fields == {"thinking_blocks": [expected_thinking_block]} def test_streaming_unsigned_thinking_deltas_keep_reasoning_content(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) chunks = [ { "type": "content_block_start", @@ -156,17 +136,12 @@ def test_streaming_unsigned_thinking_deltas_keep_reasoning_content(): {"type": "content_block_stop", "index": 0}, ] - parsed_chunks = [ - model_response_iterator.chunk_parser(chunk=chunk) for chunk in chunks - ] + parsed_chunks = [model_response_iterator.chunk_parser(chunk=chunk) for chunk in chunks] reasoning_content = "".join( - getattr(chunk.choices[0].delta, "reasoning_content", None) or "" - for chunk in parsed_chunks + getattr(chunk.choices[0].delta, "reasoning_content", None) or "" for chunk in parsed_chunks ) thinking_blocks = tuple( - block - for chunk in parsed_chunks - for block in (getattr(chunk.choices[0].delta, "thinking_blocks", None) or []) + block for chunk in parsed_chunks for block in (getattr(chunk.choices[0].delta, "thinking_blocks", None) or []) ) assert reasoning_content == "Step 1. Step 2." @@ -177,9 +152,7 @@ def test_streaming_unsigned_thinking_deltas_keep_reasoning_content(): def test_streaming_truncated_thinking_deltas_keep_reasoning_content(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) chunks = [ { "type": "content_block_start", @@ -198,17 +171,12 @@ def test_streaming_truncated_thinking_deltas_keep_reasoning_content(): }, ] - parsed_chunks = [ - model_response_iterator.chunk_parser(chunk=chunk) for chunk in chunks - ] + parsed_chunks = [model_response_iterator.chunk_parser(chunk=chunk) for chunk in chunks] reasoning_content = "".join( - getattr(chunk.choices[0].delta, "reasoning_content", None) or "" - for chunk in parsed_chunks + getattr(chunk.choices[0].delta, "reasoning_content", None) or "" for chunk in parsed_chunks ) thinking_blocks = tuple( - block - for chunk in parsed_chunks - for block in (getattr(chunk.choices[0].delta, "thinking_blocks", None) or []) + block for chunk in parsed_chunks for block in (getattr(chunk.choices[0].delta, "thinking_blocks", None) or []) ) assert reasoning_content == "Step 1. Step 2." @@ -219,9 +187,7 @@ def test_streaming_truncated_thinking_deltas_keep_reasoning_content(): def test_handle_json_mode_chunk_response_format_tool(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=True - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=True) response_format_tool = ChatCompletionToolCallChunk( id="tool_123", type="function", @@ -232,9 +198,7 @@ def test_handle_json_mode_chunk_response_format_tool(): index=0, ) - text, tool_use = model_response_iterator._handle_json_mode_chunk( - "", response_format_tool - ) + text, tool_use = model_response_iterator._handle_json_mode_chunk("", response_format_tool) print(f"\n\nresponse_format_tool text: {text}\n\n") print(f"\n\nresponse_format_tool tool_use: {tool_use}\n\n") @@ -243,15 +207,11 @@ def test_handle_json_mode_chunk_response_format_tool(): def test_handle_json_mode_chunk_regular_tool(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=True - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=True) regular_tool = ChatCompletionToolCallChunk( id="tool_456", type="function", - function=ChatCompletionToolCallFunctionChunk( - name="get_weather", arguments='{"location": "San Francisco, CA"}' - ), + function=ChatCompletionToolCallFunctionChunk(name="get_weather", arguments='{"location": "San Francisco, CA"}'), index=0, ) @@ -265,17 +225,13 @@ def test_handle_json_mode_chunk_regular_tool(): def test_handle_json_mode_chunk_streaming_response_format_tool(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=True - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=True) # First chunk: response_format tool with id and name, but no arguments first_chunk = ChatCompletionToolCallChunk( id="tool_123", type="function", - function=ChatCompletionToolCallFunctionChunk( - name=RESPONSE_FORMAT_TOOL_NAME, arguments="" - ), + function=ChatCompletionToolCallFunctionChunk(name=RESPONSE_FORMAT_TOOL_NAME, arguments=""), index=0, ) @@ -283,9 +239,7 @@ def test_handle_json_mode_chunk_streaming_response_format_tool(): second_chunk = ChatCompletionToolCallChunk( id=None, type="function", - function=ChatCompletionToolCallFunctionChunk( - name=None, arguments='{"question": "What is the weather?"' - ), + function=ChatCompletionToolCallFunctionChunk(name=None, arguments='{"question": "What is the weather?"'), index=0, ) @@ -293,9 +247,7 @@ def test_handle_json_mode_chunk_streaming_response_format_tool(): third_chunk = ChatCompletionToolCallChunk( id=None, type="function", - function=ChatCompletionToolCallFunctionChunk( - name=None, arguments=', "answer": "It is sunny"}' - ), + function=ChatCompletionToolCallFunctionChunk(name=None, arguments=', "answer": "It is sunny"}'), index=0, ) @@ -326,9 +278,7 @@ def test_handle_json_mode_chunk_streaming_response_format_tool(): def test_handle_json_mode_chunk_streaming_regular_tool(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=True - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=True) # First chunk: regular tool with id and name, but no arguments first_chunk = ChatCompletionToolCallChunk( @@ -342,9 +292,7 @@ def test_handle_json_mode_chunk_streaming_regular_tool(): second_chunk = ChatCompletionToolCallChunk( id=None, type="function", - function=ChatCompletionToolCallFunctionChunk( - name=None, arguments='{"location": "San Francisco, CA"}' - ), + function=ChatCompletionToolCallFunctionChunk(name=None, arguments='{"location": "San Francisco, CA"}'), index=0, ) @@ -369,27 +317,19 @@ def test_handle_json_mode_chunk_streaming_regular_tool(): def test_response_format_tool_finish_reason(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=True - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=True) # First chunk: response_format tool response_format_tool = ChatCompletionToolCallChunk( id="tool_123", type="function", - function=ChatCompletionToolCallFunctionChunk( - name=RESPONSE_FORMAT_TOOL_NAME, arguments='{"answer": "test"}' - ), + function=ChatCompletionToolCallFunctionChunk(name=RESPONSE_FORMAT_TOOL_NAME, arguments='{"answer": "test"}'), index=0, ) # Process the tool call (should set converted_response_format_tool flag) - text, tool_use = model_response_iterator._handle_json_mode_chunk( - "", response_format_tool - ) - print( - f"\n\nconverted_response_format_tool flag: {model_response_iterator.converted_response_format_tool}\n\n" - ) + text, tool_use = model_response_iterator._handle_json_mode_chunk("", response_format_tool) + print(f"\n\nconverted_response_format_tool flag: {model_response_iterator.converted_response_format_tool}\n\n") # Simulate message_delta chunk with tool_use stop_reason message_delta_chunk = { @@ -408,25 +348,19 @@ def test_response_format_tool_finish_reason(): def test_regular_tool_finish_reason(): - model_response_iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=True - ) + model_response_iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=True) # First chunk: regular tool (not response_format) regular_tool = ChatCompletionToolCallChunk( id="tool_456", type="function", - function=ChatCompletionToolCallFunctionChunk( - name="get_weather", arguments='{"location": "San Francisco, CA"}' - ), + function=ChatCompletionToolCallFunctionChunk(name="get_weather", arguments='{"location": "San Francisco, CA"}'), index=0, ) # Process the tool call (should NOT set converted_response_format_tool flag) text, tool_use = model_response_iterator._handle_json_mode_chunk("", regular_tool) - print( - f"\n\nconverted_response_format_tool flag: {model_response_iterator.converted_response_format_tool}\n\n" - ) + print(f"\n\nconverted_response_format_tool flag: {model_response_iterator.converted_response_format_tool}\n\n") # Simulate message_delta chunk with tool_use stop_reason message_delta_chunk = { @@ -486,9 +420,7 @@ def test_text_only_streaming_has_index_zero(): for chunk in chunks: parsed = iterator.chunk_parser(chunk) if parsed.choices: - assert ( - parsed.choices[0].index == 0 - ), f"Expected index=0, got {parsed.choices[0].index}" + assert parsed.choices[0].index == 0, f"Expected index=0, got {parsed.choices[0].index}" def test_streaming_thinking_deltas_count_reasoning_tokens_in_usage(): @@ -665,9 +597,7 @@ def test_anthropic_completion_streaming_usage_matches_non_streaming_with_thinkin ] self._write_response( content_type="text/event-stream", - body="".join( - f"data: {json.dumps(event)}\n\n" for event in events - ).encode("utf-8"), + body="".join(f"data: {json.dumps(event)}\n\n" for event in events).encode("utf-8"), ) return @@ -748,13 +678,9 @@ def test_anthropic_completion_streaming_usage_matches_non_streaming_with_thinkin assert content_chunks == [answer_text] assert stream_usage is not None stream_completion_details = stream_usage["completion_tokens_details"] - assert ( - stream_completion_details["reasoning_tokens"] - == non_stream_details.reasoning_tokens - ) + assert stream_completion_details["reasoning_tokens"] == non_stream_details.reasoning_tokens assert stream_completion_details["text_tokens"] == ( - stream_usage["completion_tokens"] - - stream_completion_details["reasoning_tokens"] + stream_usage["completion_tokens"] - stream_completion_details["reasoning_tokens"] ) assert requests_seen == [ { @@ -846,9 +772,9 @@ def test_text_and_tool_streaming_has_index_zero(): for chunk in chunks: parsed = iterator.chunk_parser(chunk) if parsed.choices: - assert ( - parsed.choices[0].index == 0 - ), f"Expected index=0 for chunk type {chunk.get('type')}, got {parsed.choices[0].index}" + assert parsed.choices[0].index == 0, ( + f"Expected index=0 for chunk type {chunk.get('type')}, got {parsed.choices[0].index}" + ) def test_multiple_tools_streaming_has_index_zero(): @@ -901,15 +827,11 @@ def test_multiple_tools_streaming_has_index_zero(): for chunk in chunks: parsed = iterator.chunk_parser(chunk) if parsed.choices: - assert ( - parsed.choices[0].index == 0 - ), f"Expected index=0, got {parsed.choices[0].index}" + assert parsed.choices[0].index == 0, f"Expected index=0, got {parsed.choices[0].index}" def test_streaming_chunks_have_stable_ids(): - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=False, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=False, json_mode=False) first_chunk = { "type": "content_block_delta", "index": 0, @@ -934,9 +856,7 @@ def test_partial_json_chunk_accumulation(): This tests the fix for https://github.com/BerriAI/litellm/issues/17473 where network fragmentation can cause SSE data to arrive in partial chunks. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) partial_chunk_1 = '{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hel' partial_chunk_2 = 'lo"}}' @@ -944,31 +864,21 @@ def test_partial_json_chunk_accumulation(): # First partial chunk should return None (still accumulating) result1 = iterator._parse_sse_data(f"data:{partial_chunk_1}") assert result1 is None, "First partial chunk should return None while accumulating" - assert ( - iterator.chunk_type == "accumulated_json" - ), "Should switch to accumulated_json mode" - assert ( - iterator.accumulated_json == partial_chunk_1 - ), "Should have accumulated first part" + assert iterator.chunk_type == "accumulated_json", "Should switch to accumulated_json mode" + assert iterator.accumulated_json == partial_chunk_1, "Should have accumulated first part" # Second partial chunk should complete the JSON and return a parsed result result2 = iterator._parse_sse_data(f"data:{partial_chunk_2}") assert result2 is not None, "Second chunk should return parsed result" - assert ( - iterator.accumulated_json == "" - ), "Buffer should be cleared after successful parse" - assert ( - result2.choices[0].delta.content == "Hello" - ), f"Expected 'Hello', got '{result2.choices[0].delta.content}'" + assert iterator.accumulated_json == "", "Buffer should be cleared after successful parse" + assert result2.choices[0].delta.content == "Hello", f"Expected 'Hello', got '{result2.choices[0].delta.content}'" def test_complete_json_chunk_no_accumulation(): """ Test that complete JSON chunks are parsed immediately without accumulation. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) complete_chunk = '{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}' @@ -976,18 +886,14 @@ def test_complete_json_chunk_no_accumulation(): assert result is not None, "Complete chunk should return parsed result immediately" assert iterator.chunk_type == "valid_json", "Should remain in valid_json mode" assert iterator.accumulated_json == "", "Buffer should remain empty" - assert ( - result.choices[0].delta.content == "Hello" - ), f"Expected 'Hello', got '{result.choices[0].delta.content}'" + assert result.choices[0].delta.content == "Hello", f"Expected 'Hello', got '{result.choices[0].delta.content}'" def test_multiple_partial_chunks_accumulation(): """ Test that multiple partial chunks can be accumulated across several iterations. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) # Split a JSON chunk into three parts part1 = '{"type":"content_block_del' @@ -1018,9 +924,7 @@ def test_web_search_tool_result_no_extra_tool_calls(): The issue was that web_search_tool_result blocks have input_json_delta events with {} that were incorrectly being converted to tool calls. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) # Simulate the streaming sequence: # 1. server_tool_use block starts (web_search) @@ -1095,9 +999,7 @@ def test_web_search_tool_result_no_extra_tool_calls(): # Should have exactly 2 tool calls: # 1. From content_block_start (server_tool_use) with id and name # 2. From content_block_delta with the actual query - assert ( - len(tool_calls_emitted) == 2 - ), f"Expected 2 tool calls, got {len(tool_calls_emitted)}" + assert len(tool_calls_emitted) == 2, f"Expected 2 tool calls, got {len(tool_calls_emitted)}" # First tool call should have the id and name assert tool_calls_emitted[0]["id"] == "srvtoolu_01ABC123" @@ -1113,9 +1015,7 @@ def test_current_content_block_type_tracking(): """ Test that current_content_block_type is properly tracked and reset. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) # Initially should be None assert iterator.current_content_block_type is None @@ -1168,9 +1068,7 @@ def test_web_search_tool_result_captured_in_provider_specific_fields(): The web_search_tool_result content comes ALL AT ONCE in content_block_start, not in deltas, so we need to capture it there. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) # Simulate the streaming sequence with web_search_tool_result chunks = [ @@ -1241,23 +1139,15 @@ def test_web_search_tool_result_captured_in_provider_specific_fields(): and parsed.choices[0].delta.provider_specific_fields and "web_search_results" in parsed.choices[0].delta.provider_specific_fields ): - web_search_results = parsed.choices[0].delta.provider_specific_fields[ - "web_search_results" - ] + web_search_results = parsed.choices[0].delta.provider_specific_fields["web_search_results"] # Verify web_search_results was captured assert web_search_results is not None, "web_search_results should be captured" assert len(web_search_results) == 1, "Should have 1 web_search_tool_result block" - assert ( - web_search_results[0]["type"] == "web_search_tool_result" - ), "Block type should be web_search_tool_result" - assert ( - web_search_results[0]["tool_use_id"] == "srvtoolu_01ABC123" - ), "tool_use_id should match" + assert web_search_results[0]["type"] == "web_search_tool_result", "Block type should be web_search_tool_result" + assert web_search_results[0]["tool_use_id"] == "srvtoolu_01ABC123", "tool_use_id should match" assert len(web_search_results[0]["content"]) == 2, "Should have 2 search results" - assert ( - web_search_results[0]["content"][0]["title"] == "Fun Otter Facts" - ), "First result title should match" + assert web_search_results[0]["content"][0]["title"] == "Fun Otter Facts", "First result title should match" def test_web_fetch_tool_result_captured_in_provider_specific_fields(): @@ -1271,9 +1161,7 @@ def test_web_fetch_tool_result_captured_in_provider_specific_fields(): The web_fetch_tool_result content comes ALL AT ONCE in content_block_start, not in deltas, so we need to capture it there. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) # Simulate the streaming sequence with web_fetch_tool_result chunks = [ @@ -1344,25 +1232,15 @@ def test_web_fetch_tool_result_captured_in_provider_specific_fields(): and parsed.choices[0].delta.provider_specific_fields and "web_search_results" in parsed.choices[0].delta.provider_specific_fields ): - web_search_results = parsed.choices[0].delta.provider_specific_fields[ - "web_search_results" - ] + web_search_results = parsed.choices[0].delta.provider_specific_fields["web_search_results"] # Verify web_fetch_tool_result was captured (stored in web_search_results list) assert web_search_results is not None, "web_search_results should be captured" assert len(web_search_results) == 1, "Should have 1 web_fetch_tool_result block" - assert ( - web_search_results[0]["type"] == "web_fetch_tool_result" - ), "Block type should be web_fetch_tool_result" - assert ( - web_search_results[0]["tool_use_id"] == "srvtoolu_01ABC123" - ), "tool_use_id should match" - assert ( - web_search_results[0]["content"]["url"] == "https://example.com" - ), "URL should match" - assert ( - web_search_results[0]["content"]["content"]["title"] == "Example Page" - ), "Title should match" + assert web_search_results[0]["type"] == "web_fetch_tool_result", "Block type should be web_fetch_tool_result" + assert web_search_results[0]["tool_use_id"] == "srvtoolu_01ABC123", "tool_use_id should match" + assert web_search_results[0]["content"]["url"] == "https://example.com", "URL should match" + assert web_search_results[0]["content"]["content"]["title"] == "Example Page", "Title should match" def test_web_fetch_tool_result_no_extra_tool_calls(): @@ -1375,9 +1253,7 @@ def test_web_fetch_tool_result_no_extra_tool_calls(): The issue was that web_fetch_tool_result blocks have input_json_delta events with {} that were incorrectly being converted to tool calls. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) # to verify it doesn't emit tool calls chunks = [ @@ -1421,9 +1297,9 @@ def test_web_fetch_tool_result_no_extra_tool_calls(): tool_call_count += 1 # Should have 0 tool calls - web_fetch_tool_result should not emit tool calls - assert ( - tool_call_count == 0 - ), f"Expected 0 tool calls, got {tool_call_count}. web_fetch_tool_result should not emit tool calls" + assert tool_call_count == 0, ( + f"Expected 0 tool calls, got {tool_call_count}. web_fetch_tool_result should not emit tool calls" + ) def test_container_in_provider_specific_fields_streaming(): @@ -1433,9 +1309,7 @@ def test_container_in_provider_specific_fields_streaming(): When container with skills is used, the container field should be present in the provider_specific_fields of the message_delta chunk. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=True, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) # Simulate streaming chunks chunks = [ @@ -1503,20 +1377,12 @@ def test_container_in_provider_specific_fields_streaming(): and parsed.choices[0].delta.provider_specific_fields and "container" in parsed.choices[0].delta.provider_specific_fields ): - container_field = parsed.choices[0].delta.provider_specific_fields[ - "container" - ] + container_field = parsed.choices[0].delta.provider_specific_fields["container"] # Verify container was captured - assert ( - container_field is not None - ), "container should be captured in provider_specific_fields" - assert ( - container_field["id"] == "container_011CW9hA9zpZ8xD3bjjShy4p" - ), "container id should match" - assert ( - container_field["expires_at"] == "2025-12-16T04:57:16.913181Z" - ), "expires_at should match" + assert container_field is not None, "container should be captured in provider_specific_fields" + assert container_field["id"] == "container_011CW9hA9zpZ8xD3bjjShy4p", "container id should match" + assert container_field["expires_at"] == "2025-12-16T04:57:16.913181Z", "expires_at should match" assert len(container_field["skills"]) == 1, "Should have 1 skill" assert container_field["skills"][0]["skill_id"] == "pptx", "skill_id should be pptx" assert container_field["skills"][0]["version"] == "20251013", "version should match" @@ -1529,9 +1395,7 @@ def test_container_in_provider_specific_fields_non_streaming(): When container with skills is used in non-streaming, the container field should be present in the provider_specific_fields of the response. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=False, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=False, json_mode=False) # Simulate a message_delta chunk with container (as it would appear in non-streaming) message_delta_chunk = { @@ -1567,21 +1431,13 @@ def test_container_in_provider_specific_fields_non_streaming(): # Verify container is in provider_specific_fields assert model_response.choices[0].delta.provider_specific_fields is not None assert "container" in model_response.choices[0].delta.provider_specific_fields - container_field = model_response.choices[0].delta.provider_specific_fields[ - "container" - ] + container_field = model_response.choices[0].delta.provider_specific_fields["container"] assert container_field["id"] == "container_abc123xyz", "container id should match" - assert ( - container_field["expires_at"] == "2025-12-20T10:30:00.000000Z" - ), "expires_at should match" + assert container_field["expires_at"] == "2025-12-20T10:30:00.000000Z", "expires_at should match" assert len(container_field["skills"]) == 2, "Should have 2 skills" - assert ( - container_field["skills"][0]["skill_id"] == "code_execution" - ), "First skill_id should be code_execution" - assert ( - container_field["skills"][1]["skill_id"] == "pptx" - ), "Second skill_id should be pptx" + assert container_field["skills"][0]["skill_id"] == "code_execution", "First skill_id should be code_execution" + assert container_field["skills"][1]["skill_id"] == "pptx", "Second skill_id should be pptx" def test_container_absent_when_not_provided(): @@ -1590,9 +1446,7 @@ def test_container_absent_when_not_provided(): This ensures we don't add empty or None container fields. """ - iterator = ModelResponseIterator( - streaming_response=MagicMock(), sync_stream=False, json_mode=False - ) + iterator = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=False, json_mode=False) # message_delta without container message_delta_chunk = { @@ -1611,9 +1465,9 @@ def test_container_absent_when_not_provided(): # Verify container is NOT in provider_specific_fields when not provided if model_response.choices[0].delta.provider_specific_fields: - assert ( - "container" not in model_response.choices[0].delta.provider_specific_fields - ), "container should not be present when not provided in delta" + assert "container" not in model_response.choices[0].delta.provider_specific_fields, ( + "container should not be present when not provided in delta" + ) def test_streaming_code_execution_produces_code_interpreter_results(): @@ -1809,8 +1663,7 @@ def test_streaming_multiple_code_executions_no_duplicates(): # Second (final) emission: cumulative list with BOTH results # This is what stream_chunk_builder will pick as "last value wins" assert len(emissions[1]) == 2, ( - f"Expected final emission to have 2 results, got {len(emissions[1])}. " - f"IDs: {[r.id for r in emissions[1]]}" + f"Expected final emission to have 2 results, got {len(emissions[1])}. IDs: {[r.id for r in emissions[1]]}" ) assert emissions[1][0].id == "srvtoolu_01AAA" assert emissions[1][0].code == "echo first" @@ -1974,9 +1827,7 @@ def test_empty_output_produces_null_outputs(): assert code_results is not None, "No code_interpreter_results emitted" assert len(code_results) == 1 assert code_results[0].id == "srvtoolu_01AAA" - assert ( - code_results[0].outputs is None - ), f"Expected outputs=None for empty execution, got {code_results[0].outputs}" + assert code_results[0].outputs is None, f"Expected outputs=None for empty execution, got {code_results[0].outputs}" def test_non_bash_tool_result_skipped(): @@ -2039,12 +1890,10 @@ def test_non_bash_tool_result_skipped(): code_results = psf["code_interpreter_results"] # code_interpreter_results should be emitted but empty (no bash results) - assert ( - code_results is not None - ), "Expected code_interpreter_results key to be emitted" - assert ( - len(code_results) == 0 - ), f"Expected 0 code_interpreter_results for text_editor result, got {len(code_results)}" + assert code_results is not None, "Expected code_interpreter_results key to be emitted" + assert len(code_results) == 0, ( + f"Expected 0 code_interpreter_results for text_editor result, got {len(code_results)}" + ) class TestRustChatCompletionsHook: @@ -2081,13 +1930,9 @@ class TestRustChatCompletionsHook: from litellm.rust_bridge import chat_completions as bridge monkeypatch.delenv("LITELLM_RUST", raising=False) - bridge.set_rust_chat_completions( - chat_completions=None, achat_completions=None, decline=None - ) + bridge.set_rust_chat_completions(chat_completions=None, achat_completions=None, decline=None) yield - bridge.set_rust_chat_completions( - chat_completions=None, achat_completions=None, decline=None - ) + bridge.set_rust_chat_completions(chat_completions=None, achat_completions=None, decline=None) @staticmethod def _completion_kwargs(**overrides): @@ -2185,9 +2030,7 @@ class TestRustChatCompletionsHook: from litellm.llms.anthropic.chat.handler import AnthropicChatCompletion seen = self._inject() - AnthropicChatCompletion().completion( - **self._completion_kwargs(optional_params={"max_tokens": 7}) - ) + AnthropicChatCompletion().completion(**self._completion_kwargs(optional_params={"max_tokens": 7})) assert seen["call"][0]["optional_params"]["max_tokens"] == 7 def test_without_the_opt_in_the_core_is_never_consulted(self): @@ -2195,15 +2038,14 @@ class TestRustChatCompletionsHook: from litellm.llms.anthropic.chat.transformation import AnthropicConfig seen = self._inject() - with patch.object( - AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []} - ) as transform, patch.object( - AnthropicChatCompletion, "acompletion_function" + with ( + patch.object( + AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []} + ) as transform, + patch.object(AnthropicChatCompletion, "acompletion_function"), ): try: - AnthropicChatCompletion().completion( - **self._completion_kwargs(litellm_params={}) - ) + AnthropicChatCompletion().completion(**self._completion_kwargs(litellm_params={})) except Exception: # The Python path goes on to make an HTTP call; reaching it is # the assertion, so the network failure below is expected. @@ -2217,9 +2059,7 @@ class TestRustChatCompletionsHook: from litellm.llms.anthropic.chat.transformation import AnthropicConfig seen = self._inject(decline_reason="unrecognized request parameter") - with patch.object( - AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []} - ): + with patch.object(AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []}): try: AnthropicChatCompletion().completion(**self._completion_kwargs()) except Exception: @@ -2232,9 +2072,7 @@ class TestRustChatCompletionsHook: from litellm.llms.anthropic.chat.transformation import AnthropicConfig seen = self._inject() - with patch.object( - AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []} - ): + with patch.object(AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []}): try: AnthropicChatCompletion().completion( **self._completion_kwargs(optional_params={"max_tokens": 16, "stream": True}) @@ -2248,9 +2086,7 @@ class TestRustChatCompletionsHook: seen = self._inject() logging_obj = MagicMock() - AnthropicChatCompletion().completion( - **self._completion_kwargs(logging_obj=logging_obj) - ) + AnthropicChatCompletion().completion(**self._completion_kwargs(logging_obj=logging_obj)) assert logging_obj.pre_call.call_count == 1 assert len(seen["call"]) == 1 @@ -2264,9 +2100,7 @@ class TestRustChatCompletionsHook: self._inject() logging_obj = MagicMock() - AnthropicChatCompletion().completion( - **self._completion_kwargs(logging_obj=logging_obj) - ) + AnthropicChatCompletion().completion(**self._completion_kwargs(logging_obj=logging_obj)) assert logging_obj.post_call.call_count == 1 logged = logging_obj.post_call.call_args.kwargs["original_response"] @@ -2291,18 +2125,12 @@ class TestRustChatCompletionsHook: raise _Declined("blank message text") monkeypatch.setattr(bridge, "get_native_bridge", lambda: _FakeNative()) - bridge.set_rust_chat_completions( - decline=lambda **_kwargs: None, chat_completions=declining_native - ) + bridge.set_rust_chat_completions(decline=lambda **_kwargs: None, chat_completions=declining_native) logging_obj, calls = self._recording_logging_obj() - with patch.object( - AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []} - ): + with patch.object(AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []}): try: - AnthropicChatCompletion().completion( - **self._completion_kwargs(logging_obj=logging_obj) - ) + AnthropicChatCompletion().completion(**self._completion_kwargs(logging_obj=logging_obj)) except Exception: # The Python path goes on to make an HTTP call; the log count is # the assertion, so a failure past this point is expected. @@ -2327,21 +2155,15 @@ class TestRustChatCompletionsHook: async def declining_native(**_kwargs): raise _Declined("blank message text") - bridge.set_rust_chat_completions( - decline=lambda **_kwargs: None, achat_completions=declining_native - ) + bridge.set_rust_chat_completions(decline=lambda **_kwargs: None, achat_completions=declining_native) sentinel = object() async def python_path(**_kwargs): return sentinel - with patch.object( - AnthropicChatCompletion, "acompletion_function", side_effect=python_path - ) as python_call: - result = await AnthropicChatCompletion().completion( - **self._completion_kwargs(acompletion=True) - ) + with patch.object(AnthropicChatCompletion, "acompletion_function", side_effect=python_path) as python_call: + result = await AnthropicChatCompletion().completion(**self._completion_kwargs(acompletion=True)) assert result is sentinel assert python_call.called, "a failing rust call must re-enter the python path" @@ -2354,20 +2176,15 @@ class TestRustChatCompletionsHook: async def native(**_kwargs): return dict(self.RUST_RESPONSE) - bridge.set_rust_chat_completions( - decline=lambda **_kwargs: None, achat_completions=native - ) + bridge.set_rust_chat_completions(decline=lambda **_kwargs: None, achat_completions=native) with patch.object(AnthropicChatCompletion, "acompletion_function") as python_call: - result = await AnthropicChatCompletion().completion( - **self._completion_kwargs(acompletion=True) - ) + result = await AnthropicChatCompletion().completion(**self._completion_kwargs(acompletion=True)) assert result.choices[0].message.content == "hello from rust" assert result._hidden_params["additional_headers"] == {"x-litellm-rust": "true"} assert not python_call.called - def test_pre_call_logging_fires_once_when_the_sync_rust_call_declines(self, monkeypatch): """One request, one pre_call, on the synchronous path too. Without the suppression the Python path logs a second time for the same attempt.""" @@ -2387,27 +2204,19 @@ class TestRustChatCompletionsHook: def declining_native(**_kwargs): raise _Declined("blank message text") - bridge.set_rust_chat_completions( - decline=lambda **_kwargs: None, chat_completions=declining_native - ) + bridge.set_rust_chat_completions(decline=lambda **_kwargs: None, chat_completions=declining_native) logging_obj, calls = self._recording_logging_obj() - with patch.object( - AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []} - ): + with patch.object(AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []}): try: - AnthropicChatCompletion().completion( - **self._completion_kwargs(logging_obj=logging_obj) - ) + AnthropicChatCompletion().completion(**self._completion_kwargs(logging_obj=logging_obj)) except Exception: # The Python path goes on to make an HTTP call; the log count is # the assertion, so a failure past this point is expected. pass assert len(calls["pre_call"]) == 1 - assert calls["pre_call"][0]["additional_args"]["complete_input_dict"]["model"] == ( - "claude-sonnet-4-5" - ) + assert calls["pre_call"][0]["additional_args"]["complete_input_dict"]["model"] == ("claude-sonnet-4-5") def test_pre_call_logging_still_fires_when_rust_is_not_involved(self, monkeypatch): """The suppression must not swallow the log on the ordinary path.""" @@ -2416,9 +2225,7 @@ class TestRustChatCompletionsHook: self._inject() logging_obj, calls = self._recording_logging_obj() - with patch.object( - AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []} - ): + with patch.object(AnthropicConfig, "transform_request", return_value={"model": "m", "messages": []}): try: AnthropicChatCompletion().completion( **self._completion_kwargs(litellm_params={}, logging_obj=logging_obj) diff --git a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py index 4f340ee0f3f..1f250bd3ae5 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py +++ b/tests/test_litellm/llms/anthropic/chat/test_anthropic_chat_transformation.py @@ -1,4 +1,3 @@ - import pytest from unittest.mock import MagicMock, patch @@ -33,13 +32,9 @@ def test_response_format_transformation_unit_test(): "additionalProperties": False, } - result = config._create_json_tool_call_for_response_format( - json_schema=response_format_json_schema - ) + result = config._create_json_tool_call_for_response_format(json_schema=response_format_json_schema) - assert result["input_schema"]["properties"] == { - "agent_doing": {"title": "Agent Doing", "type": "string"} - } + assert result["input_schema"]["properties"] == {"agent_doing": {"title": "Agent Doing", "type": "string"}} print(result) @@ -508,9 +503,7 @@ def test_extract_response_content_with_citations(): }, } - _, citations, _, _, _, _, _, _ = config.extract_response_content( - completion_response - ) + _, citations, _, _, _, _, _, _ = config.extract_response_content(completion_response) assert citations == [ [ { @@ -583,12 +576,8 @@ def test_web_search_tool_transformation(): assert anthropic_web_search_tool["user_location"]["city"] == "San Francisco" -@pytest.mark.parametrize( - "search_context_size, expected_max_uses", [("low", 1), ("medium", 5), ("high", 10)] -) -def test_web_search_tool_transformation_with_search_context_size( - search_context_size, expected_max_uses -): +@pytest.mark.parametrize("search_context_size, expected_max_uses", [("low", 1), ("medium", 5), ("high", 10)]) +def test_web_search_tool_transformation_with_search_context_size(search_context_size, expected_max_uses): from litellm.types.llms.openai import OpenAIWebSearchOptions config = AnthropicConfig() @@ -763,10 +752,7 @@ def test_web_search_tool_result_in_provider_specific_fields(): assert "web_search_results" in provider_fields assert len(provider_fields["web_search_results"]) == 1 assert provider_fields["web_search_results"][0]["type"] == "web_search_tool_result" - assert ( - provider_fields["web_search_results"][0]["tool_use_id"] - == "srvtoolu_provider_test" - ) + assert provider_fields["web_search_results"][0]["tool_use_id"] == "srvtoolu_provider_test" def test_multiple_web_search_tool_results(): @@ -990,10 +976,7 @@ def test_transform_response_with_prefix_prompt(): ) assert result is not None - assert ( - result.choices[0].message.content - == "You are a helpful assistant. The grass is green." - ) + assert result.choices[0].message.content == "You are a helpful assistant. The grass is green." def test_get_supported_params_thinking(): @@ -1107,18 +1090,12 @@ def test_anthropic_beta_header_merging_with_output_format(): } } - result_headers = config.update_headers_with_optional_anthropic_beta( - headers, optional_params - ) + result_headers = config.update_headers_with_optional_anthropic_beta(headers, optional_params) # Both beta headers should be present beta_value = result_headers["anthropic-beta"] - assert ( - "context-1m-2025-08-07" in beta_value - ), f"User's context-1m beta header missing from: {beta_value}" - assert ( - "structured-outputs-2025-11-13" in beta_value - ), f"Structured output beta header missing from: {beta_value}" + assert "context-1m-2025-08-07" in beta_value, f"User's context-1m beta header missing from: {beta_value}" + assert "structured-outputs-2025-11-13" in beta_value, f"Structured output beta header missing from: {beta_value}" def test_anthropic_beta_header_merging_with_multiple_features(): @@ -1140,9 +1117,7 @@ def test_anthropic_beta_header_merging_with_multiple_features(): "tools": [{"type": "web_fetch_20250910", "name": "web_fetch"}], } - result_headers = config.update_headers_with_optional_anthropic_beta( - headers, optional_params - ) + result_headers = config.update_headers_with_optional_anthropic_beta(headers, optional_params) beta_value = result_headers["anthropic-beta"] @@ -1185,9 +1160,7 @@ def test_anthropic_structured_output_beta_header(): "strict": True, "schema": { "description": 'Progress report for the thinking process\n\nThis model represents a snapshot of the agent\'s current progress during\nthe thinking process, providing a brief description of the current activity.\n\nAttributes:\n agent_doing: Brief description of what the agent is currently doing.\n Should be kept under 10 words. Example: "Learning about home automation"', - "properties": { - "agent_doing": {"title": "Agent Doing", "type": "string"} - }, + "properties": {"agent_doing": {"title": "Agent Doing", "type": "string"}}, "required": ["agent_doing"], "title": "ThinkingStep", "type": "object", @@ -1201,10 +1174,7 @@ def test_anthropic_structured_output_beta_header(): assert response is not None print(f"response: {response}") print(f"raw_request_headers: {response['raw_request_headers']}") - assert ( - "structured-outputs-2025-11-13" - in response["raw_request_headers"]["anthropic-beta"] - ) + assert "structured-outputs-2025-11-13" in response["raw_request_headers"]["anthropic-beta"] @pytest.mark.parametrize( @@ -1340,9 +1310,7 @@ def test_tool_search_regex_detection(): config = AnthropicModelInfo() # Test with tool search regex tool - tools = [ - {"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"} - ] + tools = [{"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"}] assert config.is_tool_search_used(tools) is True # Test without tool search @@ -1357,9 +1325,7 @@ def test_tool_search_bm25_detection(): config = AnthropicModelInfo() # Test with tool search BM25 tool - tools = [ - {"type": "tool_search_tool_bm25_20251119", "name": "tool_search_tool_bm25"} - ] + tools = [{"type": "tool_search_tool_bm25_20251119", "name": "tool_search_tool_bm25"}] assert config.is_tool_search_used(tools) is True @@ -1551,9 +1517,7 @@ def test_tool_search_complete_response_parsing(): "tool_use_id": "srvtoolu_015i6aVA2niwzv4RG4DtnxDJ", "content": { "type": "tool_search_tool_search_result", - "tool_references": [ - {"type": "tool_reference", "tool_name": "get_weather"} - ], + "tool_references": [{"type": "tool_reference", "tool_name": "get_weather"}], }, }, {"type": "text", "text": "Great! I found a weather tool."}, @@ -1604,9 +1568,7 @@ def test_tool_search_complete_response_parsing(): assert usage.server_tool_use is not None assert usage.server_tool_use.web_search_requests == 0 - assert ( - usage.server_tool_use.tool_search_requests == 1 - ) # Counted from server_tool_use blocks + assert usage.server_tool_use.tool_search_requests == 1 # Counted from server_tool_use blocks def test_allowed_callers_field_preservation(): @@ -1658,9 +1620,7 @@ def test_programmatic_tool_calling_beta_header(): assert is_programmatic is True # Test header generation - headers = model_info.get_anthropic_headers( - api_key="test-key", programmatic_tool_calling_used=True - ) + headers = model_info.get_anthropic_headers(api_key="test-key", programmatic_tool_calling_used=True) assert "anthropic-beta" in headers assert "advanced-tool-use-2025-11-20" in headers["anthropic-beta"] @@ -1804,9 +1764,7 @@ def test_input_examples_beta_header(): assert is_examples_used is True # Test header generation - headers = model_info.get_anthropic_headers( - api_key="test-key", input_examples_used=True - ) + headers = model_info.get_anthropic_headers(api_key="test-key", input_examples_used=True) assert "anthropic-beta" in headers assert "advanced-tool-use-2025-11-20" in headers["anthropic-beta"] @@ -1892,10 +1850,7 @@ def test_input_examples_empty_list_not_added(): transformed_tool, _ = config._map_tool_helper(tool) assert transformed_tool is not None # Empty list should not be added - assert ( - "input_examples" not in transformed_tool - or len(transformed_tool.get("input_examples", [])) == 0 - ) + assert "input_examples" not in transformed_tool or len(transformed_tool.get("input_examples", [])) == 0 # ============ Effort Parameter Tests ============ @@ -1955,9 +1910,7 @@ def test_effort_beta_header_injection(): effort_used = model_info.is_effort_used(optional_params=optional_params, custom_llm_provider="anthropic") assert effort_used is True - headers = model_info.get_anthropic_headers( - api_key="test-key", effort_used=effort_used - ) + headers = model_info.get_anthropic_headers(api_key="test-key", effort_used=effort_used) assert "anthropic-beta" in headers assert "effort-2025-11-24" in headers["anthropic-beta"] @@ -1983,9 +1936,7 @@ def test_effort_validation(): optional_params = {"output_config": {"effort": "invalid"}} - with pytest.raises( - litellm.exceptions.BadRequestError, match="Invalid effort value" - ): + with pytest.raises(litellm.exceptions.BadRequestError, match="Invalid effort value"): config.transform_request( model="claude-opus-4-5-20251101", messages=messages, @@ -2221,16 +2172,8 @@ def test_anthropic_model_supports_speed_param_rejects_non_anthropic_providers( ): """Fast mode is direct-Anthropic-only. Vertex/Azure/Bedrock strip their prefix before the shared transform runs, so the bare Opus id must still be rejected.""" - assert ( - AnthropicConfig._model_supports_speed_param( - "claude-opus-4-8", custom_llm_provider - ) - is False - ) - assert ( - AnthropicConfig._model_supports_speed_param("claude-opus-4-8", "anthropic") - is True - ) + assert AnthropicConfig._model_supports_speed_param("claude-opus-4-8", custom_llm_provider) is False + assert AnthropicConfig._model_supports_speed_param("claude-opus-4-8", "anthropic") is True def test_vertex_anthropic_drops_speed_for_opus_with_drop_params(monkeypatch): @@ -2529,9 +2472,7 @@ def test_supports_effort_level_handles_provider_prefixes(model, level, expected) ("claude-opus-4-5-20251101", None, False), ], ) -def test_validate_effort_for_model_centralises_per_model_gating( - model, effort, expect_error -): +def test_validate_effort_for_model_centralises_per_model_gating(model, effort, expect_error): err = AnthropicConfig._validate_effort_for_model(model, effort, "anthropic") if expect_error: assert err is not None @@ -2580,11 +2521,7 @@ def test_transform_request_injects_dummy_tool_without_tools_param(): litellm.modify_params = prev_modify_params assert "tools" in result - names = [ - t.get("name") - for t in result["tools"] - if isinstance(t, dict) and t.get("name") is not None - ] + names = [t.get("name") for t in result["tools"] if isinstance(t, dict) and t.get("name") is not None] assert "dummy_tool" in names @@ -2673,13 +2610,9 @@ def test_calculate_usage_completion_tokens_details_with_reasoning(): "output_tokens": 500, } # Simulating reasoning content that would count as ~50 tokens - reasoning_content = ( - "Let me think about this step by step. " * 10 - ) # Roughly 50 tokens + reasoning_content = "Let me think about this step by step. " * 10 # Roughly 50 tokens - usage = config.calculate_usage( - usage_object=usage_object, reasoning_content=reasoning_content - ) + usage = config.calculate_usage(usage_object=usage_object, reasoning_content=reasoning_content) # completion_tokens_details should be populated with both reasoning and text tokens assert usage.completion_tokens_details is not None @@ -2730,9 +2663,7 @@ def test_reasoning_effort_maps_to_adaptive_thinking_for_claude_4_6_models(): # reasoning_effort should not be in the result (it's transformed to thinking) assert "reasoning_effort" not in result # Should set output_config with the mapped effort value - assert ( - "output_config" in result - ), f"output_config missing for {model} with effort={effort}" + assert "output_config" in result, f"output_config missing for {model} with effort={effort}" assert result["output_config"]["effort"] == effort_map[effort] @@ -2808,7 +2739,6 @@ def test_raw_adaptive_thinking_untouched_for_46_plus_model(): assert result["thinking"] == {"type": "adaptive"} - @pytest.mark.parametrize( "model, expected", [ @@ -2834,9 +2764,7 @@ def test_raw_adaptive_thinking_untouched_for_46_plus_model(): ("gpt-4o", False), ], ) -def test_is_adaptive_thinking_model_is_sourced_from_cost_map( - local_model_cost_map, model, expected -): +def test_is_adaptive_thinking_model_is_sourced_from_cost_map(local_model_cost_map, model, expected): """Adaptive thinking resolves from the cost map first (an explicit supports_adaptive_thinking entry, or the anthropic-claude fallback rule for unmapped future Claudes), then from a date-safe opus/sonnet/haiku >= 4.6 name version as a @@ -2952,9 +2880,7 @@ def test_reasoning_effort_sets_output_config_for_46_models(): drop_params=False, ) - assert ( - "output_config" in result - ), f"output_config missing for {model} with effort={effort}" + assert "output_config" in result, f"output_config missing for {model} with effort={effort}" assert result["output_config"]["effort"] == effort @@ -2993,9 +2919,7 @@ def test_reasoning_effort_does_not_set_output_config_for_older_models(): drop_params=False, ) - assert ( - "output_config" not in result - ), f"output_config should not be set for {model}" + assert "output_config" not in result, f"output_config should not be set for {model}" @pytest.mark.parametrize( @@ -3035,14 +2959,10 @@ def test_reasoning_effort_accepts_dict_shape_for_adaptive_model(reasoning_effort ) # thinking must be set (adaptive for 4.6+) - assert ( - "thinking" in result - ), f"thinking missing for reasoning_effort={reasoning_effort_value!r}" + assert "thinking" in result, f"thinking missing for reasoning_effort={reasoning_effort_value!r}" assert result["thinking"]["type"] == "adaptive" # output_config must carry the mapped effort - assert ( - "output_config" in result - ), f"output_config missing for reasoning_effort={reasoning_effort_value!r}" + assert "output_config" in result, f"output_config missing for reasoning_effort={reasoning_effort_value!r}" assert result["output_config"]["effort"] == "low" @@ -3071,16 +2991,13 @@ def test_reasoning_effort_accepts_dict_shape_for_non_adaptive_model( drop_params=False, ) - assert ( - "thinking" in result - ), f"thinking missing for reasoning_effort={reasoning_effort_value!r}" + assert "thinking" in result, f"thinking missing for reasoning_effort={reasoning_effort_value!r}" assert result["thinking"]["type"] == "enabled" assert "budget_tokens" in result["thinking"] assert result["thinking"]["budget_tokens"] > 0 # Older models must not get adaptive-thinking output_config assert "output_config" not in result, ( - f"output_config should not be set for non-adaptive model " - f"(reasoning_effort={reasoning_effort_value!r})" + f"output_config should not be set for non-adaptive model (reasoning_effort={reasoning_effort_value!r})" ) @@ -3106,12 +3023,8 @@ def test_reasoning_effort_unparseable_dict_is_dropped(bad_value): model="claude-sonnet-4-6-20260219", drop_params=False, ) - assert ( - "thinking" not in result - ), f"thinking should not be set for bad value {bad_value!r}" - assert ( - "output_config" not in result - ), f"output_config should not be set for bad value {bad_value!r}" + assert "thinking" not in result, f"thinking should not be set for bad value {bad_value!r}" + assert "output_config" not in result, f"output_config should not be set for bad value {bad_value!r}" @pytest.mark.parametrize( @@ -3242,9 +3155,7 @@ def test_reasoning_effort_garbage_raises_bad_request(effort): ("max", DEFAULT_REASONING_EFFORT_MAX_THINKING_BUDGET), ], ) -def test_reasoning_effort_xhigh_max_maps_to_budget_on_budget_model( - effort, expected_budget -): +def test_reasoning_effort_xhigh_max_maps_to_budget_on_budget_model(effort, expected_budget): """``xhigh`` / ``max`` extend the budget_tokens progression on budget-mode models.""" config = AnthropicConfig() @@ -3391,17 +3302,11 @@ def test_code_execution_tool_results_extraction(): # Verify first tool call assert transformed_response.choices[0].message.tool_calls[0].id == "srvtoolu_01ABC" - assert ( - transformed_response.choices[0].message.tool_calls[0].function.name - == "bash_code_execution" - ) + assert transformed_response.choices[0].message.tool_calls[0].function.name == "bash_code_execution" # Verify second tool call assert transformed_response.choices[0].message.tool_calls[1].id == "srvtoolu_01DEF" - assert ( - transformed_response.choices[0].message.tool_calls[1].function.name - == "text_editor_code_execution" - ) + assert transformed_response.choices[0].message.tool_calls[1].function.name == "text_editor_code_execution" # Verify tool results are in provider_specific_fields provider_fields = transformed_response.choices[0].message.provider_specific_fields @@ -3424,10 +3329,7 @@ def test_code_execution_tool_results_extraction(): assert editor_result["content"]["is_file_update"] is False # Verify text content is properly concatenated - assert ( - "I'll calculate that for you." - in transformed_response.choices[0].message.content - ) + assert "I'll calculate that for you." in transformed_response.choices[0].message.content assert "Done!" in transformed_response.choices[0].message.content @@ -3495,10 +3397,7 @@ def test_code_execution_tool_results_in_hidden_params(): assert "provider_specific_fields" in hidden assert "tool_results" in hidden["provider_specific_fields"] assert len(hidden["provider_specific_fields"]["tool_results"]) == 1 - assert ( - hidden["provider_specific_fields"]["tool_results"][0]["content"]["stdout"] - == "hello\n" - ) + assert hidden["provider_specific_fields"]["tool_results"][0]["content"]["stdout"] == "hello\n" def test_tool_search_tool_result_not_in_tool_results(): @@ -3694,10 +3593,7 @@ def test_compaction_block_in_provider_specific_fields(): assert "compaction_blocks" in provider_fields assert len(provider_fields["compaction_blocks"]) == 1 assert provider_fields["compaction_blocks"][0]["type"] == "compaction" - assert ( - "Summary of the conversation" - in provider_fields["compaction_blocks"][0]["content"] - ) + assert "Summary of the conversation" in provider_fields["compaction_blocks"][0]["content"] def test_multiple_compaction_blocks(): @@ -3745,9 +3641,7 @@ def test_compaction_block_request_transformation(): {"role": "user", "content": "What is the weather in San Francisco?"}, { "role": "assistant", - "content": [ - {"type": "text", "text": "I don't have access to real-time data."} - ], + "content": [{"type": "text", "text": "I don't have access to real-time data."}], "provider_specific_fields": { "compaction_blocks": [ { @@ -3760,9 +3654,7 @@ def test_compaction_block_request_transformation(): {"role": "user", "content": "What about New York?"}, ] - result = anthropic_messages_pt( - messages=messages, model="claude-opus-4-6", llm_provider="anthropic" - ) + result = anthropic_messages_pt(messages=messages, model="claude-opus-4-6", llm_provider="anthropic") # Find the assistant message assistant_message = None @@ -3876,9 +3768,7 @@ def test_map_openai_context_management_to_anthropic(): "instructions": "Focus on preserving code snippets", } ] - result = config.map_openai_context_management_to_anthropic( - openai_format_with_instructions - ) + result = config.map_openai_context_management_to_anthropic(openai_format_with_instructions) assert result is not None assert result["edits"][0]["trigger"]["value"] == 150000 @@ -3905,9 +3795,7 @@ def test_map_openai_params_with_context_management(): config = AnthropicConfig() # Test with OpenAI list format - non_default_params = { - "context_management": [{"type": "compaction", "compact_threshold": 200000}] - } + non_default_params = {"context_management": [{"type": "compaction", "compact_threshold": 200000}]} optional_params = {} result = config.map_openai_params( @@ -3944,10 +3832,7 @@ def test_map_openai_params_with_context_management(): ) assert "context_management" in result - assert ( - result["context_management"] - == non_default_params_anthropic["context_management"] - ) + assert result["context_management"] == non_default_params_anthropic["context_management"] def test_cache_control_in_supported_params(): @@ -4058,10 +3943,7 @@ def test_compaction_block_empty_list_not_added(): # Verify compaction_blocks is not in provider_specific_fields when there are none provider_fields = result.choices[0].message.provider_specific_fields if provider_fields: - assert ( - "compaction_blocks" not in provider_fields - or provider_fields.get("compaction_blocks") is None - ) + assert "compaction_blocks" not in provider_fields or provider_fields.get("compaction_blocks") is None def test_fast_mode_beta_header(): @@ -4110,9 +3992,7 @@ def test_fast_mode_usage_calculation(): "output_tokens": 500, } - usage = config.calculate_usage( - usage_object=usage_object, reasoning_content=None, speed="fast" - ) + usage = config.calculate_usage(usage_object=usage_object, reasoning_content=None, speed="fast") assert usage.prompt_tokens == 1000 assert usage.completion_tokens == 500 @@ -4133,9 +4013,7 @@ def test_fast_mode_cost_calculation(): base_completion = 0.025 with ( - patch( - "litellm.llms.anthropic.cost_calculation.generic_cost_per_token" - ) as mock_cost, + patch("litellm.llms.anthropic.cost_calculation.generic_cost_per_token") as mock_cost, patch("litellm.get_model_info") as mock_info, ): mock_cost.return_value = (base_prompt, base_completion) @@ -4175,9 +4053,7 @@ def test_fast_mode_with_inference_geo(): base_completion = 0.025 with ( - patch( - "litellm.llms.anthropic.cost_calculation.generic_cost_per_token" - ) as mock_cost, + patch("litellm.llms.anthropic.cost_calculation.generic_cost_per_token") as mock_cost, patch("litellm.get_model_info") as mock_info, ): mock_cost.return_value = (base_prompt, base_completion) @@ -4368,9 +4244,7 @@ def test_map_tool_helper_enforces_object_type_when_missing(): "name": "search_code", "description": "Search for code patterns", "parameters": { - "properties": { - "query": {"type": "string", "description": "Search query"} - }, + "properties": {"query": {"type": "string", "description": "Search query"}}, "required": ["query"], }, }, @@ -4383,9 +4257,9 @@ def test_map_tool_helper_enforces_object_type_when_missing(): assert "properties" in result["input_schema"] assert "query" in result["input_schema"]["properties"] # Original parameters dict must not be modified in place - assert ( - tool["function"]["parameters"] == original_params - ), "parameters dict was mutated; _map_tool_helper should not modify caller data" + assert tool["function"]["parameters"] == original_params, ( + "parameters dict was mutated; _map_tool_helper should not modify caller data" + ) def test_map_tool_helper_enforces_object_type_when_wrong_type(): @@ -4411,13 +4285,13 @@ def test_map_tool_helper_enforces_object_type_when_wrong_type(): result, _ = config._map_tool_helper(tool) assert result is not None assert result["input_schema"]["type"] == "object" - assert ( - result["input_schema"].get("properties") == {} - ), "properties should be injected as {} when schema has non-object type and no properties key" + assert result["input_schema"].get("properties") == {}, ( + "properties should be injected as {} when schema has non-object type and no properties key" + ) # Original parameters dict must not be modified in place - assert ( - tool["function"]["parameters"] == original_params - ), "parameters dict was mutated; _map_tool_helper should not modify caller data" + assert tool["function"]["parameters"] == original_params, ( + "parameters dict was mutated; _map_tool_helper should not modify caller data" + ) def test_map_tool_helper_preserves_valid_object_schema(): @@ -4484,12 +4358,8 @@ def test_extract_response_content_thinking_block_null_thinking(): {"type": "text", "text": "Hello"}, ] } - text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content( - completion_response_null - ) - assert ( - thinking_blocks is not None - ), "thinking blocks should not be None when thinking=null" + text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content(completion_response_null) + assert thinking_blocks is not None, "thinking blocks should not be None when thinking=null" assert len(thinking_blocks) == 1 assert "Hello" in text @@ -4500,12 +4370,8 @@ def test_extract_response_content_thinking_block_null_thinking(): {"type": "text", "text": "World"}, ] } - text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content( - completion_response_missing - ) - assert ( - thinking_blocks is not None - ), "thinking blocks should not be None when thinking key is absent" + text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content(completion_response_missing) + assert thinking_blocks is not None, "thinking blocks should not be None when thinking key is absent" assert len(thinking_blocks) == 1 assert "World" in text @@ -4516,9 +4382,7 @@ def test_extract_response_content_thinking_block_null_thinking(): {"type": "text", "text": "Done"}, ] } - text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content( - completion_response_text - ) + text, _, thinking_blocks, _, _, _, _, _ = config.extract_response_content(completion_response_text) assert thinking_blocks is not None assert len(thinking_blocks) == 1 assert thinking_blocks[0]["thinking"] == "Let me think..." @@ -4577,12 +4441,8 @@ def test_advisor_beta_header_injected(): } ] } - result = config.update_headers_with_optional_anthropic_beta( - headers, optional_params - ) - assert ANTHROPIC_BETA_HEADER_VALUES.ADVISOR_TOOL_2026_03_01.value in result.get( - "anthropic-beta", "" - ) + result = config.update_headers_with_optional_anthropic_beta(headers, optional_params) + assert ANTHROPIC_BETA_HEADER_VALUES.ADVISOR_TOOL_2026_03_01.value in result.get("anthropic-beta", "") def test_advisor_beta_header_not_injected_without_tool(): @@ -4590,9 +4450,7 @@ def test_advisor_beta_header_not_injected_without_tool(): config = AnthropicConfig() headers: dict = {} optional_params: dict = {"tools": []} - result = config.update_headers_with_optional_anthropic_beta( - headers, optional_params - ) + result = config.update_headers_with_optional_anthropic_beta(headers, optional_params) assert "advisor-tool-2026-03-01" not in result.get("anthropic-beta", "") @@ -4619,9 +4477,7 @@ def test_advisor_tool_result_preserved_in_response(): {"type": "text", "text": "Here is the implementation."}, ] } - text, _, _, _, tool_calls, _, tool_results, _ = config.extract_response_content( - completion_response - ) + text, _, _, _, tool_calls, _, tool_results, _ = config.extract_response_content(completion_response) assert "Consulting advisor." in text assert "Here is the implementation." in text # server_tool_use (advisor) should be a tool_call @@ -4736,9 +4592,7 @@ def test_basic_sanitize_anthropic_tool_name_replaces_invalid_chars(): ) assert ( - _basic_sanitize_anthropic_tool_name( - "github_openapi_mcp-actions/download-job-logs-for-workflow-run" - ) + _basic_sanitize_anthropic_tool_name("github_openapi_mcp-actions/download-job-logs-for-workflow-run") == "github_openapi_mcp-actions_download-job-logs-for-workflow-run" ) # other punctuation @@ -4767,9 +4621,7 @@ def test_build_anthropic_tool_name_maps_no_collisions(): ] ) assert forward == { - "actions/download-job-logs-for-workflow-run": ( - "actions_download-job-logs-for-workflow-run" - ), + "actions/download-job-logs-for-workflow-run": ("actions_download-job-logs-for-workflow-run"), "pulls/list-files": "pulls_list-files", } assert reverse == {v: k for k, v in forward.items()} @@ -4820,9 +4672,7 @@ def test_build_anthropic_tool_name_maps_three_way_collision(): _build_anthropic_tool_name_maps, ) - forward, reverse = _build_anthropic_tool_name_maps( - ["foo_bar", "foo/bar", "foo.bar"] - ) + forward, reverse = _build_anthropic_tool_name_maps(["foo_bar", "foo/bar", "foo.bar"]) assert "foo_bar" not in forward # untouched assert forward["foo/bar"] == "foo_bar_2" assert forward["foo.bar"] == "foo_bar_3" @@ -4895,16 +4745,13 @@ def test_map_openai_params_does_not_pollute_optional_params_with_internal_keys() ) # No internal keys may appear in optional_params for ANY input. for key in optional_params: - assert not key.startswith( - "_anthropic_tool_name" - ), f"optional_params leaked internal key {key!r}: {optional_params}" + assert not key.startswith("_anthropic_tool_name"), ( + f"optional_params leaked internal key {key!r}: {optional_params}" + ) # And no key starting with `_` either; optional_params should only # contain documented Anthropic Messages API parameters. for key in optional_params: - assert not key.startswith("_"), ( - f"optional_params leaked underscore-prefixed key {key!r}: " - f"{optional_params}" - ) + assert not key.startswith("_"), f"optional_params leaked underscore-prefixed key {key!r}: {optional_params}" def test_map_openai_params_no_maps_when_all_names_already_valid(): @@ -4933,11 +4780,7 @@ def test_map_openai_params_no_maps_when_all_names_already_valid(): def test_rewrite_tool_names_in_messages_uses_forward_map(): config = AnthropicConfig() - forward_map = { - "actions/download-job-logs-for-workflow-run": ( - "actions_download-job-logs-for-workflow-run" - ) - } + forward_map = {"actions/download-job-logs-for-workflow-run": ("actions_download-job-logs-for-workflow-run")} messages = [ {"role": "user", "content": "go"}, { @@ -4960,15 +4803,9 @@ def test_rewrite_tool_names_in_messages_uses_forward_map(): out = config._rewrite_tool_names_in_messages(messages, forward_map) # input list must not be mutated - assert ( - messages[1]["tool_calls"][0]["function"]["name"] - == "actions/download-job-logs-for-workflow-run" - ) + assert messages[1]["tool_calls"][0]["function"]["name"] == "actions/download-job-logs-for-workflow-run" # output rewritten according to forward map - assert ( - out[1]["tool_calls"][0]["function"]["name"] - == "actions_download-job-logs-for-workflow-run" - ) + assert out[1]["tool_calls"][0]["function"]["name"] == "actions_download-job-logs-for-workflow-run" # non-tool-call messages pass through unchanged (same object) assert out[0] is messages[0] assert out[2] is messages[2] @@ -5044,9 +4881,7 @@ def test_sanitize_tool_names_in_request_does_not_mutate_caller_tool_dicts(): caller_tools = [caller_tool] optional_params: dict = {"tools": caller_tools} - forward, reverse = config._sanitize_tool_names_in_request( - optional_params=optional_params - ) + forward, reverse = config._sanitize_tool_names_in_request(optional_params=optional_params) assert forward.get(original_name) sanitized = forward[original_name] @@ -5195,10 +5030,7 @@ def test_streaming_iterator_reverse_maps_tool_use_name(): parsed = iterator.chunk_parser(chunk=chunk) tool_calls = parsed.choices[0].delta.tool_calls assert tool_calls is not None and len(tool_calls) == 1 - assert ( - tool_calls[0]["function"]["name"] - == "actions/download-job-logs-for-workflow-run" - ) + assert tool_calls[0]["function"]["name"] == "actions/download-job-logs-for-workflow-run" def test_streaming_iterator_passthrough_when_name_not_in_map(): @@ -5294,9 +5126,9 @@ def test_transform_request_does_not_leak_internal_keys_into_body(): for tool in data.get("tools", []): name = tool.get("name") assert isinstance(name, str) - assert _re.fullmatch( - r"[a-zA-Z0-9_-]{1,128}", name - ), f"sanitized tool name {name!r} still violates Anthropic regex" + assert _re.fullmatch(r"[a-zA-Z0-9_-]{1,128}", name), ( + f"sanitized tool name {name!r} still violates Anthropic regex" + ) # Sent name for the bad tool is the disambiguated form, valid name passes through. sent_names = {t["name"] for t in data["tools"]} @@ -5432,9 +5264,7 @@ def test_transform_request_rewrites_tool_names_in_history(): for block in content: if isinstance(block, dict) and block.get("type") == "tool_use": tool_use_names.append(block.get("name")) - assert ( - tool_use_names - ), "expected at least one tool_use block in transformed messages" + assert tool_use_names, "expected at least one tool_use block in transformed messages" for name in tool_use_names: assert name == "actions_download-job-logs-for-workflow-run", ( f"history tool_use.name {name!r} not rewritten -- Anthropic will " @@ -5458,19 +5288,12 @@ def test_sanitize_tool_names_in_request_skips_hosted_tools(): } forward, reverse = AnthropicConfig._sanitize_tool_names_in_request(optional_params) # Only the custom tool was rewritten. - assert forward == { - "actions/download-job-logs-for-workflow-run": "actions_download-job-logs-for-workflow-run" - } - assert reverse == { - "actions_download-job-logs-for-workflow-run": "actions/download-job-logs-for-workflow-run" - } + assert forward == {"actions/download-job-logs-for-workflow-run": "actions_download-job-logs-for-workflow-run"} + assert reverse == {"actions_download-job-logs-for-workflow-run": "actions/download-job-logs-for-workflow-run"} # Hosted tool's name unchanged. assert optional_params["tools"][0]["name"] == "web_search" # Custom tool's name updated in place. - assert ( - optional_params["tools"][1]["name"] - == "actions_download-job-logs-for-workflow-run" - ) + assert optional_params["tools"][1]["name"] == "actions_download-job-logs-for-workflow-run" def test_sanitize_tool_names_in_request_no_tools_is_noop(): @@ -5704,9 +5527,7 @@ def test_translate_system_message_keeps_billing_header_for_first_party_anthropic assert config.should_strip_billing_metadata() is False result = config.translate_system_message( - messages=_system_with_billing_header( - "You are Claude Code, Anthropic's official CLI for Claude." - ) + messages=_system_with_billing_header("You are Claude Code, Anthropic's official CLI for Claude.") ) texts = [block["text"] for block in result] @@ -5722,9 +5543,7 @@ def test_translate_system_message_strips_billing_header_for_bedrock(): config = BedrockClaudePlatformConfig() assert config.should_strip_billing_metadata() is True - result = config.translate_system_message( - messages=_system_with_billing_header("real system prompt") - ) + result = config.translate_system_message(messages=_system_with_billing_header("real system prompt")) texts = [block["text"] for block in result] assert all(not t.startswith("x-anthropic-billing-header:") for t in texts) @@ -5790,9 +5609,7 @@ def test_translate_system_message_strips_billing_header_for_bedrock_invoke(): config = AmazonAnthropicClaudeConfig() assert config.should_strip_billing_metadata() is True - result = config.translate_system_message( - messages=_system_with_billing_header("real system prompt") - ) + result = config.translate_system_message(messages=_system_with_billing_header("real system prompt")) texts = [block["text"] for block in result] assert all(not t.startswith("x-anthropic-billing-header:") for t in texts) @@ -5846,9 +5663,7 @@ def test_translate_system_message_strips_billing_header_for_bedrock_invoke(): ), ], ) -def test_should_strip_billing_metadata_by_provider( - module_path, class_name, expected_strip -): +def test_should_strip_billing_metadata_by_provider(module_path, class_name, expected_strip): import importlib config_cls = getattr(importlib.import_module(module_path), class_name) @@ -6020,12 +5835,8 @@ def test_sampling_param_gating_driven_by_model_map_flag(monkeypatch): """The drop/raise decision must come from ``supports_sampling_params`` in the model map, not just name matching: a flagged entry gates a model whose name says nothing, and an explicit ``true`` overrides the name fallback.""" - monkeypatch.setitem( - litellm.model_cost, "claude-zeta-9", {"supports_sampling_params": False} - ) - monkeypatch.setitem( - litellm.model_cost, "claude-fable-5-test", {"supports_sampling_params": True} - ) + monkeypatch.setitem(litellm.model_cost, "claude-zeta-9", {"supports_sampling_params": False}) + monkeypatch.setitem(litellm.model_cost, "claude-fable-5-test", {"supports_sampling_params": True}) config = AnthropicConfig() flagged_off = config.map_openai_params( @@ -6144,9 +5955,7 @@ def test_is_anthropic_usage_object_rejects_responses_api_usage(): ("claude-sonnet-4-5-20250929", False), ], ) -def test_disabled_thinking_omitted_only_for_always_on_models( - local_model_cost_map, model, expected_dropped -): +def test_disabled_thinking_omitted_only_for_always_on_models(local_model_cost_map, model, expected_dropped): """``thinking={"type": "disabled"}`` is omitted for always-on-thinking models (Fable/Mythos, which 400 on it: the API remedy is to omit the param) and is forwarded verbatim for every model that accepts it.""" diff --git a/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py index 60e45c9b8ce..99266f92c56 100644 --- a/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py +++ b/tests/test_litellm/llms/anthropic/chat/test_code_interpreter_results_extraction.py @@ -136,22 +136,14 @@ def test_in_place_substitution_preserves_ordering(): responses_output = [msg_item, fc_exec1, fc_regular, fc_exec2] # Apply the same logic as _transform_chat_completion_choices_to_responses_output - tool_result_items = ( - LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) - ) + tool_result_items = LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(resp) if tool_result_items: - result_by_id = { - (item.get("id") if isinstance(item, dict) else item.id): item - for item in tool_result_items - } + result_by_id = {(item.get("id") if isinstance(item, dict) else item.id): item for item in tool_result_items} replaced_ids = set(result_by_id.keys()) responses_output = [ ( result_by_id[getattr(item, "call_id", None)] - if ( - getattr(item, "type", None) == "function_call" - and getattr(item, "call_id", None) in replaced_ids - ) + if (getattr(item, "type", None) == "function_call" and getattr(item, "call_id", None) in replaced_ids) else item ) for item in responses_output @@ -255,9 +247,7 @@ def test_end_to_end_streaming_chunks_to_code_interpreter_output(): assert code_results[0]["code"] == "echo e2e_test" # Step 3: Extract via _extract_tool_result_output_items (Responses API layer) - tool_result_items = ( - LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(assembled) - ) + tool_result_items = LiteLLMCompletionResponsesConfig._extract_tool_result_output_items(assembled) assert len(tool_result_items) == 1 item = tool_result_items[0] # Items are reconstructed as Pydantic OutputCodeInterpreterCall objects diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py index e4dacc308dc..287794712ea 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_anthropic_experimental_pass_through_adapters_transformation.py @@ -5,7 +5,6 @@ import pytest import litellm - from litellm.litellm_core_utils.prompt_templates.common_utils import ( TOOL_RESULT_IMAGE_PLACEHOLDER, ) @@ -50,9 +49,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_content_block(): tool_calls=[ ChatCompletionDeltaToolCall( id="call_d581d130-e234-4315-94e8-27e7ff7c4e55", - function=Function( - arguments='{"location": "Boston"}', name="get_weather" - ), + function=Function(arguments='{"location": "Boston"}', name="get_weather"), type="function", index=0, ) @@ -66,9 +63,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_content_block(): ( block_type, content_block_start, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) print(content_block_start) @@ -98,9 +93,7 @@ def test_translate_streaming_openai_chunk_strips_gemini_thought_from_tool_call_i tool_calls=[ ChatCompletionDeltaToolCall( id=combined, - function=Function( - arguments='{"a": 17, "b": 25}', name="add_numbers" - ), + function=Function(arguments='{"a": 17, "b": 25}', name="add_numbers"), type="function", index=0, ) @@ -114,9 +107,7 @@ def test_translate_streaming_openai_chunk_strips_gemini_thought_from_tool_call_i ( block_type, content_block_start, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "tool_use" assert content_block_start["id"] == base @@ -161,9 +152,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_thinking_content_block(): ( block_type, content_block_start, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "thinking" assert content_block_start == { @@ -199,9 +188,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_reasoning_content_only_co ( block_type, content_block_start, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "thinking" assert content_block_start == { @@ -247,9 +234,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_thinking_signature_block( ( block_type, content_block_start, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "thinking" assert content_block_start == { @@ -302,9 +287,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_content_block_thinking_an ( block_type, content_block_start, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "thinking" @@ -347,10 +330,7 @@ def test_translate_anthropic_messages_to_openai_thinking_blocks(): assert "thinking_blocks" in result[1] assert len(result[1]["thinking_blocks"]) == 2 assert result[1]["thinking_blocks"][0]["type"] == "thinking" - assert ( - result[1]["thinking_blocks"][0]["thinking"] - == "I will call the get_weather tool." - ) + assert result[1]["thinking_blocks"][0]["thinking"] == "I will call the get_weather tool." assert result[1]["thinking_blocks"][0]["signature"] == "sigsig" assert result[1]["thinking_blocks"][1]["type"] == "redacted_thinking" assert result[1]["thinking_blocks"][1]["data"] == "REDACTED" @@ -411,9 +391,7 @@ def test_translate_anthropic_messages_to_openai_tool_message_placement(): assert tool_message_idx is not None, "Tool message not found" assert user_message_idx is not None, "User message not found" - assert ( - tool_message_idx < user_message_idx - ), "Tool message should be placed before user message" + assert tool_message_idx < user_message_idx, "Tool message should be placed before user message" @pytest.mark.parametrize( @@ -634,9 +612,7 @@ def test_translate_anthropic_to_openai_orders_top_level_and_midturn_system(): ] -def _translate_with_metadata( - model: str, metadata: dict[str, Any], custom_llm_provider: str | None -) -> dict[str, Any]: +def _translate_with_metadata(model: str, metadata: dict[str, Any], custom_llm_provider: str | None) -> dict[str, Any]: openai_request, _ = LiteLLMAnthropicMessagesAdapter().translate_anthropic_to_openai( anthropic_message_request={ "model": model, @@ -688,9 +664,7 @@ def test_translate_anthropic_to_openai_skips_prompt_cache_key_when_provider_lack def test_translate_anthropic_to_openai_skips_prompt_cache_key_for_chained_litellm_proxy(): - assert "prompt_cache_key" in litellm.get_supported_openai_params( - model="xai", custom_llm_provider="litellm_proxy" - ) + assert "prompt_cache_key" in litellm.get_supported_openai_params(model="xai", custom_llm_provider="litellm_proxy") openai_request = _translate_with_metadata("litellm_proxy/xai", {"user_id": "session-abc"}, "litellm_proxy") assert openai_request["user"] == "session-abc" assert "prompt_cache_key" not in openai_request @@ -735,7 +709,8 @@ def test_translate_openai_content_to_anthropic_empty_function_arguments(): id="call_empty_args", type="function", function=Function( - name="test_function", arguments="" # empty arguments string + name="test_function", + arguments="", # empty arguments string ), ) ], @@ -750,9 +725,7 @@ def test_translate_openai_content_to_anthropic_empty_function_arguments(): assert result[0]["type"] == "tool_use" assert result[0]["id"] == "call_empty_args" assert result[0]["name"] == "test_function" - assert ( - result[0]["input"] == {} - ), "Empty function arguments should result in empty dict" + assert result[0]["input"] == {}, "Empty function arguments should result in empty dict" def test_translate_openai_content_to_anthropic_text_and_tool_calls(): @@ -872,9 +845,7 @@ def test_translate_openai_response_to_anthropic_text_and_tool_calls(): ChatCompletionAssistantToolCall( id="call_tool_combo", type="function", - function=Function( - name="get_weather", arguments='{"location": "Paris"}' - ), + function=Function(name="get_weather", arguments='{"location": "Paris"}'), ) ], ), @@ -884,9 +855,7 @@ def test_translate_openai_response_to_anthropic_text_and_tool_calls(): ) adapter = LiteLLMAnthropicMessagesAdapter() - anthropic_response = adapter.translate_openai_response_to_anthropic( - response=openai_response - ) + anthropic_response = adapter.translate_openai_response_to_anthropic(response=openai_response) anthropic_content = anthropic_response.get("content") assert anthropic_content is not None @@ -927,9 +896,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_with_partial_json(): ( type_of_content, content_block_delta, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic(choices=choices) print("Type of content:", type_of_content) print("Content block delta:", content_block_delta) @@ -1004,9 +971,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_thinking_delta(): ( type_of_content, content_block_delta, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic(choices=choices) assert type_of_content == "thinking_delta" assert content_block_delta["type"] == "thinking_delta" @@ -1049,9 +1014,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_with_thinking(): ( type_of_content, content_block_delta, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic(choices=choices) assert type_of_content == "signature_delta" assert content_block_delta["type"] == "signature_delta" @@ -1115,9 +1078,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_emits_signature_when_thin ( block_type, content_block_start, - ) = adapter._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = adapter._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "thinking" @@ -1157,9 +1118,7 @@ def test_translate_anthropic_messages_to_openai_user_message_with_base64_image() # Check image content assert result[0]["content"][1]["type"] == "image_url" assert "image_url" in result[0]["content"][1] - assert result[0]["content"][1]["image_url"]["url"].startswith( - "data:image/png;base64," - ) + assert result[0]["content"][1]["image_url"]["url"].startswith("data:image/png;base64,") assert ( "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" in result[0]["content"][1]["image_url"]["url"] @@ -1197,18 +1156,14 @@ def test_translate_anthropic_messages_to_openai_user_message_with_url_image(): # Check image content assert result[0]["content"][1]["type"] == "image_url" assert "image_url" in result[0]["content"][1] - assert ( - result[0]["content"][1]["image_url"]["url"] == "https://example.com/forest.jpg" - ) + assert result[0]["content"][1]["image_url"]["url"] == "https://example.com/forest.jpg" def test_translate_anthropic_messages_to_openai_tool_result_with_base64_image(): """Test that base64 images in tool results are correctly translated to OpenAI format.""" anthropic_messages = [ - AnthropicMessagesUserMessageParam( - role="user", content=[{"type": "text", "text": "Take a screenshot"}] - ), + AnthropicMessagesUserMessageParam(role="user", content=[{"type": "text", "text": "Take a screenshot"}]), AnthopicMessagesAssistantMessageParam( role="assistant", content=[ @@ -1360,9 +1315,7 @@ def test_translate_anthropic_messages_to_openai_mixed_content_with_image(): # Check first image (base64) assert result[0]["content"][1]["type"] == "image_url" - assert result[0]["content"][1]["image_url"]["url"].startswith( - "data:image/png;base64," - ) + assert result[0]["content"][1]["image_url"]["url"].startswith("data:image/png;base64,") # Check middle text assert result[0]["content"][2]["type"] == "text" @@ -1370,9 +1323,7 @@ def test_translate_anthropic_messages_to_openai_mixed_content_with_image(): # Check second image (URL) assert result[0]["content"][3]["type"] == "image_url" - assert ( - result[0]["content"][3]["image_url"]["url"] == "https://example.com/image2.jpg" - ) + assert result[0]["content"][3]["image_url"]["url"] == "https://example.com/image2.jpg" # Check final text assert result[0]["content"][4]["type"] == "text" @@ -1418,10 +1369,7 @@ def test_translate_anthropic_messages_to_openai_tool_use_with_signature(): assert tool_call["id"] == "call_386f67af31f9415781bc35071405" assert "function" in tool_call assert "provider_specific_fields" in tool_call["function"] - assert ( - tool_call["function"]["provider_specific_fields"]["thought_signature"] - == test_signature - ) + assert tool_call["function"]["provider_specific_fields"]["thought_signature"] == test_signature def test_translate_anthropic_messages_to_openai_tool_result_with_multiple_content_items(): @@ -1479,9 +1427,7 @@ def test_translate_anthropic_messages_to_openai_tool_result_with_multiple_conten result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages) # Count how many tool messages have the same tool_call_id - tool_messages = [ - msg for msg in result if isinstance(msg, dict) and msg.get("role") == "tool" - ] + tool_messages = [msg for msg in result if isinstance(msg, dict) and msg.get("role") == "tool"] tool_call_ids = [msg.get("tool_call_id") for msg in tool_messages] # The critical assertion: each tool_call_id should appear only ONCE @@ -1497,12 +1443,8 @@ def test_translate_anthropic_messages_to_openai_tool_result_with_multiple_conten # The content should be a list with all items combined tool_message = tool_messages[0] assert tool_message["tool_call_id"] == "toolu_016hYHBkTf4JDF3p22UoYk5C" - assert isinstance( - tool_message["content"], list - ), "Multiple content items should be combined into a list" - assert ( - len(tool_message["content"]) == 3 - ), f"Expected 3 content items, got {len(tool_message['content'])}" + assert isinstance(tool_message["content"], list), "Multiple content items should be combined into a list" + assert len(tool_message["content"]) == 3, f"Expected 3 content items, got {len(tool_message['content'])}" # Verify content types assert tool_message["content"][0]["type"] == "text" @@ -1551,17 +1493,14 @@ def test_translate_anthropic_messages_to_openai_tool_result_single_item_backward adapter = LiteLLMAnthropicMessagesAdapter() result = adapter.translate_anthropic_messages_to_openai(messages=anthropic_messages) - tool_messages = [ - msg for msg in result if isinstance(msg, dict) and msg.get("role") == "tool" - ] + tool_messages = [msg for msg in result if isinstance(msg, dict) and msg.get("role") == "tool"] assert len(tool_messages) == 1 tool_message = tool_messages[0] # Single item should be a string for backward compatibility assert isinstance(tool_message["content"], str), ( - f"Single content item should be a string for backward compatibility, " - f"got {type(tool_message['content'])}" + f"Single content item should be a string for backward compatibility, got {type(tool_message['content'])}" ) assert tool_message["content"] == "72°F and sunny" @@ -1610,9 +1549,7 @@ def test_streaming_chunk_with_both_text_and_tool_calls_issue_18238(): ( block_type, content_block_start, - ) = adapter._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = adapter._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "tool_use" assert content_block_start["name"] == "Bash" @@ -1656,9 +1593,7 @@ def test_streaming_chunk_with_text_and_empty_tool_calls_returns_text_delta(): ( block_type, content_block_start, - ) = adapter._translate_streaming_openai_chunk_to_anthropic_content_block( - choices=choices - ) + ) = adapter._translate_streaming_openai_chunk_to_anthropic_content_block(choices=choices) assert block_type == "text" assert content_block_start == {"type": "text", "text": ""} @@ -1669,15 +1604,12 @@ def test_streaming_chunk_with_text_and_empty_tool_calls_returns_text_delta(): # ============================================================================ # Model constant for cache control tests -CACHE_CONTROL_BEDROCK_CONVERSE_MODEL = ( - "bedrock/converse/global.anthropic.claude-opus-4-5-20251101-v1:0" -) +CACHE_CONTROL_BEDROCK_CONVERSE_MODEL = "bedrock/converse/global.anthropic.claude-opus-4-5-20251101-v1:0" CACHE_CONTROL_NON_ANTHROPIC_MODEL = "gpt-4" # Bedrock Application Inference Profile ARN: the string contains neither # "anthropic" nor "claude", so the model can only be recognized via its ARN shape CACHE_CONTROL_BEDROCK_ARN_MODEL = ( - "bedrock/converse/arn:aws:bedrock:us-east-1:123456789012:" - "application-inference-profile/abcdef123456" + "bedrock/converse/arn:aws:bedrock:us-east-1:123456789012:application-inference-profile/abcdef123456" ) @@ -1693,9 +1625,7 @@ def test_should_add_cache_control_for_anthropic_model(): "vertex_ai/claude-3-sonnet@20240229", ]: target = {} - adapter._add_cache_control_if_applicable( - {"cache_control": cache_control}, target, model - ) + adapter._add_cache_control_if_applicable({"cache_control": cache_control}, target, model) assert "cache_control" in target assert target["cache_control"] == cache_control @@ -1711,9 +1641,7 @@ def test_should_not_add_cache_control_for_non_anthropic_model(): "gemini-pro", ]: target = {} - adapter._add_cache_control_if_applicable( - {"cache_control": cache_control}, target, model - ) + adapter._add_cache_control_if_applicable({"cache_control": cache_control}, target, model) assert "cache_control" not in target @@ -1728,9 +1656,7 @@ def test_should_not_add_cache_control_when_none(): {}, ]: target = {} - adapter._add_cache_control_if_applicable( - source, target, CACHE_CONTROL_BEDROCK_CONVERSE_MODEL - ) + adapter._add_cache_control_if_applicable(source, target, CACHE_CONTROL_BEDROCK_CONVERSE_MODEL) assert "cache_control" not in target @@ -1741,9 +1667,7 @@ def test_should_not_add_cache_control_when_model_none(): for model in [None, ""]: target = {} - adapter._add_cache_control_if_applicable( - {"cache_control": cache_control}, target, model - ) + adapter._add_cache_control_if_applicable({"cache_control": cache_control}, target, model) assert "cache_control" not in target @@ -1849,12 +1773,7 @@ def test_cache_control_fix_does_not_broaden_claude_detection(): make is_anthropic_claude_model treat ARN profiles as Claude, which would route thinking params through unmodified and break non-Claude Bedrock profiles. """ - assert ( - LiteLLMAnthropicMessagesAdapter.is_anthropic_claude_model( - CACHE_CONTROL_BEDROCK_ARN_MODEL - ) - is False - ) + assert LiteLLMAnthropicMessagesAdapter.is_anthropic_claude_model(CACHE_CONTROL_BEDROCK_ARN_MODEL) is False def test_thinking_preserved_for_bedrock_arn_inference_profile(): @@ -2322,9 +2241,7 @@ def test_translate_streaming_openai_chunk_to_anthropic_reasoning_content_without ( type_of_content, content_block_delta, - ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic( - choices=choices - ) + ) = LiteLLMAnthropicMessagesAdapter()._translate_streaming_openai_chunk_to_anthropic(choices=choices) assert type_of_content == "thinking_delta" assert content_block_delta["type"] == "thinking_delta" @@ -2356,9 +2273,7 @@ def test_translate_openai_response_to_anthropic_with_reasoning_content_only(): ) adapter = LiteLLMAnthropicMessagesAdapter() - anthropic_response = adapter.translate_openai_response_to_anthropic( - response=openai_response - ) + anthropic_response = adapter.translate_openai_response_to_anthropic(response=openai_response) anthropic_content = anthropic_response.get("content") assert anthropic_content is not None @@ -2371,9 +2286,7 @@ def test_translate_openai_response_to_anthropic_with_reasoning_content_only(): # Second block should be text assert anthropic_content[1]["type"] == "text" - assert ( - anthropic_content[1]["text"] == 'There are **3** "r"s in the word strawberry.' - ) + assert anthropic_content[1]["text"] == 'There are **3** "r"s in the word strawberry.' assert anthropic_response.get("stop_reason") == "end_turn" @@ -2425,9 +2338,7 @@ def test_truncate_tool_name_deterministic(): def test_truncate_tool_name_avoids_collisions(): """Similar long names should produce different truncated names.""" name1 = "process_user_data_with_validation_and_error_handling_for_production_environment" - name2 = ( - "process_user_data_with_validation_and_error_handling_for_staging_environment" - ) + name2 = "process_user_data_with_validation_and_error_handling_for_staging_environment" result1 = truncate_tool_name(name1) result2 = truncate_tool_name(name2) @@ -2447,9 +2358,7 @@ def test_create_tool_name_mapping_no_long_names(): def test_create_tool_name_mapping_with_long_names(): """Mapping should contain entries for truncated names.""" - long_name = ( - "a_very_long_tool_name_that_exceeds_the_64_character_limit_imposed_by_openai" - ) + long_name = "a_very_long_tool_name_that_exceeds_the_64_character_limit_imposed_by_openai" tools = [ {"name": "short_name"}, {"name": long_name}, @@ -2474,9 +2383,7 @@ def test_translate_anthropic_tools_with_long_names(): ] adapter = LiteLLMAnthropicMessagesAdapter() - result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai( - tools=tools, model="gpt-4" - ) + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai(tools=tools, model="gpt-4") assert len(result) == 1 # The tool name should be truncated @@ -2498,9 +2405,7 @@ def test_translate_anthropic_tools_mixed_names(): ] adapter = LiteLLMAnthropicMessagesAdapter() - result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai( - tools=tools, model="gpt-4" - ) + result, tool_name_mapping = adapter.translate_anthropic_tools_to_openai(tools=tools, model="gpt-4") assert len(result) == 2 # Short name unchanged @@ -2514,9 +2419,7 @@ def test_translate_anthropic_tools_mixed_names(): def test_translate_openai_response_restores_tool_names(): """Tool names in responses should be restored to original.""" - original_name = ( - "a_very_long_tool_name_that_needs_truncation_for_openai_api_compatibility" - ) + original_name = "a_very_long_tool_name_that_needs_truncation_for_openai_api_compatibility" truncated_name = truncate_tool_name(original_name) tool_name_mapping = {truncated_name: original_name} @@ -2548,9 +2451,7 @@ def test_translate_openai_response_restores_tool_names(): ) adapter = LiteLLMAnthropicMessagesAdapter() - result = adapter.translate_openai_response_to_anthropic( - response=response, tool_name_mapping=tool_name_mapping - ) + result = adapter.translate_openai_response_to_anthropic(response=response, tool_name_mapping=tool_name_mapping) # Find the tool_use block in the response tool_use_blocks = [c for c in result["content"] if c.get("type") == "tool_use"] @@ -2716,9 +2617,7 @@ def test_translate_openai_usage_to_anthropic_cache_tokens_from_dict_details_with "cache_write_tokens": 20.0, } - anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta( - usage - ) + anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta(usage) assert anthropic_usage["input_tokens"] == 70 assert anthropic_usage["output_tokens"] == 50 @@ -2737,9 +2636,7 @@ def test_translate_openai_usage_to_anthropic_ignores_fractional_cache_tokens(): "cache_creation_tokens": 20.25, } - anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta( - usage - ) + anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta(usage) assert anthropic_usage["input_tokens"] == 120 assert anthropic_usage["output_tokens"] == 50 @@ -2756,9 +2653,7 @@ def test_translate_openai_usage_to_anthropic_ignores_bool_cache_tokens(): usage.cache_read_input_tokens = True usage.cache_creation_input_tokens = True - anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta( - usage - ) + anthropic_usage = LiteLLMAnthropicMessagesAdapter._translate_openai_usage_to_anthropic_usage_delta(usage) assert anthropic_usage["input_tokens"] == 120 assert anthropic_usage["output_tokens"] == 50 @@ -2977,9 +2872,7 @@ def test_translate_streaming_openai_response_to_anthropic_cache_tokens_with_appl assert message_delta["usage"]["output_tokens"] == 50 assert message_delta["usage"]["cache_read_input_tokens"] == 30 assert message_delta["usage"]["cache_creation_input_tokens"] == 20 - assert message_delta["context_management"]["applied_edits"][0]["type"] == ( - "compact_20260112" - ) + assert message_delta["context_management"]["applied_edits"][0]["type"] == ("compact_20260112") # ===================================================================== @@ -3154,15 +3047,8 @@ class TestTranslateAnthropicOutputFormatToOpenAI: assert schema["required"] == ["user"] assert schema["properties"]["user"]["additionalProperties"] is False assert schema["properties"]["user"]["required"] == ["name", "address"] - assert ( - schema["properties"]["user"]["properties"]["address"][ - "additionalProperties" - ] - is False - ) - assert schema["properties"]["user"]["properties"]["address"]["required"] == [ - "city" - ] + assert schema["properties"]["user"]["properties"]["address"]["additionalProperties"] is False + assert schema["properties"]["user"]["properties"]["address"]["required"] == ["city"] def test_array_items_object_adds_additional_properties_false(self): output_format = { @@ -3237,19 +3123,9 @@ class TestTranslateAnthropicOutputFormatToOpenAI: assert sorted(schema["required"]) == ["age", "email", "name"] def test_invalid_output_format_returns_none(self): - assert ( - self.adapter.translate_anthropic_output_format_to_openai("invalid") is None - ) - assert ( - self.adapter.translate_anthropic_output_format_to_openai({"type": "text"}) - is None - ) - assert ( - self.adapter.translate_anthropic_output_format_to_openai( - {"type": "json_schema"} - ) - is None - ) + assert self.adapter.translate_anthropic_output_format_to_openai("invalid") is None + assert self.adapter.translate_anthropic_output_format_to_openai({"type": "text"}) is None + assert self.adapter.translate_anthropic_output_format_to_openai({"type": "json_schema"}) is None class TestAnthropicStreamWrapperToolArgs: @@ -3453,9 +3329,7 @@ def test_translate_openai_response_to_anthropic_with_polyfill_compaction_block() ) response = _make_simple_openai_response(text="Hello after compaction.") adapter = LiteLLMAnthropicMessagesAdapter() - result = adapter.translate_openai_response_to_anthropic( - response=response, polyfill_result=polyfill - ) + result = adapter.translate_openai_response_to_anthropic(response=response, polyfill_result=polyfill) content = result.get("content") assert content is not None @@ -3487,9 +3361,7 @@ def test_translate_openai_response_to_anthropic_with_polyfill_iterations_usage() ) response = _make_simple_openai_response(prompt_tokens=100, completion_tokens=30) adapter = LiteLLMAnthropicMessagesAdapter() - result = adapter.translate_openai_response_to_anthropic( - response=response, polyfill_result=polyfill - ) + result = adapter.translate_openai_response_to_anthropic(response=response, polyfill_result=polyfill) usage = result.get("usage") assert usage is not None @@ -3544,13 +3416,9 @@ def test_translate_openai_response_to_anthropic_with_polyfill_both_compaction_an {"type": "compaction", "input_tokens": 300, "output_tokens": 75}, ], ) - response = _make_simple_openai_response( - text="After compaction.", prompt_tokens=120, completion_tokens=40 - ) + response = _make_simple_openai_response(text="After compaction.", prompt_tokens=120, completion_tokens=40) adapter = LiteLLMAnthropicMessagesAdapter() - result = adapter.translate_openai_response_to_anthropic( - response=response, polyfill_result=polyfill - ) + result = adapter.translate_openai_response_to_anthropic(response=response, polyfill_result=polyfill) # compaction block must come first content = result.get("content") @@ -3652,7 +3520,9 @@ def test_translate_anthropic_tools_to_openai_omits_unset_strict(): assert function["parameters"]["required"] == ["query"] -TOOL_RESULT_IMAGE_B64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" +TOOL_RESULT_IMAGE_B64 = ( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" +) TOOL_RESULT_IMAGE_URL = "https://example.com/screenshot.png" @@ -3660,8 +3530,7 @@ def _anthropic_tool_use_turn(*tool_use_ids): return AnthopicMessagesAssistantMessageParam( role="assistant", content=[ - {"type": "tool_use", "id": tid, "name": "read_file", "input": {"path": "img.png"}} - for tid in tool_use_ids + {"type": "tool_use", "id": tid, "name": "read_file", "input": {"path": "img.png"}} for tid in tool_use_ids ], ) @@ -3781,9 +3650,7 @@ def test_tool_result_parallel_tool_calls_keep_tool_message_adjacency(): result = _run_chat_completions_pipeline( [ _anthropic_tool_use_turn("toolu_01", "toolu_02"), - _anthropic_tool_result_turn( - {"toolu_01": [_base64_image_block()], "toolu_02": [_url_image_block()]} - ), + _anthropic_tool_result_turn({"toolu_01": [_base64_image_block()], "toolu_02": [_url_image_block()]}), ] ) @@ -3878,7 +3745,9 @@ def test_translate_anthropic_to_openai_without_prompt_cache_breakpoint_adds_noth def test_translate_anthropic_messages_to_openai_carries_midturn_system_prompt_cache_breakpoint(): explicit = {"mode": "explicit"} result = LiteLLMAnthropicMessagesAdapter().translate_anthropic_messages_to_openai( - messages=[{"role": "system", "content": [{"type": "text", "text": "fix", "prompt_cache_breakpoint": explicit}]}], + messages=[ + {"role": "system", "content": [{"type": "text", "text": "fix", "prompt_cache_breakpoint": explicit}]} + ], model="gpt-5.6", ) assert result == [ diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py index a944afc6152..71d7974c2d4 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_handler_output_config_passthrough.py @@ -34,9 +34,7 @@ import pytest # Anchor sys.path to this file's location — not the working-directory-relative # pattern Greptile flagged on PR #23706. Resolves correctly regardless of # where pytest is invoked from. -sys.path.insert( - 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../../..")) -) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../../.."))) from litellm.llms.anthropic.experimental_pass_through.adapters.handler import ( ANTHROPIC_ONLY_REQUEST_KEYS, @@ -174,9 +172,7 @@ class TestOutputConfigStrippedFromCompletionKwargs: result = _call_prepare( extra_kwargs={ "custom_llm_provider": "azure", - "output_config": { - "format": {"type": "json_schema", "schema": losing_schema} - }, + "output_config": {"format": {"type": "json_schema", "schema": losing_schema}}, }, output_format={"type": "json_schema", "schema": winning_schema}, ) diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_combined_chunk.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_combined_chunk.py index 6973340101e..0a4d9c693d6 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_combined_chunk.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_combined_chunk.py @@ -31,9 +31,7 @@ from litellm.types.utils import ( ) -def _build_fake_stream( - content: str, finish_reason: str = "stop" -) -> MockResponseIterator: +def _build_fake_stream(content: str, finish_reason: str = "stop") -> MockResponseIterator: """Mimic a Vertex Gemma `:predict` fake stream: one collapsed chunk.""" model_response = ModelResponse() model_response.choices = [ @@ -133,9 +131,7 @@ def test_delayed_usage_chunk_preserves_cache_tokens(): wrapper = AnthropicStreamWrapper(completion_stream=iter(chunks), model="gpt-4o") events = list(wrapper) - message_delta = next( - event for event in events if event.get("type") == "message_delta" - ) + message_delta = next(event for event in events if event.get("type") == "message_delta") assert message_delta["usage"]["input_tokens"] == 70 assert message_delta["usage"]["output_tokens"] == 5 @@ -145,13 +141,7 @@ def test_delayed_usage_chunk_preserves_cache_tokens(): def test_splitter_passes_through_non_combined_chunks(): """A chunk with content but no finish_reason is not split.""" - chunk = ModelResponseStream( - choices=[ - StreamingChoices( - index=0, delta=Delta(content="partial"), finish_reason=None - ) - ] - ) + chunk = ModelResponseStream(choices=[StreamingChoices(index=0, delta=Delta(content="partial"), finish_reason=None)]) chunks = list(_CombinedChunkSplitter(iter([chunk]))) assert len(chunks) == 1 assert chunks[0].choices[0].delta.content == "partial" @@ -159,11 +149,7 @@ def test_splitter_passes_through_non_combined_chunks(): def test_splitter_splits_combined_chunk_into_content_then_finish(): """A chunk with both content and finish_reason becomes two chunks.""" - chunk = ModelResponseStream( - choices=[ - StreamingChoices(index=0, delta=Delta(content="done"), finish_reason="stop") - ] - ) + chunk = ModelResponseStream(choices=[StreamingChoices(index=0, delta=Delta(content="done"), finish_reason="stop")]) content_chunk, finish_chunk = list(_CombinedChunkSplitter(iter([chunk]))) assert content_chunk.choices[0].delta.content == "done" @@ -193,9 +179,7 @@ def test_split_clears_reasoning_and_thinking_on_finish_chunk(): reasoning_content="some reasoning", thinking_blocks=[{"type": "thinking"}], ) - chunk = SimpleNamespace( - choices=[SimpleNamespace(finish_reason="stop", delta=delta)] - ) + chunk = SimpleNamespace(choices=[SimpleNamespace(finish_reason="stop", delta=delta)]) content_chunk, finish_chunk = _CombinedChunkSplitter._split(chunk) diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_compaction.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_compaction.py index 5c53a8fc317..646d63e08bd 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_compaction.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_compaction.py @@ -22,9 +22,7 @@ def _make_text_chunk( StreamingChoices( finish_reason=finish_reason, index=0, - delta=Delta( - content=text, role="assistant" if text else None, tool_calls=None - ), + delta=Delta(content=text, role="assistant" if text else None, tool_calls=None), logprobs=None, ) ] @@ -73,34 +71,23 @@ async def test_stream_emits_compaction_block_before_text(): compaction_start = next( e for e in events - if e.get("type") == "content_block_start" - and e.get("content_block", {}).get("type") == "compaction" + if e.get("type") == "content_block_start" and e.get("content_block", {}).get("type") == "compaction" ) assert compaction_start["index"] == 0 compaction_delta = next( e for e in events - if e.get("type") == "content_block_delta" - and e.get("delta", {}).get("type") == "compaction_delta" + if e.get("type") == "content_block_delta" and e.get("delta", {}).get("type") == "compaction_delta" ) assert compaction_delta["index"] == 0 - assert ( - compaction_delta["delta"]["content"] == "Summary of prior conversation turns." - ) + assert compaction_delta["delta"]["content"] == "Summary of prior conversation turns." - compaction_stop = next( - e - for e in events - if e.get("type") == "content_block_stop" and e.get("index") == 0 - ) + compaction_stop = next(e for e in events if e.get("type") == "content_block_stop" and e.get("index") == 0) assert compaction_stop is not None text_start = next( - e - for e in events - if e.get("type") == "content_block_start" - and e.get("content_block", {}).get("type") == "text" + e for e in events if e.get("type") == "content_block_start" and e.get("content_block", {}).get("type") == "text" ) assert text_start["index"] == 1 @@ -177,14 +164,9 @@ async def test_stream_without_compaction_block_unchanged(): events = await _collect_events_async(wrapper) assert not any( - e.get("content_block", {}).get("type") == "compaction" - for e in events - if e.get("type") == "content_block_start" + e.get("content_block", {}).get("type") == "compaction" for e in events if e.get("type") == "content_block_start" ) text_start = next( - e - for e in events - if e.get("type") == "content_block_start" - and e.get("content_block", {}).get("type") == "text" + e for e in events if e.get("type") == "content_block_start" and e.get("content_block", {}).get("type") == "text" ) assert text_start["index"] == 0 diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_empty_choices.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_empty_choices.py index 3e85872f1e5..ce6d6c0656e 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_empty_choices.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_empty_choices.py @@ -19,9 +19,7 @@ from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices, Us def _text_chunk(text: str) -> ModelResponseStream: - return ModelResponseStream( - choices=[StreamingChoices(index=0, delta=Delta(content=text), finish_reason=None)] - ) + return ModelResponseStream(choices=[StreamingChoices(index=0, delta=Delta(content=text), finish_reason=None)]) def _finish_chunk() -> ModelResponseStream: @@ -61,9 +59,7 @@ def test_leading_metadata_chunk_without_choices_does_not_kill_stream(): wrapper = AnthropicStreamWrapper(completion_stream=iter(chunks), model="mock-model") events = list(wrapper) - text = "".join( - event["delta"]["text"] for event in events if event.get("type") == "content_block_delta" - ) + text = "".join(event["delta"]["text"] for event in events if event.get("type") == "content_block_delta") assert text == "Hello there" assert events[-1]["type"] == "message_stop" diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py index f64ffb6d233..e34223d72d3 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_first_delta.py @@ -507,11 +507,7 @@ def _thinking_first_chunks() -> List[MagicMock]: def _assert_thinking_first_block_opens_at_index_zero(events: List[dict]) -> None: - starts = [ - (e["index"], e["content_block"]["type"]) - for e in events - if e.get("type") == "content_block_start" - ] + starts = [(e["index"], e["content_block"]["type"]) for e in events if e.get("type") == "content_block_start"] assert starts == [(0, "thinking"), (1, "text")], starts assert "" not in _text_deltas(events) assert _thinking_deltas(events) == ["Let me think", "about it."] @@ -980,9 +976,7 @@ def test_tool_block_start_emitted_without_awaiting_the_next_chunk_sync(): "name": "Write", "input": {}, } - assert stream.pulled == 1, ( - f"content_block_start was withheld until {stream.pulled} upstream chunks had arrived" - ) + assert stream.pulled == 1, f"content_block_start was withheld until {stream.pulled} upstream chunks had arrived" @pytest.mark.asyncio @@ -997,9 +991,7 @@ async def test_tool_block_start_emitted_without_awaiting_the_next_chunk_async(): start = await wrapper.__anext__() assert start["type"] == "content_block_start" assert start["content_block"]["name"] == "Write" - assert stream.pulled == 1, ( - f"content_block_start was withheld until {stream.pulled} upstream chunks had arrived" - ) + assert stream.pulled == 1, f"content_block_start was withheld until {stream.pulled} upstream chunks had arrived" @pytest.mark.parametrize("is_async", [False, True]) diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_tool_args.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_tool_args.py index 29e9279731d..cf0df4d7ea3 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_tool_args.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/test_streaming_iterator_tool_args.py @@ -127,21 +127,17 @@ async def test_async_stream_emits_input_json_delta_for_bundled_tool_args(): ): input_json_delta_idx = i - assert ( - tool_start_idx is not None - ), f"Expected content_block_start with type=tool_use; events: {event_types}" - assert ( - input_json_delta_idx is not None - ), f"Expected content_block_delta with input_json_delta; events: {event_types}" - assert ( - input_json_delta_idx == tool_start_idx + 1 - ), "input_json_delta should immediately follow the tool_use content_block_start" + assert tool_start_idx is not None, f"Expected content_block_start with type=tool_use; events: {event_types}" + assert input_json_delta_idx is not None, ( + f"Expected content_block_delta with input_json_delta; events: {event_types}" + ) + assert input_json_delta_idx == tool_start_idx + 1, ( + "input_json_delta should immediately follow the tool_use content_block_start" + ) # Verify the delta carries the tool arguments delta_event = events[input_json_delta_idx] - assert delta_event["delta"][ - "partial_json" - ], "input_json_delta should have non-empty partial_json" + assert delta_event["delta"]["partial_json"], "input_json_delta should have non-empty partial_json" @pytest.mark.asyncio @@ -230,8 +226,7 @@ async def test_async_stream_no_extra_delta_when_tool_args_empty(): and e["delta"].get("type") == "input_json_delta" ] assert len(input_json_deltas) == 1, ( - f"Expected exactly 1 input_json_delta (from the follow-up chunk), " - f"got {len(input_json_deltas)}" + f"Expected exactly 1 input_json_delta (from the follow-up chunk), got {len(input_json_deltas)}" ) assert input_json_deltas[0]["delta"]["partial_json"] == '{"location": "NYC"}' @@ -291,15 +286,13 @@ def test_sync_stream_emits_input_json_delta_for_bundled_tool_args(): ): input_json_delta_idx = i - assert ( - tool_start_idx is not None - ), f"Expected content_block_start with type=tool_use; events: {event_types}" - assert ( - input_json_delta_idx is not None - ), f"Expected content_block_delta with input_json_delta; events: {event_types}" - assert ( - input_json_delta_idx == tool_start_idx + 1 - ), "input_json_delta should immediately follow the tool_use content_block_start" + assert tool_start_idx is not None, f"Expected content_block_start with type=tool_use; events: {event_types}" + assert input_json_delta_idx is not None, ( + f"Expected content_block_delta with input_json_delta; events: {event_types}" + ) + assert input_json_delta_idx == tool_start_idx + 1, ( + "input_json_delta should immediately follow the tool_use content_block_start" + ) assert events[input_json_delta_idx]["delta"]["partial_json"] @@ -343,9 +336,7 @@ def test_sync_stream_no_extra_delta_when_tool_args_empty(): ) wrapper = AnthropicStreamWrapper( - completion_stream=iter( - [text_chunk, tool_name_chunk, tool_args_chunk, finish_chunk] - ), + completion_stream=iter([text_chunk, tool_name_chunk, tool_args_chunk, finish_chunk]), model="test-model", ) @@ -374,7 +365,6 @@ def test_sync_stream_no_extra_delta_when_tool_args_empty(): and e["delta"].get("type") == "input_json_delta" ] assert len(input_json_deltas) == 1, ( - f"Expected exactly 1 input_json_delta (from the follow-up chunk), " - f"got {len(input_json_deltas)}" + f"Expected exactly 1 input_json_delta (from the follow-up chunk), got {len(input_json_deltas)}" ) assert input_json_deltas[0]["delta"]["partial_json"] == '{"location": "NYC"}' diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_clear_tool_uses.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_clear_tool_uses.py index 09ac95ab16e..cc4852f25de 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_clear_tool_uses.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_clear_tool_uses.py @@ -258,9 +258,7 @@ def test_tool_result_list_content_shape_preserved(): {"role": "user", "content": "Hi"}, { "role": "assistant", - "content": [ - {"type": "tool_use", "id": "toolu_a", "name": "f", "input": {}} - ], + "content": [{"type": "tool_use", "id": "toolu_a", "name": "f", "input": {}}], }, { "role": "user", @@ -274,9 +272,7 @@ def test_tool_result_list_content_shape_preserved(): }, { "role": "assistant", - "content": [ - {"type": "tool_use", "id": "toolu_b", "name": "f", "input": {}} - ], + "content": [{"type": "tool_use", "id": "toolu_b", "name": "f", "input": {}}], }, { "role": "user", diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_compact.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_compact.py index 28c82fdf528..da03f3c00ab 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_compact.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_compact.py @@ -193,10 +193,7 @@ def test_select_last_user_question_strips_tool_result_from_mixed_turn(): content = selected[0]["content"] assert isinstance(content, list) assert all(b.get("type") != "tool_result" for b in content) - assert any( - b.get("type") == "text" and b.get("text") == "follow-up question" - for b in content - ) + assert any(b.get("type") == "text" and b.get("text") == "follow-up question" for b in content) def test_select_last_user_question_skips_pure_tool_result_turn(): @@ -425,9 +422,7 @@ def test_client_compaction_block_history_without_context_management(): def test_client_compaction_block_history_no_compaction_returns_none(): - result = apply_client_compaction_block_history( - messages=_simple_messages(), system="base" - ) + result = apply_client_compaction_block_history(messages=_simple_messages(), system="base") assert result is None @@ -512,9 +507,7 @@ async def test_slice_only_no_compaction_block_under_threshold(): async def test_full_summary_path(): """Over threshold: summary call fires, compaction_block and iterations_usage returned.""" messages = _simple_messages() - mock_response = _make_mock_response( - "Condensed history", prompt_tokens=200, completion_tokens=50 - ) + mock_response = _make_mock_response("Condensed history", prompt_tokens=200, completion_tokens=50) with ( patch( @@ -1068,13 +1061,11 @@ async def test_summary_call_does_not_emit_consecutive_user_turns(): ) summary_messages = captured_calls[0]["summary_messages"] - user_indices = [ - idx for idx, msg in enumerate(summary_messages) if msg.get("role") == "user" - ] + user_indices = [idx for idx, msg in enumerate(summary_messages) if msg.get("role") == "user"] # No two adjacent indices. - assert all( - b - a > 1 for a, b in zip(user_indices, user_indices[1:]) - ), f"two consecutive user turns produced: {summary_messages}" + assert all(b - a > 1 for a, b in zip(user_indices, user_indices[1:])), ( + f"two consecutive user turns produced: {summary_messages}" + ) async def test_summary_call_sends_default_max_tokens(): @@ -1157,9 +1148,9 @@ def test_summary_max_tokens_setting_falls_back_for_invalid_values(): "litellm.proxy.proxy_server.general_settings", {"context_management_summary_max_tokens": bad}, ): - assert ( - _read_summary_max_tokens_setting() == COMPACT_SUMMARY_MAX_TOKENS - ), f"expected default for invalid override {bad!r}" + assert _read_summary_max_tokens_setting() == COMPACT_SUMMARY_MAX_TOKENS, ( + f"expected default for invalid override {bad!r}" + ) async def test_summary_call_sends_default_timeout(): @@ -1282,9 +1273,7 @@ async def test_summary_model_denied_when_team_not_in_allowlist(): tools=None, system=None, edit_spec=_EDIT_SPEC_DEFAULT, - user_api_key_auth=_fake_user_api_key_auth( - key_models=["all-proxy-models"], team_models=["gpt-4o"] - ), + user_api_key_auth=_fake_user_api_key_auth(key_models=["all-proxy-models"], team_models=["gpt-4o"]), ) mock_call.assert_not_awaited() @@ -1313,9 +1302,7 @@ async def test_summary_model_allowed_when_in_key_allowlist(): tools=None, system=None, edit_spec=_EDIT_SPEC_DEFAULT, - user_api_key_auth=_fake_user_api_key_auth( - key_models=["claude-haiku-4-5", "gpt-4o"] - ), + user_api_key_auth=_fake_user_api_key_auth(key_models=["claude-haiku-4-5", "gpt-4o"]), ) mock_call.assert_awaited_once() @@ -1521,9 +1508,7 @@ async def test_summary_model_denied_when_key_over_model_budget(): limiter = MagicMock() limiter.is_key_within_model_budget = AsyncMock( - side_effect=litellm.BudgetExceededError( - message="over budget", current_cost=10, max_budget=5 - ) + side_effect=litellm.BudgetExceededError(message="over budget", current_cost=10, max_budget=5) ) with ( @@ -1574,9 +1559,7 @@ async def test_summary_model_denied_when_user_over_model_budget(): limiter = MagicMock() limiter.is_user_within_model_budget = AsyncMock( - side_effect=litellm.BudgetExceededError( - message="over budget", current_cost=10, max_budget=5 - ) + side_effect=litellm.BudgetExceededError(message="over budget", current_cost=10, max_budget=5) ) with ( @@ -1617,9 +1600,7 @@ async def test_summary_model_denied_when_user_over_model_budget(): _PROXY_VirtualKeyModelMaxBudgetLimiter, ) - real_params = inspect.signature( - _PROXY_VirtualKeyModelMaxBudgetLimiter.is_user_within_model_budget - ).parameters + real_params = inspect.signature(_PROXY_VirtualKeyModelMaxBudgetLimiter.is_user_within_model_budget).parameters for kwarg in ("user_id", "user_model_max_budget", "model"): assert kwarg in real_params, f"compact.py passes {kwarg}=, which the limiter no longer accepts" @@ -1641,9 +1622,7 @@ async def test_summary_model_denied_when_end_user_over_model_budget(): limiter = MagicMock() limiter.is_key_within_model_budget = AsyncMock(return_value=True) limiter.is_end_user_within_model_budget = AsyncMock( - side_effect=litellm.BudgetExceededError( - message="over budget", current_cost=10, max_budget=5 - ) + side_effect=litellm.BudgetExceededError(message="over budget", current_cost=10, max_budget=5) ) with ( @@ -1956,9 +1935,7 @@ async def test_model_budget_metadata_propagated_to_summary_call(): parent_litellm_metadata = { "user_api_key": "sk-test", "user_api_key_model_max_budget": {"claude-haiku-4-5": {"budget_limit": 5}}, - "user_api_key_end_user_model_max_budget": { - "claude-haiku-4-5": {"budget_limit": 2} - }, + "user_api_key_end_user_model_max_budget": {"claude-haiku-4-5": {"budget_limit": 2}}, } with ( @@ -1983,12 +1960,8 @@ async def test_model_budget_metadata_propagated_to_summary_call(): ) propagated = mock_call.call_args.kwargs["metadata"] - assert propagated["user_api_key_model_max_budget"] == { - "claude-haiku-4-5": {"budget_limit": 5} - } - assert propagated["user_api_key_end_user_model_max_budget"] == { - "claude-haiku-4-5": {"budget_limit": 2} - } + assert propagated["user_api_key_model_max_budget"] == {"claude-haiku-4-5": {"budget_limit": 5}} + assert propagated["user_api_key_end_user_model_max_budget"] == {"claude-haiku-4-5": {"budget_limit": 2}} async def test_summary_call_propagates_allowed_model_region(): @@ -2460,9 +2433,7 @@ def test_endpoint_returns_anthropic_400_on_context_management_error(): mock_proxy_server.version = "test" with patch.dict(sys.modules, {"litellm.proxy.proxy_server": mock_proxy_server}): - with patch( - "litellm.proxy.anthropic_endpoints.endpoints.ProxyBaseLLMRequestProcessing" - ) as mock_cls: + with patch("litellm.proxy.anthropic_endpoints.endpoints.ProxyBaseLLMRequestProcessing") as mock_cls: mock_instance = MagicMock() mock_instance.base_process_llm_request = AsyncMock( side_effect=AnthropicContextManagementError( @@ -2521,9 +2492,7 @@ def test_endpoint_runs_failure_hook_on_500_context_management_error(): mock_proxy_server.version = "test" with patch.dict(sys.modules, {"litellm.proxy.proxy_server": mock_proxy_server}): - with patch( - "litellm.proxy.anthropic_endpoints.endpoints.ProxyBaseLLMRequestProcessing" - ) as mock_cls: + with patch("litellm.proxy.anthropic_endpoints.endpoints.ProxyBaseLLMRequestProcessing") as mock_cls: mock_instance = MagicMock() mock_instance.base_process_llm_request = AsyncMock( side_effect=AnthropicContextManagementError( diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_dispatcher.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_dispatcher.py index 50c72cfe8d0..56b1ce16ebc 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_dispatcher.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/context_management/test_dispatcher.py @@ -50,9 +50,7 @@ async def test_unknown_edit_type_is_noop(): messages=messages, tools=None, system=None, - context_management_spec={ - "edits": [{"type": "totally_not_a_real_edit_20999999"}] - }, + context_management_spec={"edits": [{"type": "totally_not_a_real_edit_20999999"}]}, ) assert result.applied_edits == [] assert result.messages == messages diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_advisor_integration.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_advisor_integration.py index 414ba8f0f5c..b6df9ecf3bd 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_advisor_integration.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_advisor_integration.py @@ -39,9 +39,7 @@ def _text_resp(text: str, model: str = "gpt-4o-mini") -> Dict: } -def _advisor_call_resp( - question: str = "How do I approach this?", tool_id: str = "tid_01" -) -> Dict: +def _advisor_call_resp(question: str = "How do I approach this?", tool_id: str = "tid_01") -> Dict: return { "id": "msg_int_test", "type": "message", @@ -106,14 +104,10 @@ async def test_full_dispatch_interceptor_fires_and_loop_completes(): assert isinstance(result, dict) content = result.get("content", []) text_blocks = [b for b in content if b.get("type") == "text"] - advisor_uses = [ - b for b in content if b.get("type") == "tool_use" and b.get("name") == "advisor" - ] + advisor_uses = [b for b in content if b.get("type") == "tool_use" and b.get("name") == "advisor"] assert len(text_blocks) >= 1, "Final response must have text" - assert ( - len(advisor_uses) == 0 - ), "No advisor tool_use blocks must appear in final output" + assert len(advisor_uses) == 0, "No advisor tool_use blocks must appear in final output" # --------------------------------------------------------------------------- @@ -221,9 +215,7 @@ async def test_named_params_forwarded_into_advisor_executor_subcall(): captured_executor_kwargs: Dict = {} - async def mock_handler( - model, messages, tools, stream, max_tokens, custom_llm_provider, **kwargs - ): + async def mock_handler(model, messages, tools, stream, max_tokens, custom_llm_provider, **kwargs): # First call is the executor sub-call (returns advisor tool_use). # Capture its kwargs so we can assert the forwarded params. if not captured_executor_kwargs: @@ -267,8 +259,7 @@ async def test_named_params_forwarded_into_advisor_executor_subcall(): ) assert captured_executor_kwargs["thinking"] == {"type": "adaptive"}, ( - "thinking must be forwarded into executor sub-call — see " - "anthropic_messages.handler interceptor invocation." + "thinking must be forwarded into executor sub-call — see anthropic_messages.handler interceptor invocation." ) # The advisor enriches metadata with `advisor_sub_call` / `parent_request_id`, # but the original caller fields must survive into the executor sub-call. @@ -304,9 +295,7 @@ async def test_pre_request_hook_override_does_not_collide_with_explicit_kwargs() captured: Dict = {} - async def mock_handler( - model, messages, tools, stream, max_tokens, custom_llm_provider, **kwargs - ): + async def mock_handler(model, messages, tools, stream, max_tokens, custom_llm_provider, **kwargs): if not captured: captured.update( { @@ -320,9 +309,7 @@ async def test_pre_request_hook_override_does_not_collide_with_explicit_kwargs() return _text_resp("Some advice.", model="claude-opus-4-6") return _text_resp("Final answer.") - async def fake_pre_request_hooks( - model, messages, tools, stream, custom_llm_provider, **hook_kwargs - ): + async def fake_pre_request_hooks(model, messages, tools, stream, custom_llm_provider, **hook_kwargs): # Simulate a CustomLogger.async_pre_request_hook that overrides several # named params on its way through. Without the request_kwargs.pop() # extraction in handler.py, these would collide with the explicit diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py index db8aae6702f..4a8a38a8acc 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_agentic_streaming_iterator.py @@ -70,9 +70,7 @@ def _build_simple_text_stream() -> List[bytes]: }, ) ) - chunks.append( - _sse_event("content_block_stop", {"type": "content_block_stop", "index": 0}) - ) + chunks.append(_sse_event("content_block_stop", {"type": "content_block_stop", "index": 0})) chunks.append( _sse_event( "message_delta", @@ -145,9 +143,7 @@ def _build_tool_use_stream() -> List[bytes]: }, ) ) - chunks.append( - _sse_event("content_block_stop", {"type": "content_block_stop", "index": 0}) - ) + chunks.append(_sse_event("content_block_stop", {"type": "content_block_stop", "index": 0})) # tool_use block chunks.append( _sse_event( @@ -187,9 +183,7 @@ def _build_tool_use_stream() -> List[bytes]: }, ) ) - chunks.append( - _sse_event("content_block_stop", {"type": "content_block_stop", "index": 1}) - ) + chunks.append(_sse_event("content_block_stop", {"type": "content_block_stop", "index": 1})) chunks.append( _sse_event( "message_delta", @@ -234,9 +228,7 @@ class MockAsyncStream: class TestParseSSEEvents: def test_should_parse_single_event(self): - raw = _sse_event( - "message_start", {"type": "message_start", "message": {"id": "1"}} - ) + raw = _sse_event("message_start", {"type": "message_start", "message": {"id": "1"}}) events = _parse_sse_events(raw) assert len(events) == 1 assert events[0][0] == "message_start" @@ -407,9 +399,7 @@ class TestHandleMessageDelta: class TestRebuildAnthropicResponse: def test_should_rebuild_simple_text_response(self): raw_bytes = _build_simple_text_stream() - result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse( - raw_bytes - ) + result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse(raw_bytes) assert result is not None assert result["id"] == "msg_123" assert result["model"] == "claude-sonnet-4-20250514" @@ -422,9 +412,7 @@ class TestRebuildAnthropicResponse: def test_should_rebuild_tool_use_response(self): raw_bytes = _build_tool_use_stream() - result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse( - raw_bytes - ) + result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse(raw_bytes) assert result is not None assert result["id"] == "msg_tool_456" assert result["stop_reason"] == "tool_use" @@ -452,23 +440,17 @@ class TestRebuildAnthropicResponse: }, ) ] - result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse( - raw_bytes - ) + result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse(raw_bytes) assert result is None def test_should_handle_empty_bytes(self): - result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse( - [] - ) + result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse([]) assert result is None def test_should_handle_multi_event_chunks(self): """When multiple SSE events arrive in a single bytes chunk.""" combined = b"".join(_build_simple_text_stream()) - result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse( - [combined] - ) + result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse([combined]) assert result is not None assert result["content"][0]["text"] == "Hello, world!" @@ -500,9 +482,7 @@ class TestRebuildAnthropicResponse: ), _sse_event("message_stop", {"type": "message_stop"}), ] - result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse( - raw_bytes - ) + result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse(raw_bytes) assert result is not None assert result["usage"]["cache_creation_input_tokens"] == 50 assert result["usage"]["cache_read_input_tokens"] == 30 @@ -543,9 +523,7 @@ class TestRebuildAnthropicResponse: ), _sse_event("message_stop", {"type": "message_stop"}), ] - result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse( - raw_bytes - ) + result = AgenticAnthropicStreamingIterator._rebuild_anthropic_response_from_sse(raw_bytes) assert result is not None assert result["content"][0]["type"] == "redacted_thinking" @@ -672,9 +650,7 @@ class TestAgenticStreamingIteratorPhase2: } mock_handler = MagicMock() - mock_handler._call_agentic_completion_hooks = AsyncMock( - return_value=fake_response - ) + mock_handler._call_agentic_completion_hooks = AsyncMock(return_value=fake_response) iterator = AgenticAnthropicStreamingIterator( completion_stream=mock_stream, @@ -707,9 +683,7 @@ class TestAgenticStreamingIteratorErrorHandling: mock_stream = MockAsyncStream(chunks) mock_handler = MagicMock() - mock_handler._call_agentic_completion_hooks = AsyncMock( - side_effect=RuntimeError("hook exploded") - ) + mock_handler._call_agentic_completion_hooks = AsyncMock(side_effect=RuntimeError("hook exploded")) mock_logging = MagicMock() mock_logging.litellm_call_id = "test_call_123" diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py index 9e58ded81bd..3d7582135a2 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_experimental_pass_through_messages_handler.py @@ -290,21 +290,15 @@ async def test_bedrock_converse_budget_tokens_preserved(): mock_acompletion.assert_called_once() call_kwargs = mock_acompletion.call_args.kwargs - print( - "acompletion call kwargs: ", json.dumps(call_kwargs, indent=4, default=str) - ) + print("acompletion call kwargs: ", json.dumps(call_kwargs, indent=4, default=str)) # Verify thinking parameter is passed through with budget_tokens preserved thinking_param = call_kwargs.get("thinking") - assert ( - thinking_param is not None - ), "thinking parameter should be passed to acompletion" - assert ( - thinking_param.get("type") == "enabled" - ), "thinking.type should be 'enabled'" - assert ( - thinking_param.get("budget_tokens") == 1024 - ), f"thinking.budget_tokens should be 1024, but got {thinking_param.get('budget_tokens')}" + assert thinking_param is not None, "thinking parameter should be passed to acompletion" + assert thinking_param.get("type") == "enabled", "thinking.type should be 'enabled'" + assert thinking_param.get("budget_tokens") == 1024, ( + f"thinking.budget_tokens should be 1024, but got {thinking_param.get('budget_tokens')}" + ) def test_openai_model_with_thinking_converts_to_reasoning(): @@ -336,23 +330,18 @@ def test_openai_model_with_thinking_converts_to_reasoning(): call_kwargs = mock_responses.call_args.kwargs # Verify reasoning is set (converted from thinking) - assert ( - "reasoning" in call_kwargs - ), "reasoning should be passed to litellm.responses" + assert "reasoning" in call_kwargs, "reasoning should be passed to litellm.responses" # budget_tokens=1024 -> effort="low" (at the LOW budget threshold) # reasoning_auto_summary is False by default, so no summary key expected_reasoning = {"effort": "low"} assert call_kwargs["reasoning"] == expected_reasoning, ( - f"reasoning should be {expected_reasoning} for budget_tokens=1024, " - f"got {call_kwargs.get('reasoning')}" + f"reasoning should be {expected_reasoning} for budget_tokens=1024, got {call_kwargs.get('reasoning')}" ) assert "summary" not in call_kwargs["reasoning"] # Verify thinking is NOT passed directly to the Responses API - assert ( - "thinking" not in call_kwargs - ), "thinking should NOT be passed directly to litellm.responses" + assert "thinking" not in call_kwargs, "thinking should NOT be passed directly to litellm.responses" class TestThinkingParameterTransformation: @@ -405,9 +394,7 @@ class TestThinkingParameterTransformation: thinking=thinking, model="openai/gpt-5.2", ) - assert result == { - "reasoning_effort": {"effort": "high", "summary": "detailed"} - } + assert result == {"reasoning_effort": {"effort": "high", "summary": "detailed"}} finally: litellm.reasoning_auto_summary = original @@ -605,9 +592,9 @@ class TestThinkingSummaryPreservation: mock_responses.assert_called_once() call_kwargs = mock_responses.call_args.kwargs reasoning = call_kwargs["reasoning"] - assert ( - reasoning["summary"] == "concise" - ), f"Expected summary='concise', got summary='{reasoning.get('summary')}'" + assert reasoning["summary"] == "concise", ( + f"Expected summary='concise', got summary='{reasoning.get('summary')}'" + ) def test_responses_adapter_preserves_summary(self): """translate_thinking_to_reasoning should include summary when user provides it.""" @@ -616,9 +603,7 @@ class TestThinkingSummaryPreservation: ) thinking = {"type": "enabled", "budget_tokens": 5000, "summary": "concise"} - result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning( - thinking - ) + result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking) assert result == {"effort": "high", "summary": "concise"} def test_responses_adapter_no_summary_by_default(self): @@ -632,11 +617,7 @@ class TestThinkingSummaryPreservation: try: litellm.reasoning_auto_summary = False thinking = {"type": "enabled", "budget_tokens": 5000} - result = ( - LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning( - thinking - ) - ) + result = LiteLLMAnthropicToResponsesAPIAdapter.translate_thinking_to_reasoning(thinking) assert result == {"effort": "high"} assert result is not None and "summary" not in result finally: @@ -653,9 +634,7 @@ class TestThinkingSummaryPreservation: thinking=thinking, model="openai/gpt-5.2", ) - assert result == { - "reasoning_effort": {"effort": "high", "summary": "concise"} - } + assert result == {"reasoning_effort": {"effort": "high", "summary": "concise"}} def test_translate_thinking_for_model_disabled_stays_plain_string_when_auto_summary_enabled(self): """Disabled thinking must stay a plain string even when reasoning_auto_summary is on.""" @@ -801,9 +780,7 @@ def test_presanitized_flag_not_leaked_to_provider_params(): def fake_base_handler(*args, **kwargs): captured.update(kwargs) - captured["optional"] = kwargs.get( - "anthropic_messages_optional_request_params", {} - ) + captured["optional"] = kwargs.get("anthropic_messages_optional_request_params", {}) return "stub" with patch.object( @@ -981,9 +958,7 @@ def test_first_party_claude_4_8_plus_cost_map_entries_carry_mid_conversation_sys import litellm - cost_map_path = os.path.join( - os.path.dirname(litellm.__file__), "model_prices_and_context_window_backup.json" - ) + cost_map_path = os.path.join(os.path.dirname(litellm.__file__), "model_prices_and_context_window_backup.json") with open(cost_map_path) as f: cost_map = json.load(f) rules = cost_map["fallback_generalizations"]["rules"] @@ -1022,9 +997,7 @@ def test_first_party_claude_4_8_plus_cost_map_entries_carry_mid_conversation_sys ("perplexity/sonar", "sonar", "https://api.perplexity.ai/chat/completions"), ], ) -async def test_messages_strips_provider_prefix_exactly_once( - requested_model, expected_wire_model, expected_url -): +async def test_messages_strips_provider_prefix_exactly_once(requested_model, expected_wire_model, expected_url): """ BerriAI/litellm#37716: only the leading provider segment may be stripped on the way upstream. diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py index 6900f1062bf..87d2a6fed0f 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py @@ -91,13 +91,11 @@ def test_messages_drops_speed_for_vertex_opus_with_drop_params(monkeypatch): """Regression: a vertex_ai Opus passthrough must drop ``speed`` even though the prefix-stripped model id maps to a fast-mode-capable direct-Anthropic entry.""" monkeypatch.setattr(litellm, "drop_params", True) - optional_params = ( - AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( - params={"max_tokens": 1024, "speed": "fast"}, - model="claude-opus-4-8", - drop_params=False, - custom_llm_provider="vertex_ai", - ) + optional_params = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"max_tokens": 1024, "speed": "fast"}, + model="claude-opus-4-8", + drop_params=False, + custom_llm_provider="vertex_ai", ) assert "speed" not in optional_params diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_structured_outputs.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_structured_outputs.py index e6d5c6f4ee1..8a0d195107b 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_structured_outputs.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_structured_outputs.py @@ -13,9 +13,7 @@ def test_output_format_supported_and_transforms_correctly(): config = AnthropicMessagesConfig() # 1. Verify it's in supported parameters - supported_params = config.get_supported_anthropic_messages_params( - "claude-sonnet-4-5" - ) + supported_params = config.get_supported_anthropic_messages_params("claude-sonnet-4-5") assert "output_format" in supported_params # 2. Verify transformation preserves output_format and adds beta header diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py index a0d1f9de6ec..5736de66670 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_content_after_stop_reason.py @@ -30,26 +30,14 @@ class MockCompletionStreamWithContentAfterStopReason: self.responses = [ # Initial text content ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content="Hello"), index=0, finish_reason=None - ) - ], + choices=[StreamingChoices(delta=Delta(content="Hello"), index=0, finish_reason=None)], ), ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content=" world"), index=0, finish_reason=None - ) - ], + choices=[StreamingChoices(delta=Delta(content=" world"), index=0, finish_reason=None)], ), # Message delta with stop_reason AND usage (this is how it actually comes from the API) ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content=""), index=0, finish_reason="stop" - ) - ], + choices=[StreamingChoices(delta=Delta(content=""), index=0, finish_reason="stop")], usage=Usage(prompt_tokens=230, completion_tokens=65, total_tokens=295), ), # Additional content after the stop_reason - this simulates the scenario @@ -118,9 +106,9 @@ def test_anthropic_stream_wrapper_content_after_stop_reason(): print(f"Expected chunk types: {expected_types}") # Verify we have the expected number of chunks - assert len(chunk_types) >= len( - expected_types - ), f"Expected at least {len(expected_types)} chunks, got {len(chunk_types)}" + assert len(chunk_types) >= len(expected_types), ( + f"Expected at least {len(expected_types)} chunks, got {len(chunk_types)}" + ) # Verify key chunk types are present assert "message_start" in chunk_types @@ -143,15 +131,9 @@ def test_anthropic_stream_wrapper_content_after_stop_reason(): delta = message_delta_chunk.get("delta", {}) usage = message_delta_chunk.get("usage", {}) - assert ( - delta.get("stop_reason") == "end_turn" - ), f"Expected stop_reason 'end_turn', got {delta.get('stop_reason')}" - assert ( - usage.get("input_tokens") == 230 - ), f"Expected input_tokens 230, got {usage.get('input_tokens')}" - assert ( - usage.get("output_tokens") == 65 - ), f"Expected output_tokens 65, got {usage.get('output_tokens')}" + assert delta.get("stop_reason") == "end_turn", f"Expected stop_reason 'end_turn', got {delta.get('stop_reason')}" + assert usage.get("input_tokens") == 230, f"Expected input_tokens 230, got {usage.get('input_tokens')}" + assert usage.get("output_tokens") == 65, f"Expected output_tokens 65, got {usage.get('output_tokens')}" # Verify content_block_stop comes before message_delta content_block_stop_index = None @@ -165,9 +147,7 @@ def test_anthropic_stream_wrapper_content_after_stop_reason(): assert content_block_stop_index is not None, "content_block_stop not found" assert message_delta_index is not None, "message_delta not found" - assert ( - content_block_stop_index < message_delta_index - ), "content_block_stop should come before message_delta" + assert content_block_stop_index < message_delta_index, "content_block_stop should come before message_delta" @pytest.mark.asyncio @@ -210,15 +190,9 @@ async def test_async_anthropic_stream_wrapper_content_after_stop_reason(): delta = message_delta_chunk.get("delta", {}) usage = message_delta_chunk.get("usage", {}) - assert ( - delta.get("stop_reason") == "end_turn" - ), f"Expected stop_reason 'end_turn', got {delta.get('stop_reason')}" - assert ( - usage.get("input_tokens") == 230 - ), f"Expected input_tokens 230, got {usage.get('input_tokens')}" - assert ( - usage.get("output_tokens") == 65 - ), f"Expected output_tokens 65, got {usage.get('output_tokens')}" + assert delta.get("stop_reason") == "end_turn", f"Expected stop_reason 'end_turn', got {delta.get('stop_reason')}" + assert usage.get("input_tokens") == 230, f"Expected input_tokens 230, got {usage.get('input_tokens')}" + assert usage.get("output_tokens") == 65, f"Expected output_tokens 65, got {usage.get('output_tokens')}" def test_usage_merging_behavior(): @@ -234,18 +208,10 @@ def test_usage_merging_behavior(): for chunk in wrapper: chunks.append(chunk) # If this is a message_delta with stop_reason, verify it has usage - if ( - chunk.get("type") == "message_delta" - and chunk.get("delta", {}).get("stop_reason") is not None - ): - + if chunk.get("type") == "message_delta" and chunk.get("delta", {}).get("stop_reason") is not None: usage = chunk.get("usage", {}) - assert ( - usage.get("input_tokens") is not None - ), "Usage should be merged with stop_reason chunk" - assert ( - usage.get("output_tokens") is not None - ), "Usage should be merged with stop_reason chunk" + assert usage.get("input_tokens") is not None, "Usage should be merged with stop_reason chunk" + assert usage.get("output_tokens") is not None, "Usage should be merged with stop_reason chunk" break @@ -273,12 +239,8 @@ def test_sse_wrapper_with_content_after_stop_reason(): lines = chunk_str.split("\n") # Should have event and data lines - assert any( - line.startswith("event: ") for line in lines - ), f"Missing event line in: {chunk_str}" - assert any( - line.startswith("data: ") for line in lines - ), f"Missing data line in: {chunk_str}" + assert any(line.startswith("event: ") for line in lines), f"Missing event line in: {chunk_str}" + assert any(line.startswith("data: ") for line in lines), f"Missing data line in: {chunk_str}" @pytest.mark.asyncio @@ -306,12 +268,8 @@ async def test_async_sse_wrapper_with_content_after_stop_reason(): lines = chunk_str.split("\n") # Should have event and data lines - assert any( - line.startswith("event: ") for line in lines - ), f"Missing event line in: {chunk_str}" - assert any( - line.startswith("data: ") for line in lines - ), f"Missing data line in: {chunk_str}" + assert any(line.startswith("event: ") for line in lines), f"Missing event line in: {chunk_str}" + assert any(line.startswith("data: ") for line in lines), f"Missing data line in: {chunk_str}" if __name__ == "__main__": diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py index b6914809263..c2daf11a1af 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_mcp_handler.py @@ -58,7 +58,7 @@ def test_anthropic_messages_handler_skips_the_gateway_on_recursion(): "litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp", new=AsyncMock(return_value={"routed": True}), ) as routed: - with pytest.raises(ValueError, match='anthropic_messages_handler is not implemented for sync calls'): + with pytest.raises(ValueError, match="anthropic_messages_handler is not implemented for sync calls"): anthropic_messages_handler( max_tokens=100, messages=[{"role": "user", "content": "hi"}], @@ -77,7 +77,7 @@ def test_anthropic_messages_handler_leaves_native_tools_alone(): "litellm.llms.anthropic.experimental_pass_through.messages.mcp_handler.anthropic_messages_with_mcp", new=AsyncMock(return_value={"routed": True}), ) as routed: - with pytest.raises(ValueError, match='anthropic_messages_handler is not implemented for sync calls'): + with pytest.raises(ValueError, match="anthropic_messages_handler is not implemented for sync calls"): anthropic_messages_handler( max_tokens=100, messages=[{"role": "user", "content": "hi"}], @@ -114,9 +114,7 @@ def test_build_tool_result_message_uses_anthropic_tool_result_blocks(): message = _build_tool_result_message([{"tool_call_id": "toolu_1", "result": "9 sections", "name": "read_wiki"}]) assert message["role"] == "user" - assert list(message["content"]) == [ - {"type": "tool_result", "tool_use_id": "toolu_1", "content": "9 sections"} - ] + assert list(message["content"]) == [{"type": "tool_result", "tool_use_id": "toolu_1", "content": "9 sections"}] @pytest.mark.asyncio @@ -155,19 +153,22 @@ async def test_anthropic_messages_with_mcp_forwards_the_callers_mcp_credentials( {"stop_reason": "end_turn", "content": [{"type": "text", "text": "done"}]}, ] - with patch.object(MCPRequestContext, "resolve", return_value=context), patch.object( - mcp_handler.LiteLLM_Proxy_MCP_Handler - if hasattr(mcp_handler, "LiteLLM_Proxy_MCP_Handler") - else __import__( - "litellm.responses.mcp.litellm_proxy_mcp_handler", fromlist=["LiteLLM_Proxy_MCP_Handler"] - ).LiteLLM_Proxy_MCP_Handler, - "_process_mcp_tools_without_openai_transform", - new=process, - ), patch( - "litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._execute_tool_calls", - new=execute, - ), patch( - "litellm.anthropic_messages", new=AsyncMock(side_effect=responses) + with ( + patch.object(MCPRequestContext, "resolve", return_value=context), + patch.object( + mcp_handler.LiteLLM_Proxy_MCP_Handler + if hasattr(mcp_handler, "LiteLLM_Proxy_MCP_Handler") + else __import__( + "litellm.responses.mcp.litellm_proxy_mcp_handler", fromlist=["LiteLLM_Proxy_MCP_Handler"] + ).LiteLLM_Proxy_MCP_Handler, + "_process_mcp_tools_without_openai_transform", + new=process, + ), + patch( + "litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._execute_tool_calls", + new=execute, + ), + patch("litellm.anthropic_messages", new=AsyncMock(side_effect=responses)), ): await mcp_handler.anthropic_messages_with_mcp( max_tokens=100, @@ -216,16 +217,17 @@ async def test_anthropic_messages_with_mcp_stops_when_every_tool_call_is_skipped } anthropic_messages_mock = AsyncMock(return_value=tool_use_response) - with patch.object( - MCPRequestContext, "resolve", return_value=MCPRequestContext(user_api_key_auth="auth") - ), patch( - "litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._process_mcp_tools_without_openai_transform", - new=AsyncMock(return_value=([], {})), - ), patch( - "litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._execute_tool_calls", - new=AsyncMock(return_value=[]), - ), patch( - "litellm.anthropic_messages", new=anthropic_messages_mock + with ( + patch.object(MCPRequestContext, "resolve", return_value=MCPRequestContext(user_api_key_auth="auth")), + patch( + "litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._process_mcp_tools_without_openai_transform", + new=AsyncMock(return_value=([], {})), + ), + patch( + "litellm.responses.mcp.litellm_proxy_mcp_handler.LiteLLM_Proxy_MCP_Handler._execute_tool_calls", + new=AsyncMock(return_value=[]), + ), + patch("litellm.anthropic_messages", new=anthropic_messages_mock), ): result = await mcp_handler.anthropic_messages_with_mcp( max_tokens=100, diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py index 137286a18c4..b13dc850f61 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_parallel_tool_calls.py @@ -1,7 +1,6 @@ from typing import List - from litellm.llms.anthropic.experimental_pass_through.adapters.streaming_iterator import ( AnthropicStreamWrapper, ) @@ -53,9 +52,7 @@ def construct_text_chunk(text: str) -> ModelResponseStream: ) -def construct_split_tool_call( - id: str, function_name: str, function_arg_parts: List[str] -) -> List[ModelResponseStream]: +def construct_split_tool_call(id: str, function_name: str, function_arg_parts: List[str]) -> List[ModelResponseStream]: return [ # https://platform.openai.com/docs/guides/function-calling#streaming ModelResponseStream( @@ -144,10 +141,7 @@ def test_anthropic_stream_wrapper_single_tool_call(): get_weather_calls = 0 for chunk in chunks: - if ( - chunk.get("type") == "content_block_start" - and chunk["content_block"]["type"] == "tool_use" - ): + if chunk.get("type") == "content_block_start" and chunk["content_block"]["type"] == "tool_use": if chunk["content_block"]["name"] == "get_weather": get_weather_calls += 1 @@ -203,10 +197,7 @@ def test_anthropic_stream_wrapper_back_to_back_tool_calls(): get_weather_calls = 0 for chunk in chunks: - if ( - chunk.get("type") == "content_block_start" - and chunk["content_block"]["type"] == "tool_use" - ): + if chunk.get("type") == "content_block_start" and chunk["content_block"]["type"] == "tool_use": if chunk["content_block"]["name"] == "get_weather": get_weather_calls += 1 @@ -218,9 +209,7 @@ def test_anthropic_stream_wrapper_interleaved_tool_calls_and_text(): *construct_split_tool_call("tooluse_foo", "get_weather", ['{"city":', '"NY"}']), construct_text_chunk("The weather is nice today."), *construct_split_tool_call("tooluse_bar", "get_weather", ['{"city":', '"SF"}']), - *construct_split_tool_call( - "tooluse_bar", "get_weather", ['{"city":', '"CHI"}'] - ), + *construct_split_tool_call("tooluse_bar", "get_weather", ['{"city":', '"CHI"}']), construct_text_chunk("The weather is not so nice today."), ModelResponseStream( choices=[ @@ -280,8 +269,7 @@ def test_anthropic_stream_wrapper_interleaved_tool_calls_and_text(): text_deltas = [ chunk["delta"]["text"] for chunk in chunks - if chunk.get("type") == "content_block_delta" - and chunk["delta"].get("type") == "text_delta" + if chunk.get("type") == "content_block_delta" and chunk["delta"].get("type") == "text_delta" ] assert text_deltas == [ "The weather is nice today.", @@ -291,10 +279,7 @@ def test_anthropic_stream_wrapper_interleaved_tool_calls_and_text(): get_weather_calls = 0 for chunk in chunks: - if ( - chunk.get("type") == "content_block_start" - and chunk["content_block"]["type"] == "tool_use" - ): + if chunk.get("type") == "content_block_start" and chunk["content_block"]["type"] == "tool_use": if chunk["content_block"]["name"] == "get_weather": get_weather_calls += 1 diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py index f478bbb9b50..dacb59c9fc0 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_auto_summary_messages.py @@ -29,13 +29,12 @@ def _call_handler_and_capture_optional_params(thinking=None, **extra_kwargs): """ captured = {} - with patch( - "litellm.llms.anthropic.experimental_pass_through.messages.handler." - "base_llm_http_handler" - ) as mock_handler, patch( - "litellm.llms.anthropic.experimental_pass_through.messages.handler." - "ProviderConfigManager" - ) as mock_pcm: + with ( + patch( + "litellm.llms.anthropic.experimental_pass_through.messages.handler.base_llm_http_handler" + ) as mock_handler, + patch("litellm.llms.anthropic.experimental_pass_through.messages.handler.ProviderConfigManager") as mock_pcm, + ): # Make get_provider_anthropic_messages_config return a non-None config # so the handler takes the native Anthropic path mock_pcm.get_provider_anthropic_messages_config.return_value = MagicMock() @@ -71,9 +70,7 @@ class TestReasoningAutoSummaryMessages: def test_adaptive_thinking_gets_display_summarized(self): """reasoning_auto_summary=True + thinking.type='adaptive' -> display='summarized'.""" with patch.object(litellm, "reasoning_auto_summary", True): - params = _call_handler_and_capture_optional_params( - thinking={"type": "adaptive", "budget_tokens": 5000} - ) + params = _call_handler_and_capture_optional_params(thinking={"type": "adaptive", "budget_tokens": 5000}) thinking = params.get("thinking", {}) assert thinking.get("display") == "summarized" assert thinking.get("type") == "adaptive" @@ -82,9 +79,7 @@ class TestReasoningAutoSummaryMessages: def test_enabled_thinking_gets_display_summarized(self): """reasoning_auto_summary=True + thinking.type='enabled' -> display='summarized'.""" with patch.object(litellm, "reasoning_auto_summary", True): - params = _call_handler_and_capture_optional_params( - thinking={"type": "enabled", "budget_tokens": 10000} - ) + params = _call_handler_and_capture_optional_params(thinking={"type": "enabled", "budget_tokens": 10000}) thinking = params.get("thinking", {}) assert thinking.get("display") == "summarized" assert thinking.get("type") == "enabled" @@ -92,18 +87,14 @@ class TestReasoningAutoSummaryMessages: def test_disabled_thinking_no_display(self): """reasoning_auto_summary=True + thinking.type='disabled' -> display NOT set.""" with patch.object(litellm, "reasoning_auto_summary", True): - params = _call_handler_and_capture_optional_params( - thinking={"type": "disabled"} - ) + params = _call_handler_and_capture_optional_params(thinking={"type": "disabled"}) thinking = params.get("thinking", {}) assert "display" not in thinking def test_no_injection_when_flag_false(self): """reasoning_auto_summary=False + active thinking -> display NOT set.""" with patch.object(litellm, "reasoning_auto_summary", False): - params = _call_handler_and_capture_optional_params( - thinking={"type": "enabled", "budget_tokens": 10000} - ) + params = _call_handler_and_capture_optional_params(thinking={"type": "enabled", "budget_tokens": 10000}) thinking = params.get("thinking", {}) assert "display" not in thinking @@ -117,12 +108,11 @@ class TestReasoningAutoSummaryMessages: def test_env_var_enables_auto_summary(self): """LITELLM_REASONING_AUTO_SUMMARY=true env var enables the feature.""" - with patch.object(litellm, "reasoning_auto_summary", False), patch.dict( - os.environ, {"LITELLM_REASONING_AUTO_SUMMARY": "true"} + with ( + patch.object(litellm, "reasoning_auto_summary", False), + patch.dict(os.environ, {"LITELLM_REASONING_AUTO_SUMMARY": "true"}), ): - params = _call_handler_and_capture_optional_params( - thinking={"type": "adaptive", "budget_tokens": 5000} - ) + params = _call_handler_and_capture_optional_params(thinking={"type": "adaptive", "budget_tokens": 5000}) thinking = params.get("thinking", {}) assert thinking.get("display") == "summarized" diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py index f393a7b50b1..b7b628f94a4 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_reasoning_effort_translation.py @@ -17,7 +17,6 @@ from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_tran ) - @pytest.mark.parametrize( "reasoning_effort,expected_effort", [ @@ -29,9 +28,7 @@ from litellm.llms.bedrock.messages.invoke_transformations.anthropic_claude3_tran ("max", "max"), ], ) -def test_reasoning_effort_maps_to_output_config_for_adaptive_model( - reasoning_effort, expected_effort -): +def test_reasoning_effort_maps_to_output_config_for_adaptive_model(reasoning_effort, expected_effort): config = AnthropicMessagesConfig() optional_params = {"max_tokens": 1024, "reasoning_effort": reasoning_effort} @@ -141,9 +138,7 @@ def test_reasoning_effort_unsupported_tier_raises_400_messages(model, bad_effort ("invoke/us.anthropic.claude-opus-4-7", "xhigh", "xhigh"), ], ) -def test_bedrock_invoke_messages_clamps_effort_to_ceiling( - local_model_cost_map, model, effort, expected_effort -): +def test_bedrock_invoke_messages_clamps_effort_to_ceiling(local_model_cost_map, model, effort, expected_effort): """Bedrock Invoke /v1/messages degrades effort to the model's ceiling. Claude Code "goal mode" sends ``xhigh``; Opus 4.6 must clamp to ``max`` @@ -189,9 +184,7 @@ def test_bedrock_invoke_messages_rejects_xhigh_without_ceiling(local_model_cost_ "bedrock/invoke/us.anthropic.claude-sonnet-4-6", ], ) -def test_reasoning_effort_max_accepted_on_sonnet_46_messages( - local_model_cost_map, model -): +def test_reasoning_effort_max_accepted_on_sonnet_46_messages(local_model_cost_map, model): config = AnthropicMessagesConfig() optional_params = {"max_tokens": 1024, "reasoning_effort": "max"} @@ -249,9 +242,7 @@ def test_explicit_thinking_wins_over_reasoning_effort(): def test_reasoning_effort_in_supported_params(): config = AnthropicMessagesConfig() - assert "reasoning_effort" in config.get_supported_anthropic_messages_params( - "claude-opus-4-7" - ) + assert "reasoning_effort" in config.get_supported_anthropic_messages_params("claude-opus-4-7") @pytest.mark.parametrize( @@ -265,9 +256,7 @@ def test_reasoning_effort_in_supported_params(): "vertex_ai/claude-opus-4-6", ], ) -def test_legacy_thinking_high_budget_clamps_to_high_when_xhigh_unsupported( - local_model_cost_map, model -): +def test_legacy_thinking_high_budget_clamps_to_high_when_xhigh_unsupported(local_model_cost_map, model): """Claude Code sends ``thinking.budget_tokens=31999``; Sonnet 4.6 and Opus 4.6 have no ``xhigh`` tier, so the translator must emit ``high`` rather than the provider-invalid ``xhigh`` (regression for issue #29282).""" @@ -317,9 +306,7 @@ def test_legacy_thinking_high_budget_keeps_xhigh_when_supported(): "bedrock/invoke/us.anthropic.claude-opus-4-8", ], ) -def test_legacy_thinking_translates_to_adaptive_for_opus_48( - model, local_model_cost_map -): +def test_legacy_thinking_translates_to_adaptive_for_opus_48(model, local_model_cost_map): """Regression for issue #29188: Opus 4.8 requires adaptive thinking, but the legacy ``thinking.type='enabled'`` shape was passed through unchanged for Bedrock 4.8 (its cost-map entry lacked ``supports_adaptive_thinking`` and the @@ -419,9 +406,7 @@ def test_legacy_thinking_left_untouched_on_non_adaptive_model(): ("claude-sonnet-4-5", False), ], ) -def test_disabled_thinking_omitted_for_always_on_models_messages( - local_model_cost_map, model, expected_dropped -): +def test_disabled_thinking_omitted_for_always_on_models_messages(local_model_cost_map, model, expected_dropped): """/v1/messages: ``thinking={"type": "disabled"}`` is omitted for always-on-thinking models and forwarded verbatim for models that accept it.""" config = AnthropicMessagesConfig() diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py index dc2e107928f..95696a536f1 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_request_optional_param_utils.py @@ -23,11 +23,7 @@ def test_optional_param_filtering_unchanged(): "not_a_real_param": "drop me", # invalid key dropped "stream": True, } - result = ( - AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( - params - ) - ) + result = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param(params) assert result == {"temperature": 0.5, "tools": [{"name": "x"}], "stream": True} assert "top_p" not in result assert "not_a_real_param" not in result @@ -37,9 +33,7 @@ def test_valid_keys_are_memoized(): _anthropic_messages_optional_param_keys.cache_clear() first = _anthropic_messages_optional_param_keys() for _ in range(50): - AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( - {"temperature": 0.1} - ) + AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param({"temperature": 0.1}) info = _anthropic_messages_optional_param_keys.cache_info() # Resolved exactly once despite many calls. assert info.misses == 1 @@ -51,23 +45,16 @@ def test_valid_keys_are_memoized(): def test_empty_params(): - assert ( - AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( - {} - ) - == {} - ) + assert AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param({}) == {} def test_drop_params_strips_speed_for_unsupported_model(): original = litellm.drop_params litellm.drop_params = True try: - result = ( - AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( - params={"speed": "fast", "temperature": 0.5}, - model="claude-sonnet-4-6", - ) + result = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"speed": "fast", "temperature": 0.5}, + model="claude-sonnet-4-6", ) finally: litellm.drop_params = original @@ -80,11 +67,9 @@ def test_drop_params_keeps_speed_for_supporting_model(): original = litellm.drop_params litellm.drop_params = True try: - result = ( - AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( - params={"speed": "fast"}, - model="claude-opus-4-6", - ) + result = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"speed": "fast"}, + model="claude-opus-4-6", ) finally: litellm.drop_params = original diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py index bebdbe9f512..cd5f3f3f327 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_sse_wrapper.py @@ -1,4 +1,3 @@ - import pytest from fastapi.testclient import TestClient @@ -14,25 +13,13 @@ class MockCompletionStream: def __init__(self): self.responses = [ ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content="Hello"), index=0, finish_reason=None - ) - ], + choices=[StreamingChoices(delta=Delta(content="Hello"), index=0, finish_reason=None)], ), ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content=" World"), index=0, finish_reason=None - ) - ], + choices=[StreamingChoices(delta=Delta(content=" World"), index=0, finish_reason=None)], ), ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content=""), index=0, finish_reason="stop" - ) - ], + choices=[StreamingChoices(delta=Delta(content=""), index=0, finish_reason="stop")], ), ] self.index = 0 @@ -50,9 +37,7 @@ class MockCompletionStream: def test_anthropic_sse_wrapper_format(): """Test that the SSE wrapper produces proper event and data formatting""" - wrapper = AnthropicStreamWrapper( - completion_stream=MockCompletionStream(), model="claude-3" - ) + wrapper = AnthropicStreamWrapper(completion_stream=MockCompletionStream(), model="claude-3") # Get the first chunk from the SSE wrapper first_chunk = next(wrapper.anthropic_sse_wrapper()) @@ -73,9 +58,7 @@ def test_anthropic_sse_wrapper_format(): def test_anthropic_sse_wrapper_event_types(): """Test that different chunk types produce correct event types""" - wrapper = AnthropicStreamWrapper( - completion_stream=MockCompletionStream(), model="claude-3" - ) + wrapper = AnthropicStreamWrapper(completion_stream=MockCompletionStream(), model="claude-3") chunks = [] for chunk in wrapper.anthropic_sse_wrapper(): @@ -104,18 +87,10 @@ async def test_async_anthropic_sse_wrapper(): def __init__(self): self.responses = [ ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content="Hello"), index=0, finish_reason=None - ) - ], + choices=[StreamingChoices(delta=Delta(content="Hello"), index=0, finish_reason=None)], ), ModelResponseStream( - choices=[ - StreamingChoices( - delta=Delta(content=" World"), index=0, finish_reason=None - ) - ], + choices=[StreamingChoices(delta=Delta(content=" World"), index=0, finish_reason=None)], ), ] self.index = 0 @@ -130,9 +105,7 @@ async def test_async_anthropic_sse_wrapper(): self.index += 1 return response - wrapper = AnthropicStreamWrapper( - completion_stream=AsyncMockCompletionStream(), model="claude-3" - ) + wrapper = AnthropicStreamWrapper(completion_stream=AsyncMockCompletionStream(), model="claude-3") # Get the first chunk from the async SSE wrapper first_chunk = None diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_streaming_iterator.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_streaming_iterator.py index f33bb3dda8b..40218e8b0f2 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_streaming_iterator.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_streaming_iterator.py @@ -138,7 +138,7 @@ async def test_async_sse_wrapper_treats_message_stop_bytes_as_complete(): def test_is_message_stop_chunk(): assert _is_message_stop_chunk({"type": "message_stop"}) is True assert _is_message_stop_chunk({"type": "message_delta"}) is False - assert _is_message_stop_chunk(b'event: message_stop\ndata: {}\n\n') is True + assert _is_message_stop_chunk(b"event: message_stop\ndata: {}\n\n") is True assert _is_message_stop_chunk(b"raw-bytes") is False assert _is_message_stop_chunk("message_stop") is False @@ -150,7 +150,7 @@ def test_is_message_stop_chunk_ignores_substring_in_payload(): not be treated as a terminal stop event. """ delta_frame_with_substring = ( - b'event: content_block_delta\n' + b"event: content_block_delta\n" b'data: {"type": "content_block_delta", "delta": ' b'{"type": "input_json_delta", "partial_json": "\\"message_stop\\""}}\n\n' ) @@ -164,10 +164,11 @@ async def test_async_sse_wrapper_emits_error_when_bytes_stream_only_mentions_mes payload text contains `message_stop` (but never emits the actual `event: message_stop` frame) must still be flagged as incomplete. """ + async def _byte_stream(): yield b'event: message_start\ndata: {"type": "message_start"}\n\n' yield ( - b'event: content_block_delta\n' + b"event: content_block_delta\n" b'data: {"type": "content_block_delta", "delta": ' b'{"type": "input_json_delta", "partial_json": "\\"message_stop\\""}}\n\n' ) diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py index 964f4b9f68b..80de6665ac9 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/responses_adapters/test_responses_adapters_transformation.py @@ -1308,9 +1308,7 @@ class TestToolResultImages: }, { "role": "user", - "content": [ - {"type": "tool_result", "tool_use_id": "toolu_01", "content": tool_result_content} - ], + "content": [{"type": "tool_result", "tool_use_id": "toolu_01", "content": tool_result_content}], }, ] @@ -1473,7 +1471,9 @@ class TestPromptCacheBreakpointToResponses: ] def test_system_without_breakpoint_still_becomes_instructions(self): - request = _make_request(system=[{"type": "text", "text": "Be concise."}, {"type": "text", "text": "Be helpful."}]) + request = _make_request( + system=[{"type": "text", "text": "Be concise."}, {"type": "text", "text": "Be helpful."}] + ) kwargs = _ADAPTER.translate_request(request) assert kwargs["instructions"] == "Be concise.\nBe helpful." assert kwargs["input"] == [ diff --git a/tests/test_litellm/llms/anthropic/experimental_pass_through/test_reasoning_effort_fields.py b/tests/test_litellm/llms/anthropic/experimental_pass_through/test_reasoning_effort_fields.py index 08fef8c6a24..9c67aa96247 100644 --- a/tests/test_litellm/llms/anthropic/experimental_pass_through/test_reasoning_effort_fields.py +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/test_reasoning_effort_fields.py @@ -188,9 +188,7 @@ class TestNormalizeReasoningEffortValue: "litellm.utils.get_model_info", return_value=_mock_model_info(supports_minimal_reasoning_effort=True), ): - assert ( - normalize_reasoning_effort_value("minimal", model="test") == "minimal" - ) + assert normalize_reasoning_effort_value("minimal", model="test") == "minimal" def test_minimal_degrades_to_low(self): with patch( @@ -239,9 +237,7 @@ class TestAdapterAdaptiveThinking: ) adapter = LiteLLMAnthropicMessagesAdapter() - result = adapter.translate_anthropic_thinking_to_reasoning_effort( - {"type": "adaptive"} - ) + result = adapter.translate_anthropic_thinking_to_reasoning_effort({"type": "adaptive"}) assert result == "medium" def test_messages_adapter_adaptive_overridden_by_output_config(self): diff --git a/tests/test_litellm/llms/anthropic/files/test_anthropic_files_transformation.py b/tests/test_litellm/llms/anthropic/files/test_anthropic_files_transformation.py index 01aa4799c8d..f385c2f2211 100644 --- a/tests/test_litellm/llms/anthropic/files/test_anthropic_files_transformation.py +++ b/tests/test_litellm/llms/anthropic/files/test_anthropic_files_transformation.py @@ -221,10 +221,7 @@ class TestAnthropicFilesConfig: litellm_params={}, ) - assert ( - url - == f"{ANTHROPIC_FILES_API_BASE}/v1/files/..%2F..%2Fv1%2Fmessages%2Fbatches%3Flimit%3D1%23frag" - ) + assert url == f"{ANTHROPIC_FILES_API_BASE}/v1/files/..%2F..%2Fv1%2Fmessages%2Fbatches%3Flimit%3D1%23frag" assert params == {} def test_transform_retrieve_file_response(self): diff --git a/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py b/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py index da5b5ac3867..ab14d914640 100644 --- a/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py +++ b/tests/test_litellm/llms/anthropic/messages/test_advisor_orchestration.py @@ -101,9 +101,7 @@ async def test_anthropic_native_interceptor_skipped(): ) h = AdvisorOrchestrationHandler() - assert not h.can_handle( - [ADVISOR_TOOL], "anthropic" - ), "Interceptor must NOT trigger for anthropic provider" + assert not h.can_handle([ADVISOR_TOOL], "anthropic"), "Interceptor must NOT trigger for anthropic provider" # --------------------------------------------------------------------------- @@ -204,9 +202,7 @@ async def test_loop_one_advisor_call(): assert "is_prime" in texts[0]["text"] # No advisor tool_use blocks in final response - advisor_uses = [ - b for b in content if b.get("type") == "tool_use" and b.get("name") == "advisor" - ] + advisor_uses = [b for b in content if b.get("type") == "tool_use" and b.get("name") == "advisor"] assert len(advisor_uses) == 0 @@ -366,9 +362,7 @@ async def test_prior_advisor_blocks_replaced_in_history(): # Text block with advisor feedback must be present text_blocks = [b for b in content if b.get("type") == "text"] - feedback_blocks = [ - b for b in text_blocks if "advisor_feedback" in b.get("text", "") - ] + feedback_blocks = [b for b in text_blocks if "advisor_feedback" in b.get("text", "")] assert len(feedback_blocks) >= 1 assert "trial division" in feedback_blocks[0]["text"] @@ -707,11 +701,7 @@ async def test_advisor_ignores_tool_credentials_when_clientside_disabled(): with patch.dict( sys.modules, - { - "litellm.proxy.proxy_server": _fake_proxy_server( - {"allow_client_side_credentials": False} - ) - }, + {"litellm.proxy.proxy_server": _fake_proxy_server({"allow_client_side_credentials": False})}, ): captured = await _run_advisor_and_capture_subcall_kwargs() assert captured["api_key"] is None @@ -726,11 +716,7 @@ async def test_advisor_uses_tool_credentials_when_clientside_enabled(): with patch.dict( sys.modules, - { - "litellm.proxy.proxy_server": _fake_proxy_server( - {"allow_client_side_credentials": True} - ) - }, + {"litellm.proxy.proxy_server": _fake_proxy_server({"allow_client_side_credentials": True})}, ): captured = await _run_advisor_and_capture_subcall_kwargs() assert captured["api_key"] == "sk-other" diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py index 52713f32374..dd6f7e0ff8d 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_common_utils.py @@ -2316,6 +2316,31 @@ class TestWifServerOwnedAuthHeaderStrip: assert all(caller_key not in value for value in headers.values()) assert headers["user-agent"] == "caller/1.0" + @pytest.mark.parametrize("header_name", PROXY_CREDENTIAL_HEADER_NAMES) + def test_files_surface_strips_caller_credentials_too(self, monkeypatch, wif_engine, header_name): + """The files surface builds its own headers, so it needs the same strip the chat surface + has: without it a minted federation Bearer travels beside the caller's own credential.""" + from litellm.llms.anthropic.files.transformation import AnthropicFilesConfig + + for name, value in WIF_ENV.items(): + monkeypatch.setenv(name, value) + caller_key = "sk-litellm-CALLER-VIRTUAL-KEY" + + headers = AnthropicFilesConfig().validate_environment( + headers={header_name.title(): caller_key, "user-agent": "caller/1.0"}, + model="claude-sonnet-4-5", + messages=[], + optional_params={}, + litellm_params={}, + api_key=None, + api_base=None, + ) + + assert headers["authorization"] == f"Bearer {FAKE_MINTED_TOKEN}" + assert header_name == "authorization" or header_name not in {name.lower() for name in headers} + assert all(caller_key not in value for value in headers.values()) + assert headers["user-agent"] == "caller/1.0" + def test_no_mint_preserves_caller_supplied_authorization(self, monkeypatch, clean_anthropic_env): """No-regression: LiteLLM deliberately lets a caller-forwarded credential header ride alongside a statically configured ANTHROPIC_API_KEY, because the @@ -3019,6 +3044,23 @@ class TestModelDiscovery: WIF source configured in litellm_params, rather than the environment, could not discover).""" + @pytest.mark.parametrize( + "configured_base", ["https://api.anthropic.com/v1", "https://api.anthropic.com/v1/messages"] + ) + def test_discovery_does_not_double_the_version_segment(self, monkeypatch, clean_anthropic_env, configured_base): + """Regression: /v1/models is appended here, so a base an operator already wrote as + .../v1 (or the chat URL they copied) would be asked for /v1/v1/models and 404.""" + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + + monkeypatch.setenv("ANTHROPIC_API_KEY", FAKE_REGULAR_KEY) + client = RecordingModelsClient([{"data": [{"id": "claude-a"}], "has_more": False, "last_id": "claude-a"}]) + monkeypatch.setattr("litellm.module_level_client", client) + + models = AnthropicModelInfo().get_models(api_base=configured_base) + + assert models == ["anthropic/claude-a"] + assert client.calls[0].url == "https://api.anthropic.com/v1/models" + def test_get_models_paginates_via_has_more_and_last_id(self, monkeypatch, clean_anthropic_env): from litellm.llms.anthropic.common_utils import AnthropicModelInfo diff --git a/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py b/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py index ddac561f337..a436744f648 100644 --- a/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py +++ b/tests/test_litellm/llms/anthropic/test_anthropic_count_tokens_transformation.py @@ -1,4 +1,3 @@ - from litellm.llms.anthropic.count_tokens.transformation import ( AnthropicCountTokensConfig, ) diff --git a/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py b/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py index 69738118d7a..8b22acc4a23 100644 --- a/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py +++ b/tests/test_litellm/llms/anthropic/test_azure_ai_cache_pricing.py @@ -3,8 +3,6 @@ Test that Azure AI Anthropic models have cache pricing configured. Verifies the fix for issue #19532. """ - - import litellm from litellm import get_model_info from litellm.litellm_core_utils.get_model_cost_map import get_model_cost_map @@ -27,16 +25,11 @@ def reload_model_costs(): ("claude-sonnet-4-5", 3.75e-06, 3e-07), ], ) -def test_azure_ai_claude_cache_pricing( - model, expected_cache_creation_cost, expected_cache_read_cost -): +def test_azure_ai_claude_cache_pricing(model, expected_cache_creation_cost, expected_cache_read_cost): """Test that Azure AI Claude models have correct cache pricing.""" model_info = get_model_info(model=model, custom_llm_provider="azure_ai") assert model_info.get("cache_creation_input_token_cost") is not None assert model_info.get("cache_read_input_token_cost") is not None - assert ( - model_info.get("cache_creation_input_token_cost") - == expected_cache_creation_cost - ) + assert model_info.get("cache_creation_input_token_cost") == expected_cache_creation_cost assert model_info.get("cache_read_input_token_cost") == expected_cache_read_cost diff --git a/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py b/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py index 5c88ae17679..5a262153272 100644 --- a/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py +++ b/tests/test_litellm/llms/anthropic/test_cost_calculation_dict_safety.py @@ -5,7 +5,6 @@ being either a ``dict`` or a ``ServerToolUse`` pydantic instance. See https://github.com/BerriAI/litellm/issues/26153. """ - import pytest @@ -57,7 +56,8 @@ def test_get_cost_for_anthropic_web_search_with_dict_server_tool_use(): info = _make_model_info(cost_per_query=0.01) cost = get_cost_for_anthropic_web_search( - model_info=info, usage=usage # type: ignore[arg-type] + model_info=info, + usage=usage, # type: ignore[arg-type] ) assert cost == pytest.approx(0.03) @@ -68,7 +68,8 @@ def test_get_cost_for_anthropic_web_search_with_pydantic_server_tool_use(): info = _make_model_info(cost_per_query=0.01) cost = get_cost_for_anthropic_web_search( - model_info=info, usage=usage # type: ignore[arg-type] + model_info=info, + usage=usage, # type: ignore[arg-type] ) assert cost == pytest.approx(0.03) @@ -79,7 +80,8 @@ def test_get_cost_for_anthropic_web_search_with_none_server_tool_use(): info = _make_model_info(cost_per_query=0.01) cost = get_cost_for_anthropic_web_search( - model_info=info, usage=usage # type: ignore[arg-type] + model_info=info, + usage=usage, # type: ignore[arg-type] ) assert cost == 0.0 diff --git a/tests/test_litellm/llms/anthropic/test_count_tokens_oauth.py b/tests/test_litellm/llms/anthropic/test_count_tokens_oauth.py index bcfc56577eb..1551292b246 100644 --- a/tests/test_litellm/llms/anthropic/test_count_tokens_oauth.py +++ b/tests/test_litellm/llms/anthropic/test_count_tokens_oauth.py @@ -10,9 +10,7 @@ Regression test for https://github.com/BerriAI/litellm/issues/22040 import os import sys -sys.path.insert( - 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../..")) -) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../../.."))) from litellm.llms.anthropic.count_tokens.transformation import ( AnthropicCountTokensConfig, @@ -78,9 +76,5 @@ class TestCountTokensOAuthHeaders: headers = config.get_required_headers(FAKE_OAUTH_TOKEN) beta_value = headers.get("anthropic-beta", "") - assert ( - "token-counting" in beta_value - ), f"token-counting beta missing from OAuth headers: {beta_value}" - assert ( - "oauth-2025-04-20" in beta_value - ), f"oauth beta missing from OAuth headers: {beta_value}" + assert "token-counting" in beta_value, f"token-counting beta missing from OAuth headers: {beta_value}" + assert "oauth-2025-04-20" in beta_value, f"oauth beta missing from OAuth headers: {beta_value}" diff --git a/tests/test_litellm/llms/anthropic/test_message_sanitization.py b/tests/test_litellm/llms/anthropic/test_message_sanitization.py index 79ed321d0ee..7afa60baf7e 100644 --- a/tests/test_litellm/llms/anthropic/test_message_sanitization.py +++ b/tests/test_litellm/llms/anthropic/test_message_sanitization.py @@ -12,9 +12,7 @@ import sys import os # Add the parent directory to the path so we can import litellm -sys.path.insert( - 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) -) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../.."))) import litellm from litellm.litellm_core_utils.prompt_templates.factory import ( @@ -68,10 +66,7 @@ class TestMessageSanitization: assert sanitized[1]["role"] == "assistant" assert sanitized[2]["role"] == "tool" assert sanitized[2]["tool_call_id"] == "toolu_01Kus2cC3ydjBW7UK4GJqBP4" - assert ( - "skipped" in sanitized[2]["content"].lower() - or "interrupted" in sanitized[2]["content"].lower() - ) + assert "skipped" in sanitized[2]["content"].lower() or "interrupted" in sanitized[2]["content"].lower() assert "get_weather" in sanitized[2]["content"] def test_case_a_orphaned_tool_call_multiple(self): @@ -115,12 +110,8 @@ class TestMessageSanitization: assert len(sanitized) == 4 assert sanitized[0]["role"] == "user" assert sanitized[1]["role"] == "assistant" - assert ( - sanitized[2]["tool_call_id"] == "call_1" - ) # Original tool result (first in tool_calls) - assert ( - sanitized[3]["tool_call_id"] == "call_2" - ) # Dummy added for missing call_2 + assert sanitized[2]["tool_call_id"] == "call_1" # Original tool result (first in tool_calls) + assert sanitized[3]["tool_call_id"] == "call_2" # Dummy added for missing call_2 def test_case_b_orphaned_tool_result(self): """ @@ -188,10 +179,7 @@ class TestMessageSanitization: assert len(sanitized) == 2 assert sanitized[0]["role"] == "user" - assert ( - sanitized[0]["content"] - == "[System: Empty message content sanitised to satisfy protocol]" - ) + assert sanitized[0]["content"] == "[System: Empty message content sanitised to satisfy protocol]" def test_case_c_whitespace_only_content(self): """ @@ -206,14 +194,8 @@ class TestMessageSanitization: sanitized = sanitize_messages_for_tool_calling(messages) assert len(sanitized) == 2 - assert ( - sanitized[0]["content"] - == "[System: Empty message content sanitised to satisfy protocol]" - ) - assert ( - sanitized[1]["content"] - == "[System: Empty message content sanitised to satisfy protocol]" - ) + assert sanitized[0]["content"] == "[System: Empty message content sanitised to satisfy protocol]" + assert sanitized[1]["content"] == "[System: Empty message content sanitised to satisfy protocol]" def test_case_c_valid_content_preserved(self): """ @@ -270,10 +252,7 @@ class TestMessageSanitization: assert sanitized[2]["role"] == "tool" assert sanitized[2]["tool_call_id"] == "call_1" # Dummy added assert sanitized[3]["role"] == "user" - assert ( - sanitized[3]["content"] - == "[System: Empty message content sanitised to satisfy protocol]" - ) + assert sanitized[3]["content"] == "[System: Empty message content sanitised to satisfy protocol]" assert sanitized[4]["role"] == "assistant" def test_modify_params_false_no_sanitization(self): @@ -329,9 +308,7 @@ class TestMessageSanitization: ] # This should not raise an error and should add dummy tool result - result = anthropic_messages_pt( - messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic" - ) + result = anthropic_messages_pt(messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic") # Should have at least 2 messages (user and assistant) # The tool result will be merged into user content @@ -355,23 +332,17 @@ class TestMessageSanitization: {"role": "user", "content": ""}, ] - result = anthropic_messages_pt( - messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic" - ) + result = anthropic_messages_pt(messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic") # All three user messages get merged into one user turn for Anthropic. assert len(result) == 1 assert result[0]["role"] == "user" - text_blocks = [ - b for b in result[0]["content"] if isinstance(b, dict) and b.get("type") == "text" - ] + text_blocks = [b for b in result[0]["content"] if isinstance(b, dict) and b.get("type") == "text"] assert len(text_blocks) == 3 # No text block may be empty — that's the contract Anthropic enforces. for block in text_blocks: assert block["text"].strip() != "" - assert text_blocks[2]["text"] == ( - "[System: Empty message content sanitised to satisfy protocol]" - ) + assert text_blocks[2]["text"] == ("[System: Empty message content sanitised to satisfy protocol]") def test_empty_text_block_in_list_content_sanitized(self): """ @@ -392,14 +363,10 @@ class TestMessageSanitization: }, ] - result = anthropic_messages_pt( - messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic" - ) + result = anthropic_messages_pt(messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic") assert len(result) == 1 - text_blocks = [ - b for b in result[0]["content"] if isinstance(b, dict) and b.get("type") == "text" - ] + text_blocks = [b for b in result[0]["content"] if isinstance(b, dict) and b.get("type") == "text"] assert len(text_blocks) == 3 assert text_blocks[0]["text"] == "real content" for block in text_blocks[1:]: @@ -418,9 +385,7 @@ class TestMessageSanitization: {"role": "user", "content": "How are you?"}, ] - result = anthropic_messages_pt( - messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic" - ) + result = anthropic_messages_pt(messages=messages, model="claude-sonnet-4-5", llm_provider="anthropic") # Two user turns + one assistant turn (alternation preserved). assert len(result) == 3