Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 30 additions & 11 deletions astrbot/core/provider/sources/anthropic_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,18 +195,37 @@ def _prepare_payload(self, messages: list[dict]):
},
)
elif message["role"] == "tool":
new_messages.append(
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": message["tool_call_id"],
"content": message["content"] or "<empty response>",
},
],
},
tool_result_block = {
"type": "tool_result",
"tool_use_id": message["tool_call_id"],
"content": message["content"] or "<empty response>",
}
last_message = new_messages[-1] if new_messages else None
last_content = (
last_message.get("content")
if isinstance(last_message, dict)
else None
)
can_append_to_previous_tool_results = (
last_message is not None
and last_message.get("role") == "user"
and isinstance(last_content, list)
and len(last_content) > 0
and all(
isinstance(block, dict) and block.get("type") == "tool_result"
for block in last_content
)
)

if can_append_to_previous_tool_results:
last_content.append(tool_result_block)
else:
new_messages.append(
{
"role": "user",
"content": [tool_result_block],
},
)
elif message["role"] == "user":
if isinstance(message.get("content"), list):
converted_content = []
Expand Down
178 changes: 178 additions & 0 deletions tests/test_anthropic_kimi_code_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,181 @@ def test_anthropic_empty_output_raises_empty_model_output_error():
completion_id="msg_empty",
stop_reason="end_turn",
)


def _make_anthropic_provider_for_payload_tests() -> anthropic_source.ProviderAnthropic:
return anthropic_source.ProviderAnthropic(
provider_config={"model": "claude-test"},
provider_settings={},
use_api_key=False,
Comment on lines +98 to +102
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider one test where the sequence starts with a tool message to guard the empty-new_messages path

The new last_message = new_messages[-1] if new_messages else None path isn’t covered by a case where the first input message is role: "tool". Please add a test where messages begins with a tool message (no prior user/assistant), to confirm _prepare_payload correctly handles initially empty new_messages and still produces a single user message with one tool_result.

)


def test_prepare_payload_merges_consecutive_tool_results_into_single_user_message():
provider = _make_anthropic_provider_for_payload_tests()

_, new_messages = provider._prepare_payload(
[
{
"role": "assistant",
"content": [{"type": "text", "text": "Reading files"}],
"tool_calls": [
{
"type": "function",
"id": "call_00",
"function": {
"name": "astrbot_file_read_tool",
"arguments": '{"path": "/tmp/one.txt"}',
},
},
{
"type": "function",
"id": "call_01",
"function": {
"name": "astrbot_file_read_tool",
"arguments": '{"path": "/tmp/two.txt"}',
},
},
],
},
{
"role": "tool",
"tool_call_id": "call_00",
"content": "one",
},
{
"role": "tool",
"tool_call_id": "call_01",
"content": "two",
},
]
)

assert len(new_messages) == 2
assert new_messages[1]["role"] == "user"
assert new_messages[1]["content"] == [
{"type": "tool_result", "tool_use_id": "call_00", "content": "one"},
{"type": "tool_result", "tool_use_id": "call_01", "content": "two"},
]


def test_prepare_payload_keeps_single_tool_result_as_single_user_message():
provider = _make_anthropic_provider_for_payload_tests()

_, new_messages = provider._prepare_payload(
[
{
"role": "assistant",
"content": [{"type": "text", "text": "Reading file"}],
"tool_calls": [
{
"type": "function",
"id": "call_00",
"function": {
"name": "astrbot_file_read_tool",
"arguments": '{"path": "/tmp/one.txt"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_00",
"content": "one",
},
]
)

assert len(new_messages) == 2
assert new_messages[1] == {
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "call_00", "content": "one"}
],
}


def test_prepare_payload_does_not_merge_non_consecutive_tool_results():
provider = _make_anthropic_provider_for_payload_tests()

_, new_messages = provider._prepare_payload(
[
{
"role": "assistant",
"content": [{"type": "text", "text": "First tool"}],
"tool_calls": [
{
"type": "function",
"id": "call_00",
"function": {
"name": "astrbot_file_read_tool",
"arguments": '{"path": "/tmp/one.txt"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_00",
"content": "one",
},
{
"role": "assistant",
"content": [{"type": "text", "text": "Second tool"}],
"tool_calls": [
{
"type": "function",
"id": "call_01",
"function": {
"name": "astrbot_file_read_tool",
"arguments": '{"path": "/tmp/two.txt"}',
},
}
],
},
{
"role": "tool",
"tool_call_id": "call_01",
"content": "two",
},
]
)

assert new_messages == [
{
"role": "assistant",
"content": [
{"type": "text", "text": "First tool"},
{
"type": "tool_use",
"name": "astrbot_file_read_tool",
"input": {"path": "/tmp/one.txt"},
"id": "call_00",
},
],
},
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "call_00", "content": "one"}
],
},
{
"role": "assistant",
"content": [
{"type": "text", "text": "Second tool"},
{
"type": "tool_use",
"name": "astrbot_file_read_tool",
"input": {"path": "/tmp/two.txt"},
"id": "call_01",
},
],
},
{
"role": "user",
"content": [
{"type": "tool_result", "tool_use_id": "call_01", "content": "two"}
],
},
]
Loading