diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index 34d6daa876f..76e2a5169d2 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -22,6 +22,7 @@ import httpx from pydantic import BaseModel from litellm.constants import RESPONSE_FORMAT_TOOL_NAME +from litellm.exceptions import BadRequestError from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response import ( _handle_invalid_parallel_tool_calls, _should_convert_tool_call_to_json_mode, @@ -402,9 +403,16 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): optional_params.pop("thinking", None) optional_params.pop("output_config", None) else: + is_adaptive = mapped_thinking.get("type") == "adaptive" + if is_adaptive and not is_claude: + raise BadRequestError( + message=(f"Adaptive thinking is only supported on Databricks Claude models, not {model!r}."), + model=model, + llm_provider="databricks", + ) optional_params["thinking"] = mapped_thinking # output_config + adaptive thinking is an Anthropic-only feature. - if is_claude and AnthropicConfig._is_adaptive_thinking_model(model, "databricks"): + if is_claude and is_adaptive: mapped_effort: Optional[str] = None if isinstance(reasoning_effort_value, str): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort_value) diff --git a/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py b/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py index 91a6f0683e8..c00f5a5d1a8 100644 --- a/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py +++ b/tests/test_litellm/llms/databricks/chat/test_databricks_chat_transformation.py @@ -21,7 +21,7 @@ from litellm.llms.databricks.chat.transformation import ( ) -@pytest.fixture(autouse=True) +@pytest.fixture() def _use_local_model_cost_map(monkeypatch): monkeypatch.setenv("LITELLM_LOCAL_MODEL_COST_MAP", "True") monkeypatch.setattr(litellm, "model_cost", litellm.get_model_cost_map(url="")) @@ -466,7 +466,7 @@ def _map_reasoning_effort(model: str, reasoning_effort, **extra_non_default): ) -def test_claude_translates_reasoning_effort_to_thinking(): +def test_claude_translates_reasoning_effort_to_thinking(_use_local_model_cost_map): """Regression: Claude path must still translate to Anthropic-style thinking.""" params = _map_reasoning_effort("databricks-claude-3-7-sonnet", "low") assert params.get("thinking") == { @@ -476,7 +476,7 @@ def test_claude_translates_reasoning_effort_to_thinking(): assert "reasoning_effort" not in params -def test_gemini_2_5_low_translates_to_thinking_budget(): +def test_gemini_2_5_low_translates_to_thinking_budget(_use_local_model_cost_map): params = _map_reasoning_effort("databricks-gemini-2-5-flash", "low") assert params.get("thinking") == { "type": "enabled", @@ -485,7 +485,7 @@ def test_gemini_2_5_low_translates_to_thinking_budget(): assert "reasoning_effort" not in params -def test_gemini_2_5_medium_translates_to_thinking_budget(): +def test_gemini_2_5_medium_translates_to_thinking_budget(_use_local_model_cost_map): params = _map_reasoning_effort("databricks-gemini-2-5-flash", "medium") assert params.get("thinking") == { "type": "enabled", @@ -494,7 +494,7 @@ def test_gemini_2_5_medium_translates_to_thinking_budget(): assert "reasoning_effort" not in params -def test_gemini_2_5_high_translates_to_thinking_budget(): +def test_gemini_2_5_high_translates_to_thinking_budget(_use_local_model_cost_map): params = _map_reasoning_effort("databricks-gemini-2-5-flash", "high") assert params.get("thinking") == { "type": "enabled", @@ -503,7 +503,7 @@ def test_gemini_2_5_high_translates_to_thinking_budget(): assert "reasoning_effort" not in params -def test_gemini_2_5_pro_translates_to_thinking_budget(): +def test_gemini_2_5_pro_translates_to_thinking_budget(_use_local_model_cost_map): """Cover the gemini-2-5-pro endpoint too, not just flash.""" params = _map_reasoning_effort("databricks-gemini-2-5-pro", "high") assert params.get("thinking") == { @@ -513,7 +513,7 @@ def test_gemini_2_5_pro_translates_to_thinking_budget(): assert "reasoning_effort" not in params -def test_gemini_2_5_with_dot_notation_translates(): +def test_gemini_2_5_with_dot_notation_translates(_use_local_model_cost_map): """A user passing the upstream Google-style `gemini-2.5-...` form should still trigger the Anthropic-thinking translation, not pass through.""" params = _map_reasoning_effort("databricks-gemini-2.5-flash", "low") @@ -524,7 +524,7 @@ def test_gemini_2_5_with_dot_notation_translates(): assert "reasoning_effort" not in params -def test_gemini_2_0_does_not_match(): +def test_gemini_2_0_does_not_match(_use_local_model_cost_map): """Guard against over-matching: `gemini-2-0` (hypothetical or future) is NOT a Gemini 2.5 endpoint and must not get the thinking translation.""" params = _map_reasoning_effort("databricks-gemini-2-0-flash", "low") @@ -532,29 +532,47 @@ def test_gemini_2_0_does_not_match(): assert params.get("reasoning_effort") == "low" -def test_gemini_2_5_none_drops_thinking_and_reasoning_effort(): +def test_gemini_2_5_none_drops_thinking_and_reasoning_effort(_use_local_model_cost_map): """`reasoning_effort='none'` mirrors the Claude behavior: no thinking emitted.""" params = _map_reasoning_effort("databricks-gemini-2-5-flash", "none") assert "thinking" not in params assert "reasoning_effort" not in params -def test_gemini_3_passes_reasoning_effort_through(): +def test_gemini_3_passes_reasoning_effort_through(_use_local_model_cost_map): """Databricks-Gemini-3+ accepts reasoning_effort natively — do not translate.""" params = _map_reasoning_effort("databricks-gemini-3-1-pro", "low") assert params.get("reasoning_effort") == "low" assert "thinking" not in params -def test_gpt_5_passes_reasoning_effort_through(): +def test_gpt_5_passes_reasoning_effort_through(_use_local_model_cost_map): """Databricks-GPT-5 family accepts reasoning_effort natively.""" params = _map_reasoning_effort("databricks-gpt-5-1", "low") assert params.get("reasoning_effort") == "low" assert "thinking" not in params -def test_gpt_oss_passes_reasoning_effort_through(): +def test_gpt_oss_passes_reasoning_effort_through(_use_local_model_cost_map): """Databricks-GPT-OSS accepts reasoning_effort natively.""" params = _map_reasoning_effort("databricks-gpt-oss-120b", "high") assert params.get("reasoning_effort") == "high" assert "thinking" not in params + + +def test_non_claude_adaptive_thinking_flag_is_rejected(monkeypatch, _use_local_model_cost_map): + """Adaptive thinking + output_config is Claude-only; a non-Claude model that + resolves to an adaptive payload would send Databricks' Gemini endpoint a shape + it can't parse, so the translation must fail loudly instead of passing it through.""" + fake_model = "databricks-gemini-2-5-adaptive-probe" + monkeypatch.setitem( + litellm.model_cost, + fake_model, + { + "litellm_provider": "databricks", + "supports_anthropic_thinking_payload": True, + "supports_adaptive_thinking": True, + }, + ) + with pytest.raises(litellm.exceptions.BadRequestError): + _map_reasoning_effort(fake_model, "high")