diff --git a/litellm/llms/databricks/chat/transformation.py b/litellm/llms/databricks/chat/transformation.py index e59db2dac19..4f61a39f692 100644 --- a/litellm/llms/databricks/chat/transformation.py +++ b/litellm/llms/databricks/chat/transformation.py @@ -56,6 +56,14 @@ from ...openai_like.chat.transformation import OpenAILikeChatConfig from ..common_utils import DatabricksBase, DatabricksException +def _is_bare_assistant_message(message_dict: dict[str, Any]) -> bool: + """Databricks rejects assistant messages with neither content nor tool calls, e.g. a replayed + thinking-only turn once its `thinking_blocks` are stripped.""" + return message_dict.get("role") == "assistant" and not any( + message_dict.get(key) for key in ("content", "tool_calls", "function_call") + ) + + def _sanitize_empty_content(message_dict: dict[str, Any]) -> None: """ Remove or filter content so empty text blocks are not sent. @@ -434,6 +442,8 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig): if "cache_control" in _message and isinstance(_message.get("content"), str): _message = self._move_cache_control_into_string_content_block(_message) _sanitize_empty_content(cast(dict[str, Any], _message)) + if _is_bare_assistant_message(cast(dict[str, Any], _message)): + continue new_messages.append(_message) if "claude" not in model: 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 0a792a546e4..b2c52617e9d 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 @@ -284,11 +284,53 @@ def test_transform_request_strips_thinking_blocks_and_reasoning_content(): assert result[1] == {"role": "assistant", "content": "Hello! How can I help?"} assert not any( - key in message for message in result for key in ("thinking_blocks", "reasoning_content", "provider_specific_fields") + key in message + for message in result + for key in ("thinking_blocks", "reasoning_content", "provider_specific_fields") ) assert "thinking_blocks" in messages[1] +def test_transform_request_drops_thinking_only_assistant_turn_but_keeps_tool_call_turn(): + """A replayed thinking-only assistant turn has nothing left once `thinking_blocks` are stripped, so it must be + dropped instead of being sent as a bare {"role": "assistant"}. A thinking + tool_use turn keeps its tool_calls.""" + config = DatabricksConfig() + tool_call = {"id": "call_1", "type": "function", "function": {"name": "f", "arguments": "{}"}} + messages = [ + {"role": "user", "content": "hi"}, + { + "role": "assistant", + "content": None, + "thinking_blocks": [{"type": "thinking", "thinking": "hmm", "signature": "sig_1"}], + "reasoning_content": "hmm", + }, + {"role": "user", "content": "again"}, + { + "role": "assistant", + "content": None, + "thinking_blocks": [{"type": "thinking", "thinking": "call f", "signature": "sig_2"}], + "reasoning_content": "call f", + "tool_calls": [tool_call], + }, + {"role": "tool", "tool_call_id": "call_1", "content": "ok"}, + ] + + result = config.transform_request( + model="databricks-claude-opus-5", + messages=messages, + optional_params={}, + litellm_params={}, + headers={}, + )["messages"] + + assert result == [ + {"role": "user", "content": "hi"}, + {"role": "user", "content": "again"}, + {"role": "assistant", "tool_calls": [tool_call]}, + {"role": "tool", "tool_call_id": "call_1", "content": "ok"}, + ] + + def _parallel_tool_calls(): return [ {