mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-09 03:18:44 +00:00
Merge pull request #37356 from BerriAI/litellm_fix_v1_messages_double_content_block_stop
test(anthropic): pin one content_block_stop per tool_use block on the Responses adapter
This commit is contained in:
commit
589f6859d8
1 changed files with 70 additions and 0 deletions
|
|
@ -148,6 +148,76 @@ class TestReasoningItemWithoutSummaryText:
|
|||
assert "".join(c["delta"]["thinking"] for c in chunks[2:4]) == "Weighing options"
|
||||
|
||||
|
||||
class TestToolUseBlockClosedExactlyOnce:
|
||||
"""Regression for https://github.com/BerriAI/litellm/issues/37273.
|
||||
|
||||
With ``custom_llm_provider: openai`` + ``use_chat_completions_api: true``,
|
||||
``/v1/messages`` streams through ``LiteLLMCompletionStreamingIterator``,
|
||||
which ends a tool-call turn with two ``response.output_item.done`` events:
|
||||
one for the function_call item (id = call_id) and one for a synthetic
|
||||
message item whose id is the upstream chatcmpl id and was never opened as a
|
||||
content block. Resolving that unknown item id to ``_current_block_index``
|
||||
closed the tool_use block a second time::
|
||||
|
||||
content_block_start[0](tool_use) -> content_block_stop[0]
|
||||
-> content_block_stop[0] -> message_delta(stop_reason=tool_use)
|
||||
|
||||
Anthropic SDK clients (e.g. Claude Code) materialize one tool_use block per
|
||||
``content_block_stop``, so the tool executed twice. An ``output_item.done``
|
||||
for an item that never opened a block must emit nothing.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _chat_completions_bridge_tool_turn() -> list[dict[str, object]]:
|
||||
return [
|
||||
{"type": "response.created"},
|
||||
{
|
||||
"type": "response.output_item.added",
|
||||
"item": {"type": "function_call", "id": "call_1", "call_id": "call_1", "name": "get_weather"},
|
||||
},
|
||||
{"type": "response.function_call_arguments.delta", "item_id": "call_1", "delta": '{"city": "'},
|
||||
{"type": "response.function_call_arguments.delta", "item_id": "call_1", "delta": 'Tokyo"}'},
|
||||
{
|
||||
"type": "response.function_call_arguments.done",
|
||||
"item_id": "call_1",
|
||||
"arguments": '{"city": "Tokyo"}',
|
||||
},
|
||||
{
|
||||
"type": "response.output_item.done",
|
||||
"item": {"type": "function_call", "id": "call_1", "call_id": "call_1", "status": "completed"},
|
||||
},
|
||||
{
|
||||
"type": "response.output_item.done",
|
||||
"item": {"type": "message", "id": "chatcmpl-123", "status": "completed"},
|
||||
},
|
||||
]
|
||||
|
||||
def test_one_content_block_stop_per_content_block_start(self):
|
||||
chunks = _drain_async(self._chat_completions_bridge_tool_turn())
|
||||
|
||||
starts = [c["index"] for c in chunks if c["type"] == "content_block_start"]
|
||||
stops = [c["index"] for c in chunks if c["type"] == "content_block_stop"]
|
||||
assert starts == [0]
|
||||
assert stops == [0]
|
||||
|
||||
def test_tool_turn_event_order(self):
|
||||
chunks = _drain_async(self._chat_completions_bridge_tool_turn())
|
||||
|
||||
assert [(c["type"], c.get("index")) for c in chunks] == [
|
||||
("message_start", None),
|
||||
("content_block_start", 0),
|
||||
("content_block_delta", 0),
|
||||
("content_block_delta", 0),
|
||||
("content_block_stop", 0),
|
||||
]
|
||||
assert chunks[1]["content_block"] == {
|
||||
"type": "tool_use",
|
||||
"id": "call_1",
|
||||
"name": "get_weather",
|
||||
"input": {},
|
||||
}
|
||||
|
||||
|
||||
class TestProcessEventTextDeltaWithoutOutputItemAdded:
|
||||
"""Streams that skip response.output_item.added (e.g. LMStudio) must still
|
||||
open a text block before any delta and never emit index -1."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue