Skip to content

Commit 9ec39f6

Browse files
committed
fix(litellm): generate toolUseId when missing
1 parent eaa6efb commit 9ec39f6

File tree

2 files changed

+42
-1
lines changed

2 files changed

+42
-1
lines changed

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 typing import Any, AsyncGenerator, Optional, Type, TypedDict, TypeVar, Union, cast
910

1011
import litellm
@@ -321,7 +322,11 @@ async def stream(
321322
break
322323

323324
for tool_deltas in tool_calls.values():
324-
yield self.format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": tool_deltas[0]})
325+
first_delta = tool_deltas[0]
326+
if not first_delta.id:
327+
first_delta.id = f"call_{uuid.uuid4()}"
328+
329+
yield self.format_chunk({"chunk_type": "content_start", "data_type": "tool", "data": first_delta})
325330

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

tests/strands/models/test_litellm.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,3 +478,39 @@ def test_format_request_messages_cache_point_support():
478478
]
479479

480480
assert result == expected
481+
482+
483+
@pytest.mark.asyncio
484+
async def test_stream_generates_tool_call_id_when_null(litellm_acompletion, model, agenerator, alist):
485+
"""Test that stream generates a tool call ID when LiteLLM returns null."""
486+
mock_tool_call = unittest.mock.Mock(index=0)
487+
mock_tool_call.id = None
488+
mock_tool_call.function.name = "test_tool"
489+
mock_tool_call.function.arguments = '{"arg": "value"}'
490+
491+
mock_delta = unittest.mock.Mock(content=None, tool_calls=[mock_tool_call], reasoning_content=None)
492+
493+
mock_event_1 = unittest.mock.Mock(choices=[unittest.mock.Mock(finish_reason=None, delta=mock_delta)])
494+
mock_event_2 = unittest.mock.Mock(
495+
choices=[
496+
unittest.mock.Mock(
497+
finish_reason="tool_calls",
498+
delta=unittest.mock.Mock(content=None, tool_calls=None, reasoning_content=None),
499+
)
500+
]
501+
)
502+
503+
litellm_acompletion.side_effect = unittest.mock.AsyncMock(return_value=agenerator([mock_event_1, mock_event_2]))
504+
505+
messages = [{"role": "user", "content": [{"text": "test"}]}]
506+
response = model.stream(messages)
507+
tru_events = await alist(response)
508+
509+
tool_start_event = next(
510+
e for e in tru_events if "contentBlockStart" in e and "toolUse" in e["contentBlockStart"]["start"]
511+
)
512+
513+
tool_id = tool_start_event["contentBlockStart"]["start"]["toolUse"]["toolUseId"]
514+
assert tool_id is not None
515+
assert tool_id.startswith("call_")
516+
assert len(tool_id) > 5

0 commit comments

Comments
 (0)