fix(agent): accept empty content with stop_reason=end_turn as valid anthropic response
Anthropic's API can legitimately return content=[] with stop_reason="end_turn" when the model has nothing more to add after a turn that already delivered the user-facing text alongside a trivial tool call (e.g. memory write). The transport validator was treating that as an invalid response, triggering 3 retries that each returned the same valid-but-empty response, then failing the run with "Invalid API response after 3 retries." The downstream normalizer already handles empty content correctly (empty loop over response.content, content=None, finish_reason="stop"), so the only fix needed is at the validator boundary. Tests: - Empty content + stop_reason="end_turn" → valid (the fix) - Empty content + stop_reason="tool_use" → still invalid (regression guard) - Empty content without stop_reason → still invalid (existing behavior preserved)
This commit is contained in:
parent
ea67e49574
commit
b49a1b71a7
@ -87,14 +87,20 @@ class AnthropicTransport(ProviderTransport):
|
|||||||
return normalize_anthropic_response_v2(response, strip_tool_prefix=strip_tool_prefix)
|
return normalize_anthropic_response_v2(response, strip_tool_prefix=strip_tool_prefix)
|
||||||
|
|
||||||
def validate_response(self, response: Any) -> bool:
|
def validate_response(self, response: Any) -> bool:
|
||||||
"""Check Anthropic response structure is valid."""
|
"""Check Anthropic response structure is valid.
|
||||||
|
|
||||||
|
An empty content list is legitimate when ``stop_reason == "end_turn"``
|
||||||
|
— the model's canonical way of signalling "nothing more to add" after
|
||||||
|
a tool turn that already delivered the user-facing text. Treating it
|
||||||
|
as invalid falsely retries a completed response.
|
||||||
|
"""
|
||||||
if response is None:
|
if response is None:
|
||||||
return False
|
return False
|
||||||
content_blocks = getattr(response, "content", None)
|
content_blocks = getattr(response, "content", None)
|
||||||
if not isinstance(content_blocks, list):
|
if not isinstance(content_blocks, list):
|
||||||
return False
|
return False
|
||||||
if not content_blocks:
|
if not content_blocks:
|
||||||
return False
|
return getattr(response, "stop_reason", None) == "end_turn"
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def extract_cache_stats(self, response: Any) -> Optional[Dict[str, int]]:
|
def extract_cache_stats(self, response: Any) -> Optional[Dict[str, int]]:
|
||||||
|
|||||||
@ -114,6 +114,14 @@ class TestAnthropicTransport:
|
|||||||
r = SimpleNamespace(content=[])
|
r = SimpleNamespace(content=[])
|
||||||
assert transport.validate_response(r) is False
|
assert transport.validate_response(r) is False
|
||||||
|
|
||||||
|
def test_validate_response_empty_content_with_end_turn_is_valid(self, transport):
|
||||||
|
r = SimpleNamespace(content=[], stop_reason="end_turn")
|
||||||
|
assert transport.validate_response(r) is True
|
||||||
|
|
||||||
|
def test_validate_response_empty_content_with_tool_use_is_invalid(self, transport):
|
||||||
|
r = SimpleNamespace(content=[], stop_reason="tool_use")
|
||||||
|
assert transport.validate_response(r) is False
|
||||||
|
|
||||||
def test_validate_response_valid(self, transport):
|
def test_validate_response_valid(self, transport):
|
||||||
r = SimpleNamespace(content=[SimpleNamespace(type="text", text="hello")])
|
r = SimpleNamespace(content=[SimpleNamespace(type="text", text="hello")])
|
||||||
assert transport.validate_response(r) is True
|
assert transport.validate_response(r) is True
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user