From c7b607c46e4efa7cc04726e49c96030ea374bb08 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Sat, 12 Sep 2026 13:13:38 -0700 Subject: [PATCH] fix(databricks): keep the Claude fallback when gating the anthropic thinking payload Gate the reasoning_effort translation on the cost-map flag or the model name containing claude, so unmapped Claude serving endpoints keep translating. Flag the newer Claude entries that were missing it. Expose supports_anthropic_thinking_payload as a public helper next to the other supports_* wrappers instead of importing the private factory. Drop the adaptive-only guard, since the adaptive flags only ever match Claude ids, and add regression tests for an unmapped Claude endpoint and an adaptive Claude model --- .../llms/databricks/chat/transformation.py | 30 +---- ...odel_prices_and_context_window_backup.json | 6 + litellm/utils.py | 6 + model_prices_and_context_window.json | 6 + .../test_databricks_chat_transformation.py | 106 ++++++------------ 5 files changed, 61 insertions(+), 93 deletions(-) diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index 517c79c2e17..82c3b5d91d3 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -10,7 +10,6 @@ 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, @@ -274,21 +273,12 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): ] @staticmethod - def _databricks_model_uses_anthropic_thinking_param(model: str) -> bool: - """ - Per Databricks docs, Claude and Gemini 2.5 endpoints accept the - Anthropic-style `thinking={"type":"enabled","budget_tokens":N}` payload - and do NOT accept OpenAI's top-level `reasoning_effort`. Gemini 3+ and - GPT-5/GPT-OSS accept `reasoning_effort` natively and need no - translation. - """ - from litellm.utils import _supports_factory + def _uses_anthropic_thinking_param(model: str) -> bool: + from litellm.utils import supports_anthropic_thinking_payload normalized: Final = model.lower().replace(".", "-") - return _supports_factory( - model=normalized, - custom_llm_provider="databricks", - key="supports_anthropic_thinking_payload", + return "claude" in normalized or supports_anthropic_thinking_payload( + model=normalized, custom_llm_provider="databricks" ) def convert_anthropic_tool_to_databricks_tool(self, tool: AllAnthropicToolsValues | None) -> DatabricksTool | None: @@ -396,7 +386,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): "response_format", None ) # unsupported for claude models - if json_schema -> convert to tool call - if "reasoning_effort" in non_default_params and self._databricks_model_uses_anthropic_thinking_param(model): + if "reasoning_effort" in non_default_params and self._uses_anthropic_thinking_param(model): reasoning_effort_value: Final = non_default_params.get("reasoning_effort") mapped_thinking: Final = AnthropicConfig._map_reasoning_effort( reasoning_effort=reasoning_effort_value, @@ -404,20 +394,12 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): custom_llm_provider="databricks", llm_provider="databricks", ) - is_claude: Final = "claude" in model.lower() if mapped_thinking is None: optional_params.pop("thinking", None) optional_params.pop("output_config", None) else: - is_adaptive: Final = 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 - if is_claude and is_adaptive: + if AnthropicConfig._is_adaptive_thinking_model(model, "databricks"): mapped_effort: str | None = None if isinstance(reasoning_effort_value, str): mapped_effort = REASONING_EFFORT_TO_OUTPUT_CONFIG_EFFORT.get(reasoning_effort_value) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 8da437d85fd..39856b0d933 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -17623,6 +17623,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": false, @@ -17652,6 +17653,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true, @@ -17801,6 +17803,7 @@ "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true @@ -17828,6 +17831,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true @@ -17855,6 +17859,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true @@ -17979,6 +17984,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true diff --git a/litellm/utils.py b/litellm/utils.py index 784d02ccc00..c52a8fbc42b 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -2850,6 +2850,12 @@ def supports_reasoning(model: str, custom_llm_provider: str | None = None) -> bo return _supports_factory(model=model, custom_llm_provider=custom_llm_provider, key="supports_reasoning") +def supports_anthropic_thinking_payload(model: str, custom_llm_provider: str | None = None) -> bool: + return _supports_factory( + model=model, custom_llm_provider=custom_llm_provider, key="supports_anthropic_thinking_payload" + ) + + def supports_none_reasoning_effort(model: str, custom_llm_provider: str | None = None) -> bool: """ Check if the given model accepts reasoning effort "none" and return a boolean value. diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 8da437d85fd..39856b0d933 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -17623,6 +17623,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": false, @@ -17652,6 +17653,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true, @@ -17801,6 +17803,7 @@ "supports_function_calling": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true @@ -17828,6 +17831,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true @@ -17855,6 +17859,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true @@ -17979,6 +17984,7 @@ "supports_mid_conversation_system": true, "supports_prompt_caching": true, "supports_reasoning": true, + "supports_anthropic_thinking_payload": true, "supports_sampling_params": false, "supports_tool_choice": true, "supports_vision": true 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 40d7f834052..52bb89fed5a 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 @@ -523,29 +523,30 @@ def test_databricks_config_probes_capabilities_under_databricks_namespace(): assert DatabricksConfig().custom_llm_provider == "databricks" -# --------------------------------------------------------------------------- -# reasoning_effort translation -# -# Databricks foundation-model endpoints take reasoning controls via different -# payload shapes depending on the underlying model family: -# -# Claude: Anthropic-style `thinking={"type":"enabled","budget_tokens":N}` -# Gemini 2.5: Same Anthropic-style `thinking` payload as Claude -# (per docs.databricks.com/.../query-reason-models) -# Gemini 3+: Native OpenAI-style top-level `reasoning_effort` -# GPT-5/GPT-OSS: Native OpenAI-style top-level `reasoning_effort` -# -# LiteLLM should translate `reasoning_effort` into the right shape for the -# first two families and pass it through unchanged for the latter two. -# --------------------------------------------------------------------------- +@pytest.mark.parametrize( + "model, expected_thinking, expected_output_config", + [ + ("databricks-claude-opus-4-8", {"type": "adaptive"}, {"effort": "high"}), + ("databricks-claude-opus-4-6", {"type": "enabled", "budget_tokens": 4096}, None), + ], + ids=["adaptive_only_upgrades_to_adaptive", "legacy_capable_forwards_verbatim"], +) +def test_map_openai_params_upgrades_legacy_thinking_on_adaptive_only_claude( + model, expected_thinking, expected_output_config +): + mapped = DatabricksConfig().map_openai_params( + non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}}, + optional_params={}, + model=model, + drop_params=False, + ) + assert mapped["thinking"] == expected_thinking + assert mapped.get("output_config") == expected_output_config -def _map_reasoning_effort(model: str, reasoning_effort, **extra_non_default): - """Run map_openai_params with reasoning_effort + optional extras.""" - non_default = {"reasoning_effort": reasoning_effort} - non_default.update(extra_non_default) +def _map_reasoning_effort(model: str, reasoning_effort: str): return DatabricksConfig().map_openai_params( - non_default_params=non_default, + non_default_params={"reasoning_effort": reasoning_effort}, optional_params={}, model=model, drop_params=False, @@ -553,7 +554,6 @@ def _map_reasoning_effort(model: str, reasoning_effort, **extra_non_default): 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") == { "type": "enabled", @@ -562,6 +562,22 @@ def test_claude_translates_reasoning_effort_to_thinking(_use_local_model_cost_ma assert "reasoning_effort" not in params +def test_adaptive_claude_translates_reasoning_effort_to_output_config(_use_local_model_cost_map): + params = _map_reasoning_effort("databricks-claude-opus-4-7", "high") + assert params.get("thinking") == {"type": "adaptive", "display": "summarized"} + assert params.get("output_config") == {"effort": "high"} + assert "reasoning_effort" not in params + + +def test_unmapped_claude_endpoint_still_translates(_use_local_model_cost_map): + params = _map_reasoning_effort("my-claude-serving-endpoint", "low") + assert params.get("thinking") == { + "type": "enabled", + "budget_tokens": DEFAULT_REASONING_EFFORT_LOW_THINKING_BUDGET, + } + assert "reasoning_effort" not in params + + 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") == { @@ -590,7 +606,6 @@ def test_gemini_2_5_high_translates_to_thinking_budget(_use_local_model_cost_map 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") == { "type": "enabled", @@ -600,8 +615,6 @@ def test_gemini_2_5_pro_translates_to_thinking_budget(_use_local_model_cost_map) 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") assert params.get("thinking") == { "type": "enabled", @@ -611,80 +624,35 @@ def test_gemini_2_5_with_dot_notation_translates(_use_local_model_cost_map): 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") assert "thinking" not in params assert params.get("reasoning_effort") == "low" 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(_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(_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(_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") - - -@pytest.mark.parametrize( - "model, expected_thinking, expected_output_config", - [ - ("databricks-claude-opus-4-8", {"type": "adaptive"}, {"effort": "high"}), - ("databricks-claude-opus-4-6", {"type": "enabled", "budget_tokens": 4096}, None), - ], - ids=["adaptive_only_upgrades_to_adaptive", "legacy_capable_forwards_verbatim"], -) -def test_map_openai_params_upgrades_legacy_thinking_on_adaptive_only_claude( - model, expected_thinking, expected_output_config -): - mapped = DatabricksConfig().map_openai_params( - non_default_params={"thinking": {"type": "enabled", "budget_tokens": 4096}}, - optional_params={}, - model=model, - drop_params=False, - ) - assert mapped["thinking"] == expected_thinking - assert mapped.get("output_config") == expected_output_config - - def _streaming_chunk(usage=None, choices=None): base = { "id": "chatcmpl-test",