diff --git a/litellm/llms/anthropic/chat/transformation.py b/litellm/llms/anthropic/chat/transformation.py index 0bfe7dddd7f..aa805ccea71 100644 --- a/litellm/llms/anthropic/chat/transformation.py +++ b/litellm/llms/anthropic/chat/transformation.py @@ -1516,7 +1516,7 @@ class AnthropicConfig(AnthropicModelInfo, BaseConfig): _tool = self.map_response_format_to_anthropic_tool(value, optional_params, is_thinking_enabled) if _tool is None: continue - if not is_thinking_enabled: + if not is_thinking_enabled and not AnthropicModelInfo.forced_tool_use_unsupported(model): _tool_choice = { "name": RESPONSE_FORMAT_TOOL_NAME, "type": "tool", diff --git a/litellm/llms/anthropic/common_utils.py b/litellm/llms/anthropic/common_utils.py index b1f927fd8f9..c60ebd844ba 100644 --- a/litellm/llms/anthropic/common_utils.py +++ b/litellm/llms/anthropic/common_utils.py @@ -325,13 +325,17 @@ class AnthropicModelInfo(BaseLLMModelInfo): status_code=400, ) + @staticmethod + def forced_tool_use_unsupported(model: str) -> bool: + return AnthropicModelInfo._get_model_capability(model, "supports_forced_tool_use") is False + @staticmethod def forced_tool_use_downgraded(model: str, drop_params: bool) -> bool: """True when the model map flags the model with ``supports_forced_tool_use: false`` (Fable 5.1 / Mythos 5.1 400 on ``any``/``tool``) and ``drop_params`` asks for the ``auto`` downgrade; raises a clean client-side 400 for such models without ``drop_params``.""" - if AnthropicModelInfo._get_model_capability(model, "supports_forced_tool_use") is not False: + if not AnthropicModelInfo.forced_tool_use_unsupported(model): return False if not (litellm.drop_params or drop_params): raise litellm.utils.UnsupportedParamsError( diff --git a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py index 8e709349400..7254417ce47 100644 --- a/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py +++ b/litellm/llms/bedrock/chat/invoke_transformations/anthropic_claude3_transformation.py @@ -74,10 +74,14 @@ class AmazonAnthropicClaudeConfig(AmazonInvokeConfig, AnthropicConfig): drop_params: bool, ) -> dict: # Force tool-based structured outputs for Bedrock Invoke - # (similar to VertexAI fix in #19201) - # Bedrock Invoke doesn't support output_format parameter + # (similar to VertexAI fix in #19201) unless the model map advertises + # native structured output + from litellm.utils import supports_native_structured_output + original_model: Final = model - if "response_format" in non_default_params: + if "response_format" in non_default_params and not supports_native_structured_output( + model=model, custom_llm_provider="bedrock" + ): # Use a model name that forces tool-based approach model = "claude-3-sonnet-20240229" diff --git a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py index d7ad69593c6..ef03e61a858 100644 --- a/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py +++ b/litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/transformation.py @@ -153,14 +153,17 @@ class VertexAIAnthropicConfig(AnthropicConfig): drop_params: bool, ) -> dict: """ - Override parent method to ensure VertexAI always uses tool-based structured outputs. - VertexAI doesn't support the output_format parameter, so we force all models - to use the tool-based approach for structured outputs. + Override parent method so VertexAI uses tool-based structured outputs + unless the vertex map entry advertises native structured output + (``output_format``, which Vertex AI Claude forwards for those models). """ - # Temporarily override model name to force tool-based approach - # This ensures Claude Sonnet 4.5 uses tools instead of output_format + from litellm.llms.anthropic.common_utils import AnthropicModelInfo + original_model: Final = model - if "response_format" in non_default_params: + native_structured_output: Final = AnthropicModelInfo._get_provider_resolved_capability( + model, "supports_native_structured_output", "vertex_ai" + ) + if "response_format" in non_default_params and native_structured_output is not True: model = "claude-3-sonnet-20240229" # Use a model that will use tool-based approach # Call parent method with potentially modified model name diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 1b2001cdadd..241d1d252b2 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -3254,6 +3254,7 @@ "supports_computer_use": true, "supports_forced_tool_use": false, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -44132,6 +44133,7 @@ "supports_computer_use": true, "supports_forced_tool_use": false, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -44202,6 +44204,7 @@ "supports_computer_use": true, "supports_forced_tool_use": false, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 1b2001cdadd..241d1d252b2 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -3254,6 +3254,7 @@ "supports_computer_use": true, "supports_forced_tool_use": false, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -44132,6 +44133,7 @@ "supports_computer_use": true, "supports_forced_tool_use": false, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, @@ -44202,6 +44204,7 @@ "supports_computer_use": true, "supports_forced_tool_use": false, "supports_function_calling": true, + "supports_native_structured_output": true, "supports_pdf_input": true, "supports_prompt_caching": true, "supports_reasoning": true, 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 2a955845861..0f9f8259bef 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 @@ -6354,3 +6354,33 @@ def test_anthropic_drop_params_reduces_mixed_output_config_to_format(monkeypatch ) assert result.get("output_config") == {"format": schema_format} + + +def test_response_format_tool_path_skips_forced_tool_choice_when_unsupported(local_model_cost_map, monkeypatch): + """Backstop: on the tool-based structured-output path, a model flagged + ``supports_forced_tool_use: false`` must not get the forced response-format + tool_choice the provider would 400 on.""" + monkeypatch.setitem( + litellm.model_cost, + "claude-test-no-forced-tools", + {"litellm_provider": "anthropic", "mode": "chat", "supports_forced_tool_use": False}, + ) + config = AnthropicConfig() + + result = config.map_openai_params( + non_default_params={ + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "test_schema", + "schema": {"type": "object", "properties": {"result": {"type": "string"}}}, + }, + } + }, + optional_params={}, + model="claude-test-no-forced-tools", + drop_params=False, + ) + + assert "tools" in result + assert "tool_choice" not in result diff --git a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py index a122d97a0f0..6b87b67d925 100644 --- a/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py +++ b/tests/test_litellm/llms/bedrock/chat/invoke_transformations/test_bedrock_chat_invoke_transformations_anthropic_claude3_transformation.py @@ -643,3 +643,30 @@ def test_bedrock_chat_invoke_drop_params_still_inlines_for_non_native(local_mode assert "output_config" not in result last_content = result["messages"][-1]["content"] assert json.loads(last_content[-1]["text"]) == schema + + +@pytest.mark.parametrize( + "model", + ["us.anthropic.claude-fable-5-1", "anthropic.claude-fable-5-1"], +) +def test_bedrock_chat_invoke_fable_5_1_response_format_uses_native_path(local_model_cost_map, model): + """Regression: Fable 5.1 rejects forced tool use, so invoke must skip the + tool-based structured-output stub and emit ``output_format`` instead of a + forced ``tool_choice``.""" + result = AmazonAnthropicClaudeConfig().map_openai_params( + non_default_params={ + "response_format": { + "type": "json_schema", + "json_schema": { + "name": "test_schema", + "schema": {"type": "object", "properties": {"result": {"type": "string"}}}, + }, + } + }, + optional_params={}, + model=model, + drop_params=False, + ) + + assert "output_format" in result + assert "tool_choice" not in result diff --git a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py index 552ca98441f..9419f88a981 100644 --- a/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py +++ b/tests/test_litellm/llms/vertex_ai/vertex_ai_partner_models/anthropic/test_vertex_ai_partner_models_anthropic_transformation.py @@ -727,3 +727,28 @@ def test_sanitize_strips_effort_for_haiku_45(): data = {"output_config": {"effort": "high"}} sanitize_vertex_anthropic_output_params(data, "vertex_ai/claude-opus-4-6") assert data["output_config"] == {"effort": "high"} + + +def test_vertex_ai_fable_5_1_response_format_uses_native_output_format(local_model_cost_map): + """Regression: Fable 5.1 rejects forced tool use, so the vertex map entry + advertises native structured output and ``response_format`` must map to + ``output_format`` instead of the tool-based path's forced tool_choice.""" + config = VertexAIAnthropicConfig() + response_format = { + "type": "json_schema", + "json_schema": { + "name": "test_schema", + "schema": {"type": "object", "properties": {"result": {"type": "string"}}}, + }, + } + + result_params = config.map_openai_params( + non_default_params={"response_format": response_format}, + optional_params={}, + model="claude-fable-5-1", + drop_params=False, + ) + + assert "output_format" in result_params + assert "tool_choice" not in result_params + assert "tools" not in result_params