fix(anthropic): Add proxy (#32409)

Thank you for contributing to LangChain! Follow these steps to mark your
pull request as ready for review. **If any of these steps are not
completed, your PR will not be considered for review.**

- [x] **PR title**: Follows the format: {TYPE}({SCOPE}): {DESCRIPTION}
- [x] **PR message**: ***Delete this entire checklist*** and replace
with
fix #30146
- [x] **Add tests and docs**: If you're adding a new integration, you
must include:
- [x] **Lint and test**: Run `make format`, `make lint` and `make test`
from the root of the package(s) you've modified. **We will not consider
a PR unless these three are passing in CI.** See [contribution
guidelines](https://python.langchain.com/docs/contributing/) for more.

Additional guidelines:

- Make sure optional dependencies are imported within a function.
- Please do not add dependencies to `pyproject.toml` files (even
optional ones) unless they are **required** for unit tests.
- Most PRs should not touch more than one package.
- Changes should be backwards compatible.

---------

Co-authored-by: Mason Daugherty <mason@langchain.dev>
Co-authored-by: Mason Daugherty <github@mdrxy.com>
This commit is contained in:
Jack 2025-08-13 05:21:26 +08:00 committed by GitHub
parent be83ce74a7
commit b9dcce95be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 132 additions and 0 deletions

View File

@ -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)

View File

@ -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,

View File

@ -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]

View File

@ -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