This commit is contained in:
Eugenio "Jay" Zuccarelli 2026-11-04 01:41:07 +00:00 • committed by GitHub
commit 9d63ffb3c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 85 additions and 3 deletions

View file

@ -1750,11 +1750,9 @@ def convert_to_gemini_tool_call_result(
# Not valid JSON, wrap in content field
response_data = {"content": content_str}
# We can't determine from openai message format whether it's a successful or
# error call result so default to the successful result template
_function_response: Final = VertexFunctionResponse(
name=name,
response=response_data,
response={"error": content_str} if message.get("is_error") is True else response_data,
)
if gemini_call_id:
_function_response["id"] = gemini_call_id

View file

@ -562,6 +562,11 @@ class LiteLLMAnthropicMessagesAdapter:
content=self._tool_result_content(content.get("content")),
)
self._add_cache_control_if_applicable(content, tool_result, model)
tool_result_block = cast(Mapping[str, object], content) # cast-ok: untrusted client payload
if tool_result_block.get("is_error") is True and self.target_consumes_tool_error(
model, custom_llm_provider
):
tool_result["is_error"] = True
tool_message_list.append(tool_result)
if len(tool_message_list) > 0:
@ -737,6 +742,13 @@ class LiteLLMAnthropicMessagesAdapter:
def target_consumes_cache_control(cls, model: str) -> bool:
return cls.is_anthropic_claude_model(model) or cls.is_bedrock_arn_model(model) or "gemini" in model.lower()
@staticmethod
def target_consumes_tool_error(model: str | None, custom_llm_provider: str | None) -> bool:
# Decide on the provider alone: aliased model names ("prod-model" routed to
# gemini) carry no "gemini" substring.
provider: Final = custom_llm_provider or (model.split("/", 1)[0] if model and "/" in model else None)
return provider in ("gemini", "vertex_ai", "vertex_ai_beta")
@staticmethod
def translate_thinking_for_model(
thinking: AnthropicThinkingParam,

View file

@ -49412,6 +49412,10 @@
],
"title": "Content"
},
"is_error": {
"title": "Is Error",
"type": "boolean"
},
"role": {
"const": "tool",
"title": "Role",

View file

@ -957,6 +957,7 @@ class ChatCompletionToolMessage(TypedDict):
role: Literal["tool"]
content: str | Iterable[ToolMessageContentPart] # writable-ok: Pydantic warns on ReadOnly TypedDict fields
tool_call_id: str
is_error: NotRequired[bool] # writable-ok: Pydantic warns on ReadOnly TypedDict fields
class ChatCompletionFunctionMessage(TypedDict):

View file

@ -2279,6 +2279,36 @@ def test_convert_gemini_tool_call_result_with_data_url_extra_params():
)
def test_convert_gemini_tool_call_result_error_goes_to_response_error():
"""https://github.com/BerriAI/litellm/issues/44979"""
last_message_with_tool_calls = {
"role": "assistant",
"content": "",
"tool_calls": [
{
"id": "call_1",
"type": "function",
"index": 0,
"function": {"name": "deploy", "arguments": "{}"},
}
],
}
failed = convert_to_gemini_tool_call_result(
message=ChatCompletionToolMessage(
role="tool", tool_call_id="call_1", content="exit code 1: migration failed", is_error=True
),
last_message_with_tool_calls=last_message_with_tool_calls,
)
succeeded = convert_to_gemini_tool_call_result(
message=ChatCompletionToolMessage(role="tool", tool_call_id="call_1", content="deployed"),
last_message_with_tool_calls=last_message_with_tool_calls,
)
assert failed["function_response"]["response"] == {"error": "exit code 1: migration failed"}
assert succeeded["function_response"]["response"] == {"content": "deployed"}
def _gemini_tool_result_fixture(
content: str,
) -> tuple[ChatCompletionToolMessage, dict]:

View file

@ -2736,6 +2736,41 @@ def test_cache_control_not_preserved_in_tool_result_for_non_claude():
assert "cache_control" not in tool_message
@pytest.mark.parametrize(
"model,custom_llm_provider,keeps_is_error",
[
("gemini/gemini-3.5-flash-lite", None, True),
("gemini-3.5-flash-lite", "vertex_ai", True),
("gpt-4", "openai", False),
("openrouter/google/gemini-3.5-flash-lite", None, False),
("prod-model", "gemini", True),
],
)
def test_tool_result_is_error_kept_only_for_gemini_targets(model, custom_llm_provider, keeps_is_error):
"""https://github.com/BerriAI/litellm/issues/44979"""
anthropic_messages = [
AnthropicMessagesUserMessageParam(
role="user",
content=[
{
"type": "tool_result",
"tool_use_id": "toolu_1",
"content": "exit code 1: migration failed",
"is_error": True,
}
],
)
]
result = LiteLLMAnthropicMessagesAdapter().translate_anthropic_messages_to_openai(
messages=anthropic_messages, model=model, custom_llm_provider=custom_llm_provider
)
tool_message = next(msg for msg in result if msg.get("role") == "tool")
assert tool_message["content"] == "exit code 1: migration failed"
assert tool_message.get("is_error") is (True if keeps_is_error else None)
def test_cache_control_preserved_in_assistant_text_for_claude():
"""Cache control should be preserved in assistant text blocks for Claude models."""
anthropic_messages = [

View file

@ -29462,6 +29462,8 @@ export interface components {
ChatCompletionToolMessage: {
/** Content */
content: string | (components["schemas"]["ChatCompletionTextObject"] | components["schemas"]["ChatCompletionImageObject"] | components["schemas"]["ChatCompletionAudioObject"] | components["schemas"]["ChatCompletionFileObject"] | components["schemas"]["ChatCompletionToolReferenceObject"])[];
/** Is Error */
is_error?: boolean;
/**
* Role
* @constant