This commit is contained in:
ump45nose 2026-11-06 02:31:39 +00:00 • committed by GitHub
commit d8a8aef5c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 3 deletions

View file

@ -25,7 +25,7 @@ from litellm.litellm_core_utils.prompt_templates.reasoning_utils import (
from litellm.litellm_core_utils.reasoning_effort_utils import (
reasoning_effort_from_thinking,
)
from litellm.llms.anthropic.messages.convertible_blocks import tool_result_text_parts
from litellm.llms.anthropic.messages.convertible_blocks import tool_result_text_part
from litellm.llms.anthropic.messages.native.utils import (
refusal_stop_details,
responses_output_refusal_text,
@ -60,6 +60,22 @@ REASONING_SUMMARY_PART_SEPARATOR: Final = "\n\n"
RESPONSES_INCLUDE_ENCRYPTED_REASONING: Final = "reasoning.encrypted_content"
def _responses_tool_result_part(block: object) -> str | None:
match block:
case {"type": "tool_reference", "tool_name": str(tool_name)}:
return f"[Loaded tool: {tool_name}]"
case {"type": "tool_reference"}:
return "[Loaded tool: ]"
case _:
return tool_result_text_part(block)
def _responses_tool_result_text_parts(tool_result: Mapping[str, object]) -> tuple[str, ...]:
content: Final = tool_result.get("content")
parts: Final = map(_responses_tool_result_part, content if isinstance(content, list) else ())
return tuple(part for part in parts if part is not None)
class LiteLLMAnthropicToResponsesAPIAdapter:
"""
Converts Anthropic /v1/messages requests to OpenAI Responses API format and
@ -387,7 +403,7 @@ class LiteLLMAnthropicToResponsesAPIAdapter:
elif isinstance(inner, str):
output_text = inner
elif isinstance(inner, list):
output_text = "\n".join(tool_result_text_parts(tool_result_block))
output_text = "\n".join(_responses_tool_result_text_parts(tool_result_block))
if covered_image_parts or later_image_parts:
output_text = (
f"{output_text}\n{TOOL_RESULT_IMAGE_PLACEHOLDER}"

View file

@ -490,6 +490,33 @@ class TestTranslateMessagesToResponsesInput:
result = _translate_messages(messages)
assert result[0]["output"] == "Line 1\nLine 2"
def test_user_tool_result_list_content_preserves_tool_references(self):
messages = [
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "call_search",
"content": [
{"type": "text", "text": "Available:"},
{"type": "tool_reference", "tool_name": "get_weather"},
],
}
],
}
]
result = _translate_messages(messages)
assert result == [
{
"type": "function_call_output",
"call_id": "call_search",
"output": "Available:\n[Loaded tool: get_weather]",
}
]
def test_user_tool_result_null_content(self):
"""tool_result with null content becomes empty string output."""
messages = [

View file

@ -136,7 +136,6 @@ _DROPPED_INSIDE_TOOL_RESULT: Final[tuple[tuple[Mapping[str, object], str, str],
"text-notes",
"document with a text source inside a tool_result",
),
({"type": "tool_reference", "tool_name": "find_docs"}, "find_docs", "tool_reference inside a tool_result"),
)
@ -158,6 +157,22 @@ def test_tool_result_content_both_counters_drop_is_named(inner: Mapping[str, obj
assert unconvertible_anthropic_blocks(_tool_turn(inner)) == (label,)
def test_tool_reference_count_guard_rejects_when_only_responses_preserves_it() -> None:
inner: Final = {"type": "tool_reference", "tool_name": "find_docs"}
gemini, openai = _sent_by_each_counter(inner)
assert "find_docs" not in gemini
assert _JSON_OBJECTS.validate_json(openai) == [
{"type": "function_call", "call_id": "toolu_01", "name": "lookup", "arguments": "{}"},
{"type": "function_call_output", "call_id": "toolu_01", "output": "[Loaded tool: find_docs]"},
]
assert unconvertible_anthropic_blocks(_tool_turn(inner)) == ("tool_reference inside a tool_result",)
assert unconvertible_blocks_error(_tool_turn(inner), "anthropic") == (
"count_tokens can't convert these content blocks for this model, so the count would leave them out: "
"tool_reference inside a tool_result"
)
def _replayed_web_search(encrypted_content: str) -> tuple[Mapping[str, object], ...]:
return (
{"role": "user", "content": "What is new?"},