diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index c24c990f356..822b75b37f4 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -229,6 +229,11 @@ DROP_UNSUPPORTED_OUTPUT_CONFIG_WARNING = ( "Sonnet 4.6+, and Mythos Preview." ) +DROP_UNSUPPORTED_SPEED_WARNING = ( + "Dropping unsupported `speed` for model=%s " + "(drop_params=True). Fast mode is only supported on select Opus models." +) + class AnthropicConfig(AnthropicModelInfo, BaseConfig): """ @@ -374,6 +379,51 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): for level in ("low", "minimal", "medium", "high", "xhigh", "max") ) + @staticmethod + def _model_supports_speed_param( + model: str, custom_llm_provider: Optional[str] = None + ) -> bool: + """Whether the model accepts Anthropic's ``speed`` parameter (fast mode). + + Fast mode is direct Anthropic API-only (not Bedrock, Vertex, or Azure). + Those providers strip their prefix before this shared transform runs, so a + bare ``claude-opus-4-8`` would otherwise resolve to the direct-API entry; + the routed provider is checked explicitly to keep them out. + """ + if custom_llm_provider is not None and custom_llm_provider != "anthropic": + return False + return ( + AnthropicModelInfo._get_exact_model_capability(model, "supports_speed") + is True + ) + + @staticmethod + def _maybe_drop_speed_param( + model: str, + optional_params: dict, + drop_params: bool, + custom_llm_provider: Optional[str] = None, + ) -> None: + if "speed" not in optional_params: + return + if AnthropicConfig._model_supports_speed_param(model, custom_llm_provider): + return + if not (litellm.drop_params or drop_params): + speed_value = optional_params.get("speed") + raise litellm.utils.UnsupportedParamsError( + message=( + f"{model} does not support speed={speed_value!r}. " + "To drop unsupported params, set " + "`litellm.drop_params = True`." + ), + status_code=400, + ) + litellm.verbose_logger.warning( + DROP_UNSUPPORTED_SPEED_WARNING, + model, + ) + optional_params.pop("speed", None) + @staticmethod def _raise_invalid_reasoning_effort( model: str, value: Any, llm_provider: str @@ -1569,8 +1619,13 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): anthropic_context_management ) elif param == "speed" and isinstance(value, str): - # Pass through Anthropic-specific speed parameter for fast mode optional_params["speed"] = value + AnthropicConfig._maybe_drop_speed_param( + model=model, + optional_params=optional_params, + drop_params=drop_params, + custom_llm_provider=self.custom_llm_provider, + ) elif param == "cache_control" and isinstance(value, dict): # Pass through top-level cache_control for automatic prompt caching optional_params["cache_control"] = value @@ -1875,6 +1930,14 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): "has no thinking_blocks. The model won't use extended thinking for this turn." ) + AnthropicConfig._maybe_drop_speed_param( + model=model, + optional_params=optional_params, + drop_params=litellm.drop_params + or litellm_params.get("drop_params") is True, + custom_llm_provider=self.custom_llm_provider, + ) + headers = self.update_headers_with_optional_anthropic_beta( headers=headers, optional_params=optional_params ) diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index 5741513903c..0e41ef619ba 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -367,6 +367,16 @@ class AnthropicModelInfo(BaseLLMModelInfo): pass return None + @staticmethod + def _get_exact_model_capability(model: str, key: str) -> Optional[bool]: + """Read boolean capability ``key`` from the exact model-map entry only. + + Unlike ``_get_model_capability``, does not walk stripped provider aliases. + Use when a feature is tied to a specific host (e.g. Anthropic API fast mode). + """ + value = litellm.model_cost.get(model, {}).get(key) + return value if isinstance(value, bool) else None + @staticmethod def _supports_model_capability(model: str, key: str) -> bool: """Check a boolean capability ``key`` in the model map. diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py index a3ac465c463..7b10a447bc8 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/handler.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/handler.py @@ -507,7 +507,10 @@ def anthropic_messages_handler( local_vars.update(kwargs) anthropic_messages_optional_request_params = ( AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( - params=local_vars + params=local_vars, + model=model, + drop_params=litellm_params.get("drop_params") is True, + custom_llm_provider=custom_llm_provider, ) ) if is_reasoning_auto_summary_enabled(): diff --git a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py index 88832fb3f63..42167e0fdaa 100644 --- a/litellm/llms/anthropic/experimental_pass_through/messages/utils.py +++ b/litellm/llms/anthropic/experimental_pass_through/messages/utils.py @@ -23,12 +23,19 @@ class AnthropicMessagesRequestUtils: @staticmethod def get_requested_anthropic_messages_optional_param( params: Dict[str, Any], + *, + model: str | None = None, + drop_params: bool = False, + custom_llm_provider: str | None = None, ) -> AnthropicMessagesRequestOptionalParams: """ Filter parameters to only include those defined in AnthropicMessagesRequestOptionalParams. Args: params: Dictionary of parameters to filter + model: Resolved model id; when set, unsupported params may be dropped + drop_params: Per-request drop_params flag (also respects litellm.drop_params) + custom_llm_provider: Routed provider; fast mode is gated to direct Anthropic Returns: AnthropicMessagesRequestOptionalParams instance with only the valid parameters @@ -37,6 +44,15 @@ class AnthropicMessagesRequestUtils: filtered_params = { k: v for k, v in params.items() if k in valid_keys and v is not None } + if model is not None: + from litellm.llms.anthropic.chat.transformation import AnthropicConfig + + AnthropicConfig._maybe_drop_speed_param( + model=model, + optional_params=filtered_params, + drop_params=drop_params, + custom_llm_provider=custom_llm_provider, + ) return cast(AnthropicMessagesRequestOptionalParams, filtered_params) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index dc7b9838941..6ebac7efc8d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -10443,7 +10443,8 @@ "fast": 6.0 }, "supports_output_config": true, - "supports_max_reasoning_effort": true + "supports_max_reasoning_effort": true, + "supports_speed": true }, "claude-opus-4-6-20260205": { "cache_creation_input_token_cost": 6.25e-06, @@ -10476,7 +10477,8 @@ "fast": 6.0 }, "supports_max_reasoning_effort": true, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-opus-4-7": { "cache_creation_input_token_cost": 6.25e-06, @@ -10511,7 +10513,8 @@ "us": 1.1, "fast": 6.0 }, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-opus-4-7-20260416": { "cache_creation_input_token_cost": 6.25e-06, @@ -10546,7 +10549,8 @@ "us": 1.1, "fast": 6.0 }, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-fable-5": { "cache_creation_input_token_cost": 1.25e-05, @@ -10615,7 +10619,8 @@ "us": 1.1, "fast": 2.0 }, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-sonnet-4-20250514": { "deprecation_date": "2026-05-14", diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 3e844a8e3ed..5f3f2294147 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -10443,7 +10443,8 @@ "fast": 6.0 }, "supports_output_config": true, - "supports_max_reasoning_effort": true + "supports_max_reasoning_effort": true, + "supports_speed": true }, "claude-opus-4-6-20260205": { "cache_creation_input_token_cost": 6.25e-06, @@ -10476,7 +10477,8 @@ "fast": 6.0 }, "supports_max_reasoning_effort": true, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-opus-4-7": { "cache_creation_input_token_cost": 6.25e-06, @@ -10511,7 +10513,8 @@ "us": 1.1, "fast": 6.0 }, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-opus-4-7-20260416": { "cache_creation_input_token_cost": 6.25e-06, @@ -10546,7 +10549,8 @@ "us": 1.1, "fast": 6.0 }, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-fable-5": { "cache_creation_input_token_cost": 1.25e-05, @@ -10615,7 +10619,8 @@ "us": 1.1, "fast": 2.0 }, - "supports_output_config": true + "supports_output_config": true, + "supports_speed": true }, "claude-sonnet-4-20250514": { "deprecation_date": "2026-05-14", 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 2876b56f516..b111b65e3af 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 @@ -1886,6 +1886,89 @@ def test_anthropic_model_supports_effort_param_rejects_non_supporting_models(mod assert AnthropicConfig._model_supports_effort_param(model) is False +@pytest.mark.parametrize( + "model", + [ + "claude-opus-4-6", + "claude-opus-4-7", + "claude-opus-4-8", + "claude-opus-4-6-20260205", + "claude-opus-4-7-20260416", + ], +) +def test_anthropic_model_supports_speed_param_recognizes_supporting_models(model): + assert AnthropicConfig._model_supports_speed_param(model) is True + + +@pytest.mark.parametrize( + "model", + [ + "claude-sonnet-4-6", + "claude-fable-5", + "claude-3-haiku-20240307", + "vertex_ai/claude-opus-4-8", + "azure_ai/claude-opus-4-8", + "anthropic.claude-opus-4-8", + ], +) +def test_anthropic_model_supports_speed_param_rejects_non_supporting_models(model): + assert AnthropicConfig._model_supports_speed_param(model) is False + + +@pytest.mark.parametrize("custom_llm_provider", ["vertex_ai", "azure_ai", "bedrock"]) +def test_anthropic_model_supports_speed_param_rejects_non_anthropic_providers( + custom_llm_provider, +): + """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 + ) + + +def test_vertex_anthropic_drops_speed_for_opus_with_drop_params(monkeypatch): + """Regression: vertex_ai Opus must drop ``speed`` even though the prefix-stripped + ``claude-opus-4-8`` maps to a fast-mode-capable direct-Anthropic entry.""" + from litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.transformation import ( + VertexAIAnthropicConfig, + ) + + monkeypatch.setattr(litellm, "drop_params", True) + result = VertexAIAnthropicConfig().transform_request( + model="claude-opus-4-8", + messages=[{"role": "user", "content": "Hello"}], + optional_params={"speed": "fast", "max_tokens": 1024}, + litellm_params={}, + headers={}, + ) + + assert "speed" not in result + + +def test_vertex_anthropic_raises_on_speed_without_drop_params(monkeypatch): + """Regression: vertex_ai Opus raises rather than forwarding an unsupported + ``speed`` when neither global nor per-request drop_params is set.""" + from litellm.llms.vertex_ai.vertex_ai_partner_models.anthropic.transformation import ( + VertexAIAnthropicConfig, + ) + + monkeypatch.setattr(litellm, "drop_params", False) + with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"): + VertexAIAnthropicConfig().map_openai_params( + non_default_params={"speed": "fast"}, + optional_params={}, + model="claude-opus-4-8", + drop_params=False, + ) + + def test_translate_system_message_skips_empty_string_content(): """ Test that translate_system_message skips system messages with empty string content. @@ -3766,6 +3849,61 @@ def test_fast_mode_parameter_mapping(): assert result["speed"] == "fast" +def test_anthropic_drop_params_strips_speed_for_unsupported_models(): + """``drop_params=True`` strips unsupported ``speed`` for non-Opus models.""" + config = AnthropicConfig() + messages = [{"role": "user", "content": "Hello"}] + + original = litellm.drop_params + litellm.drop_params = True + try: + result = config.transform_request( + model="claude-sonnet-4-6", + messages=messages, + optional_params={"speed": "fast", "max_tokens": 1024}, + litellm_params={}, + headers={}, + ) + finally: + litellm.drop_params = original + + assert "speed" not in result + + +def test_anthropic_drop_params_keeps_speed_for_supporting_models(): + """``drop_params=True`` must not strip ``speed`` on Opus fast-mode models.""" + config = AnthropicConfig() + messages = [{"role": "user", "content": "Hello"}] + + original = litellm.drop_params + litellm.drop_params = True + try: + result = config.transform_request( + model="claude-opus-4-6", + messages=messages, + optional_params={"speed": "fast", "max_tokens": 1024}, + litellm_params={}, + headers={}, + ) + finally: + litellm.drop_params = original + + assert result.get("speed") == "fast" + + +def test_speed_raises_clean_error_without_drop_params(monkeypatch): + monkeypatch.setattr(litellm, "drop_params", False) + config = AnthropicConfig() + + with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"): + config.map_openai_params( + non_default_params={"speed": "fast"}, + optional_params={}, + model="claude-sonnet-4-6", + drop_params=False, + ) + + def test_map_openai_params_max_tokens_normalized_to_int(): """ Test that map_openai_params normalizes max_tokens to an integer (e.g. 0.7 -> 1). 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 new file mode 100644 index 00000000000..6900f1062bf --- /dev/null +++ b/tests/test_litellm/llms/anthropic/experimental_pass_through/messages/test_anthropic_messages_speed.py @@ -0,0 +1,117 @@ +import litellm +import pytest +from litellm.llms.anthropic.experimental_pass_through.messages.transformation import ( + AnthropicMessagesConfig, +) +from litellm.llms.anthropic.experimental_pass_through.messages.utils import ( + AnthropicMessagesRequestUtils, +) + + +def test_messages_drop_params_strips_speed_for_unsupported_models(): + original = litellm.drop_params + litellm.drop_params = True + try: + optional_params = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={ + "max_tokens": 1024, + "speed": "fast", + "messages": [{"role": "user", "content": "Hello"}], + }, + model="claude-sonnet-4-6", + drop_params=False, + ) + config = AnthropicMessagesConfig() + headers, _ = config.validate_anthropic_messages_environment( + headers={}, + model="claude-sonnet-4-6", + messages=[{"role": "user", "content": "Hello"}], + optional_params=dict(optional_params), + litellm_params={}, + ) + result = config.transform_anthropic_messages_request( + model="claude-sonnet-4-6", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=dict(optional_params), + litellm_params={}, + headers=headers, + ) + finally: + litellm.drop_params = original + + assert "speed" not in optional_params + assert "speed" not in result + assert "fast-mode-2026-02-01" not in headers.get("anthropic-beta", "") + + +def test_messages_drop_params_keeps_speed_for_supporting_models(): + original = litellm.drop_params + litellm.drop_params = True + try: + optional_params = AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"max_tokens": 1024, "speed": "fast"}, + model="claude-opus-4-6", + drop_params=False, + ) + config = AnthropicMessagesConfig() + headers, _ = config.validate_anthropic_messages_environment( + headers={}, + model="claude-opus-4-6", + messages=[{"role": "user", "content": "Hello"}], + optional_params=dict(optional_params), + litellm_params={}, + ) + result = config.transform_anthropic_messages_request( + model="claude-opus-4-6", + messages=[{"role": "user", "content": "Hello"}], + anthropic_messages_optional_request_params=dict(optional_params), + litellm_params={}, + headers=headers, + ) + finally: + litellm.drop_params = original + + assert optional_params.get("speed") == "fast" + assert result.get("speed") == "fast" + assert "fast-mode-2026-02-01" in headers.get("anthropic-beta", "") + + +def test_messages_raises_when_speed_unsupported_and_drop_params_false(monkeypatch): + monkeypatch.setattr(litellm, "drop_params", False) + + with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_params"): + AnthropicMessagesRequestUtils.get_requested_anthropic_messages_optional_param( + params={"max_tokens": 1024, "speed": "fast"}, + model="claude-sonnet-4-6", + drop_params=False, + ) + + +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", + ) + ) + + assert "speed" not in optional_params + + +def test_messages_raises_for_vertex_opus_without_drop_params(monkeypatch): + """Regression: vertex_ai Opus passthrough raises rather than forwarding an + unsupported ``speed`` when drop_params is unset.""" + monkeypatch.setattr(litellm, "drop_params", False) + + with pytest.raises(litellm.utils.UnsupportedParamsError, match="drop_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", + ) 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 3ce076640e8..f0252e13336 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 @@ -6,6 +6,7 @@ Regression tests for the /v1/messages request-parse fast paths: while resolving the (static) type hints only once per process. """ +import litellm from litellm.llms.anthropic.experimental_pass_through.messages.utils import ( AnthropicMessagesRequestUtils, _anthropic_messages_optional_param_keys, @@ -54,3 +55,36 @@ def test_empty_params(): ) == {} ) + + +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", + ) + ) + finally: + litellm.drop_params = original + + assert result == {"temperature": 0.5} + assert "speed" not in result + + +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", + ) + ) + finally: + litellm.drop_params = original + + assert result == {"speed": "fast"} diff --git a/tests/test_litellm/test_utils.py b/tests/test_litellm/test_utils.py index d94a86d8e55..cf7afc1af68 100644 --- a/tests/test_litellm/test_utils.py +++ b/tests/test_litellm/test_utils.py @@ -865,6 +865,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "supports_service_tier": {"type": "boolean"}, "supports_preset": {"type": "boolean"}, "supports_output_config": {"type": "boolean"}, + "supports_speed": {"type": "boolean"}, "bedrock_output_config_effort_ceiling": { "type": "string", "enum": ["low", "medium", "high", "max", "xhigh"],