diff --git a/python/frameworks/anthropic/CHANGELOG.md b/python/frameworks/anthropic/CHANGELOG.md index fffc68e2..72a38f23 100644 --- a/python/frameworks/anthropic/CHANGELOG.md +++ b/python/frameworks/anthropic/CHANGELOG.md @@ -1,3 +1,12 @@ +## [Unreleased] +### Fixed +- `AnthropicInstrumentor().instrument()` raised `ImportError` on `anthropic>=1.0` + and instrumented nothing. `anthropic` 1.0 removed the legacy Completions API + (`anthropic.resources.completions`) entirely; `_instrument()`/`_uninstrument()` + imported it unconditionally, so the whole method aborted before it got to wrap + Messages. Completions wrapping is now skipped (with old `anthropic<1` + installs unaffected) instead of failing instrumentation altogether. + ## [0.1.7] - 2025-06-10 ### Feature - Added support for ai-evaluation diff --git a/python/frameworks/anthropic/traceai_anthropic/__init__.py b/python/frameworks/anthropic/traceai_anthropic/__init__.py index 7e1c13cc..06257d48 100644 --- a/python/frameworks/anthropic/traceai_anthropic/__init__.py +++ b/python/frameworks/anthropic/traceai_anthropic/__init__.py @@ -40,8 +40,16 @@ def instrumentation_dependencies(self) -> Collection[str]: return _instruments def _instrument(self, **kwargs: Any) -> None: - from anthropic.resources.completions import AsyncCompletions, Completions from anthropic.resources.messages import AsyncMessages, Messages + + try: + # anthropic>=1.0 removed the legacy Completions API (`/v1/complete`) + # entirely - see anthropic-sdk-python's MIGRATION.md, "Removed: the + # legacy Text Completions API". Messages is unaffected, so degrade to + # instrumenting only Messages instead of failing to instrument anything. + from anthropic.resources.completions import AsyncCompletions, Completions + except ImportError: + AsyncCompletions = Completions = None try: from fi.evals import Protect except ImportError: @@ -60,19 +68,25 @@ def _instrument(self, **kwargs: Any) -> None: config=config, ) - self._original_completions_create = Completions.create - wrap_function_wrapper( - module="anthropic.resources.completions", - name="Completions.create", - wrapper=_CompletionsWrapper(tracer=self._tracer), - ) + if Completions is not None: + self._original_completions_create = Completions.create + wrap_function_wrapper( + module="anthropic.resources.completions", + name="Completions.create", + wrapper=_CompletionsWrapper(tracer=self._tracer), + ) + else: + self._original_completions_create = None - self._original_async_completions_create = AsyncCompletions.create - wrap_function_wrapper( - module="anthropic.resources.completions", - name="AsyncCompletions.create", - wrapper=_AsyncCompletionsWrapper(tracer=self._tracer), - ) + if AsyncCompletions is not None: + self._original_async_completions_create = AsyncCompletions.create + wrap_function_wrapper( + module="anthropic.resources.completions", + name="AsyncCompletions.create", + wrapper=_AsyncCompletionsWrapper(tracer=self._tracer), + ) + else: + self._original_async_completions_create = None self._original_messages_create = Messages.create wrap_function_wrapper( @@ -105,17 +119,24 @@ def _instrument(self, **kwargs: Any) -> None: self._original_protect = None def _uninstrument(self, **kwargs: Any) -> None: - from anthropic.resources.completions import AsyncCompletions, Completions from anthropic.resources.messages import AsyncMessages, Messages + + try: + from anthropic.resources.completions import AsyncCompletions, Completions + except ImportError: + AsyncCompletions = Completions = None try: from fi.evals import Protect except ImportError: logger.warning("ai-evaluation is not installed, please install it to trace protect") Protect = None - if self._original_completions_create is not None: + if Completions is not None and self._original_completions_create is not None: Completions.create = self._original_completions_create # type: ignore[method-assign] - if self._original_async_completions_create is not None: + if ( + AsyncCompletions is not None + and self._original_async_completions_create is not None + ): AsyncCompletions.create = self._original_async_completions_create # type: ignore[method-assign] if self._original_messages_create is not None: diff --git a/python/tests/test_framework_anthropic.py b/python/tests/test_framework_anthropic.py index 23e09b62..e6f08c1b 100644 --- a/python/tests/test_framework_anthropic.py +++ b/python/tests/test_framework_anthropic.py @@ -26,6 +26,14 @@ ) from fi_instrumentation.instrumentation.context_attributes import using_attributes +try: + import anthropic.resources.completions # noqa: F401 + + _HAS_LEGACY_COMPLETIONS = True +except ImportError: + # anthropic>=1.0 removed the legacy Completions API entirely. + _HAS_LEGACY_COMPLETIONS = False + class TestAnthropicFramework: """Test Anthropic framework instrumentation.""" @@ -81,11 +89,34 @@ def mock_anthropic_requests(self): def test_anthropic_import(self): """Test that we can import the Anthropic instrumentor.""" assert AnthropicInstrumentor is not None - + # Test basic instantiation instrumentor = AnthropicInstrumentor() assert instrumentor is not None - + + def test_instrument_without_legacy_completions_api(self): + """anthropic>=1.0 removed anthropic.resources.completions entirely + (the legacy Text Completions API). instrument() must degrade to + wrapping only Messages instead of raising ImportError and + instrumenting nothing - regression test for that failure mode. + """ + instrumentor = AnthropicInstrumentor() + + with patch.dict(sys.modules, {"anthropic.resources.completions": None}): + instrumentor.instrument(tracer_provider=self.trace_provider) + + try: + from anthropic.resources.messages import Messages + + assert type(Messages.create).__name__ == 'BoundFunctionWrapper' + assert instrumentor._original_messages_create is not None + assert instrumentor._original_completions_create is None + assert instrumentor._original_async_completions_create is None + finally: + instrumentor.uninstrument() + + assert type(Messages.create).__name__ != 'BoundFunctionWrapper' + def test_anthropic_basic_instrumentation(self, mock_anthropic_requests): """Test basic Anthropic messages instrumentation.""" # Initialize instrumentor @@ -202,6 +233,10 @@ async def test_anthropic_async_instrumentation(self, mock_anthropic_requests): finally: instrumentor.uninstrument() + @pytest.mark.skipif( + not _HAS_LEGACY_COMPLETIONS, + reason="anthropic>=1.0 removed the legacy Completions API entirely", + ) def test_anthropic_completions_legacy(self, mock_anthropic_requests): """Test Anthropic completions (legacy) instrumentation setup.""" # Don't need to modify mock for this test - just testing setup @@ -261,6 +296,10 @@ def test_anthropic_error_handling(self, mock_anthropic_requests): finally: instrumentor.uninstrument() + @pytest.mark.skipif( + not _HAS_LEGACY_COMPLETIONS, + reason="anthropic>=1.0 removed the legacy Completions API entirely", + ) def test_instrumentor_uninstrumentation(self): """Test that uninstrumentation properly restores original behavior.""" from anthropic.resources.messages import Messages