mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge 7c2c185444 into 77b4644523
This commit is contained in:
commit
7cdfe11a37
4 changed files with 169 additions and 5 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
Loading…
Add table
Reference in a new issue