This commit is contained in:
Nafay Rizwani 2026-11-06 23:23:54 +00:00 • committed by GitHub
commit 7cdfe11a37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 169 additions and 5 deletions

View file

@ -1946,6 +1946,8 @@ def convert_to_anthropic_tool_result(
tool_use_id=sanitized_tool_use_id,
content=anthropic_content,
)
if "is_error" in tool_message:
anthropic_tool_result["is_error"] = tool_message["is_error"]
if message["role"] == "function":
function_message: Final[ChatCompletionFunctionMessage] = message

View file

@ -857,7 +857,7 @@ class AnthropicMessagesHandler(BaseTranslation):
)
translation_source: Final = {**{key: value for key, value in data.items() if key != "system"}, "model": model}
chat_completion_compatible_request: Final = self._translate_to_openai(
translation_source, keep_cache_control=keep_cache_control
translation_source, keep_cache_control=keep_cache_control, keep_tool_errors=True
)
full_structured_messages: Final = cast(
@ -1047,7 +1047,10 @@ class AnthropicMessagesHandler(BaseTranslation):
"""Convert an OpenAI system message to the client's Anthropic-shaped entry."""
content: Final = message.get("content")
if isinstance(content, str):
return {"role": "system", "content": content} if content else None
result: dict[str, object] = {"role": "system", "content": content}
if "output_config" in message:
result["output_config"] = deepcopy(message["output_config"])
return result if content else None
if not isinstance(content, list):
return None
blocks: Final[list[dict[str, object]]] = [] # mutable-ok: API message payload
@ -1065,7 +1068,12 @@ class AnthropicMessagesHandler(BaseTranslation):
if cache_control:
anthropic_block["cache_control"] = deepcopy(cache_control)
blocks.append(anthropic_block)
return {"role": "system", "content": blocks} if blocks else None
if not blocks:
return None
result: dict[str, object] = {"role": "system", "content": blocks}
if "output_config" in message:
result["output_config"] = deepcopy(message["output_config"])
return result
@staticmethod
def _fold_leading_systems_into_top_level(

View file

@ -1025,7 +1025,10 @@ class LiteLLMAnthropicMessagesAdapter:
"""Translate an in-sequence system entry without changing its role or position."""
content: Final = message.get("content")
if isinstance(content, str):
return ChatCompletionSystemMessage(role="system", content=content) if content else None
result = ChatCompletionSystemMessage(role="system", content=content) if content else None
if result and "output_config" in message:
result["output_config"] = message["output_config"]
return result
if not isinstance(content, list):
return None
text_parts: Final[list[ChatCompletionTextObject]] = [] # mutable-ok: API message payload
@ -1038,7 +1041,12 @@ class LiteLLMAnthropicMessagesAdapter:
text_obj = ChatCompletionTextObject(type="text", text=text)
self._add_cache_control_if_applicable(block, text_obj, model)
text_parts.append(self._add_prompt_cache_breakpoint_if_present(block, text_obj))
return ChatCompletionSystemMessage(role="system", content=text_parts) if text_parts else None
if not text_parts:
return None
result = ChatCompletionSystemMessage(role="system", content=text_parts)
if "output_config" in message:
result["output_config"] = message["output_config"]
return result
def _add_system_message_to_messages(
self,

View file

@ -0,0 +1,146 @@
"""
Test that Anthropic guardrail write-back preserves tool_result.is_error and system output_config.
Regression tests for https://github.com/BerriAI/litellm/issues/50269
"""
from litellm.litellm_core_utils.prompt_templates.factory import convert_to_anthropic_tool_result
from litellm.llms.anthropic.chat.guardrail_translation.handler import AnthropicMessagesHandler
from litellm.types.llms.openai import ChatCompletionToolMessage
def test_convert_to_anthropic_tool_result_preserves_is_error_true():
"""Tool message with is_error=true must survive OpenAI->Anthropic conversion."""
tool_message: ChatCompletionToolMessage = {
"role": "tool",
"tool_call_id": "toolu_01abc",
"content": "cat: file.txt: No such file or directory",
"is_error": True,
}
result = convert_to_anthropic_tool_result(tool_message)
assert result["type"] == "tool_result"
assert result["tool_use_id"] == "toolu_01abc"
assert result["content"] == "cat: file.txt: No such file or directory"
assert result.get("is_error") is True
def test_convert_to_anthropic_tool_result_preserves_is_error_false():
"""Tool message with is_error=false must survive OpenAI->Anthropic conversion."""
tool_message: ChatCompletionToolMessage = {
"role": "tool",
"tool_call_id": "toolu_02xyz",
"content": "success output",
"is_error": False,
}
result = convert_to_anthropic_tool_result(tool_message)
assert result["type"] == "tool_result"
assert result["tool_use_id"] == "toolu_02xyz"
assert result["content"] == "success output"
assert result.get("is_error") is False
def test_convert_to_anthropic_tool_result_no_is_error():
"""Tool message without is_error field should not add one."""
tool_message: ChatCompletionToolMessage = {
"role": "tool",
"tool_call_id": "toolu_03def",
"content": "normal output",
}
result = convert_to_anthropic_tool_result(tool_message)
assert result["type"] == "tool_result"
assert result["tool_use_id"] == "toolu_03def"
assert result["content"] == "normal output"
assert "is_error" not in result
def test_openai_system_message_to_anthropic_preserves_output_config():
"""System message with output_config must survive OpenAI->Anthropic conversion."""
openai_system_message = {
"role": "system",
"content": [
{
"type": "text",
"text": "Environment update",
}
],
"output_config": {"effort": "medium"},
}
result = AnthropicMessagesHandler._openai_system_message_to_anthropic(openai_system_message)
assert result is not None
assert result["role"] == "system"
assert isinstance(result["content"], list)
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "text"
assert result["content"][0]["text"] == "Environment update"
assert result.get("output_config") == {"effort": "medium"}
def test_openai_system_message_to_anthropic_preserves_cache_control():
"""System message with cache_control must still be preserved."""
openai_system_message = {
"role": "system",
"content": [
{
"type": "text",
"text": "Cached content",
"cache_control": {"type": "ephemeral"},
}
],
}
result = AnthropicMessagesHandler._openai_system_message_to_anthropic(openai_system_message)
assert result is not None
assert result["role"] == "system"
assert isinstance(result["content"], list)
assert len(result["content"]) == 1
assert result["content"][0]["type"] == "text"
assert result["content"][0]["text"] == "Cached content"
assert result["content"][0]["cache_control"] == {"type": "ephemeral"}
def test_openai_system_message_to_anthropic_preserves_both_fields():
"""System message with both output_config and cache_control must preserve both."""
openai_system_message = {
"role": "system",
"content": [
{
"type": "text",
"text": "Complex system message",
"cache_control": {"type": "ephemeral"},
}
],
"output_config": {"effort": "high"},
}
result = AnthropicMessagesHandler._openai_system_message_to_anthropic(openai_system_message)
assert result is not None
assert result["role"] == "system"
assert isinstance(result["content"], list)
assert result["content"][0]["cache_control"] == {"type": "ephemeral"}
assert result.get("output_config") == {"effort": "high"}
def test_openai_system_message_to_anthropic_string_content_with_output_config():
"""System message with string content and output_config must preserve output_config."""
openai_system_message = {
"role": "system",
"content": "Simple system prompt",
"output_config": {"effort": "low"},
}
result = AnthropicMessagesHandler._openai_system_message_to_anthropic(openai_system_message)
assert result is not None
assert result["role"] == "system"
assert result["content"] == "Simple system prompt"
assert result.get("output_config") == {"effort": "low"}