diff --git a/tests/test_anthropic_content_text.py b/tests/test_anthropic_content_text.py new file mode 100644 index 0000000..59c3143 --- /dev/null +++ b/tests/test_anthropic_content_text.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Tests for Anthropic content-block text extraction.""" + +import unittest +from types import SimpleNamespace + +from util.translation import _anthropic_content_text + + +class AnthropicContentTextTests(unittest.TestCase): + def test_empty_content(self): + self.assertEqual(_anthropic_content_text(None), "") + self.assertEqual(_anthropic_content_text([]), "") + + def test_text_block_only(self): + content = [SimpleNamespace(type="text", text='{"0":"Hello"}')] + self.assertEqual(_anthropic_content_text(content), '{"0":"Hello"}') + + def test_skips_thinking_block_without_text(self): + # Mirrors Anthropic ThinkingBlock: has .thinking, no .text. + thinking = SimpleNamespace(type="thinking", thinking="internal notes") + text = SimpleNamespace(type="text", text='{"0":"Alice"}') + self.assertEqual(_anthropic_content_text([thinking, text]), '{"0":"Alice"}') + + def test_accessing_first_block_text_would_fail(self): + thinking = SimpleNamespace(type="thinking", thinking="notes") + text = SimpleNamespace(type="text", text="ok") + content = [thinking, text] + with self.assertRaises(AttributeError): + _ = content[0].text + self.assertEqual(_anthropic_content_text(content), "ok") + + +if __name__ == "__main__": + unittest.main() diff --git a/util/translation.py b/util/translation.py index c139d1a..b2a8870 100644 --- a/util/translation.py +++ b/util/translation.py @@ -1187,7 +1187,7 @@ def fetchTranslationBatches(batches=None): errored.append((r.custom_id, detail)) continue msg = res.message - text = "".join(getattr(b, "text", "") or "" for b in msg.content) + text = _anthropic_content_text(msg.content) u = msg.usage cr = getattr(u, "cache_read_input_tokens", 0) or 0 cw = getattr(u, "cache_creation_input_tokens", 0) or 0 @@ -1998,6 +1998,17 @@ def createTranslationSchema(numLines): } +def _anthropic_content_text(content) -> str: + """Join text from Anthropic message content blocks. + + Newer Claude models often return a ThinkingBlock (no ``.text``) before the + TextBlock. Only blocks that expose ``.text`` contribute to the result. + """ + if not content: + return "" + return "".join(getattr(b, "text", "") or "" for b in content) + + class _AnthropicCompat: """OpenAI-shaped wrapper around an Anthropic response (text + usage).""" class _Usage: @@ -2232,7 +2243,7 @@ def translateText(system, user, history, penalty, formatType, model, numLines=No except Exception as e: raise Exception(f"Anthropic API error: {e}") - _ant_text = ant_resp.content[0].text if ant_resp.content else "" + _ant_text = _anthropic_content_text(ant_resp.content) _u = ant_resp.usage _cr = getattr(_u, "cache_read_input_tokens", 0) or 0 _cw = getattr(_u, "cache_creation_input_tokens", 0) or 0