mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-27 18:20:57 +00:00
fix(responses): preserve cached tool call objects in tool result recovery (#20700)
* fix(responses): preserve cached tool call objects in tool result recovery * fix(responses): support attr-based cached tool call recovery * Update litellm/responses/litellm_completion_transformation/transformation.py Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
committed by
Sameer Kankute
co-authored by
greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
parent
4256c547c2
commit
28e15f4fd6
@@ -602,18 +602,53 @@ class LiteLLMCompletionResponsesConfig:
|
||||
}
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_mapping_or_attr_value(obj: Any, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
Safely read a field from dict-like or attribute-based objects.
|
||||
"""
|
||||
if obj is None:
|
||||
return default
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return obj.get(key, default)
|
||||
|
||||
getter = getattr(obj, "get", None)
|
||||
if callable(getter):
|
||||
try:
|
||||
return getter(key, default)
|
||||
except (TypeError, AttributeError):
|
||||
pass
|
||||
|
||||
return getattr(obj, key, default)
|
||||
|
||||
@staticmethod
|
||||
def _create_tool_call_chunk(
|
||||
tool_use_definition: Dict[str, Any], tool_call_id: str, index: int
|
||||
) -> ChatCompletionToolCallChunk:
|
||||
"""Create a ChatCompletionToolCallChunk from tool_use_definition."""
|
||||
function_raw = tool_use_definition.get("function")
|
||||
function: Dict[str, Any] = function_raw if isinstance(function_raw, dict) else {}
|
||||
tool_use_id_raw = tool_use_definition.get("id")
|
||||
function_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
tool_use_definition, "function"
|
||||
)
|
||||
function_name_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
function_raw, "name"
|
||||
)
|
||||
function_arguments_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
function_raw, "arguments"
|
||||
)
|
||||
function: Dict[str, Any] = {
|
||||
"name": function_name_raw or "",
|
||||
"arguments": function_arguments_raw or "{}",
|
||||
}
|
||||
tool_use_id_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
tool_use_definition, "id"
|
||||
)
|
||||
tool_use_id: str = (
|
||||
str(tool_use_id_raw) if tool_use_id_raw is not None else str(tool_call_id)
|
||||
)
|
||||
tool_use_type_raw = tool_use_definition.get("type")
|
||||
tool_use_type_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
tool_use_definition, "type"
|
||||
)
|
||||
tool_use_type: str = (
|
||||
str(tool_use_type_raw) if tool_use_type_raw is not None else "function"
|
||||
)
|
||||
@@ -627,6 +662,63 @@ class LiteLLMCompletionResponsesConfig:
|
||||
index=index,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_tool_use_definition(
|
||||
tool_use_definition: Any, tool_call_id: str
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Normalize cached tool_call definitions to a dict-like shape consumed by _create_tool_call_chunk.
|
||||
"""
|
||||
if not tool_use_definition:
|
||||
return None
|
||||
|
||||
if isinstance(tool_use_definition, dict):
|
||||
normalized_definition: Dict[str, Any] = dict(tool_use_definition)
|
||||
else:
|
||||
tool_use_id_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
tool_use_definition, "id"
|
||||
)
|
||||
tool_use_type_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
tool_use_definition, "type"
|
||||
)
|
||||
function_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
tool_use_definition, "function"
|
||||
)
|
||||
|
||||
# Object does not expose the expected tool_call fields.
|
||||
if (
|
||||
tool_use_id_raw is None
|
||||
and tool_use_type_raw is None
|
||||
and function_raw is None
|
||||
):
|
||||
return None
|
||||
|
||||
normalized_definition = {
|
||||
"id": tool_use_id_raw,
|
||||
"type": tool_use_type_raw,
|
||||
"function": function_raw,
|
||||
}
|
||||
|
||||
function_raw = normalized_definition.get("function")
|
||||
if function_raw is not None and not isinstance(function_raw, dict):
|
||||
function_name_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
function_raw, "name"
|
||||
)
|
||||
function_arguments_raw = LiteLLMCompletionResponsesConfig._get_mapping_or_attr_value(
|
||||
function_raw, "arguments"
|
||||
)
|
||||
if function_name_raw is not None or function_arguments_raw is not None:
|
||||
normalized_definition["function"] = {
|
||||
"name": function_name_raw,
|
||||
"arguments": function_arguments_raw,
|
||||
}
|
||||
|
||||
normalized_definition["id"] = normalized_definition.get("id") or tool_call_id
|
||||
normalized_definition["type"] = (
|
||||
normalized_definition.get("type") or "function"
|
||||
)
|
||||
return normalized_definition
|
||||
|
||||
@staticmethod
|
||||
def _add_tool_call_to_assistant(
|
||||
assistant_message: Any, tool_call_chunk: ChatCompletionToolCallChunk
|
||||
@@ -740,13 +832,19 @@ class LiteLLMCompletionResponsesConfig:
|
||||
tool_call_id, tools
|
||||
)
|
||||
)
|
||||
|
||||
if _tool_use_definition:
|
||||
if not isinstance(_tool_use_definition, dict):
|
||||
_tool_use_definition = {}
|
||||
|
||||
normalized_tool_use_definition = (
|
||||
LiteLLMCompletionResponsesConfig._normalize_tool_use_definition(
|
||||
_tool_use_definition, tool_call_id
|
||||
)
|
||||
)
|
||||
|
||||
if normalized_tool_use_definition:
|
||||
tool_call_chunk = (
|
||||
LiteLLMCompletionResponsesConfig._create_tool_call_chunk(
|
||||
_tool_use_definition, tool_call_id, len(tool_calls)
|
||||
normalized_tool_use_definition,
|
||||
tool_call_id,
|
||||
len(tool_calls),
|
||||
)
|
||||
)
|
||||
LiteLLMCompletionResponsesConfig._add_tool_call_to_assistant(
|
||||
|
||||
+96
-1
@@ -7,6 +7,7 @@ sys.path.insert(
|
||||
|
||||
from litellm.responses.litellm_completion_transformation.transformation import (
|
||||
LiteLLMCompletionResponsesConfig,
|
||||
TOOL_CALLS_CACHE,
|
||||
)
|
||||
from litellm.types.llms.openai import (
|
||||
ChatCompletionResponseMessage,
|
||||
@@ -17,6 +18,8 @@ from litellm.types.utils import (
|
||||
CompletionTokensDetailsWrapper,
|
||||
Message,
|
||||
ModelResponse,
|
||||
Function,
|
||||
ChatCompletionMessageToolCall,
|
||||
PromptTokensDetailsWrapper,
|
||||
Usage,
|
||||
)
|
||||
@@ -755,6 +758,98 @@ class TestFunctionCallTransformation:
|
||||
tool_call = tool_calls[0]
|
||||
assert tool_call.get("id") == "fallback_id"
|
||||
|
||||
def test_ensure_tool_results_preserves_cached_openai_object_tool_call(self):
|
||||
"""
|
||||
Test cached ChatCompletionMessageToolCall objects are normalized correctly.
|
||||
"""
|
||||
tool_call_id = "call_cached_openai_object"
|
||||
TOOL_CALLS_CACHE.set_cache(
|
||||
key=tool_call_id,
|
||||
value=ChatCompletionMessageToolCall(
|
||||
id=tool_call_id,
|
||||
type="function",
|
||||
function=Function(
|
||||
name="search_web",
|
||||
arguments='{"query": "python bugs"}',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
messages_missing_tool_calls = [
|
||||
{"role": "user", "content": "Search for python bugs"},
|
||||
{"role": "assistant", "content": None, "tool_calls": []},
|
||||
{"role": "tool", "content": "Found 5 results", "tool_call_id": tool_call_id},
|
||||
]
|
||||
|
||||
try:
|
||||
fixed_messages = LiteLLMCompletionResponsesConfig._ensure_tool_results_have_corresponding_tool_calls(
|
||||
messages=messages_missing_tool_calls,
|
||||
tools=None,
|
||||
)
|
||||
finally:
|
||||
TOOL_CALLS_CACHE.delete_cache(key=tool_call_id)
|
||||
|
||||
assistant_msg = fixed_messages[1]
|
||||
tool_calls = assistant_msg.get("tool_calls", [])
|
||||
assert len(tool_calls) == 1
|
||||
|
||||
tool_call = tool_calls[0]
|
||||
function = tool_call.get("function", {})
|
||||
assert function.get("name") == "search_web"
|
||||
assert function.get("arguments") == '{"query": "python bugs"}'
|
||||
|
||||
def test_ensure_tool_results_preserves_cached_attr_object_tool_call(self):
|
||||
"""
|
||||
Test cached attribute-only tool call objects are normalized correctly.
|
||||
"""
|
||||
|
||||
class AttrOnlyFunction:
|
||||
def __init__(self, name: str, arguments: str):
|
||||
self.name = name
|
||||
self.arguments = arguments
|
||||
|
||||
class AttrOnlyToolCall:
|
||||
def __init__(self, id: str, type: str, function: AttrOnlyFunction):
|
||||
self.id = id
|
||||
self.type = type
|
||||
self.function = function
|
||||
|
||||
tool_call_id = "call_cached_attr_object"
|
||||
TOOL_CALLS_CACHE.set_cache(
|
||||
key=tool_call_id,
|
||||
value=AttrOnlyToolCall(
|
||||
id=tool_call_id,
|
||||
type="function",
|
||||
function=AttrOnlyFunction(
|
||||
name="search_web",
|
||||
arguments='{"query": "attribute objects"}',
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
messages_missing_tool_calls = [
|
||||
{"role": "user", "content": "Search using attr object"},
|
||||
{"role": "assistant", "content": None, "tool_calls": []},
|
||||
{"role": "tool", "content": "Found 3 results", "tool_call_id": tool_call_id},
|
||||
]
|
||||
|
||||
try:
|
||||
fixed_messages = LiteLLMCompletionResponsesConfig._ensure_tool_results_have_corresponding_tool_calls(
|
||||
messages=messages_missing_tool_calls,
|
||||
tools=None,
|
||||
)
|
||||
finally:
|
||||
TOOL_CALLS_CACHE.delete_cache(key=tool_call_id)
|
||||
|
||||
assistant_msg = fixed_messages[1]
|
||||
tool_calls = assistant_msg.get("tool_calls", [])
|
||||
assert len(tool_calls) == 1
|
||||
|
||||
tool_call = tool_calls[0]
|
||||
function = tool_call.get("function", {})
|
||||
assert function.get("name") == "search_web"
|
||||
assert function.get("arguments") == '{"query": "attribute objects"}'
|
||||
|
||||
|
||||
class TestToolChoiceTransformation:
|
||||
"""Test the tool_choice transformation fix for Cursor IDE bug"""
|
||||
@@ -1637,4 +1732,4 @@ class TestStreamingIDConsistency:
|
||||
|
||||
# Verify it matches the cached ID
|
||||
assert iterator._cached_item_id is not None
|
||||
assert iterator._cached_item_id == text_done_id
|
||||
assert iterator._cached_item_id == text_done_id
|
||||
|
||||
Reference in New Issue
Block a user