mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-01 13:18:10 +00:00
Fix Snowflake Claude incomplete tool result histories
This commit is contained in:
@@ -133,6 +133,9 @@ class SnowflakeCompletion(OpenAICompletion):
|
||||
def _format_messages(self, messages: str | list[LLMMessage]) -> list[LLMMessage]:
|
||||
formatted_messages = super()._format_messages(messages)
|
||||
if self._is_claude_model():
|
||||
formatted_messages = self._remove_incomplete_claude_tool_uses(
|
||||
formatted_messages
|
||||
)
|
||||
return self._ensure_claude_conversation_ends_with_user(formatted_messages)
|
||||
return formatted_messages
|
||||
|
||||
@@ -140,6 +143,59 @@ class SnowflakeCompletion(OpenAICompletion):
|
||||
model = self.model.lower()
|
||||
return model.startswith(("claude-", "anthropic."))
|
||||
|
||||
@staticmethod
|
||||
def _remove_incomplete_claude_tool_uses(
|
||||
messages: list[LLMMessage],
|
||||
) -> list[LLMMessage]:
|
||||
"""Drop dangling Claude tool-use turns before sending to Snowflake.
|
||||
|
||||
Snowflake-hosted Claude models reject histories where an assistant tool
|
||||
use is not accompanied by matching tool results. CrewAI may retry or
|
||||
summarize after an interrupted tool cycle, leaving an assistant
|
||||
``tool_calls`` message in history without every corresponding
|
||||
``role='tool'`` result. OpenAI-family models tolerate that more often,
|
||||
but Claude through Snowflake returns:
|
||||
"Each 'toolUse' block must be accompanied with a matching 'toolResult' block."
|
||||
"""
|
||||
sanitized: list[LLMMessage] = []
|
||||
index = 0
|
||||
|
||||
while index < len(messages):
|
||||
message = messages[index]
|
||||
tool_calls = message.get("tool_calls") or []
|
||||
if message.get("role") != "assistant" or not tool_calls:
|
||||
sanitized.append(message)
|
||||
index += 1
|
||||
continue
|
||||
|
||||
expected_ids = {
|
||||
tool_call.get("id")
|
||||
for tool_call in tool_calls
|
||||
if isinstance(tool_call, dict) and tool_call.get("id")
|
||||
}
|
||||
if not expected_ids:
|
||||
sanitized.append(message)
|
||||
index += 1
|
||||
continue
|
||||
|
||||
tool_result_ids: set[str] = set()
|
||||
lookahead = index + 1
|
||||
while (
|
||||
lookahead < len(messages) and messages[lookahead].get("role") == "tool"
|
||||
):
|
||||
tool_call_id = messages[lookahead].get("tool_call_id")
|
||||
if isinstance(tool_call_id, str):
|
||||
tool_result_ids.add(tool_call_id)
|
||||
lookahead += 1
|
||||
|
||||
if expected_ids.issubset(tool_result_ids):
|
||||
sanitized.append(message)
|
||||
sanitized.extend(messages[index + 1 : lookahead])
|
||||
|
||||
index = lookahead
|
||||
|
||||
return sanitized
|
||||
|
||||
@staticmethod
|
||||
def _ensure_claude_conversation_ends_with_user(
|
||||
messages: list[LLMMessage],
|
||||
|
||||
@@ -156,7 +156,7 @@ class TestSnowflakeRequests:
|
||||
|
||||
assert messages == [{"role": "user", "content": "Write a summary."}]
|
||||
|
||||
def test_claude_model_adds_user_turn_after_tool_call_assistant_message(
|
||||
def test_claude_model_removes_dangling_tool_call_without_result(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
_snowflake_env(monkeypatch)
|
||||
@@ -179,8 +179,43 @@ class TestSnowflakeRequests:
|
||||
]
|
||||
)
|
||||
|
||||
assert messages[-2]["role"] == "assistant"
|
||||
assert messages[-2]["tool_calls"][0]["id"] == "call_1"
|
||||
assert messages == [{"role": "user", "content": "Use the tool."}]
|
||||
|
||||
def test_claude_model_preserves_complete_tool_call_result_pair(
|
||||
self, monkeypatch: pytest.MonkeyPatch
|
||||
):
|
||||
_snowflake_env(monkeypatch)
|
||||
llm = SnowflakeCompletion(model="claude-sonnet-4-5")
|
||||
|
||||
messages = llm._format_messages(
|
||||
[
|
||||
{"role": "user", "content": "Use the tool."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_1",
|
||||
"content": "result",
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
assert messages[-3]["role"] == "assistant"
|
||||
assert messages[-3]["tool_calls"][0]["id"] == "call_1"
|
||||
assert messages[-2] == {
|
||||
"role": "tool",
|
||||
"tool_call_id": "call_1",
|
||||
"content": "result",
|
||||
}
|
||||
assert messages[-1]["role"] == "user"
|
||||
|
||||
def test_claude_model_maps_max_tokens_to_max_completion_tokens(
|
||||
|
||||
Reference in New Issue
Block a user