From 2772ed9fa3aaaae9c884fb80f97d0d3a431ce56d Mon Sep 17 00:00:00 2001 From: Bala Sivagnanam Date: Wed, 2 Sep 2026 00:48:30 +0800 Subject: [PATCH] fix(anthropic): don't fail instrumentation when anthropic>=1 removed Completions AnthropicInstrumentor()._instrument()/_uninstrument() unconditionally imported anthropic.resources.completions to wrap the legacy Completions API. anthropic-sdk-python 1.0 removed that module entirely (see its MIGRATION.md, "Removed: the legacy Text Completions API"), so the import raised ImportError before Messages ever got wrapped - meaning instrument() silently traced nothing at all on anthropic>=1, not just missing completions support. Make the Completions import optional: skip wrapping it when the module isn't there, continue wrapping Messages (which is unaffected) either way. Verified against both anthropic 0.125.0 and 1.2.0 - no regression on the old SDK, working Messages instrumentation on the new one. Adds a regression test that simulates the anthropic>=1 environment via sys.modules patching (fails on the old code with the exact ImportError seen in production, passes with the fix), and skips the two existing tests that specifically exercise the legacy Completions API when it isn't importable, so the suite stays green under either SDK version instead of failing on a feature that no longer exists. --- python/frameworks/anthropic/CHANGELOG.md | 9 ++++ .../anthropic/traceai_anthropic/__init__.py | 53 +++++++++++++------ python/tests/test_framework_anthropic.py | 43 ++++++++++++++- 3 files changed, 87 insertions(+), 18 deletions(-) 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