diff --git a/headroom/providers/anthropic.py b/headroom/providers/anthropic.py index ef488ee3d..b16251621 100644 --- a/headroom/providers/anthropic.py +++ b/headroom/providers/anthropic.py @@ -420,13 +420,14 @@ class AnthropicTokenCounter(TokenCounter): # str(block) catch-all would produce. tokens += count_content_blocks(content, self.count_text) - # OpenAI format tool calls - if "tool_calls" in message: - for tool_call in message.get("tool_calls", []): - if isinstance(tool_call, dict): - func = tool_call.get("function") or {} - tokens += self.count_text(coerce_countable_text(func.get("name"))) - tokens += self.count_text(coerce_countable_text(func.get("arguments"))) + # OpenAI format tool calls. Guard the value, not just the key: an + # OpenAI-format assistant message often carries `tool_calls: null` on a + # no-tool turn, and `for ... in None` would raise TypeError. + for tool_call in message.get("tool_calls") or []: + if isinstance(tool_call, dict): + func = tool_call.get("function") or {} + tokens += self.count_text(coerce_countable_text(func.get("name"))) + tokens += self.count_text(coerce_countable_text(func.get("arguments"))) return tokens diff --git a/tests/test_providers/test_anthropic.py b/tests/test_providers/test_anthropic.py index 63fa1f73f..833a6ffc9 100644 --- a/tests/test_providers/test_anthropic.py +++ b/tests/test_providers/test_anthropic.py @@ -54,6 +54,18 @@ class TestAnthropicTokenCounting: count = counter.count_messages(messages) assert count > 0 + def test_count_messages_tolerates_null_tool_calls(self, anthropic_provider): + # OpenAI-format assistant messages routinely carry `tool_calls: null` + # (and occasionally `function: null`) on a no-tool turn. The estimated + # counter iterated the value after only a key-presence check, so it + # raised `TypeError: 'NoneType' object is not iterable`. + counter = anthropic_provider.get_token_counter("claude-3-5-sonnet-20241022") + messages = [ + {"role": "assistant", "content": "hi", "tool_calls": None}, + {"role": "assistant", "content": "x", "tool_calls": [{"id": "a", "function": None}]}, + ] + assert counter.count_messages(messages) > 0 + def test_count_text_allows_literal_special_tokens(self, anthropic_provider): counter = anthropic_provider.get_token_counter("claude-3-5-sonnet-20241022") count = counter.count_text("prefix <|fim_suffix|> suffix")