diff --git a/libs/partners/anthropic/langchain_anthropic/_client_utils.py b/libs/partners/anthropic/langchain_anthropic/_client_utils.py index 40591107fb6..53fd3c801b1 100644 --- a/libs/partners/anthropic/langchain_anthropic/_client_utils.py +++ b/libs/partners/anthropic/langchain_anthropic/_client_utils.py @@ -50,6 +50,7 @@ def _get_default_httpx_client( *, base_url: Optional[str], timeout: Any = _NOT_GIVEN, + anthropic_proxy: Optional[str] = None, ) -> _SyncHttpxClientWrapper: kwargs: dict[str, Any] = { "base_url": base_url @@ -58,6 +59,8 @@ def _get_default_httpx_client( } if timeout is not _NOT_GIVEN: kwargs["timeout"] = timeout + if anthropic_proxy is not None: + kwargs["proxy"] = anthropic_proxy return _SyncHttpxClientWrapper(**kwargs) @@ -66,6 +69,7 @@ def _get_default_async_httpx_client( *, base_url: Optional[str], timeout: Any = _NOT_GIVEN, + anthropic_proxy: Optional[str] = None, ) -> _AsyncHttpxClientWrapper: kwargs: dict[str, Any] = { "base_url": base_url @@ -74,4 +78,6 @@ def _get_default_async_httpx_client( } if timeout is not _NOT_GIVEN: kwargs["timeout"] = timeout + if anthropic_proxy is not None: + kwargs["proxy"] = anthropic_proxy return _AsyncHttpxClientWrapper(**kwargs) diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index d6292c59851..454c52d4d6c 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -502,6 +502,9 @@ class ChatAnthropic(BaseChatModel): Key init args — client params: timeout: Optional[float] Timeout for requests. + anthropic_proxy: Optional[str] + Proxy to use for the Anthropic clients, will be used for every API call. + If not passed in will be read from env var ``ANTHROPIC_PROXY``. max_retries: int Max number of retries if a request fails. api_key: Optional[str] @@ -1245,6 +1248,14 @@ class ChatAnthropic(BaseChatModel): ) """Automatically read from env var ``ANTHROPIC_API_KEY`` if not provided.""" + anthropic_proxy: Optional[str] = Field( + default_factory=from_env("ANTHROPIC_PROXY", default=None) + ) + """Proxy to use for the Anthropic clients, will be used for every API call. + + If not provided, will attempt to read from the ``ANTHROPIC_PROXY`` environment + variable.""" + default_headers: Optional[Mapping[str, str]] = None """Headers to pass to the Anthropic clients, will be used for every API call.""" @@ -1360,6 +1371,8 @@ class ChatAnthropic(BaseChatModel): http_client_params = {"base_url": client_params["base_url"]} if "timeout" in client_params: http_client_params["timeout"] = client_params["timeout"] + if self.anthropic_proxy: + http_client_params["anthropic_proxy"] = self.anthropic_proxy http_client = _get_default_httpx_client(**http_client_params) params = { **client_params, @@ -1373,6 +1386,8 @@ class ChatAnthropic(BaseChatModel): http_client_params = {"base_url": client_params["base_url"]} if "timeout" in client_params: http_client_params["timeout"] = client_params["timeout"] + if self.anthropic_proxy: + http_client_params["anthropic_proxy"] = self.anthropic_proxy http_client = _get_default_async_httpx_client(**http_client_params) params = { **client_params, diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index e2c8d5a36c1..2a1b77e0b81 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -62,6 +62,55 @@ def test_anthropic_client_caching() -> None: assert llm1._client._client is not llm5._client._client +def test_anthropic_proxy_support() -> None: + """Test that both sync and async clients support proxy configuration.""" + proxy_url = "http://proxy.example.com:8080" + + # Test sync client with proxy + llm_sync = ChatAnthropic( + model="claude-3-5-sonnet-latest", anthropic_proxy=proxy_url + ) + sync_client = llm_sync._client + assert sync_client is not None + + # Test async client with proxy - this should not raise TypeError + async_client = llm_sync._async_client + assert async_client is not None + + # Test that clients with different proxy settings are not cached together + llm_no_proxy = ChatAnthropic(model="claude-3-5-sonnet-latest") + llm_with_proxy = ChatAnthropic( + model="claude-3-5-sonnet-latest", anthropic_proxy=proxy_url + ) + + # Different proxy settings should result in different cached clients + assert llm_no_proxy._client._client is not llm_with_proxy._client._client + + +def test_anthropic_proxy_from_environment() -> None: + """Test that proxy can be set from ANTHROPIC_PROXY environment variable.""" + proxy_url = "http://env-proxy.example.com:8080" + + # Test with environment variable set + with patch.dict(os.environ, {"ANTHROPIC_PROXY": proxy_url}): + llm = ChatAnthropic(model="claude-3-5-sonnet-latest") + assert llm.anthropic_proxy == proxy_url + + # Should be able to create clients successfully + sync_client = llm._client + async_client = llm._async_client + assert sync_client is not None + assert async_client is not None + + # Test that explicit parameter overrides environment variable + with patch.dict(os.environ, {"ANTHROPIC_PROXY": "http://env-proxy.com"}): + explicit_proxy = "http://explicit-proxy.com" + llm = ChatAnthropic( + model="claude-3-5-sonnet-latest", anthropic_proxy=explicit_proxy + ) + assert llm.anthropic_proxy == explicit_proxy + + @pytest.mark.requires("anthropic") def test_anthropic_model_name_param() -> None: llm = ChatAnthropic(model_name="foo") # type: ignore[call-arg, call-arg] diff --git a/libs/partners/anthropic/tests/unit_tests/test_client_utils.py b/libs/partners/anthropic/tests/unit_tests/test_client_utils.py new file mode 100644 index 00000000000..50b50ba147d --- /dev/null +++ b/libs/partners/anthropic/tests/unit_tests/test_client_utils.py @@ -0,0 +1,62 @@ +"""Test client utility functions.""" + +from __future__ import annotations + +from langchain_anthropic._client_utils import ( + _get_default_async_httpx_client, + _get_default_httpx_client, +) + + +def test_sync_client_without_proxy() -> None: + """Test sync client creation without proxy.""" + client = _get_default_httpx_client(base_url="https://api.anthropic.com") + + # Should not have proxy configured + assert not hasattr(client, "proxies") or client.proxies is None + + +def test_sync_client_with_proxy() -> None: + """Test sync client creation with proxy.""" + proxy_url = "http://proxy.example.com:8080" + client = _get_default_httpx_client( + base_url="https://api.anthropic.com", anthropic_proxy=proxy_url + ) + + # Check internal _transport since httpx stores proxy configuration in the transport + # layer + transport = getattr(client, "_transport", None) + assert transport is not None + + +def test_async_client_without_proxy() -> None: + """Test async client creation without proxy.""" + client = _get_default_async_httpx_client(base_url="https://api.anthropic.com") + + assert not hasattr(client, "proxies") or client.proxies is None + + +def test_async_client_with_proxy() -> None: + """Test async client creation with proxy.""" + proxy_url = "http://proxy.example.com:8080" + client = _get_default_async_httpx_client( + base_url="https://api.anthropic.com", anthropic_proxy=proxy_url + ) + + transport = getattr(client, "_transport", None) + assert transport is not None + + +def test_client_proxy_none_value() -> None: + """Test that explicitly passing None for proxy works correctly.""" + sync_client = _get_default_httpx_client( + base_url="https://api.anthropic.com", anthropic_proxy=None + ) + + async_client = _get_default_async_httpx_client( + base_url="https://api.anthropic.com", anthropic_proxy=None + ) + + # Both should be created successfully with None proxy + assert sync_client is not None + assert async_client is not None