Skip to content

Commit 00e0df2

Browse files
Merge pull request #970 from anthropics/release-please--branches--main--changes--next
release: 0.54.0
2 parents aedb4cb + f768bc5 commit 00e0df2

13 files changed

Lines changed: 546 additions & 91 deletions

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.53.0"
2+
".": "0.54.0"
33
}

CHANGELOG.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,29 @@
11
# Changelog
22

3+
## 0.54.0 (2025-06-10)
4+
5+
Full Changelog: [v0.53.0...v0.54.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.53.0...v0.54.0)
6+
7+
### Features
8+
9+
* **client:** add support for fine-grained-tool-streaming-2025-05-14 ([07ec081](https://github.com/anthropics/anthropic-sdk-python/commit/07ec08119dbc328934fea5ec6eacd00c8dbda089))
10+
11+
12+
### Bug Fixes
13+
14+
* **httpx:** resolve conflict between default transport and proxy settings ([#969](https://github.com/anthropics/anthropic-sdk-python/issues/969)) ([a6efded](https://github.com/anthropics/anthropic-sdk-python/commit/a6efdedcfef881ae3466bb77d92d0338c8338e20))
15+
* **tests:** update test ([99c2433](https://github.com/anthropics/anthropic-sdk-python/commit/99c243363e94f5f3f627cb8b80e3f238503c89f5))
16+
17+
18+
### Chores
19+
20+
* **internal:** version bump ([45029f4](https://github.com/anthropics/anthropic-sdk-python/commit/45029f41c96f62f26ead99a5989c9ad974fc21b9))
21+
22+
23+
### Documentation
24+
25+
* **contributing:** fix uv script for bootstrapping ([d2bde52](https://github.com/anthropics/anthropic-sdk-python/commit/d2bde52286ee8fa65995e73c579a8962087c1da4))
26+
327
## 0.53.0 (2025-06-09)
428

529
Full Changelog: [v0.52.2...v0.53.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.52.2...v0.53.0)

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ $ ./scripts/bootstrap
1111
Or [install uv manually](https://docs.astral.sh/uv/getting-started/installation/) and run:
1212

1313
```sh
14-
$ uv sync
14+
$ uv sync --all-extras
1515
```
1616

1717
You can then run scripts using `uv run python script.py` or by manually activating the virtual environment:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "anthropic"
3-
version = "0.53.0"
3+
version = "0.54.0"
44
description = "The official Python library for the anthropic API"
55
dynamic = ["readme"]
66
license = "MIT"

src/anthropic/_base_client.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
import httpx
3939
import distro
4040
import pydantic
41-
from httpx import URL
41+
from httpx import URL, Proxy, HTTPTransport, AsyncHTTPTransport
4242
from pydantic import PrivateAttr
4343

4444
from . import _exceptions
@@ -86,6 +86,7 @@
8686
APIConnectionError,
8787
APIResponseValidationError,
8888
)
89+
from ._utils._httpx import get_environment_proxies
8990
from ._legacy_response import LegacyAPIResponse
9091

9192
log: logging.Logger = logging.getLogger(__name__)
@@ -826,11 +827,24 @@ def __init__(self, **kwargs: Any) -> None:
826827
if TCP_KEEPIDLE is not None:
827828
socket_options.append((socket.IPPROTO_TCP, TCP_KEEPIDLE, 60))
828829

829-
kwargs["transport"] = httpx.HTTPTransport(
830-
# note: limits is always set above
831-
limits=kwargs["limits"],
832-
socket_options=socket_options,
833-
)
830+
proxy_map = {key: None if url is None else Proxy(url=url) for key, url in get_environment_proxies().items()}
831+
832+
transport_kwargs = {
833+
arg: kwargs[arg] for arg in ("verify", "cert", "trust_env", "http1", "http2", "limits") if arg in kwargs
834+
}
835+
836+
proxy_mounts = {
837+
key: None if proxy is None else HTTPTransport(proxy=proxy, **transport_kwargs)
838+
for key, proxy in proxy_map.items()
839+
}
840+
default_transport = HTTPTransport(**transport_kwargs)
841+
842+
# Prioritize the mounts set by the user over the environment variables.
843+
proxy_mounts.update(kwargs.get("mounts", {}))
844+
kwargs["mounts"] = proxy_mounts
845+
846+
# Sets the default transport so that HTTPX won't automatically configure proxies.
847+
kwargs["transport"] = kwargs.get("transport", default_transport)
834848

835849
super().__init__(**kwargs)
836850

@@ -1367,11 +1381,24 @@ def __init__(self, **kwargs: Any) -> None:
13671381
if TCP_KEEPIDLE is not None:
13681382
socket_options.append((socket.IPPROTO_TCP, TCP_KEEPIDLE, 60))
13691383

1370-
kwargs["transport"] = httpx.AsyncHTTPTransport(
1371-
# note: limits is always set above
1372-
limits=kwargs["limits"],
1373-
socket_options=socket_options,
1374-
)
1384+
proxy_map = {key: None if url is None else Proxy(url=url) for key, url in get_environment_proxies().items()}
1385+
1386+
transport_kwargs = {
1387+
arg: kwargs[arg] for arg in ("verify", "cert", "trust_env", "http1", "http2", "limits") if arg in kwargs
1388+
}
1389+
1390+
proxy_mounts = {
1391+
key: None if proxy is None else AsyncHTTPTransport(proxy=proxy, **transport_kwargs)
1392+
for key, proxy in proxy_map.items()
1393+
}
1394+
default_transport = AsyncHTTPTransport(**transport_kwargs)
1395+
1396+
# Prioritize the mounts set by the user over the environment variables.
1397+
proxy_mounts.update(kwargs.get("mounts", {}))
1398+
kwargs["mounts"] = proxy_mounts
1399+
1400+
# Sets the default transport so that HTTPX won't automatically configure proxies.
1401+
kwargs["transport"] = kwargs.get("transport", default_transport)
13751402

13761403
super().__init__(**kwargs)
13771404

src/anthropic/_utils/_httpx.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
This file includes code adapted from HTTPX's utility module
3+
(https://github.com/encode/httpx/blob/336204f0121a9aefdebac5cacd81f912bafe8057/httpx/_utils.py).
4+
We implement custom proxy handling to support configurations like `socket_options`,
5+
which are not currently configurable through the HTTPX client.
6+
For more context, see: https://github.com/encode/httpx/discussions/3514
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import ipaddress
12+
from typing import Mapping
13+
from urllib.request import getproxies
14+
15+
16+
def is_ipv4_hostname(hostname: str) -> bool:
17+
try:
18+
ipaddress.IPv4Address(hostname.split("/")[0])
19+
except Exception:
20+
return False
21+
return True
22+
23+
24+
def is_ipv6_hostname(hostname: str) -> bool:
25+
try:
26+
ipaddress.IPv6Address(hostname.split("/")[0])
27+
except Exception:
28+
return False
29+
return True
30+
31+
32+
def get_environment_proxies() -> Mapping[str, str | None]:
33+
"""
34+
Gets the proxy mappings based on environment variables.
35+
We use our own logic to parse these variables, as HTTPX
36+
doesn’t allow full configuration of the underlying
37+
transport when proxies are set via environment variables.
38+
"""
39+
40+
proxy_info = getproxies()
41+
mounts: dict[str, str | None] = {}
42+
43+
for scheme in ("http", "https", "all"):
44+
if proxy_info.get(scheme):
45+
hostname = proxy_info[scheme]
46+
mounts[f"{scheme}://"] = hostname if "://" in hostname else f"http://{hostname}"
47+
48+
no_proxy_hosts = [host.strip() for host in proxy_info.get("no", "").split(",")]
49+
for hostname in no_proxy_hosts:
50+
if hostname == "*":
51+
return {}
52+
elif hostname:
53+
if "://" in hostname:
54+
mounts[hostname] = None
55+
elif is_ipv4_hostname(hostname):
56+
mounts[f"all://{hostname}"] = None
57+
elif is_ipv6_hostname(hostname):
58+
mounts[f"all://[{hostname}]"] = None
59+
elif hostname.lower() == "localhost":
60+
mounts[f"all://{hostname}"] = None
61+
else:
62+
mounts[f"all://*{hostname}"] = None
63+
64+
return mounts

src/anthropic/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "anthropic"
4-
__version__ = "0.53.0" # x-release-please-version
4+
__version__ = "0.54.0" # x-release-please-version

src/anthropic/lib/streaming/_beta_messages.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def __stream__(self) -> Iterator[BetaMessageStreamEvent]:
120120
self.__final_message_snapshot = accumulate_event(
121121
event=sse_event,
122122
current_snapshot=self.__final_message_snapshot,
123+
request_headers=self.response.request.headers,
123124
)
124125

125126
events_to_fire = build_events(event=sse_event, message_snapshot=self.current_message_snapshot)
@@ -257,6 +258,7 @@ async def __stream__(self) -> AsyncIterator[BetaMessageStreamEvent]:
257258
self.__final_message_snapshot = accumulate_event(
258259
event=sse_event,
259260
current_snapshot=self.__final_message_snapshot,
261+
request_headers=self.response.request.headers,
260262
)
261263

262264
events_to_fire = build_events(event=sse_event, message_snapshot=self.current_message_snapshot)
@@ -403,6 +405,7 @@ def accumulate_event(
403405
*,
404406
event: BetaRawMessageStreamEvent,
405407
current_snapshot: BetaMessage | None,
408+
request_headers: httpx.Headers,
406409
) -> BetaMessage:
407410
if not isinstance(cast(Any, event), BaseModel):
408411
event = cast( # pyright: ignore[reportUnnecessaryCast]
@@ -445,7 +448,17 @@ def accumulate_event(
445448
json_buf += bytes(event.delta.partial_json, "utf-8")
446449

447450
if json_buf:
448-
content.input = from_json(json_buf, partial_mode=True)
451+
try:
452+
anthropic_beta = request_headers.get("anthropic-beta", "") if request_headers else ""
453+
454+
if "fine-grained-tool-streaming-2025-05-14" in anthropic_beta:
455+
content.input = from_json(json_buf, partial_mode="trailing-strings")
456+
else:
457+
content.input = from_json(json_buf, partial_mode=True)
458+
except ValueError as e:
459+
raise ValueError(
460+
f"Unable to parse tool parameter JSON from model. Please retry your request or adjust your prompt. Error: {e}. JSON: {json_buf.decode('utf-8')}"
461+
) from e
449462

450463
setattr(content, JSON_BUF_PROPERTY, json_buf)
451464
elif event.delta.type == "citations_delta":
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
event: message_start
2+
data: {"type":"message_start","message":{"id":"msg_01UdjYBBipA9omjYhicnevgq","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":450,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}} }
3+
4+
event: content_block_start
5+
data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":""} }
6+
7+
event: ping
8+
data: {"type": "ping"}
9+
10+
event: content_block_delta
11+
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"I"} }
12+
13+
event: content_block_delta
14+
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"'ll create a comprehensive tax guide for"} }
15+
16+
event: content_block_delta
17+
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" someone with multiple W2s an"} }
18+
19+
event: content_block_delta
20+
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"d save it in a file called taxes.txt. Let"} }
21+
22+
event: content_block_delta
23+
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" me do that for you now."} }
24+
25+
event: content_block_stop
26+
data: {"type":"content_block_stop","index":0 }
27+
28+
event: content_block_start
29+
data: {"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_01EKqbqmZrGRXy18eN7m9kvY","name":"make_file","input":{}} }
30+
31+
event: content_block_delta
32+
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":""} }
33+
34+
event: content_block_delta
35+
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"filename\": \"taxes.txt"} }
36+
37+
event: content_block_delta
38+
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\", \"lines_of_text\": [\n\"# COMPREHENSIVE TAX GUIDE FOR INDIVIDUALS WITH MULTIPLE W-2s\",\n\"\",\n\"## INTRODUCTION\",\n\"\","} }
39+
40+
event: content_block_delta
41+
data: {"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"\n\"Filing taxes"} }
42+
43+
event: message_delta
44+
data: {"type":"message_delta","delta":{"stop_reason":"max_tokens","stop_sequence":null},"usage":{"output_tokens":124} }
45+
46+
event: message_stop
47+
data: {"type":"message_stop" }

0 commit comments

Comments
 (0)