From de0506dd7053c60bdbdab49d2db71510b5878551 Mon Sep 17 00:00:00 2001 From: matteomedioli Date: Thu, 16 Jul 2026 21:36:41 +0200 Subject: [PATCH 1/5] anthropic-kwargs-bugfix-task-001: split http_client kwarg for sync/async Anthropic clients Route httpx.Client only to the sync anthropic.Anthropic constructor and httpx.AsyncClient only to the async anthropic.AsyncAnthropic constructor, mirroring BaseOpenAILLM's existing param-splitting pattern, so passing a sync-only or async-only http_client no longer collides across both clients. --- src/neo4j_graphrag/llm/anthropic_llm.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/neo4j_graphrag/llm/anthropic_llm.py b/src/neo4j_graphrag/llm/anthropic_llm.py index 7774f0be1..d2adeb1e7 100644 --- a/src/neo4j_graphrag/llm/anthropic_llm.py +++ b/src/neo4j_graphrag/llm/anthropic_llm.py @@ -25,6 +25,12 @@ cast, ) +# 3rd party dependencies +try: + import httpx +except ImportError: + httpx = None # type: ignore[assignment] + from pydantic import BaseModel, ValidationError from neo4j_graphrag.exceptions import LLMGenerationError @@ -218,8 +224,15 @@ def __init__( **kwargs, ) self.anthropic = anthropic - self.client = anthropic.Anthropic(**kwargs) - self.async_client = anthropic.AsyncAnthropic(**kwargs) + http_client = kwargs.pop("http_client", None) + sync_params = kwargs.copy() + async_params = kwargs.copy() + if httpx is not None and isinstance(http_client, httpx.Client): + sync_params["http_client"] = http_client + elif httpx is not None and isinstance(http_client, httpx.AsyncClient): + async_params["http_client"] = http_client + self.client = anthropic.Anthropic(**sync_params) + self.async_client = anthropic.AsyncAnthropic(**async_params) def invoke( self, From ee43307db1ca9433be981c6900d6d706b2c0625d Mon Sep 17 00:00:00 2001 From: matteomedioli Date: Thu, 16 Jul 2026 21:39:05 +0200 Subject: [PATCH 2/5] anthropic-kwargs-bugfix-task-002: warn and ignore invalid http_client type Mirror BaseOpenAILLM's existing behavior: when http_client is provided but is neither an httpx.Client nor an httpx.AsyncClient instance, emit a warning and construct both clients with the default (no custom) http_client instead of raising. --- src/neo4j_graphrag/llm/anthropic_llm.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/neo4j_graphrag/llm/anthropic_llm.py b/src/neo4j_graphrag/llm/anthropic_llm.py index d2adeb1e7..556b39199 100644 --- a/src/neo4j_graphrag/llm/anthropic_llm.py +++ b/src/neo4j_graphrag/llm/anthropic_llm.py @@ -14,6 +14,7 @@ from __future__ import annotations import json +import warnings from typing import ( TYPE_CHECKING, Any, @@ -231,6 +232,11 @@ def __init__( sync_params["http_client"] = http_client elif httpx is not None and isinstance(http_client, httpx.AsyncClient): async_params["http_client"] = http_client + elif http_client is not None: + warnings.warn( + f"Invalid http_client type (got {type(http_client)}, expected httpx.Client or httpx.AsyncClient). Using default client.", + stacklevel=2, + ) self.client = anthropic.Anthropic(**sync_params) self.async_client = anthropic.AsyncAnthropic(**async_params) From fb57091449ea8ea158cb01c3fb301686e5cf3209 Mon Sep 17 00:00:00 2001 From: matteomedioli Date: Thu, 16 Jul 2026 21:45:53 +0200 Subject: [PATCH 3/5] anthropic-kwargs-bugfix-task-003: add sync/async http_client routing tests Add unit tests verifying httpx.Client reaches only the sync Anthropic client, httpx.AsyncClient reaches only the async client, and an invalid http_client type warns and falls back to defaults for both clients. Also note the stale-venv anthropic version gotcha in AGENTS.md. --- AGENTS.md | 1 + tests/unit/llm/test_anthropic_llm.py | 61 ++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index f461517dd..cd4be80a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,6 +97,7 @@ ENVIRONMENT MANAGEMENT: - `WeakKeyDictionary` works for caching per-driver since `neo4j.Driver` is hashable - Neo4j 2026 CREATE VECTOR INDEX syntax: WITH clause must come BEFORE OPTIONS, not after - E2E tests for SEARCH clause: use `docker compose -f tests/e2e/docker-compose.neo4j2026.yml up -d` +- If `tests/unit/llm/test_anthropic_llm.py` fails with `AttributeError: module 'anthropic' has no attribute 'omit'`, or other unit test files error on missing optional-dependency imports (openai, cohere, etc.) at collection time, the local `.venv` is stale relative to `pyproject.toml` extras. Run `uv sync --all-extras` to fix. --- diff --git a/tests/unit/llm/test_anthropic_llm.py b/tests/unit/llm/test_anthropic_llm.py index 113fd3637..6e9f4bae4 100644 --- a/tests/unit/llm/test_anthropic_llm.py +++ b/tests/unit/llm/test_anthropic_llm.py @@ -19,6 +19,7 @@ from unittest.mock import AsyncMock, MagicMock, Mock, patch import anthropic +import httpx import pytest from neo4j_graphrag.exceptions import LLMGenerationError from neo4j_graphrag.experimental.components.types import Neo4jGraph @@ -538,6 +539,66 @@ def test_anthropic_llm_close(mock_anthropic: Mock) -> None: mock_anthropic.AsyncAnthropic.return_value.close.assert_called_once() +# --------------------------------------------------------------------------- +# http_client sync/async routing tests +# --------------------------------------------------------------------------- + + +def test_anthropic_llm_with_httpx_client(mock_anthropic: Mock) -> None: + """Test that httpx.Client is forwarded only to the sync Anthropic client.""" + http_client = httpx.Client() + try: + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + AnthropicLLM(model_name="claude-3-opus-20240229", http_client=http_client) + + assert not any("Invalid http_client" in str(w.message) for w in caught) + _, sync_kwargs = mock_anthropic.Anthropic.call_args + assert sync_kwargs.get("http_client") is http_client + _, async_kwargs = mock_anthropic.AsyncAnthropic.call_args + assert "http_client" not in async_kwargs + finally: + http_client.close() + + +@pytest.mark.asyncio +async def test_anthropic_llm_with_httpx_async_client(mock_anthropic: Mock) -> None: + """Test that httpx.AsyncClient is forwarded only to the async Anthropic client.""" + async_http_client = httpx.AsyncClient() + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + AnthropicLLM(model_name="claude-3-opus-20240229", http_client=async_http_client) + + assert not any("Invalid http_client" in str(w.message) for w in caught) + _, sync_kwargs = mock_anthropic.Anthropic.call_args + assert "http_client" not in sync_kwargs + _, async_kwargs = mock_anthropic.AsyncAnthropic.call_args + assert async_kwargs.get("http_client") is async_http_client + + await async_http_client.aclose() + + +def test_anthropic_llm_no_http_client_no_warning(mock_anthropic: Mock) -> None: + """Test that omitting http_client does not emit a warning.""" + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + AnthropicLLM(model_name="claude-3-opus-20240229") + + assert not any("Invalid http_client" in str(w.message) for w in caught) + + +def test_anthropic_llm_with_invalid_http_client_warns(mock_anthropic: Mock) -> None: + """Test that an invalid http_client type emits a warning and falls back to + default construction for both clients.""" + with pytest.warns(UserWarning, match="Invalid http_client type"): + AnthropicLLM(model_name="claude-3-opus-20240229", http_client="not-a-client") + + _, sync_kwargs = mock_anthropic.Anthropic.call_args + _, async_kwargs = mock_anthropic.AsyncAnthropic.call_args + assert "http_client" not in sync_kwargs + assert "http_client" not in async_kwargs + + @pytest.mark.asyncio async def test_anthropic_llm_aclose(mock_anthropic: Mock) -> None: mock_anthropic.AsyncAnthropic.return_value.close = AsyncMock() From 8a402ce104a0bb60f86ddb1d3a3811ae053ecdb1 Mon Sep 17 00:00:00 2001 From: matteomedioli Date: Thu, 16 Jul 2026 21:48:02 +0200 Subject: [PATCH 4/5] anthropic-kwargs-bugfix-task-004: update changelog for http_client fix Documents the AnthropicLLM sync/async http_client kwargs collision bug fixed in tasks 001-002, matching the repo's existing changelog style. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e54fff1dd..e080385fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ - (**breaking**) `AnthropicLLM.supports_structured_output` is now `True`. As a result, `SchemaFromTextExtractor` and `LLMEntityRelationExtractor` (and `SimpleKGPipeline`, which enables structured output automatically when the LLM supports it) now use structured output by default with `AnthropicLLM`. This requires a Claude 4.5+ model (e.g. `claude-sonnet-4-5`); using `AnthropicLLM` with an older Claude model in these components will now raise an error where it previously worked. To keep the previous behavior, use a Claude 4.5+ model, or construct `LLMEntityRelationExtractor` / `SchemaFromTextExtractor` directly with `use_structured_output=False`. +### Fixed + +- Fixed a bug in `AnthropicLLM` where an `http_client` passed via kwargs (whether an `httpx.Client` or `httpx.AsyncClient`) was forwarded to both the sync `anthropic.Anthropic` and async `anthropic.AsyncAnthropic` clients, causing a type mismatch. `http_client` is now routed to the matching sync/async client only; other kwargs remain shared. An `http_client` of an unrecognized type now emits a warning and is ignored instead of raising, matching `OpenAILLM`'s existing behavior. + ## 1.18.0 ### Changed From 4300d0959c841fc198e531808ed63644bc3e433c Mon Sep 17 00:00:00 2001 From: matteomedioli Date: Fri, 17 Jul 2026 15:19:10 +0200 Subject: [PATCH 5/5] chore: drop unrelated AGENTS.md note from bugfix PR Keep the PR scoped to the http_client routing fix. --- AGENTS.md | 1 - 1 file changed, 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index cd4be80a1..f461517dd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,7 +97,6 @@ ENVIRONMENT MANAGEMENT: - `WeakKeyDictionary` works for caching per-driver since `neo4j.Driver` is hashable - Neo4j 2026 CREATE VECTOR INDEX syntax: WITH clause must come BEFORE OPTIONS, not after - E2E tests for SEARCH clause: use `docker compose -f tests/e2e/docker-compose.neo4j2026.yml up -d` -- If `tests/unit/llm/test_anthropic_llm.py` fails with `AttributeError: module 'anthropic' has no attribute 'omit'`, or other unit test files error on missing optional-dependency imports (openai, cohere, etc.) at collection time, the local `.venv` is stale relative to `pyproject.toml` extras. Run `uv sync --all-extras` to fix. ---