mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(databricks): keep the Claude fallback when gating the anthropic thinking payload
Some checks failed
ai-gateway image / ai-gateway release image (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
Some checks failed
ai-gateway image / ai-gateway release image (push) Has been cancelled
LiteLLM Rust / rust-lint (push) Has been cancelled
LiteLLM Rust / rust-test (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Provider / gofmt, vet, build, test (push) Has been cancelled
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Has been cancelled
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
This commit is contained in:
parent
10a0da7a32
commit
c7b607c46e
5 changed files with 61 additions and 93 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue