fix(databricks): strip thinking_blocks and reasoning_content from outbound messages

Databricks Model Serving validates assistant messages with additionalProperties=false, so replaying
a thinking turn translated by the Anthropic Messages adapter 400s with
'messages.N.thinking_blocks: Extra inputs are not permitted'. Drop litellm's internal fields in
DatabricksConfig._transform_messages via a shared common_utils helper.

Resolves LIT-6762

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Devin AI 2026-09-02 21:27:26 +00:00
parent 719b67114d
commit e543ae3980
3 changed files with 51 additions and 0 deletions

View file

@ -1554,6 +1554,20 @@ def with_prompt_cache_breakpoint(target: _MarkedT, marker: object) -> _MarkedT:
return cast(_MarkedT, marked) # cast-ok: same block shape as the input plus the marker key
LITELLM_INTERNAL_MESSAGE_FIELDS: Final = frozenset({"thinking_blocks", "reasoning_content", "provider_specific_fields"})
def strip_litellm_internal_message_fields(message: AllMessageValues) -> AllMessageValues:
"""Drop the fields litellm attaches to assistant messages (e.g. when translating Anthropic thinking
blocks) that OpenAI-compatible endpoints with strict schemas reject as extra inputs."""
if LITELLM_INTERNAL_MESSAGE_FIELDS.isdisjoint(message):
return message
return cast( # cast-ok: same TypedDict minus internal keys
AllMessageValues,
{key: value for key, value in message.items() if key not in LITELLM_INTERNAL_MESSAGE_FIELDS},
)
def filter_value_from_dict(dictionary: dict, key: str, depth: int = 0) -> Any:
"""
Filters a value from a dictionary

View file

@ -15,6 +15,7 @@ from litellm.litellm_core_utils.llm_response_utils.convert_dict_to_response impo
_should_convert_tool_call_to_json_mode,
)
from litellm.litellm_core_utils.prompt_templates.common_utils import (
strip_litellm_internal_message_fields,
strip_name_from_message,
)
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
@ -419,6 +420,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
"""
Databricks does not support:
- 'name' in user message.
- litellm's internal `thinking_blocks` / `reasoning_content` on assistant messages.
"""
new_messages = []
for idx, message in enumerate(messages):
@ -427,6 +429,7 @@ class DatabricksConfig(DatabricksBase, OpenAILikeChatConfig, AnthropicConfig):
else:
_message = message
_message = strip_name_from_message(_message, allowed_name_roles=["user"])
_message = strip_litellm_internal_message_fields(_message)
# Move message-level cache_control into a content block when content is a string.
if "cache_control" in _message and isinstance(_message.get("content"), str):
_message = self._move_cache_control_into_string_content_block(_message)

View file

@ -255,6 +255,40 @@ def test_transform_messages_sanitizes_empty_content():
assert result[1]["content"] == "Hi"
def test_transform_request_strips_thinking_blocks_and_reasoning_content():
"""Regression for LIT-6762: replaying an assistant turn that litellm decorated with
`thinking_blocks` / `reasoning_content` made Databricks 400 with
'messages.N.thinking_blocks: Extra inputs are not permitted'."""
config = DatabricksConfig()
messages = [
{"role": "user", "content": "hi"},
{
"role": "assistant",
"content": "Hello! How can I help?",
"thinking_blocks": [
{"type": "thinking", "thinking": "greet briefly", "signature": "sig_abc", "cache_control": {}}
],
"reasoning_content": "greet briefly",
"provider_specific_fields": {"foo": "bar"},
},
{"role": "user", "content": "thanks"},
]
result = config.transform_request(
model="databricks-claude-opus-5",
messages=messages,
optional_params={},
litellm_params={},
headers={},
)["messages"]
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")
)
assert "thinking_blocks" in messages[1]
def _parallel_tool_calls():
return [
{