fix(databricks): guard adaptive thinking to Claude and scope test fixture

Addresses Greptile P2 feedback: reject an adaptive thinking payload for non-Claude Databricks models instead of silently emitting a shape the Gemini endpoint can't parse, and scope the local-cost-map fixture to the tests that need it rather than autouse.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
milan 2026-07-20 18:44:25 +00:00
parent e969eac8ea
commit d9c9dcf67a
2 changed files with 39 additions and 13 deletions

View file

@ -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)

View file

@ -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")