Skip to content

Commit 3de8f0a

Browse files
authored
fix(litellm): generate toolUseId when missing from provider (strands-agents#2949)
1 parent 65f7a38 commit 3de8f0a

2 files changed

Lines changed: 29 additions & 1 deletion

File tree

strands-py/src/strands/models/litellm.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import json
77
import logging
8+
import uuid
89
from collections.abc import AsyncGenerator
910
from typing import Any, TypeVar, cast
1011

@@ -519,7 +520,11 @@ async def _process_tool_calls(self, tool_calls: dict[int, list[Any]]) -> AsyncGe
519520
Formatted tool call chunks.
520521
"""
521522
for tool_deltas in tool_calls.values():
522-
yield self.format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": tool_deltas[0]})
523+
first_delta = tool_deltas[0]
524+
if not first_delta.id:
525+
first_delta.id = f"call_{uuid.uuid4()}"
526+
527+
yield self.format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": first_delta})
523528

524529
for tool_delta in tool_deltas:
525530
yield self.format_chunk({"chunk_type": "content_delta", "data_type": "tool", "data": tool_delta})

strands-py/tests/strands/models/test_litellm.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1019,3 +1019,26 @@ def test_thought_signature_round_trip():
10191019
tool_call = LiteLLMModel.format_request_message_tool_call(internal_tool_use)
10201020
assert "__thought__" in tool_call["id"]
10211021
assert signature in tool_call["id"]
1022+
1023+
1024+
@pytest.mark.asyncio
1025+
async def test_stream_generates_tool_call_id_when_null(litellm_acompletion, model, agenerator, alist):
1026+
mock_tool_call = unittest.mock.Mock(index=0)
1027+
mock_tool_call.id = None
1028+
mock_tool_call.function.name = "test_tool"
1029+
mock_tool_call.function.arguments = '{"arg": "value"}'
1030+
1031+
mock_delta_1 = unittest.mock.Mock(content=None, tool_calls=[mock_tool_call], reasoning_content=None)
1032+
mock_delta_2 = unittest.mock.Mock(content=None, tool_calls=None, reasoning_content=None)
1033+
1034+
mock_event_1 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason=None, delta=mock_delta_1)])
1035+
mock_event_2 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason="tool_calls", delta=mock_delta_2)])
1036+
1037+
litellm_acompletion.side_effect = unittest.mock.AsyncMock(return_value=agenerator([mock_event_1, mock_event_2]))
1038+
1039+
response = model.stream([{"role": "user", "content": [{"text": "test"}]}])
1040+
events = await alist(response)
1041+
1042+
start = next(e for e in events if "contentBlockStart" in e and "toolUse" in e["contentBlockStart"]["start"])
1043+
tool_id = start["contentBlockStart"]["start"]["toolUse"]["toolUseId"]
1044+
assert tool_id and tool_id.startswith("call_")

0 commit comments

Comments
 (0)