From b27e3439699174dbc41e34e2d6ef5cb1e2930c18 Mon Sep 17 00:00:00 2001 From: Qing Wang Date: Tue, 16 Jun 2026 17:30:17 -0700 Subject: [PATCH 1/5] fix(bedrock): preserve stream event type (#1682) --- src/anthropic/lib/bedrock/_stream_decoder.py | 38 +++++++++++++++----- tests/lib/test_bedrock.py | 30 ++++++++++++++++ 2 files changed, 59 insertions(+), 9 deletions(-) diff --git a/src/anthropic/lib/bedrock/_stream_decoder.py b/src/anthropic/lib/bedrock/_stream_decoder.py index 02e81a3ca..66dd658cc 100644 --- a/src/anthropic/lib/bedrock/_stream_decoder.py +++ b/src/anthropic/lib/bedrock/_stream_decoder.py @@ -1,6 +1,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterator, AsyncIterator +import json +from typing import TYPE_CHECKING, Any, Dict, Iterator, AsyncIterator, cast from ..._utils import lru_cache from ..._streaming import ServerSentEvent @@ -35,9 +36,9 @@ def iter_bytes(self, iterator: Iterator[bytes]) -> Iterator[ServerSentEvent]: for chunk in iterator: event_stream_buffer.add_data(chunk) for event in event_stream_buffer: - message = self._parse_message_from_event(event) - if message: - yield ServerSentEvent(data=message, event="completion") + sse = self._parse_message_from_event(event) + if sse: + yield sse async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[ServerSentEvent]: """Given an async iterator that yields lines, iterate over it & yield every event encountered""" @@ -47,11 +48,11 @@ async def aiter_bytes(self, iterator: AsyncIterator[bytes]) -> AsyncIterator[Ser async for chunk in iterator: event_stream_buffer.add_data(chunk) for event in event_stream_buffer: - message = self._parse_message_from_event(event) - if message: - yield ServerSentEvent(data=message, event="completion") + sse = self._parse_message_from_event(event) + if sse: + yield sse - def _parse_message_from_event(self, event: EventStreamMessage) -> str | None: + def _parse_message_from_event(self, event: EventStreamMessage) -> ServerSentEvent | None: response_dict = event.to_response_dict() parsed_response = self.parser.parse(response_dict, get_response_stream_shape()) if response_dict["status_code"] != 200: @@ -61,4 +62,23 @@ def _parse_message_from_event(self, event: EventStreamMessage) -> str | None: if not chunk: return None - return chunk.get("bytes").decode() # type: ignore[no-any-return] + return _chunk_bytes_to_sse(chunk.get("bytes")) + + +def _chunk_bytes_to_sse(raw: bytes) -> ServerSentEvent | None: + decoded = raw.decode() + data: Any + try: + data = json.loads(decoded) + except Exception: + data = None + + if not isinstance(data, dict): + return ServerSentEvent(data=decoded, event="completion") + + payload = cast("Dict[str, Any]", data) + event_type = payload.get("type") + if not isinstance(event_type, str): + event_type = "completion" + + return ServerSentEvent(data=decoded, event=event_type) diff --git a/tests/lib/test_bedrock.py b/tests/lib/test_bedrock.py index 6e45c27f7..f8aefef3a 100644 --- a/tests/lib/test_bedrock.py +++ b/tests/lib/test_bedrock.py @@ -9,6 +9,7 @@ from respx import MockRouter from anthropic import AnthropicBedrock, AsyncAnthropicBedrock +from anthropic.lib.bedrock._stream_decoder import _chunk_bytes_to_sse sync_client = AnthropicBedrock( aws_region="us-east-1", @@ -275,3 +276,32 @@ def test_region_infer_from_specified_profile( client = AnthropicBedrock() assert client.aws_region == next(profile for profile in profiles if profile["name"] == aws_profile)["region"] + + +def test_chunk_bytes_to_sse_typed_event() -> None: + raw = ( + b'{"type":"message_start","message":{"id":"msg_123","type":"message","role":"assistant",' + b'"content":[],"model":"claude-x","stop_reason":null,"stop_sequence":null,' + b'"usage":{"input_tokens":1,"output_tokens":1}}}' + ) + sse = _chunk_bytes_to_sse(raw) + assert sse is not None + assert sse.event == "message_start" + assert sse.data == raw.decode() + + +def test_chunk_bytes_to_sse_legacy_completion() -> None: + raw = b'{"completion":" Hello","stop_reason":null,"model":"claude-2"}' + sse = _chunk_bytes_to_sse(raw) + assert sse is not None + assert sse.event == "completion" + + +def test_chunk_bytes_to_sse_legacy_completion_with_metrics() -> None: + raw = ( + b'{"completion":" Hello","stop_reason":"stop_sequence","model":"claude-2",' + b'"amazon-bedrock-invocationMetrics":{"inputTokenCount":1,"outputTokenCount":1}}' + ) + sse = _chunk_bytes_to_sse(raw) + assert sse is not None + assert sse.event == "completion" From 922558e2ce52e18863dab27bcc04067068827364 Mon Sep 17 00:00:00 2001 From: Robert Craigie Date: Wed, 17 Jun 2026 13:38:27 -0700 Subject: [PATCH 2/5] fix: append x-stainless-helper across header merges instead of clobbering (#105) --- src/anthropic/_base_client.py | 69 +++++++- src/anthropic/_client.py | 5 +- src/anthropic/lib/bedrock/_client.py | 5 +- src/anthropic/lib/bedrock/_mantle.py | 5 +- src/anthropic/lib/foundry.py | 10 +- src/anthropic/lib/tools/_beta_runner.py | 3 +- .../lib/tools/_beta_session_runner.py | 5 +- src/anthropic/lib/vertex/_client.py | 5 +- src/anthropic/resources/beta/files.py | 10 +- .../resources/beta/messages/messages.py | 105 +++++------ src/anthropic/resources/messages/messages.py | 21 ++- tests/lib/test_azure.py | 22 +++ tests/lib/test_bedrock.py | 14 ++ tests/lib/test_bedrock_mantle.py | 20 +++ tests/lib/test_vertex.py | 24 +++ tests/test_client.py | 164 ++++++++++++++++++ 16 files changed, 409 insertions(+), 78 deletions(-) diff --git a/src/anthropic/_base_client.py b/src/anthropic/_base_client.py index f2eb5de21..98d154c09 100644 --- a/src/anthropic/_base_client.py +++ b/src/anthropic/_base_client.py @@ -456,7 +456,7 @@ def _make_status_error( def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0) -> httpx.Headers: custom_headers = options.headers or {} - headers_dict = _merge_mappings( + merged_headers = merge_headers( { "x-stainless-timeout": str(options.timeout.read) if isinstance(options.timeout, Timeout) @@ -465,6 +465,7 @@ def _build_headers(self, options: FinalRequestOptions, *, retries_taken: int = 0 }, custom_headers, ) + headers_dict = _strip_omit(merged_headers) self._validate_headers(headers_dict, custom_headers) # headers are case-insensitive while dictionaries are not. @@ -2538,6 +2539,11 @@ def get_architecture() -> Arch: return "unknown" +def _strip_omit(mapping: Mapping[_T_co, Union[_T, Omit]]) -> Dict[_T_co, _T]: + """Drop entries whose value is an `Omit` removal marker.""" + return {key: value for key, value in mapping.items() if not isinstance(value, Omit)} + + def _merge_mappings( obj1: Mapping[_T_co, Union[_T, Omit]], obj2: Mapping[_T_co, Union[_T, Omit]], @@ -2546,8 +2552,65 @@ def _merge_mappings( In cases with duplicate keys the second mapping takes precedence. """ - merged = {**obj1, **obj2} - return {key: value for key, value in merged.items() if not isinstance(value, Omit)} + return _strip_omit({**obj1, **obj2}) + + +# Append-on-merge header support (hand-written, upstream to Stainless). +# +# Headers whose values accumulate across a merge instead of the later mapping's +# value replacing the earlier one. When multiple mappings set one of these, the +# values are concatenated into a single comma-separated value (order-preserving, +# deduplicated) rather than clobbered. +_APPEND_HEADERS = frozenset({"x-stainless-helper"}) + + +def _append_header_value(existing: str, addition: str) -> str: + """Append `addition` to a comma-separated header value, skipping tokens that + are already present so the same helper isn't recorded twice. + + Values are joined with `", "`, the same format `lib._stainless_helpers` uses + when it builds the `x-stainless-helper` header. + """ + tokens = [token for token in (raw.strip() for raw in existing.split(",")) if token] + for token in (raw.strip() for raw in addition.split(",")): + if token and token not in tokens: + tokens.append(token) + return ", ".join(tokens) + + +def merge_headers(*mappings: Mapping[str, Union[str, Omit]]) -> Dict[str, str]: + """Merge header mappings, with later mappings taking precedence on a key + clash, exactly like `_merge_mappings`. + + The exception is the headers in `_APPEND_HEADERS`: those keys are matched + case-insensitively (and stored under their lowercase form) and their string + values accumulate into a single comma-separated, deduplicated value instead + of the later one overriding the earlier one. + + `Omit` values are preserved (they mark a header for removal and are only + dropped at request-build time, e.g. with `_strip_omit`); the + `Dict[str, str]` return type is the same fudge the rest of the header + plumbing already uses for `Omit`-bearing header mappings. + """ + merged: Dict[str, Union[str, Omit]] = {} + for mapping in mappings: + for key, value in mapping.items(): + lower = key.lower() + if lower not in _APPEND_HEADERS: + merged[key] = value + continue + + # Append headers are stored under their lowercase key so every + # case-variant lands on (and appends to) the same entry. + existing = merged.get(lower) + if isinstance(existing, str) and isinstance(value, str): + merged[lower] = _append_header_value(existing, value) + else: + # `Omit` (removal) can't take part in an append; the later + # value overrides, as it does for any other header. + merged[lower] = value + + return cast("Dict[str, str]", merged) def _middleware_entry_mode(request: APIRequest) -> Literal["raw", "stream", "true"] | None: diff --git a/src/anthropic/_client.py b/src/anthropic/_client.py index 96e45eee4..76b840608 100644 --- a/src/anthropic/_client.py +++ b/src/anthropic/_client.py @@ -34,6 +34,7 @@ DEFAULT_MAX_RETRIES, SyncAPIClient, AsyncAPIClient, + merge_headers, ) # --- credentials support (hand-written, upstream to Stainless) --- @@ -448,7 +449,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers @@ -861,7 +862,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers diff --git a/src/anthropic/lib/bedrock/_client.py b/src/anthropic/lib/bedrock/_client.py index 39c0f2574..e91ca176c 100644 --- a/src/anthropic/lib/bedrock/_client.py +++ b/src/anthropic/lib/bedrock/_client.py @@ -23,6 +23,7 @@ SyncAPIClient, AsyncAPIClient, FinalRequestOptions, + merge_headers, ) from ._stream_decoder import AWSEventStreamDecoder from ...resources.messages import Messages, AsyncMessages @@ -267,7 +268,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers @@ -447,7 +448,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers diff --git a/src/anthropic/lib/bedrock/_mantle.py b/src/anthropic/lib/bedrock/_mantle.py index 3ace182c0..e4dad6c1d 100644 --- a/src/anthropic/lib/bedrock/_mantle.py +++ b/src/anthropic/lib/bedrock/_mantle.py @@ -22,6 +22,7 @@ BaseClient, SyncAPIClient, AsyncAPIClient, + merge_headers, ) from ..aws._credentials import ( resolve_region, @@ -311,7 +312,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers @@ -507,7 +508,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers diff --git a/src/anthropic/lib/foundry.py b/src/anthropic/lib/foundry.py index 0a40083bc..3c7fe4d56 100644 --- a/src/anthropic/lib/foundry.py +++ b/src/anthropic/lib/foundry.py @@ -16,7 +16,11 @@ from .._streaming import Stream, AsyncStream from .._exceptions import AnthropicError from .._middleware import MiddlewareInput -from .._base_client import DEFAULT_MAX_RETRIES, BaseClient +from .._base_client import ( + DEFAULT_MAX_RETRIES, + BaseClient, + merge_headers, +) from ..resources.beta import Beta, AsyncBeta from ..resources.messages import Messages, AsyncMessages from ..resources.beta.messages import Messages as BetaMessages, AsyncMessages as AsyncBetaMessages @@ -229,7 +233,7 @@ def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodO headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers @@ -454,7 +458,7 @@ def copy( # type: ignore[override] # pyright: ignore[reportIncompatibleMethodO headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers diff --git a/src/anthropic/lib/tools/_beta_runner.py b/src/anthropic/lib/tools/_beta_runner.py index 95c365a5e..c7a6378a0 100644 --- a/src/anthropic/lib/tools/_beta_runner.py +++ b/src/anthropic/lib/tools/_beta_runner.py @@ -24,6 +24,7 @@ from ..._types import Body, Query, Headers, NotGiven from ..._utils import consume_sync_iterator, consume_async_iterator from ...types.beta import BetaMessage, BetaMessageParam +from ..._base_client import merge_headers from ._tool_dispatch import tool_registry, tool_error_content from ._beta_functions import ( ToolError, @@ -83,7 +84,7 @@ def __init__( messages=params.get("messages"), ) if helper_header: - merged_headers = {**helper_header, **(options.get("extra_headers") or {})} + merged_headers = merge_headers(helper_header, options.get("extra_headers") or {}) options = {**options, "extra_headers": merged_headers} self._options = options self._messages_modified = False diff --git a/src/anthropic/lib/tools/_beta_session_runner.py b/src/anthropic/lib/tools/_beta_session_runner.py index 1b2a1e7ad..790ee1588 100644 --- a/src/anthropic/lib/tools/_beta_session_runner.py +++ b/src/anthropic/lib/tools/_beta_session_runner.py @@ -354,8 +354,9 @@ def __init__( # the parent client's ``default_headers`` propagate via its # ``client.copy()``; per the SDK's standard ``extra_headers`` # precedence a caller header overrides the scoped client's same-named - # default for that request, so this is for caller passthrough (trace - # ids etc.), not auth. + # default for that request (``x-stainless-helper`` is the exception — + # a caller value appends to the runner's tag rather than replacing it), + # so this is for caller passthrough (trace ids etc.), not auth. self.extra_headers = extra_headers async def __aiter__(self) -> AsyncIterator[DispatchedToolCall]: diff --git a/src/anthropic/lib/vertex/_client.py b/src/anthropic/lib/vertex/_client.py index eff9e1407..3c713996d 100644 --- a/src/anthropic/lib/vertex/_client.py +++ b/src/anthropic/lib/vertex/_client.py @@ -22,6 +22,7 @@ BaseClient, SyncAPIClient, AsyncAPIClient, + merge_headers, ) from ...resources.messages import Messages, AsyncMessages @@ -209,7 +210,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers @@ -373,7 +374,7 @@ def copy( headers = self._custom_headers if default_headers is not None: - headers = {**headers, **default_headers} + headers = merge_headers(headers, default_headers) elif set_default_headers is not None: headers = set_default_headers diff --git a/src/anthropic/resources/beta/files.py b/src/anthropic/resources/beta/files.py index 0bc038012..3cced0358 100644 --- a/src/anthropic/resources/beta/files.py +++ b/src/anthropic/resources/beta/files.py @@ -27,7 +27,11 @@ ) from ...pagination import SyncPage, AsyncPage from ...types.beta import file_list_params, file_upload_params -from ..._base_client import AsyncPaginator, make_request_options +from ..._base_client import ( + AsyncPaginator, + merge_headers, + make_request_options, +) from ...lib._stainless_helpers import stainless_helper_header_from_file as _stainless_helper_header_from_file from ...types.beta.deleted_file import DeletedFile from ...types.beta.file_metadata import FileMetadata @@ -318,7 +322,7 @@ def upload( **(extra_headers or {}), } extra_headers = {"anthropic-beta": "files-api-2025-04-14", **(extra_headers or {})} - extra_headers = {**_stainless_helper_header_from_file(file), **extra_headers} + extra_headers = merge_headers(_stainless_helper_header_from_file(file), extra_headers) body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be @@ -618,7 +622,7 @@ async def upload( **(extra_headers or {}), } extra_headers = {"anthropic-beta": "files-api-2025-04-14", **(extra_headers or {})} - extra_headers = {**_stainless_helper_header_from_file(file), **extra_headers} + extra_headers = merge_headers(_stainless_helper_header_from_file(file), extra_headers) body = deepcopy_with_paths({"file": file}, [["file"]]) files = extract_files(cast(Mapping[str, object], body), paths=[["file"]]) # It should be noted that the actual Content-Type header that will be diff --git a/src/anthropic/resources/beta/messages/messages.py b/src/anthropic/resources/beta/messages/messages.py index 47569eb75..7dd149f9d 100644 --- a/src/anthropic/resources/beta/messages/messages.py +++ b/src/anthropic/resources/beta/messages/messages.py @@ -42,7 +42,10 @@ message_count_tokens_params, ) from ...._exceptions import AnthropicError -from ...._base_client import make_request_options +from ...._base_client import ( + merge_headers, + make_request_options, +) from ...._utils._utils import is_dict from ....lib.streaming import BetaMessageStreamManager, BetaAsyncMessageStreamManager from ...messages.messages import DEPRECATED_MODELS, MODELS_TO_WARN_WITH_THINKING_ENABLED @@ -1194,11 +1197,11 @@ def create( merged_output_config = _merge_output_configs(output_config, output_format) - extra_headers = { - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else not_given}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else not_given}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) return self._post( "/v1/messages?beta=true", body=maybe_transform( @@ -1307,12 +1310,12 @@ def parse( # Ensure structured outputs beta is included for parse method betas.append("structured-outputs-2025-12-15") - extra_headers = { - "X-Stainless-Helper": "beta.messages.parse", - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + {"X-Stainless-Helper": "beta.messages.parse"}, + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) if is_given(output_format) and output_format is not None: adapted_type: TypeAdapter[ResponseFormatT] = TypeAdapter(output_format) @@ -1574,12 +1577,12 @@ def tool_runner( stacklevel=3, ) - extra_headers = { - "X-Stainless-Helper": "BetaToolRunner", - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + {"X-Stainless-Helper": "BetaToolRunner"}, + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) runnable_tools: list[BetaRunnableTool] = [] raw_tools: list[BetaToolUnionParam] = [] @@ -1703,13 +1706,15 @@ def stream( ) """Create a Message stream""" - extra_headers = { - "X-Stainless-Helper-Method": "stream", - "X-Stainless-Stream-Helper": "beta.messages", - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + { + "X-Stainless-Helper-Method": "stream", + "X-Stainless-Stream-Helper": "beta.messages", + }, + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) transformed_output_format: BetaJSONOutputFormatParam | Omit = omit @@ -3161,11 +3166,11 @@ async def create( merged_output_config = _merge_output_configs(output_config, output_format) - extra_headers = { - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else not_given}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else not_given}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) return await self._post( "/v1/messages?beta=true", body=await async_maybe_transform( @@ -3273,12 +3278,12 @@ async def parse( # Ensure structured outputs beta is included for parse method betas.append("structured-outputs-2025-12-15") - extra_headers = { - "X-Stainless-Helper": "beta.messages.parse", - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + {"X-Stainless-Helper": "beta.messages.parse"}, + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) if is_given(output_format) and output_format is not None: adapted_type: TypeAdapter[ResponseFormatT] = TypeAdapter(output_format) @@ -3533,12 +3538,12 @@ def tool_runner( stacklevel=3, ) - extra_headers = { - "X-Stainless-Helper": "BetaToolRunner", - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + {"X-Stainless-Helper": "BetaToolRunner"}, + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) runnable_tools: list[BetaAsyncRunnableTool] = [] raw_tools: list[BetaToolUnionParam] = [] @@ -3661,13 +3666,15 @@ def stream( stacklevel=3, ) - extra_headers = { - "X-Stainless-Helper-Method": "stream", - "X-Stainless-Stream-Helper": "beta.messages", - **strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), - **_stainless_helper_header(tools, messages), - **(extra_headers or {}), - } + extra_headers = merge_headers( + { + "X-Stainless-Helper-Method": "stream", + "X-Stainless-Stream-Helper": "beta.messages", + }, + strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), + _stainless_helper_header(tools, messages), + extra_headers or {}, + ) transformed_output_format: BetaJSONOutputFormatParam | Omit = omit diff --git a/src/anthropic/resources/messages/messages.py b/src/anthropic/resources/messages/messages.py index 8b3c64ce4..30bb070ec 100644 --- a/src/anthropic/resources/messages/messages.py +++ b/src/anthropic/resources/messages/messages.py @@ -32,7 +32,10 @@ from ..._response import to_streamed_response_wrapper, async_to_streamed_response_wrapper from ..._constants import DEFAULT_TIMEOUT, MODEL_NONSTREAMING_TOKENS from ..._streaming import Stream, AsyncStream -from ..._base_client import make_request_options +from ..._base_client import ( + merge_headers, + make_request_options, +) from ..._utils._utils import is_dict from ...lib.streaming import MessageStreamManager, AsyncMessageStreamManager from ...types.message import Message @@ -1201,10 +1204,10 @@ def parse( stacklevel=3, ) - extra_headers = { - "X-Stainless-Helper": "messages.parse", - **(extra_headers or {}), - } + extra_headers = merge_headers( + {"X-Stainless-Helper": "messages.parse"}, + extra_headers or {}, + ) transformed_output_format: Optional[JSONOutputFormatParam] | NotGiven = NOT_GIVEN @@ -2654,10 +2657,10 @@ async def parse( stacklevel=3, ) - extra_headers = { - "X-Stainless-Helper": "messages.parse", - **(extra_headers or {}), - } + extra_headers = merge_headers( + {"X-Stainless-Helper": "messages.parse"}, + extra_headers or {}, + ) transformed_output_format: Optional[JSONOutputFormatParam] | NotGiven = NOT_GIVEN diff --git a/tests/lib/test_azure.py b/tests/lib/test_azure.py index 81ff92f6a..8fa04e1e7 100644 --- a/tests/lib/test_azure.py +++ b/tests/lib/test_azure.py @@ -101,6 +101,17 @@ def test_with_options_overrides(self) -> None: assert derived.default_headers.get("x-app") == "1" assert derived.default_headers.get("x-extra") == "2" + def test_copy_x_stainless_helper_header_appends(self) -> None: + """x-stainless-helper accumulates across copies instead of being clobbered.""" + client = AnthropicFoundry( + api_key="test-key", + resource="example-resource", + default_headers={"x-stainless-helper": "parent"}, + ) + + copied = client.with_options(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers.get("x-stainless-helper") == "parent, child" + class TestAsyncAnthropicFoundry: @pytest.mark.asyncio @@ -159,6 +170,17 @@ async def async_token_provider() -> str: assert copied.max_retries == 5 assert copied.timeout == 10 + def test_copy_x_stainless_helper_header_appends(self) -> None: + """x-stainless-helper accumulates across copies instead of being clobbered.""" + client = AsyncAnthropicFoundry( + api_key="test-key", + resource="example-resource", + default_headers={"x-stainless-helper": "parent"}, + ) + + copied = client.with_options(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers.get("x-stainless-helper") == "parent, child" + class TestFoundryDoesNotLeakAnthropicAPIKey: """A stray ANTHROPIC_API_KEY in the environment must never be sent to the diff --git a/tests/lib/test_bedrock.py b/tests/lib/test_bedrock.py index f8aefef3a..2bfb458a1 100644 --- a/tests/lib/test_bedrock.py +++ b/tests/lib/test_bedrock.py @@ -305,3 +305,17 @@ def test_chunk_bytes_to_sse_legacy_completion_with_metrics() -> None: sse = _chunk_bytes_to_sse(raw) assert sse is not None assert sse.event == "completion" + + +def test_copy_x_stainless_helper_header_appends() -> None: + # `x-stainless-helper` accumulates across copies instead of being clobbered + client = sync_client.with_options(default_headers={"x-stainless-helper": "parent"}) + copied = client.with_options(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers["x-stainless-helper"] == "parent, child" + + +def test_async_copy_x_stainless_helper_header_appends() -> None: + # `x-stainless-helper` accumulates across copies instead of being clobbered + client = async_client.with_options(default_headers={"x-stainless-helper": "parent"}) + copied = client.with_options(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers["x-stainless-helper"] == "parent, child" diff --git a/tests/lib/test_bedrock_mantle.py b/tests/lib/test_bedrock_mantle.py index 924ff63b9..017326325 100644 --- a/tests/lib/test_bedrock_mantle.py +++ b/tests/lib/test_bedrock_mantle.py @@ -262,3 +262,23 @@ def test_copy_overrides_region(self) -> None: ) copied = client.copy(aws_region="us-west-2") assert copied.aws_region == "us-west-2" + + def test_copy_x_stainless_helper_header_appends(self) -> None: + # `x-stainless-helper` accumulates across copies instead of being clobbered + client = AnthropicBedrockMantle( + api_key="test-key", + aws_region="us-east-1", + default_headers={"x-stainless-helper": "parent"}, + ) + copied = client.copy(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers["x-stainless-helper"] == "parent, child" + + def test_async_copy_x_stainless_helper_header_appends(self) -> None: + # `x-stainless-helper` accumulates across copies instead of being clobbered + client = AsyncAnthropicBedrockMantle( + api_key="test-key", + aws_region="us-east-1", + default_headers={"x-stainless-helper": "parent"}, + ) + copied = client.copy(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers["x-stainless-helper"] == "parent, child" diff --git a/tests/lib/test_vertex.py b/tests/lib/test_vertex.py index d683b0606..61d3a55a6 100644 --- a/tests/lib/test_vertex.py +++ b/tests/lib/test_vertex.py @@ -119,6 +119,18 @@ def test_copy_default_headers(self) -> None: ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + def test_copy_x_stainless_helper_header_appends(self) -> None: + # `x-stainless-helper` accumulates across copies instead of being clobbered + client = AnthropicVertex( + base_url=base_url, + region="region", + project_id="project", + _strict_response_validation=True, + default_headers={"x-stainless-helper": "parent"}, + ) + copied = client.copy(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers["x-stainless-helper"] == "parent, child" + def test_global_region_base_url(self) -> None: """Test that global region uses the correct base URL.""" client = AnthropicVertex(region="global", project_id="test-project", access_token="fake-token") @@ -259,6 +271,18 @@ def test_copy_default_headers(self) -> None: ): client.copy(set_default_headers={}, default_headers={"X-Foo": "Bar"}) + def test_copy_x_stainless_helper_header_appends(self) -> None: + # `x-stainless-helper` accumulates across copies instead of being clobbered + client = AsyncAnthropicVertex( + base_url=base_url, + region="region", + project_id="project", + _strict_response_validation=True, + default_headers={"x-stainless-helper": "parent"}, + ) + copied = client.copy(default_headers={"x-stainless-helper": "child"}) + assert copied.default_headers["x-stainless-helper"] == "parent, child" + def test_global_region_base_url(self) -> None: """Test that global region uses the correct base URL.""" client = AsyncAnthropicVertex(region="global", project_id="test-project", access_token="fake-token") diff --git a/tests/test_client.py b/tests/test_client.py index d8814b756..b6ebf9e4d 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -534,6 +534,88 @@ def test_request_extra_headers(self, client: Anthropic) -> None: ) assert request.headers.get("X-Bar") == "false" + def test_request_extra_headers_httpx_headers(self, client: Anthropic) -> None: + # `httpx.Headers` is accepted anywhere a header mapping is, in addition to a plain dict + request = client.with_options(default_headers=httpx.Headers({"X-Bar": "true"}))._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers=httpx.Headers({"X-Foo": "Foo", "X-Bar": "false"})), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + # `extra_headers` still takes priority over `default_headers` when keys clash + assert request.headers.get("X-Bar") == "false" + + def test_request_x_stainless_helper_header_appends(self, client: Anthropic) -> None: + # `x-stainless-helper` accumulates across mappings instead of being clobbered, + # so a helper set on the client and one passed per-request both survive. + request = client.with_options(default_headers={"x-stainless-helper": "session_runner"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "message_batches"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "session_runner, message_batches" + + def test_request_x_stainless_helper_header_dedupes(self, client: Anthropic) -> None: + # the same helper set in both places is recorded once + request = client.with_options(default_headers={"x-stainless-helper": "session_runner"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "session_runner"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "session_runner" + + def test_request_x_stainless_helper_header_collapses_case_variants(self, client: Anthropic) -> None: + # differently-cased duplicates of the helper header fold into a single + # deduplicated value instead of being sent as conflicting entries + copied = client.with_options( + default_headers={"X-Stainless-Helper": "parent", "x-stainless-helper": "scoped"}, + ) + request = copied._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "message_batches"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "parent, scoped, message_batches" + + def test_request_x_stainless_helper_header_dedupes_multi_value(self, client: Anthropic) -> None: + # comma-separated values (e.g. several tagged tools) are deduplicated per token + copied = client.with_options(default_headers={"x-stainless-helper": "session_runner, memory_tool"}) + request = copied._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "memory_tool, message_batches"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "session_runner, memory_tool, message_batches" + + def test_copy_x_stainless_helper_header_appends(self, client: Anthropic) -> None: + # stacking `default_headers` via copy()/with_options accumulates the + # helper instead of clobbering, so e.g. a scoped sub-client's tag adds to + # one already carried by the parent. + copied = client.with_options(default_headers={"x-stainless-helper": "parent"}).with_options( + default_headers={"x-stainless-helper": "child"} + ) + request = copied._build_request(FinalRequestOptions(method="post", url="/foo")) + assert request.headers.get("x-stainless-helper") == "parent, child" + + def test_copy_preserves_header_removal(self, client: Anthropic) -> None: + # an Omit removal set on the client still survives a subsequent copy() + copied = client.with_options( + default_headers=cast("dict[str, str]", {"X-Foo": Omit()}), + ).with_options(default_headers={"X-Bar": "true"}) + request = copied._build_request(FinalRequestOptions(method="post", url="/foo")) + assert request.headers.get("X-Foo") is None + assert request.headers.get("X-Bar") == "true" + def test_request_extra_query(self, client: Anthropic) -> None: request = client._build_request( FinalRequestOptions( @@ -1564,6 +1646,88 @@ def test_request_extra_headers(self, client: Anthropic) -> None: ) assert request.headers.get("X-Bar") == "false" + def test_request_extra_headers_httpx_headers(self, async_client: AsyncAnthropic) -> None: + # `httpx.Headers` is accepted anywhere a header mapping is, in addition to a plain dict + request = async_client.with_options(default_headers=httpx.Headers({"X-Bar": "true"}))._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers=httpx.Headers({"X-Foo": "Foo", "X-Bar": "false"})), + ), + ) + assert request.headers.get("X-Foo") == "Foo" + # `extra_headers` still takes priority over `default_headers` when keys clash + assert request.headers.get("X-Bar") == "false" + + def test_request_x_stainless_helper_header_appends(self, async_client: AsyncAnthropic) -> None: + # `x-stainless-helper` accumulates across mappings instead of being clobbered, + # so a helper set on the client and one passed per-request both survive. + request = async_client.with_options(default_headers={"x-stainless-helper": "session_runner"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "message_batches"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "session_runner, message_batches" + + def test_request_x_stainless_helper_header_dedupes(self, async_client: AsyncAnthropic) -> None: + # the same helper set in both places is recorded once + request = async_client.with_options(default_headers={"x-stainless-helper": "session_runner"})._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "session_runner"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "session_runner" + + def test_request_x_stainless_helper_header_collapses_case_variants(self, async_client: AsyncAnthropic) -> None: + # differently-cased duplicates of the helper header fold into a single + # deduplicated value instead of being sent as conflicting entries + copied = async_client.with_options( + default_headers={"X-Stainless-Helper": "parent", "x-stainless-helper": "scoped"}, + ) + request = copied._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "message_batches"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "parent, scoped, message_batches" + + def test_request_x_stainless_helper_header_dedupes_multi_value(self, async_client: AsyncAnthropic) -> None: + # comma-separated values (e.g. several tagged tools) are deduplicated per token + copied = async_client.with_options(default_headers={"x-stainless-helper": "session_runner, memory_tool"}) + request = copied._build_request( + FinalRequestOptions( + method="post", + url="/foo", + **make_request_options(extra_headers={"x-stainless-helper": "memory_tool, message_batches"}), + ), + ) + assert request.headers.get("x-stainless-helper") == "session_runner, memory_tool, message_batches" + + def test_copy_x_stainless_helper_header_appends(self, async_client: AsyncAnthropic) -> None: + # stacking `default_headers` via copy()/with_options accumulates the + # helper instead of clobbering, so e.g. a scoped sub-client's tag adds to + # one already carried by the parent. + copied = async_client.with_options(default_headers={"x-stainless-helper": "parent"}).with_options( + default_headers={"x-stainless-helper": "child"} + ) + request = copied._build_request(FinalRequestOptions(method="post", url="/foo")) + assert request.headers.get("x-stainless-helper") == "parent, child" + + def test_copy_preserves_header_removal(self, async_client: AsyncAnthropic) -> None: + # an Omit removal set on the client still survives a subsequent copy() + copied = async_client.with_options( + default_headers=cast("dict[str, str]", {"X-Foo": Omit()}), + ).with_options(default_headers={"X-Bar": "true"}) + request = copied._build_request(FinalRequestOptions(method="post", url="/foo")) + assert request.headers.get("X-Foo") is None + assert request.headers.get("X-Bar") == "true" + def test_request_extra_query(self, client: Anthropic) -> None: request = client._build_request( FinalRequestOptions( From e6f7a56bb624f4c946cb15ba7973fd6fe052e10f Mon Sep 17 00:00:00 2001 From: dtmeadows-ant Date: Thu, 18 Jun 2026 12:12:18 -0400 Subject: [PATCH 3/5] fix(helpers): single source of truth for x-stainless-helper key + closed value vocabulary (#95) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit lib/_stainless_helpers becomes the constants/vocabulary module for the helper-telemetry headers: lowercase key constants, the closed StainlessHelperHeaderValue Literal, and a helper_header() factory that returns the {key: value} dict typed via the Literal — so a typo at any call site is a type error rather than silently mistagged telemetry. Every call site that wrote an inline {"X-Stainless-Helper": "..."} (under mixed casings — the parse() double-set bug) now uses helper_header(); the scoped-client HelperTag Literal folds into the shared type. tag_helper() already type-checks its value via the Literal param, so its callers stay on inline literals. Tag values are unchanged. --- src/anthropic/lib/_scoped_client.py | 16 +-- src/anthropic/lib/_stainless_helpers.py | 113 +++++++++++++---- src/anthropic/lib/tools/_beta_runner.py | 6 +- .../lib/tools/_beta_session_runner.py | 12 +- .../resources/beta/messages/messages.py | 24 ++-- src/anthropic/resources/messages/messages.py | 18 ++- tests/lib/test_stainless_helpers.py | 115 ++++++++++++++++++ 7 files changed, 249 insertions(+), 55 deletions(-) create mode 100644 tests/lib/test_stainless_helpers.py diff --git a/src/anthropic/lib/_scoped_client.py b/src/anthropic/lib/_scoped_client.py index 1f2756588..be83e4145 100644 --- a/src/anthropic/lib/_scoped_client.py +++ b/src/anthropic/lib/_scoped_client.py @@ -13,25 +13,21 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Dict, Literal, TypeVar, cast +from typing import TYPE_CHECKING, Dict, TypeVar, cast + +from ._stainless_helpers import STAINLESS_HELPER_HEADER, StainlessHelperHeaderValue if TYPE_CHECKING: from .._client import Anthropic, AsyncAnthropic -__all__ = ["HelperTag", "_copy_client_with_bearer_auth"] - - -# The closed set of ``x-stainless-helper`` telemetry tags the runner helpers -# stamp on outgoing requests. Constrained via ``Literal`` so a typo at any -# call site is a type error rather than silently mistagged telemetry. -HelperTag = Literal["environments-work-poller", "environments-worker", "session-tool-runner"] +__all__ = ["_copy_client_with_bearer_auth"] ClientT = TypeVar("ClientT", "Anthropic", "AsyncAnthropic") -def _copy_client_with_bearer_auth(client: ClientT, *, auth_token: str, helper: HelperTag) -> ClientT: +def _copy_client_with_bearer_auth(client: ClientT, *, auth_token: str, helper: StainlessHelperHeaderValue) -> ClientT: """Return a copy of ``client`` authenticated with ``auth_token`` as Bearer. The returned sub-client inherits the parent's full configuration via @@ -61,7 +57,7 @@ def _copy_client_with_bearer_auth(client: ClientT, *, auth_token: str, helper: H scoped = client.copy( auth_token=auth_token, credentials=None, - default_headers={"x-stainless-helper": helper}, + default_headers={STAINLESS_HELPER_HEADER: helper}, ) scoped.api_key = None # ``_custom_headers`` is typed as ``Mapping[str, str]`` (immutable diff --git a/src/anthropic/lib/_stainless_helpers.py b/src/anthropic/lib/_stainless_helpers.py index 6b894142e..ea6525130 100644 --- a/src/anthropic/lib/_stainless_helpers.py +++ b/src/anthropic/lib/_stainless_helpers.py @@ -1,13 +1,86 @@ -"""Tracking for SDK helper usage via the x-stainless-helper header.""" +"""Tracking for SDK helper usage via the x-stainless-helper header. + +This module is the single source of truth for the helper-telemetry header +keys and the closed tag vocabulary. The append-don't-clobber merge for the +header itself lives in :func:`anthropic._base_client.merge_headers`; here +we only carry the constants and the per-object tagging machinery. +""" from __future__ import annotations -from typing import Any, Dict, cast +from typing import Any, Dict, List, Optional, cast +from typing_extensions import Literal + +__all__ = [ + "STAINLESS_HELPER_HEADER", + "STAINLESS_HELPER_METHOD_HEADER", + "STAINLESS_STREAM_HELPER_HEADER", + "HELPER_METHOD_STREAM", + "StainlessHelperHeaderValue", + "helper_header", + "tag_helper", + "get_helper_tag", + "collect_helpers", + "stainless_helper_header", + "stainless_helper_header_from_file", +] + + +STAINLESS_HELPER_HEADER = "x-stainless-helper" +"""Telemetry header naming the SDK helper(s) a request came from. + +Always this lowercase form. ``merge_headers`` matches this key +case-insensitively for its append semantics, but a single canonical casing +keeps every call site greppable and avoids two literal casings of the same +key reaching a plain dict merge anywhere upstream of it. +""" + +STAINLESS_HELPER_METHOD_HEADER = "x-stainless-helper-method" +"""Telemetry header naming the SDK method (e.g. ``stream``) in use.""" + +STAINLESS_STREAM_HELPER_HEADER = "x-stainless-stream-helper" +"""Telemetry header naming the streaming surface (e.g. ``beta.messages``).""" + +HELPER_METHOD_STREAM = "stream" + + +StainlessHelperHeaderValue = Literal[ + "beta.messages.parse", + "BetaToolRunner", + "compaction", + "environments-work-poller", + "environments-worker", + "mcp_content", + "mcp_message", + "mcp_resource_to_content", + "mcp_resource_to_file", + "mcp_tool", + "messages.parse", + "session-tool-runner", +] +"""The closed set of helper telemetry tags, shared verbatim across SDKs. + +Constrained so a typo at any call site is a type error rather than silently +mistagged telemetry. Existing values keep their original spellings — telemetry +consumers match on them, so renames lose history. New tags are hyphenated +lowercase; add them here (and to the matching set in every other SDK) before +using them. +""" + + +def helper_header(value: StainlessHelperHeaderValue) -> Dict[str, str]: + """The ``x-stainless-helper: `` header dict, for passing into a + ``merge_headers`` call or as ``extra_headers``/``default_headers``. + + Typing keeps the value drawn from the closed vocabulary above. + """ + return {STAINLESS_HELPER_HEADER: value} + _HELPER_ATTR = "_stainless_helper" -def tag_helper(obj: Any, name: str) -> None: +def tag_helper(obj: Any, name: StainlessHelperHeaderValue) -> None: """Mark an object as created by a named SDK helper.""" try: object.__setattr__(obj, _HELPER_ATTR, name) @@ -15,7 +88,7 @@ def tag_helper(obj: Any, name: str) -> None: pass -def get_helper_tag(obj: object) -> str | None: +def get_helper_tag(obj: object) -> Optional[str]: """Get the helper name from an object, if any.""" return getattr(obj, _HELPER_ATTR, None) # type: ignore[return-value] @@ -23,21 +96,21 @@ def get_helper_tag(obj: object) -> str | None: def collect_helpers( tools: Any = None, messages: Any = None, -) -> list[str]: +) -> List[str]: """Collect deduplicated helper names from tools and messages.""" - helpers: set[str] = set() + helpers: List[str] = [] + + def _add(tag: Optional[str]) -> None: + if tag is not None and tag not in helpers: + helpers.append(tag) if tools: for tool in tools: - tag = get_helper_tag(tool) - if tag is not None: - helpers.add(tag) + _add(get_helper_tag(tool)) if messages: for message in messages: - tag = get_helper_tag(message) - if tag is not None: - helpers.add(tag) + _add(get_helper_tag(message)) # Check content blocks within messages if isinstance(message, dict): @@ -45,18 +118,16 @@ def collect_helpers( else: blocks = getattr(message, "content", None) if isinstance(blocks, list): - for block in cast(list[object], blocks): - tag = get_helper_tag(block) - if tag is not None: - helpers.add(tag) + for block in cast(List[object], blocks): + _add(get_helper_tag(block)) - return list(helpers) + return helpers def stainless_helper_header( tools: Any = None, messages: Any = None, -) -> dict[str, str]: +) -> Dict[str, str]: """Build x-stainless-helper header dict from tools and messages. Returns an empty dict if no helpers are found. @@ -64,12 +135,12 @@ def stainless_helper_header( helpers = collect_helpers(tools, messages) if not helpers: return {} - return {"x-stainless-helper": ", ".join(helpers)} + return {STAINLESS_HELPER_HEADER: ", ".join(helpers)} -def stainless_helper_header_from_file(file: object) -> dict[str, str]: +def stainless_helper_header_from_file(file: object) -> Dict[str, str]: """Build x-stainless-helper header dict from a file object.""" tag = get_helper_tag(file) if tag is None: return {} - return {"x-stainless-helper": tag} + return {STAINLESS_HELPER_HEADER: tag} diff --git a/src/anthropic/lib/tools/_beta_runner.py b/src/anthropic/lib/tools/_beta_runner.py index c7a6378a0..1dd76e64f 100644 --- a/src/anthropic/lib/tools/_beta_runner.py +++ b/src/anthropic/lib/tools/_beta_runner.py @@ -35,7 +35,7 @@ BetaBuiltinFunctionTool, BetaAsyncBuiltinFunctionTool, ) -from .._stainless_helpers import stainless_helper_header +from .._stainless_helpers import helper_header, stainless_helper_header from ._beta_compaction_control import DEFAULT_THRESHOLD, DEFAULT_SUMMARY_PROMPT, CompactionControl from ..streaming._beta_messages import BetaMessageStream, BetaAsyncMessageStream from ...types.beta.parsed_beta_message import ResponseFormatT, ParsedBetaMessage, ParsedBetaContentBlock @@ -231,7 +231,7 @@ def _check_and_compact(self) -> bool: model=model, messages=messages, max_tokens=self._params["max_tokens"], - extra_headers={"X-Stainless-Helper": "compaction"}, + extra_headers=helper_header("compaction"), ) log.info(f"Compaction complete. New token usage: {response.usage.output_tokens}") @@ -519,7 +519,7 @@ async def _check_and_compact(self) -> bool: model=model, messages=messages, max_tokens=self._params["max_tokens"], - extra_headers={"X-Stainless-Helper": "compaction"}, + extra_headers=helper_header("compaction"), ) log.info(f"Compaction complete. New token usage: {response.usage.output_tokens}") diff --git a/src/anthropic/lib/tools/_beta_session_runner.py b/src/anthropic/lib/tools/_beta_session_runner.py index 790ee1588..35dc51926 100644 --- a/src/anthropic/lib/tools/_beta_session_runner.py +++ b/src/anthropic/lib/tools/_beta_session_runner.py @@ -28,7 +28,7 @@ from .._retry import TRANSIENT_ERRORS, is_fatal_status_error from ..._types import Headers from ._tool_dispatch import tool_registry, run_runnable_tool, tool_error_content -from .._scoped_client import HelperTag, _copy_client_with_bearer_auth +from .._scoped_client import _copy_client_with_bearer_auth from ._beta_functions import ( ToolError, BetaRunnableTool, @@ -36,6 +36,7 @@ BetaFunctionToolResultType, aclose_runnable_tool, ) +from .._stainless_helpers import helper_header from ...types.beta.sessions import BetaManagedAgentsAgentToolUseEvent, BetaManagedAgentsAgentCustomToolUseEvent from ...types.beta.sessions.beta_managed_agents_user_tool_result_event_params import ( Content as _SessionContent, @@ -208,9 +209,6 @@ class DispatchedToolCall: owner (the split-client partial-fulfilment behavior).""" -_HELPER: HelperTag = "session-tool-runner" - - def _scoped_client(client: AsyncAnthropic, environment_key: str | None) -> AsyncAnthropic: """Build the runner's request client. @@ -220,8 +218,10 @@ def _scoped_client(client: AsyncAnthropic, environment_key: str | None) -> Async mutated). """ if environment_key is not None: - return _copy_client_with_bearer_auth(client, auth_token=environment_key, helper=_HELPER) - return client.with_options(default_headers={"x-stainless-helper": _HELPER}) + return _copy_client_with_bearer_auth( + client, auth_token=environment_key, helper="session-tool-runner" + ) + return client.with_options(default_headers=helper_header("session-tool-runner")) def _to_session_content(content: BetaFunctionToolResultType) -> list[_SessionContent]: diff --git a/src/anthropic/resources/beta/messages/messages.py b/src/anthropic/resources/beta/messages/messages.py index 7dd149f9d..57a35fb45 100644 --- a/src/anthropic/resources/beta/messages/messages.py +++ b/src/anthropic/resources/beta/messages/messages.py @@ -52,7 +52,13 @@ from ....types.model_param import ModelParam from ....lib._parse._response import ResponseFormatT, parse_beta_response from ....lib._parse._transform import transform_schema -from ....lib._stainless_helpers import stainless_helper_header as _stainless_helper_header +from ....lib._stainless_helpers import ( + HELPER_METHOD_STREAM as _HELPER_METHOD_STREAM, + STAINLESS_HELPER_METHOD_HEADER as _STAINLESS_HELPER_METHOD_HEADER, + STAINLESS_STREAM_HELPER_HEADER as _STAINLESS_STREAM_HELPER_HEADER, + helper_header as _helper_header, + stainless_helper_header as _stainless_helper_header, +) from ....types.beta.beta_message import BetaMessage from ....lib.tools._beta_functions import ( BetaFunctionTool, @@ -1311,7 +1317,7 @@ def parse( betas.append("structured-outputs-2025-12-15") extra_headers = merge_headers( - {"X-Stainless-Helper": "beta.messages.parse"}, + _helper_header("beta.messages.parse"), strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), _stainless_helper_header(tools, messages), extra_headers or {}, @@ -1578,7 +1584,7 @@ def tool_runner( ) extra_headers = merge_headers( - {"X-Stainless-Helper": "BetaToolRunner"}, + _helper_header("BetaToolRunner"), strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), _stainless_helper_header(tools, messages), extra_headers or {}, @@ -1708,8 +1714,8 @@ def stream( """Create a Message stream""" extra_headers = merge_headers( { - "X-Stainless-Helper-Method": "stream", - "X-Stainless-Stream-Helper": "beta.messages", + _STAINLESS_HELPER_METHOD_HEADER: _HELPER_METHOD_STREAM, + _STAINLESS_STREAM_HELPER_HEADER: "beta.messages", }, strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), _stainless_helper_header(tools, messages), @@ -3279,7 +3285,7 @@ async def parse( betas.append("structured-outputs-2025-12-15") extra_headers = merge_headers( - {"X-Stainless-Helper": "beta.messages.parse"}, + _helper_header("beta.messages.parse"), strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), _stainless_helper_header(tools, messages), extra_headers or {}, @@ -3539,7 +3545,7 @@ def tool_runner( ) extra_headers = merge_headers( - {"X-Stainless-Helper": "BetaToolRunner"}, + _helper_header("BetaToolRunner"), strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), _stainless_helper_header(tools, messages), extra_headers or {}, @@ -3668,8 +3674,8 @@ def stream( extra_headers = merge_headers( { - "X-Stainless-Helper-Method": "stream", - "X-Stainless-Stream-Helper": "beta.messages", + _STAINLESS_HELPER_METHOD_HEADER: _HELPER_METHOD_STREAM, + _STAINLESS_STREAM_HELPER_HEADER: "beta.messages", }, strip_not_given({"anthropic-beta": ",".join(str(e) for e in betas) if is_given(betas) else NOT_GIVEN}), _stainless_helper_header(tools, messages), diff --git a/src/anthropic/resources/messages/messages.py b/src/anthropic/resources/messages/messages.py index 30bb070ec..27e6e0014 100644 --- a/src/anthropic/resources/messages/messages.py +++ b/src/anthropic/resources/messages/messages.py @@ -45,6 +45,12 @@ from ...types.metadata_param import MetadataParam from ...types.parsed_message import ParsedMessage from ...lib._parse._transform import transform_schema +from ...lib._stainless_helpers import ( + HELPER_METHOD_STREAM as _HELPER_METHOD_STREAM, + STAINLESS_HELPER_METHOD_HEADER as _STAINLESS_HELPER_METHOD_HEADER, + STAINLESS_STREAM_HELPER_HEADER as _STAINLESS_STREAM_HELPER_HEADER, + helper_header as _helper_header, +) from ...types.text_block_param import TextBlockParam from ...types.tool_union_param import ToolUnionParam from ...types.tool_choice_param import ToolChoiceParam @@ -1088,8 +1094,8 @@ def stream( ) extra_headers = { - "X-Stainless-Helper-Method": "stream", - "X-Stainless-Stream-Helper": "messages", + _STAINLESS_HELPER_METHOD_HEADER: _HELPER_METHOD_STREAM, + _STAINLESS_STREAM_HELPER_HEADER: "messages", **(extra_headers or {}), } @@ -1205,7 +1211,7 @@ def parse( ) extra_headers = merge_headers( - {"X-Stainless-Helper": "messages.parse"}, + _helper_header("messages.parse"), extra_headers or {}, ) @@ -2542,8 +2548,8 @@ def stream( ) extra_headers = { - "X-Stainless-Helper-Method": "stream", - "X-Stainless-Stream-Helper": "messages", + _STAINLESS_HELPER_METHOD_HEADER: _HELPER_METHOD_STREAM, + _STAINLESS_STREAM_HELPER_HEADER: "messages", **(extra_headers or {}), } @@ -2658,7 +2664,7 @@ async def parse( ) extra_headers = merge_headers( - {"X-Stainless-Helper": "messages.parse"}, + _helper_header("messages.parse"), extra_headers or {}, ) diff --git a/tests/lib/test_stainless_helpers.py b/tests/lib/test_stainless_helpers.py new file mode 100644 index 000000000..f375f9069 --- /dev/null +++ b/tests/lib/test_stainless_helpers.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from typing import cast + +import httpx +import respx +import pytest + +from anthropic import Anthropic, AsyncAnthropic, _compat +from anthropic.types.beta import BetaToolParam +from anthropic._base_client import _APPEND_HEADERS +from anthropic.lib._stainless_helpers import ( + STAINLESS_HELPER_HEADER, + tag_helper, + helper_header, +) + +sync_client = Anthropic(base_url="http://127.0.0.1:4010", api_key="my-anthropic-api-key") +async_client = AsyncAnthropic(base_url="http://127.0.0.1:4010", api_key="my-anthropic-api-key") + + +class _TaggedDict(dict): # type: ignore[type-arg] + """Plain dicts reject ``object.__setattr__`` — helpers tag attribute-capable subclasses.""" + + +def test_helper_header() -> None: + assert helper_header("BetaToolRunner") == {STAINLESS_HELPER_HEADER: "BetaToolRunner"} + + +def test_helper_header_is_an_append_header() -> None: + # ``merge_headers`` only appends keys it knows about — keep this aligned + assert STAINLESS_HELPER_HEADER in _APPEND_HEADERS + + +def _message_json() -> dict[str, object]: + return { + "id": "msg_abc123", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [{"type": "text", "text": "hi"}], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 1, "output_tokens": 1}, + } + + +@pytest.mark.respx(base_url="http://127.0.0.1:4010") +class TestSyncWireHeaders: + def test_caller_tag_is_appended_not_clobbered(self, respx_mock: respx.MockRouter) -> None: + respx_mock.post("/v1/messages").mock(return_value=httpx.Response(200, json=_message_json())) + + tool = cast("BetaToolParam", _TaggedDict({"name": "t", "description": "d", "input_schema": {"type": "object"}})) + tag_helper(tool, "mcp_tool") + sync_client.beta.messages.create( + model="claude-sonnet-4-5", + max_tokens=16, + messages=[{"role": "user", "content": "hello"}], + tools=[tool], + extra_headers={"X-Stainless-Helper": "caller-tag"}, + ) + + request = respx_mock.calls.last.request + values = request.headers.get_list(STAINLESS_HELPER_HEADER) + assert values == ["mcp_tool, caller-tag"] + + @pytest.mark.skipif(_compat.PYDANTIC_V1, reason="parse() response post-parser is pydantic-v2 only") + def test_parse_sends_single_header_line(self, respx_mock: respx.MockRouter) -> None: + # regression: the literal tag and the collected tags used to land under + # two casings of the key, producing two header lines on the wire + respx_mock.post("/v1/messages").mock(return_value=httpx.Response(200, json=_message_json())) + + sync_client.beta.messages.parse( + model="claude-sonnet-4-5", + max_tokens=16, + messages=[{"role": "user", "content": "hello"}], + ) + + request = respx_mock.calls.last.request + values = request.headers.get_list(STAINLESS_HELPER_HEADER) + assert values == ["beta.messages.parse"] + + +@pytest.mark.respx(base_url="http://127.0.0.1:4010") +class TestAsyncWireHeaders: + async def test_caller_tag_is_appended_not_clobbered(self, respx_mock: respx.MockRouter) -> None: + respx_mock.post("/v1/messages").mock(return_value=httpx.Response(200, json=_message_json())) + + tool = cast("BetaToolParam", _TaggedDict({"name": "t", "description": "d", "input_schema": {"type": "object"}})) + tag_helper(tool, "mcp_tool") + await async_client.beta.messages.create( + model="claude-sonnet-4-5", + max_tokens=16, + messages=[{"role": "user", "content": "hello"}], + tools=[tool], + extra_headers={"X-Stainless-Helper": "caller-tag"}, + ) + + request = respx_mock.calls.last.request + values = request.headers.get_list(STAINLESS_HELPER_HEADER) + assert values == ["mcp_tool, caller-tag"] + + @pytest.mark.skipif(_compat.PYDANTIC_V1, reason="parse() response post-parser is pydantic-v2 only") + async def test_parse_sends_single_header_line(self, respx_mock: respx.MockRouter) -> None: + respx_mock.post("/v1/messages").mock(return_value=httpx.Response(200, json=_message_json())) + + await async_client.beta.messages.parse( + model="claude-sonnet-4-5", + max_tokens=16, + messages=[{"role": "user", "content": "hello"}], + ) + + request = respx_mock.calls.last.request + values = request.headers.get_list(STAINLESS_HELPER_HEADER) + assert values == ["beta.messages.parse"] From 5e23212dc0883174c879b97ef8e7e33ead4e8da5 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:09:32 +0000 Subject: [PATCH 4/5] feat(api): add support for new code_execution_20260120 tool --- .stats.yml | 6 +-- api.md | 4 ++ src/anthropic/lib/middleware/_fallbacks.py | 28 +++++++++++--- src/anthropic/lib/tools/_beta_functions.py | 14 +++---- src/anthropic/lib/tools/mcp.py | 4 +- src/anthropic/types/__init__.py | 1 + src/anthropic/types/beta/__init__.py | 7 ++++ .../beta/beta_advisor_tool_20260301_param.py | 4 +- ...beta_code_execution_tool_20250522_param.py | 4 +- ...beta_code_execution_tool_20250825_param.py | 4 +- ...beta_code_execution_tool_20260120_param.py | 4 +- ...beta_code_execution_tool_20260521_param.py | 38 +++++++++++++++++++ .../types/beta/beta_fallback_block.py | 10 +++-- .../types/beta/beta_fallback_block_param.py | 30 ++++++++------- .../beta/beta_fallback_refusal_trigger.py | 17 +++++++++ .../beta/beta_memory_tool_20250818_param.py | 4 +- .../types/beta/beta_refusal_stop_details.py | 5 +-- .../beta/beta_tool_bash_20241022_param.py | 4 +- .../beta/beta_tool_bash_20250124_param.py | 4 +- .../beta_tool_computer_use_20241022_param.py | 4 +- .../beta_tool_computer_use_20250124_param.py | 4 +- .../beta_tool_computer_use_20251124_param.py | 4 +- src/anthropic/types/beta/beta_tool_param.py | 4 +- ...ta_tool_search_tool_bm25_20251119_param.py | 4 +- ...a_tool_search_tool_regex_20251119_param.py | 4 +- .../beta_tool_text_editor_20241022_param.py | 4 +- .../beta_tool_text_editor_20250124_param.py | 4 +- .../beta_tool_text_editor_20250429_param.py | 4 +- .../beta_tool_text_editor_20250728_param.py | 4 +- .../types/beta/beta_tool_union_param.py | 2 + .../beta_web_fetch_tool_20250910_param.py | 4 +- .../beta_web_fetch_tool_20260209_param.py | 4 +- .../beta_web_fetch_tool_20260309_param.py | 4 +- .../beta_web_search_tool_20250305_param.py | 4 +- .../beta_web_search_tool_20260209_param.py | 4 +- .../types/beta/beta_webhook_event_data.py | 2 + ...beta_webhook_session_updated_event_data.py | 18 +++++++++ .../types/beta/message_count_tokens_params.py | 2 + .../code_execution_tool_20250522_param.py | 4 +- .../code_execution_tool_20250825_param.py | 4 +- .../code_execution_tool_20260120_param.py | 4 +- .../code_execution_tool_20260521_param.py | 38 +++++++++++++++++++ .../types/memory_tool_20250818_param.py | 4 +- .../types/message_count_tokens_tool_param.py | 2 + src/anthropic/types/refusal_stop_details.py | 5 +-- .../types/tool_bash_20250124_param.py | 4 +- src/anthropic/types/tool_param.py | 4 +- .../tool_search_tool_bm25_20251119_param.py | 4 +- .../tool_search_tool_regex_20251119_param.py | 4 +- .../types/tool_text_editor_20250124_param.py | 4 +- .../types/tool_text_editor_20250429_param.py | 4 +- .../types/tool_text_editor_20250728_param.py | 4 +- src/anthropic/types/tool_union_param.py | 2 + .../types/web_fetch_tool_20250910_param.py | 4 +- .../types/web_fetch_tool_20260209_param.py | 4 +- .../types/web_fetch_tool_20260309_param.py | 4 +- .../types/web_search_tool_20250305_param.py | 4 +- .../types/web_search_tool_20260209_param.py | 4 +- .../streaming/fixtures/fallback_response.txt | 2 +- .../streaming/test_parsed_content_blocks.py | 1 + tests/lib/test_refusal_fallback.py | 10 ++--- tests/test_transform.py | 8 +++- 62 files changed, 322 insertions(+), 86 deletions(-) create mode 100644 src/anthropic/types/beta/beta_code_execution_tool_20260521_param.py create mode 100644 src/anthropic/types/beta/beta_fallback_refusal_trigger.py create mode 100644 src/anthropic/types/beta/beta_webhook_session_updated_event_data.py create mode 100644 src/anthropic/types/code_execution_tool_20260521_param.py diff --git a/.stats.yml b/.stats.yml index 7772a99f5..09cef8716 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 116 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-04d2899e1e4dd48e29347b98f1265e6345ee20b75e29c12eca1068a8f7c61095.yml -openapi_spec_hash: 989d596f7660ce55a7cea748a9292b45 -config_hash: 221b6331246cafc1f7ff1861d35a3640 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-2ecf157aa324d82fa8f3636a325aea5bd96ecab93193f6e37864ebe665c48685.yml +openapi_spec_hash: 29873ea69a87e047c4f742a960648bf0 +config_hash: 48f7cbc6648bf7f1e6c68ad3dab477fc diff --git a/api.md b/api.md index 98ab075ca..dcd6efa8b 100644 --- a/api.md +++ b/api.md @@ -56,6 +56,7 @@ from anthropic.types import ( CodeExecutionTool20250522, CodeExecutionTool20250825, CodeExecutionTool20260120, + CodeExecutionTool20260521, CodeExecutionToolResultBlock, CodeExecutionToolResultBlockContent, CodeExecutionToolResultBlockParam, @@ -347,6 +348,7 @@ from anthropic.types.beta import ( BetaCodeExecutionTool20250522, BetaCodeExecutionTool20250825, BetaCodeExecutionTool20260120, + BetaCodeExecutionTool20260521, BetaCodeExecutionToolResultBlock, BetaCodeExecutionToolResultBlockContent, BetaCodeExecutionToolResultBlockParam, @@ -382,6 +384,7 @@ from anthropic.types.beta import ( BetaFallbackInfoParam, BetaFallbackMessageIterationUsage, BetaFallbackParam, + BetaFallbackRefusalTrigger, BetaFileDocumentSource, BetaFileImageSource, BetaImageBlockParam, @@ -1155,6 +1158,7 @@ from anthropic.types.beta import ( BetaWebhookSessionThreadCreatedEventData, BetaWebhookSessionThreadIdledEventData, BetaWebhookSessionThreadTerminatedEventData, + BetaWebhookSessionUpdatedEventData, BetaWebhookVaultArchivedEventData, BetaWebhookVaultCreatedEventData, BetaWebhookVaultCredentialArchivedEventData, diff --git a/src/anthropic/lib/middleware/_fallbacks.py b/src/anthropic/lib/middleware/_fallbacks.py index db022f95a..73138e6b2 100644 --- a/src/anthropic/lib/middleware/_fallbacks.py +++ b/src/anthropic/lib/middleware/_fallbacks.py @@ -219,10 +219,11 @@ def handle(self, request: APIRequest, call_next: CallNext) -> APIResponse[Any]: index += 1 pin(index) token = _credit_token(message) + category = _refusal_category(message) res = call_next(request.copy(body=_merged_body(body, self._fallbacks[index], token))) if res.http_response.is_success: to_model = str(self._fallbacks[index]["model"]) - seams.append(_seam_block(from_model, to_model)) + seams.append(_seam_block(from_model, to_model, category)) from_model = to_model if seams and res.http_response.is_success: @@ -285,10 +286,11 @@ async def handle_async(self, request: APIRequest, call_next: AsyncCallNext) -> A index += 1 pin(index) token = _credit_token(message) + category = _refusal_category(message) res = await call_next(request.copy(body=_merged_body(body, self._fallbacks[index], token))) if res.http_response.is_success: to_model = str(self._fallbacks[index]["model"]) - seams.append(_seam_block(from_model, to_model)) + seams.append(_seam_block(from_model, to_model, category)) from_model = to_model if seams and res.http_response.is_success: @@ -694,6 +696,9 @@ class _Refusal(BaseModel): token: Optional[str] """The minted credit token; `None` for a token-less start-of-stream refusal.""" + category: Optional[str] + """The policy category that triggered the refusal; `None` when not surfaced.""" + has_prefill_claim: bool usage: Dict[str, Any] event: Dict[str, Any] @@ -865,6 +870,7 @@ def feed(self, sse: ServerSentEvent) -> list[bytes]: self.outcome = _HopOutcome( refused=_Refusal( token=token, + category=details.get("category") if details is not None else None, has_prefill_claim=details is not None and details.get("fallback_has_prefill_claim") is True, usage=usage, event=event, @@ -1062,7 +1068,7 @@ def queue_seam(self, to_model: str) -> None: { "type": "content_block_start", "index": seam_index, - "content_block": _seam_block(self.from_model, to_model), + "content_block": _seam_block(self.from_model, to_model, self.last_refusal.category), }, ), _emit("content_block_stop", {"type": "content_block_stop", "index": seam_index}), @@ -1348,9 +1354,21 @@ def _credit_token(message: Message | BetaMessage) -> str | None: return None -def _seam_block(from_model: str, to_model: str) -> dict[str, Any]: +def _refusal_category(message: Message | BetaMessage) -> str | None: + """The policy category that caused the refusal; only the beta surface carries one.""" + if isinstance(message, BetaMessage) and message.stop_details is not None: + return message.stop_details.category + return None + + +def _seam_block(from_model: str, to_model: str, category: str | None) -> dict[str, Any]: """The synthetic `fallback` content block marking one model boundary.""" - return {"type": "fallback", "from": {"model": from_model}, "to": {"model": to_model}} + return { + "type": "fallback", + "from": {"model": from_model}, + "to": {"model": to_model}, + "trigger": {"type": "refusal", "category": category}, + } def _seamed_http_response(original: httpx.Response, seams: list[dict[str, Any]]) -> httpx.Response | None: diff --git a/src/anthropic/lib/tools/_beta_functions.py b/src/anthropic/lib/tools/_beta_functions.py index 7fab17be4..c8f5c5755 100644 --- a/src/anthropic/lib/tools/_beta_functions.py +++ b/src/anthropic/lib/tools/_beta_functions.py @@ -146,7 +146,7 @@ def __init__( defer_loading: bool | None = None, cache_control: BetaCacheControlEphemeralParam | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, @@ -353,7 +353,7 @@ def beta_tool( defer_loading: bool | None = None, cache_control: BetaCacheControlEphemeralParam | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, @@ -371,7 +371,7 @@ def beta_tool( defer_loading: bool | None = None, cache_control: BetaCacheControlEphemeralParam | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, @@ -389,7 +389,7 @@ def beta_tool( defer_loading: bool | None = None, cache_control: BetaCacheControlEphemeralParam | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, @@ -478,7 +478,7 @@ def beta_async_tool( defer_loading: bool | None = None, cache_control: BetaCacheControlEphemeralParam | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, @@ -496,7 +496,7 @@ def beta_async_tool( defer_loading: bool | None = None, cache_control: BetaCacheControlEphemeralParam | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, @@ -514,7 +514,7 @@ def beta_async_tool( defer_loading: bool | None = None, cache_control: BetaCacheControlEphemeralParam | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, diff --git a/src/anthropic/lib/tools/mcp.py b/src/anthropic/lib/tools/mcp.py index c5941f6e7..faf2023d0 100644 --- a/src/anthropic/lib/tools/mcp.py +++ b/src/anthropic/lib/tools/mcp.py @@ -333,7 +333,7 @@ def mcp_tool( cache_control: BetaCacheControlEphemeralParam | None = None, defer_loading: bool | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, @@ -395,7 +395,7 @@ def async_mcp_tool( cache_control: BetaCacheControlEphemeralParam | None = None, defer_loading: bool | None = None, allowed_callers: list[ - Literal["direct", "code_execution_20250825", "code_execution_20260120"] + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] ] | None = None, eager_input_streaming: bool | None = None, diff --git a/src/anthropic/types/__init__.py b/src/anthropic/types/__init__.py index 3793ba443..b3d63c92c 100644 --- a/src/anthropic/types/__init__.py +++ b/src/anthropic/types/__init__.py @@ -174,6 +174,7 @@ from .code_execution_tool_20250522_param import CodeExecutionTool20250522Param as CodeExecutionTool20250522Param from .code_execution_tool_20250825_param import CodeExecutionTool20250825Param as CodeExecutionTool20250825Param from .code_execution_tool_20260120_param import CodeExecutionTool20260120Param as CodeExecutionTool20260120Param +from .code_execution_tool_20260521_param import CodeExecutionTool20260521Param as CodeExecutionTool20260521Param from .content_block_source_content_param import ContentBlockSourceContentParam as ContentBlockSourceContentParam from .tool_search_tool_result_error_code import ToolSearchToolResultErrorCode as ToolSearchToolResultErrorCode from .web_search_tool_result_block_param import WebSearchToolResultBlockParam as WebSearchToolResultBlockParam diff --git a/src/anthropic/types/beta/__init__.py b/src/anthropic/types/beta/__init__.py index c155d0648..39a18e9a7 100644 --- a/src/anthropic/types/beta/__init__.py +++ b/src/anthropic/types/beta/__init__.py @@ -155,6 +155,7 @@ from .beta_all_thinking_turns_param import BetaAllThinkingTurnsParam as BetaAllThinkingTurnsParam from .beta_cache_miss_model_changed import BetaCacheMissModelChanged as BetaCacheMissModelChanged from .beta_cache_miss_tools_changed import BetaCacheMissToolsChanged as BetaCacheMissToolsChanged +from .beta_fallback_refusal_trigger import BetaFallbackRefusalTrigger as BetaFallbackRefusalTrigger from .beta_json_output_format_param import BetaJSONOutputFormatParam as BetaJSONOutputFormatParam from .beta_mcp_tool_use_block_param import BetaMCPToolUseBlockParam as BetaMCPToolUseBlockParam from .beta_raw_message_stream_event import BetaRawMessageStreamEvent as BetaRawMessageStreamEvent @@ -308,6 +309,9 @@ from .beta_code_execution_tool_20260120_param import ( BetaCodeExecutionTool20260120Param as BetaCodeExecutionTool20260120Param, ) +from .beta_code_execution_tool_20260521_param import ( + BetaCodeExecutionTool20260521Param as BetaCodeExecutionTool20260521Param, +) from .beta_content_block_source_content_param import ( BetaContentBlockSourceContentParam as BetaContentBlockSourceContentParam, ) @@ -335,6 +339,9 @@ from .beta_webhook_session_running_event_data import ( BetaWebhookSessionRunningEventData as BetaWebhookSessionRunningEventData, ) +from .beta_webhook_session_updated_event_data import ( + BetaWebhookSessionUpdatedEventData as BetaWebhookSessionUpdatedEventData, +) from .beta_advisor_redacted_result_block_param import ( BetaAdvisorRedactedResultBlockParam as BetaAdvisorRedactedResultBlockParam, ) diff --git a/src/anthropic/types/beta/beta_advisor_tool_20260301_param.py b/src/anthropic/types/beta/beta_advisor_tool_20260301_param.py index 5703a6a3e..e2ec73b74 100644 --- a/src/anthropic/types/beta/beta_advisor_tool_20260301_param.py +++ b/src/anthropic/types/beta/beta_advisor_tool_20260301_param.py @@ -27,7 +27,9 @@ class BetaAdvisorTool20260301Param(TypedDict, total=False): type: Required[Literal["advisor_20260301"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_code_execution_tool_20250522_param.py b/src/anthropic/types/beta/beta_code_execution_tool_20250522_param.py index 1683a2572..689001b17 100644 --- a/src/anthropic/types/beta/beta_code_execution_tool_20250522_param.py +++ b/src/anthropic/types/beta/beta_code_execution_tool_20250522_param.py @@ -19,7 +19,9 @@ class BetaCodeExecutionTool20250522Param(TypedDict, total=False): type: Required[Literal["code_execution_20250522"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_code_execution_tool_20250825_param.py b/src/anthropic/types/beta/beta_code_execution_tool_20250825_param.py index 053c44236..e060d2fd9 100644 --- a/src/anthropic/types/beta/beta_code_execution_tool_20250825_param.py +++ b/src/anthropic/types/beta/beta_code_execution_tool_20250825_param.py @@ -19,7 +19,9 @@ class BetaCodeExecutionTool20250825Param(TypedDict, total=False): type: Required[Literal["code_execution_20250825"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_code_execution_tool_20260120_param.py b/src/anthropic/types/beta/beta_code_execution_tool_20260120_param.py index 8d96b1fc9..a9c71df99 100644 --- a/src/anthropic/types/beta/beta_code_execution_tool_20260120_param.py +++ b/src/anthropic/types/beta/beta_code_execution_tool_20260120_param.py @@ -23,7 +23,9 @@ class BetaCodeExecutionTool20260120Param(TypedDict, total=False): type: Required[Literal["code_execution_20260120"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_code_execution_tool_20260521_param.py b/src/anthropic/types/beta/beta_code_execution_tool_20260521_param.py new file mode 100644 index 000000000..0c6bd75a6 --- /dev/null +++ b/src/anthropic/types/beta/beta_code_execution_tool_20260521_param.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Literal, Required, TypedDict + +from .beta_cache_control_ephemeral_param import BetaCacheControlEphemeralParam + +__all__ = ["BetaCodeExecutionTool20260521Param"] + + +class BetaCodeExecutionTool20260521Param(TypedDict, total=False): + """Code execution tool with REPL state persistence.""" + + name: Required[Literal["code_execution"]] + """Name of the tool. + + This is how the tool will be called by the model and in `tool_use` blocks. + """ + + type: Required[Literal["code_execution_20260521"]] + + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] + + cache_control: Optional[BetaCacheControlEphemeralParam] + """Create a cache control breakpoint at this content block.""" + + defer_loading: bool + """If true, tool will not be included in initial system prompt. + + Only loaded when returned via tool_reference from tool search. + """ + + strict: bool + """When true, guarantees schema validation on tool names and inputs""" diff --git a/src/anthropic/types/beta/beta_fallback_block.py b/src/anthropic/types/beta/beta_fallback_block.py index dc14be406..c8a07dabc 100644 --- a/src/anthropic/types/beta/beta_fallback_block.py +++ b/src/anthropic/types/beta/beta_fallback_block.py @@ -6,6 +6,7 @@ from ..._models import BaseModel from .beta_fallback_info import BetaFallbackInfo +from .beta_fallback_refusal_trigger import BetaFallbackRefusalTrigger __all__ = ["BetaFallbackBlock"] @@ -14,9 +15,9 @@ class BetaFallbackBlock(BaseModel): """Marks the point in `content` where one model's output gives way to the next. One block appears per hop where a preceding model actually ran this turn and - declined. A turn routed directly by the sticky decision has no such boundary - and carries no block — the signal for whether a fallback model served the - response is the presence of a `fallback_message` entry in + declined. A turn where no preceding model ran and declined has no such + boundary and carries no block — the signal for whether a fallback model + served the response is the presence of a `fallback_message` entry in `usage.iterations`, not this block. The block is treated like a server-tool content block for streaming: it @@ -38,4 +39,7 @@ class BetaFallbackBlock(BaseModel): Its `model` is always the canonical id. """ + trigger: BetaFallbackRefusalTrigger + """What caused the `from` model to hand over at this hop.""" + type: Literal["fallback"] diff --git a/src/anthropic/types/beta/beta_fallback_block_param.py b/src/anthropic/types/beta/beta_fallback_block_param.py index f490b28da..8d8d9c41e 100644 --- a/src/anthropic/types/beta/beta_fallback_block_param.py +++ b/src/anthropic/types/beta/beta_fallback_block_param.py @@ -20,22 +20,26 @@ class BetaFallbackBlockParam(_BetaFallbackBlockParamReservedKeywords, total=False): """A `fallback` block echoed back from a prior response. - Accepted in `messages[].content` and never rendered into the prompt, - not validated against the request's `fallbacks` chain or top-level - `model`, and stripped before the sticky-routing cache key is computed. - - Callers should echo the assistant turn verbatim — block included. The - block's position is load-bearing for thinking verification: the thinking - runs on either side of a fallback hop carry independently-rooted - verification hash chains, and this block is the only record of where one - chain ends and the next begins. When thinking runs flank the boundary, - omitting the block merges the runs into one contiguous span whose hashes - cannot verify (the request is rejected), and moving it into the middle of - a single run splits that run's chain and is likewise rejected; between - non-thinking blocks the block's placement has no verification effect. + Accepted in `messages[].content` and not rendered into the prompt; not + validated against the request's `fallbacks` chain or top-level `model`. + + Echo the assistant turn back verbatim, including this block in its + original position. The block marks the boundary between content produced + before and after a fallback hop, and the server relies on that boundary + to validate the turn: when thinking runs flank the boundary, omitting + the block merges them into one span the server cannot validate (the + request is rejected), and moving it into the middle of a single run is + likewise rejected; between non-thinking blocks the block's placement has + no validation effect. """ to: Required[BetaFallbackInfoParam] """Identifies one hop of a fallback transition.""" type: Required[Literal["fallback"]] + + trigger: object + """The response block's `trigger`, echoed verbatim. + + Accepted and ignored by the server; any object or `null` is allowed. + """ diff --git a/src/anthropic/types/beta/beta_fallback_refusal_trigger.py b/src/anthropic/types/beta/beta_fallback_refusal_trigger.py new file mode 100644 index 000000000..76405cab2 --- /dev/null +++ b/src/anthropic/types/beta/beta_fallback_refusal_trigger.py @@ -0,0 +1,17 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["BetaFallbackRefusalTrigger"] + + +class BetaFallbackRefusalTrigger(BaseModel): + """The `from` model declined for policy reasons.""" + + category: Optional[Literal["cyber", "bio", "frontier_llm", "reasoning_extraction"]] = None + """The policy category that triggered a refusal.""" + + type: Literal["refusal"] diff --git a/src/anthropic/types/beta/beta_memory_tool_20250818_param.py b/src/anthropic/types/beta/beta_memory_tool_20250818_param.py index f89a65682..af833ce7f 100644 --- a/src/anthropic/types/beta/beta_memory_tool_20250818_param.py +++ b/src/anthropic/types/beta/beta_memory_tool_20250818_param.py @@ -19,7 +19,9 @@ class BetaMemoryTool20250818Param(TypedDict, total=False): type: Required[Literal["memory_20250818"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_refusal_stop_details.py b/src/anthropic/types/beta/beta_refusal_stop_details.py index 65c642da0..94e66b2b0 100644 --- a/src/anthropic/types/beta/beta_refusal_stop_details.py +++ b/src/anthropic/types/beta/beta_refusal_stop_details.py @@ -12,10 +12,7 @@ class BetaRefusalStopDetails(BaseModel): """Structured information about a refusal.""" category: Optional[Literal["cyber", "bio", "frontier_llm", "reasoning_extraction"]] = None - """The policy category that triggered the refusal. - - `null` when the refusal doesn't map to a named category. - """ + """The policy category that triggered a refusal.""" explanation: Optional[str] = None """Human-readable explanation of the refusal. diff --git a/src/anthropic/types/beta/beta_tool_bash_20241022_param.py b/src/anthropic/types/beta/beta_tool_bash_20241022_param.py index ef342f637..63cb4abc6 100644 --- a/src/anthropic/types/beta/beta_tool_bash_20241022_param.py +++ b/src/anthropic/types/beta/beta_tool_bash_20241022_param.py @@ -19,7 +19,9 @@ class BetaToolBash20241022Param(TypedDict, total=False): type: Required[Literal["bash_20241022"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_bash_20250124_param.py b/src/anthropic/types/beta/beta_tool_bash_20250124_param.py index 6394a1caf..a451b775f 100644 --- a/src/anthropic/types/beta/beta_tool_bash_20250124_param.py +++ b/src/anthropic/types/beta/beta_tool_bash_20250124_param.py @@ -19,7 +19,9 @@ class BetaToolBash20250124Param(TypedDict, total=False): type: Required[Literal["bash_20250124"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_computer_use_20241022_param.py b/src/anthropic/types/beta/beta_tool_computer_use_20241022_param.py index 177e8dd74..07f9d65f2 100644 --- a/src/anthropic/types/beta/beta_tool_computer_use_20241022_param.py +++ b/src/anthropic/types/beta/beta_tool_computer_use_20241022_param.py @@ -25,7 +25,9 @@ class BetaToolComputerUse20241022Param(TypedDict, total=False): type: Required[Literal["computer_20241022"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_computer_use_20250124_param.py b/src/anthropic/types/beta/beta_tool_computer_use_20250124_param.py index 3622b5b09..b28b4c44a 100644 --- a/src/anthropic/types/beta/beta_tool_computer_use_20250124_param.py +++ b/src/anthropic/types/beta/beta_tool_computer_use_20250124_param.py @@ -25,7 +25,9 @@ class BetaToolComputerUse20250124Param(TypedDict, total=False): type: Required[Literal["computer_20250124"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_computer_use_20251124_param.py b/src/anthropic/types/beta/beta_tool_computer_use_20251124_param.py index f4c8eb293..f646cdd3b 100644 --- a/src/anthropic/types/beta/beta_tool_computer_use_20251124_param.py +++ b/src/anthropic/types/beta/beta_tool_computer_use_20251124_param.py @@ -25,7 +25,9 @@ class BetaToolComputerUse20251124Param(TypedDict, total=False): type: Required[Literal["computer_20251124"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_param.py b/src/anthropic/types/beta/beta_tool_param.py index e64c26d2e..2f76ede98 100644 --- a/src/anthropic/types/beta/beta_tool_param.py +++ b/src/anthropic/types/beta/beta_tool_param.py @@ -41,7 +41,9 @@ class BetaToolParam(TypedDict, total=False): This is how the tool will be called by the model and in `tool_use` blocks. """ - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_search_tool_bm25_20251119_param.py b/src/anthropic/types/beta/beta_tool_search_tool_bm25_20251119_param.py index 41e973a2c..81752c51a 100644 --- a/src/anthropic/types/beta/beta_tool_search_tool_bm25_20251119_param.py +++ b/src/anthropic/types/beta/beta_tool_search_tool_bm25_20251119_param.py @@ -19,7 +19,9 @@ class BetaToolSearchToolBm25_20251119Param(TypedDict, total=False): type: Required[Literal["tool_search_tool_bm25_20251119", "tool_search_tool_bm25"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_search_tool_regex_20251119_param.py b/src/anthropic/types/beta/beta_tool_search_tool_regex_20251119_param.py index fef4bb439..c75f05a94 100644 --- a/src/anthropic/types/beta/beta_tool_search_tool_regex_20251119_param.py +++ b/src/anthropic/types/beta/beta_tool_search_tool_regex_20251119_param.py @@ -19,7 +19,9 @@ class BetaToolSearchToolRegex20251119Param(TypedDict, total=False): type: Required[Literal["tool_search_tool_regex_20251119", "tool_search_tool_regex"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_text_editor_20241022_param.py b/src/anthropic/types/beta/beta_tool_text_editor_20241022_param.py index 577f0af5f..54bca9963 100644 --- a/src/anthropic/types/beta/beta_tool_text_editor_20241022_param.py +++ b/src/anthropic/types/beta/beta_tool_text_editor_20241022_param.py @@ -19,7 +19,9 @@ class BetaToolTextEditor20241022Param(TypedDict, total=False): type: Required[Literal["text_editor_20241022"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_text_editor_20250124_param.py b/src/anthropic/types/beta/beta_tool_text_editor_20250124_param.py index fd6462ad7..44d68a028 100644 --- a/src/anthropic/types/beta/beta_tool_text_editor_20250124_param.py +++ b/src/anthropic/types/beta/beta_tool_text_editor_20250124_param.py @@ -19,7 +19,9 @@ class BetaToolTextEditor20250124Param(TypedDict, total=False): type: Required[Literal["text_editor_20250124"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_text_editor_20250429_param.py b/src/anthropic/types/beta/beta_tool_text_editor_20250429_param.py index d9caa053b..03176f7bd 100644 --- a/src/anthropic/types/beta/beta_tool_text_editor_20250429_param.py +++ b/src/anthropic/types/beta/beta_tool_text_editor_20250429_param.py @@ -19,7 +19,9 @@ class BetaToolTextEditor20250429Param(TypedDict, total=False): type: Required[Literal["text_editor_20250429"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_text_editor_20250728_param.py b/src/anthropic/types/beta/beta_tool_text_editor_20250728_param.py index 3172e0bba..41d222087 100644 --- a/src/anthropic/types/beta/beta_tool_text_editor_20250728_param.py +++ b/src/anthropic/types/beta/beta_tool_text_editor_20250728_param.py @@ -19,7 +19,9 @@ class BetaToolTextEditor20250728Param(TypedDict, total=False): type: Required[Literal["text_editor_20250728"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[BetaCacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/beta/beta_tool_union_param.py b/src/anthropic/types/beta/beta_tool_union_param.py index 1899c7da8..dafcf0688 100644 --- a/src/anthropic/types/beta/beta_tool_union_param.py +++ b/src/anthropic/types/beta/beta_tool_union_param.py @@ -26,6 +26,7 @@ from .beta_code_execution_tool_20250522_param import BetaCodeExecutionTool20250522Param from .beta_code_execution_tool_20250825_param import BetaCodeExecutionTool20250825Param from .beta_code_execution_tool_20260120_param import BetaCodeExecutionTool20260120Param +from .beta_code_execution_tool_20260521_param import BetaCodeExecutionTool20260521Param from .beta_tool_search_tool_bm25_20251119_param import BetaToolSearchToolBm25_20251119Param from .beta_tool_search_tool_regex_20251119_param import BetaToolSearchToolRegex20251119Param @@ -38,6 +39,7 @@ BetaCodeExecutionTool20250522Param, BetaCodeExecutionTool20250825Param, BetaCodeExecutionTool20260120Param, + BetaCodeExecutionTool20260521Param, BetaToolComputerUse20241022Param, BetaMemoryTool20250818Param, BetaToolComputerUse20250124Param, diff --git a/src/anthropic/types/beta/beta_web_fetch_tool_20250910_param.py b/src/anthropic/types/beta/beta_web_fetch_tool_20250910_param.py index d323c05ac..723dda6ad 100644 --- a/src/anthropic/types/beta/beta_web_fetch_tool_20250910_param.py +++ b/src/anthropic/types/beta/beta_web_fetch_tool_20250910_param.py @@ -21,7 +21,9 @@ class BetaWebFetchTool20250910Param(TypedDict, total=False): type: Required[Literal["web_fetch_20250910"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """List of domains to allow fetching from""" diff --git a/src/anthropic/types/beta/beta_web_fetch_tool_20260209_param.py b/src/anthropic/types/beta/beta_web_fetch_tool_20260209_param.py index e1b9f0452..0a6a4caa1 100644 --- a/src/anthropic/types/beta/beta_web_fetch_tool_20260209_param.py +++ b/src/anthropic/types/beta/beta_web_fetch_tool_20260209_param.py @@ -21,7 +21,9 @@ class BetaWebFetchTool20260209Param(TypedDict, total=False): type: Required[Literal["web_fetch_20260209"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """List of domains to allow fetching from""" diff --git a/src/anthropic/types/beta/beta_web_fetch_tool_20260309_param.py b/src/anthropic/types/beta/beta_web_fetch_tool_20260309_param.py index 10db1e93e..498dc66ec 100644 --- a/src/anthropic/types/beta/beta_web_fetch_tool_20260309_param.py +++ b/src/anthropic/types/beta/beta_web_fetch_tool_20260309_param.py @@ -23,7 +23,9 @@ class BetaWebFetchTool20260309Param(TypedDict, total=False): type: Required[Literal["web_fetch_20260309"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """List of domains to allow fetching from""" diff --git a/src/anthropic/types/beta/beta_web_search_tool_20250305_param.py b/src/anthropic/types/beta/beta_web_search_tool_20250305_param.py index 5b5c2a44d..53fe11c2b 100644 --- a/src/anthropic/types/beta/beta_web_search_tool_20250305_param.py +++ b/src/anthropic/types/beta/beta_web_search_tool_20250305_param.py @@ -21,7 +21,9 @@ class BetaWebSearchTool20250305Param(TypedDict, total=False): type: Required[Literal["web_search_20250305"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """If provided, only these domains will be included in results. diff --git a/src/anthropic/types/beta/beta_web_search_tool_20260209_param.py b/src/anthropic/types/beta/beta_web_search_tool_20260209_param.py index 9fd5caec4..69f3b9ea0 100644 --- a/src/anthropic/types/beta/beta_web_search_tool_20260209_param.py +++ b/src/anthropic/types/beta/beta_web_search_tool_20260209_param.py @@ -21,7 +21,9 @@ class BetaWebSearchTool20260209Param(TypedDict, total=False): type: Required[Literal["web_search_20260209"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """If provided, only these domains will be included in results. diff --git a/src/anthropic/types/beta/beta_webhook_event_data.py b/src/anthropic/types/beta/beta_webhook_event_data.py index db2cb99d0..a99c477aa 100644 --- a/src/anthropic/types/beta/beta_webhook_event_data.py +++ b/src/anthropic/types/beta/beta_webhook_event_data.py @@ -12,6 +12,7 @@ from .beta_webhook_session_deleted_event_data import BetaWebhookSessionDeletedEventData from .beta_webhook_session_pending_event_data import BetaWebhookSessionPendingEventData from .beta_webhook_session_running_event_data import BetaWebhookSessionRunningEventData +from .beta_webhook_session_updated_event_data import BetaWebhookSessionUpdatedEventData from .beta_webhook_session_archived_event_data import BetaWebhookSessionArchivedEventData from .beta_webhook_session_status_idled_event_data import BetaWebhookSessionStatusIdledEventData from .beta_webhook_session_thread_idled_event_data import BetaWebhookSessionThreadIdledEventData @@ -53,6 +54,7 @@ BetaWebhookVaultCredentialArchivedEventData, BetaWebhookVaultCredentialDeletedEventData, BetaWebhookVaultCredentialRefreshFailedEventData, + BetaWebhookSessionUpdatedEventData, ], PropertyInfo(discriminator="type"), ] diff --git a/src/anthropic/types/beta/beta_webhook_session_updated_event_data.py b/src/anthropic/types/beta/beta_webhook_session_updated_event_data.py new file mode 100644 index 000000000..2a0f875bf --- /dev/null +++ b/src/anthropic/types/beta/beta_webhook_session_updated_event_data.py @@ -0,0 +1,18 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from ..._models import BaseModel + +__all__ = ["BetaWebhookSessionUpdatedEventData"] + + +class BetaWebhookSessionUpdatedEventData(BaseModel): + id: str + """ID of the session that triggered the event.""" + + organization_id: str + + type: Literal["session.updated"] + + workspace_id: str diff --git a/src/anthropic/types/beta/message_count_tokens_params.py b/src/anthropic/types/beta/message_count_tokens_params.py index c7e4cae08..7f68ee1bf 100644 --- a/src/anthropic/types/beta/message_count_tokens_params.py +++ b/src/anthropic/types/beta/message_count_tokens_params.py @@ -37,6 +37,7 @@ from .beta_code_execution_tool_20250522_param import BetaCodeExecutionTool20250522Param from .beta_code_execution_tool_20250825_param import BetaCodeExecutionTool20250825Param from .beta_code_execution_tool_20260120_param import BetaCodeExecutionTool20260120Param +from .beta_code_execution_tool_20260521_param import BetaCodeExecutionTool20260521Param from .beta_tool_search_tool_bm25_20251119_param import BetaToolSearchToolBm25_20251119Param from .beta_tool_search_tool_regex_20251119_param import BetaToolSearchToolRegex20251119Param from .beta_request_mcp_server_url_definition_param import BetaRequestMCPServerURLDefinitionParam @@ -271,6 +272,7 @@ class MessageCountTokensParams(TypedDict, total=False): BetaCodeExecutionTool20250522Param, BetaCodeExecutionTool20250825Param, BetaCodeExecutionTool20260120Param, + BetaCodeExecutionTool20260521Param, BetaToolComputerUse20241022Param, BetaMemoryTool20250818Param, BetaToolComputerUse20250124Param, diff --git a/src/anthropic/types/code_execution_tool_20250522_param.py b/src/anthropic/types/code_execution_tool_20250522_param.py index 9ac5f611e..157bf3a12 100644 --- a/src/anthropic/types/code_execution_tool_20250522_param.py +++ b/src/anthropic/types/code_execution_tool_20250522_param.py @@ -19,7 +19,9 @@ class CodeExecutionTool20250522Param(TypedDict, total=False): type: Required[Literal["code_execution_20250522"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/code_execution_tool_20250825_param.py b/src/anthropic/types/code_execution_tool_20250825_param.py index bd9af5eab..09dd052a8 100644 --- a/src/anthropic/types/code_execution_tool_20250825_param.py +++ b/src/anthropic/types/code_execution_tool_20250825_param.py @@ -19,7 +19,9 @@ class CodeExecutionTool20250825Param(TypedDict, total=False): type: Required[Literal["code_execution_20250825"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/code_execution_tool_20260120_param.py b/src/anthropic/types/code_execution_tool_20260120_param.py index 36d5fd23e..f7cd85951 100644 --- a/src/anthropic/types/code_execution_tool_20260120_param.py +++ b/src/anthropic/types/code_execution_tool_20260120_param.py @@ -23,7 +23,9 @@ class CodeExecutionTool20260120Param(TypedDict, total=False): type: Required[Literal["code_execution_20260120"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/code_execution_tool_20260521_param.py b/src/anthropic/types/code_execution_tool_20260521_param.py new file mode 100644 index 000000000..4d1c75971 --- /dev/null +++ b/src/anthropic/types/code_execution_tool_20260521_param.py @@ -0,0 +1,38 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing import List, Optional +from typing_extensions import Literal, Required, TypedDict + +from .cache_control_ephemeral_param import CacheControlEphemeralParam + +__all__ = ["CodeExecutionTool20260521Param"] + + +class CodeExecutionTool20260521Param(TypedDict, total=False): + """Code execution tool with REPL state persistence.""" + + name: Required[Literal["code_execution"]] + """Name of the tool. + + This is how the tool will be called by the model and in `tool_use` blocks. + """ + + type: Required[Literal["code_execution_20260521"]] + + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] + + cache_control: Optional[CacheControlEphemeralParam] + """Create a cache control breakpoint at this content block.""" + + defer_loading: bool + """If true, tool will not be included in initial system prompt. + + Only loaded when returned via tool_reference from tool search. + """ + + strict: bool + """When true, guarantees schema validation on tool names and inputs""" diff --git a/src/anthropic/types/memory_tool_20250818_param.py b/src/anthropic/types/memory_tool_20250818_param.py index a8e199e05..a4e84912d 100644 --- a/src/anthropic/types/memory_tool_20250818_param.py +++ b/src/anthropic/types/memory_tool_20250818_param.py @@ -19,7 +19,9 @@ class MemoryTool20250818Param(TypedDict, total=False): type: Required[Literal["memory_20250818"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/message_count_tokens_tool_param.py b/src/anthropic/types/message_count_tokens_tool_param.py index 09cf99ca4..c500cf7a6 100644 --- a/src/anthropic/types/message_count_tokens_tool_param.py +++ b/src/anthropic/types/message_count_tokens_tool_param.py @@ -19,6 +19,7 @@ from .code_execution_tool_20250522_param import CodeExecutionTool20250522Param from .code_execution_tool_20250825_param import CodeExecutionTool20250825Param from .code_execution_tool_20260120_param import CodeExecutionTool20260120Param +from .code_execution_tool_20260521_param import CodeExecutionTool20260521Param from .tool_search_tool_bm25_20251119_param import ToolSearchToolBm25_20251119Param from .tool_search_tool_regex_20251119_param import ToolSearchToolRegex20251119Param @@ -30,6 +31,7 @@ CodeExecutionTool20250522Param, CodeExecutionTool20250825Param, CodeExecutionTool20260120Param, + CodeExecutionTool20260521Param, MemoryTool20250818Param, ToolTextEditor20250124Param, ToolTextEditor20250429Param, diff --git a/src/anthropic/types/refusal_stop_details.py b/src/anthropic/types/refusal_stop_details.py index 6a71c57bc..5ea3cb440 100644 --- a/src/anthropic/types/refusal_stop_details.py +++ b/src/anthropic/types/refusal_stop_details.py @@ -12,10 +12,7 @@ class RefusalStopDetails(BaseModel): """Structured information about a refusal.""" category: Optional[Literal["cyber", "bio", "frontier_llm", "reasoning_extraction"]] = None - """The policy category that triggered the refusal. - - `null` when the refusal doesn't map to a named category. - """ + """The policy category that triggered a refusal.""" explanation: Optional[str] = None """Human-readable explanation of the refusal. diff --git a/src/anthropic/types/tool_bash_20250124_param.py b/src/anthropic/types/tool_bash_20250124_param.py index a913c018d..d18166490 100644 --- a/src/anthropic/types/tool_bash_20250124_param.py +++ b/src/anthropic/types/tool_bash_20250124_param.py @@ -19,7 +19,9 @@ class ToolBash20250124Param(TypedDict, total=False): type: Required[Literal["bash_20250124"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/tool_param.py b/src/anthropic/types/tool_param.py index 04a93cb62..cd4220f10 100644 --- a/src/anthropic/types/tool_param.py +++ b/src/anthropic/types/tool_param.py @@ -44,7 +44,9 @@ class ToolParam(TypedDict, total=False): This is how the tool will be called by the model and in `tool_use` blocks. """ - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/tool_search_tool_bm25_20251119_param.py b/src/anthropic/types/tool_search_tool_bm25_20251119_param.py index 163053f23..18089ecb2 100644 --- a/src/anthropic/types/tool_search_tool_bm25_20251119_param.py +++ b/src/anthropic/types/tool_search_tool_bm25_20251119_param.py @@ -19,7 +19,9 @@ class ToolSearchToolBm25_20251119Param(TypedDict, total=False): type: Required[Literal["tool_search_tool_bm25_20251119", "tool_search_tool_bm25"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/tool_search_tool_regex_20251119_param.py b/src/anthropic/types/tool_search_tool_regex_20251119_param.py index f1b0291db..45543d9e9 100644 --- a/src/anthropic/types/tool_search_tool_regex_20251119_param.py +++ b/src/anthropic/types/tool_search_tool_regex_20251119_param.py @@ -19,7 +19,9 @@ class ToolSearchToolRegex20251119Param(TypedDict, total=False): type: Required[Literal["tool_search_tool_regex_20251119", "tool_search_tool_regex"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/tool_text_editor_20250124_param.py b/src/anthropic/types/tool_text_editor_20250124_param.py index cdb9a64b7..9715f66ce 100644 --- a/src/anthropic/types/tool_text_editor_20250124_param.py +++ b/src/anthropic/types/tool_text_editor_20250124_param.py @@ -19,7 +19,9 @@ class ToolTextEditor20250124Param(TypedDict, total=False): type: Required[Literal["text_editor_20250124"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/tool_text_editor_20250429_param.py b/src/anthropic/types/tool_text_editor_20250429_param.py index 22ddfd62b..bbffda94b 100644 --- a/src/anthropic/types/tool_text_editor_20250429_param.py +++ b/src/anthropic/types/tool_text_editor_20250429_param.py @@ -19,7 +19,9 @@ class ToolTextEditor20250429Param(TypedDict, total=False): type: Required[Literal["text_editor_20250429"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/tool_text_editor_20250728_param.py b/src/anthropic/types/tool_text_editor_20250728_param.py index 239e0b11a..c58025fe5 100644 --- a/src/anthropic/types/tool_text_editor_20250728_param.py +++ b/src/anthropic/types/tool_text_editor_20250728_param.py @@ -19,7 +19,9 @@ class ToolTextEditor20250728Param(TypedDict, total=False): type: Required[Literal["text_editor_20250728"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] cache_control: Optional[CacheControlEphemeralParam] """Create a cache control breakpoint at this content block.""" diff --git a/src/anthropic/types/tool_union_param.py b/src/anthropic/types/tool_union_param.py index b74efaba6..8dc4481e5 100644 --- a/src/anthropic/types/tool_union_param.py +++ b/src/anthropic/types/tool_union_param.py @@ -19,6 +19,7 @@ from .code_execution_tool_20250522_param import CodeExecutionTool20250522Param from .code_execution_tool_20250825_param import CodeExecutionTool20250825Param from .code_execution_tool_20260120_param import CodeExecutionTool20260120Param +from .code_execution_tool_20260521_param import CodeExecutionTool20260521Param from .tool_search_tool_bm25_20251119_param import ToolSearchToolBm25_20251119Param from .tool_search_tool_regex_20251119_param import ToolSearchToolRegex20251119Param @@ -30,6 +31,7 @@ CodeExecutionTool20250522Param, CodeExecutionTool20250825Param, CodeExecutionTool20260120Param, + CodeExecutionTool20260521Param, MemoryTool20250818Param, ToolTextEditor20250124Param, ToolTextEditor20250429Param, diff --git a/src/anthropic/types/web_fetch_tool_20250910_param.py b/src/anthropic/types/web_fetch_tool_20250910_param.py index 88b72913c..fefc8b495 100644 --- a/src/anthropic/types/web_fetch_tool_20250910_param.py +++ b/src/anthropic/types/web_fetch_tool_20250910_param.py @@ -21,7 +21,9 @@ class WebFetchTool20250910Param(TypedDict, total=False): type: Required[Literal["web_fetch_20250910"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """List of domains to allow fetching from""" diff --git a/src/anthropic/types/web_fetch_tool_20260209_param.py b/src/anthropic/types/web_fetch_tool_20260209_param.py index c2155cc59..9b5eca2c2 100644 --- a/src/anthropic/types/web_fetch_tool_20260209_param.py +++ b/src/anthropic/types/web_fetch_tool_20260209_param.py @@ -21,7 +21,9 @@ class WebFetchTool20260209Param(TypedDict, total=False): type: Required[Literal["web_fetch_20260209"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """List of domains to allow fetching from""" diff --git a/src/anthropic/types/web_fetch_tool_20260309_param.py b/src/anthropic/types/web_fetch_tool_20260309_param.py index 207f31353..49d1eca2d 100644 --- a/src/anthropic/types/web_fetch_tool_20260309_param.py +++ b/src/anthropic/types/web_fetch_tool_20260309_param.py @@ -23,7 +23,9 @@ class WebFetchTool20260309Param(TypedDict, total=False): type: Required[Literal["web_fetch_20260309"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """List of domains to allow fetching from""" diff --git a/src/anthropic/types/web_search_tool_20250305_param.py b/src/anthropic/types/web_search_tool_20250305_param.py index ecbdca1e5..97521cb56 100644 --- a/src/anthropic/types/web_search_tool_20250305_param.py +++ b/src/anthropic/types/web_search_tool_20250305_param.py @@ -21,7 +21,9 @@ class WebSearchTool20250305Param(TypedDict, total=False): type: Required[Literal["web_search_20250305"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """If provided, only these domains will be included in results. diff --git a/src/anthropic/types/web_search_tool_20260209_param.py b/src/anthropic/types/web_search_tool_20260209_param.py index 7bf62f39f..2b2afbe76 100644 --- a/src/anthropic/types/web_search_tool_20260209_param.py +++ b/src/anthropic/types/web_search_tool_20260209_param.py @@ -21,7 +21,9 @@ class WebSearchTool20260209Param(TypedDict, total=False): type: Required[Literal["web_search_20260209"]] - allowed_callers: List[Literal["direct", "code_execution_20250825", "code_execution_20260120"]] + allowed_callers: List[ + Literal["direct", "code_execution_20250825", "code_execution_20260120", "code_execution_20260521"] + ] allowed_domains: Optional[SequenceNotStr[str]] """If provided, only these domains will be included in results. diff --git a/tests/lib/streaming/fixtures/fallback_response.txt b/tests/lib/streaming/fixtures/fallback_response.txt index 09a6a104e..c077ecbc2 100644 --- a/tests/lib/streaming/fixtures/fallback_response.txt +++ b/tests/lib/streaming/fixtures/fallback_response.txt @@ -2,7 +2,7 @@ event: message_start data: {"type":"message_start","message":{"id":"msg_01FallbackModelRelabel000001","type":"message","role":"assistant","content":[],"model":"claude-opus-4-7","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":25,"output_tokens":1}}} event: content_block_start -data: {"type":"content_block_start","index":0,"content_block":{"type":"fallback","from":{"model":"claude-opus-4-7"},"to":{"model":"claude-sonnet-4-5"}}} +data: {"type":"content_block_start","index":0,"content_block":{"type":"fallback","from":{"model":"claude-opus-4-7"},"to":{"model":"claude-sonnet-4-5"},"trigger":{"type":"refusal","category":null}}} event: content_block_stop data: {"type":"content_block_stop","index":0} diff --git a/tests/lib/streaming/test_parsed_content_blocks.py b/tests/lib/streaming/test_parsed_content_blocks.py index a08ef5122..84401ff0a 100644 --- a/tests/lib/streaming/test_parsed_content_blocks.py +++ b/tests/lib/streaming/test_parsed_content_blocks.py @@ -79,6 +79,7 @@ def test_streamed_fallback_block_is_constructed_as_fallback_block() -> None: "type": "fallback", "from": {"model": "claude-sonnet-4-5"}, "to": {"model": "claude-haiku-4-5"}, + "trigger": {"type": "refusal", "category": None}, }, } message = accumulate_event( diff --git a/tests/lib/test_refusal_fallback.py b/tests/lib/test_refusal_fallback.py index 18390e445..2fba739cb 100644 --- a/tests/lib/test_refusal_fallback.py +++ b/tests/lib/test_refusal_fallback.py @@ -119,7 +119,7 @@ def test_retries_a_refusal_with_the_fallback_params_and_credit_token(self, respx # a `fallback` seam block is prepended at the model boundary — the same # block shape the streaming splice emits assert [block.to_dict() for block in result.content] == [ - {"type": "fallback", "from": {"model": "primary-model"}, "to": {"model": "fallback-model"}} + {"type": "fallback", "from": {"model": "primary-model"}, "to": {"model": "fallback-model"}, "trigger": {"type": "refusal", "category": None}} ] bodies = request_bodies(respx_mock) assert [body["model"] for body in bodies] == ["primary-model", "fallback-model"] @@ -235,8 +235,8 @@ def test_walks_each_hop_through_the_chain_until_a_model_accepts(self, respx_mock assert state.index == 1 # one seam per model boundary, in hop order, ahead of the served content assert [block.to_dict() for block in result.content] == [ - {"type": "fallback", "from": {"model": "primary-model"}, "to": {"model": "mid-model"}}, - {"type": "fallback", "from": {"model": "mid-model"}, "to": {"model": "last-model"}}, + {"type": "fallback", "from": {"model": "primary-model"}, "to": {"model": "mid-model"}, "trigger": {"type": "refusal", "category": None}}, + {"type": "fallback", "from": {"model": "mid-model"}, "to": {"model": "last-model"}, "trigger": {"type": "refusal", "category": None}}, {"type": "text", "text": "ok"}, ] bodies = request_bodies(respx_mock) @@ -261,7 +261,7 @@ def test_a_pinned_continuation_seams_from_the_pinned_model(self, respx_mock: Moc assert result.model == "last-model" assert [block.to_dict() for block in result.content] == [ - {"type": "fallback", "from": {"model": "mid-model"}, "to": {"model": "last-model"}}, + {"type": "fallback", "from": {"model": "mid-model"}, "to": {"model": "last-model"}, "trigger": {"type": "refusal", "category": None}}, {"type": "text", "text": "ok"}, ] bodies = request_bodies(respx_mock) @@ -428,7 +428,7 @@ async def test_retries_a_refusal_with_the_fallback_params_and_credit_token(self, assert result.model == "fallback-model" assert [block.to_dict() for block in result.content] == [ - {"type": "fallback", "from": {"model": "primary-model"}, "to": {"model": "fallback-model"}} + {"type": "fallback", "from": {"model": "primary-model"}, "to": {"model": "fallback-model"}, "trigger": {"type": "refusal", "category": None}} ] bodies = request_bodies(respx_mock) assert [body["model"] for body in bodies] == ["primary-model", "fallback-model"] diff --git a/tests/test_transform.py b/tests/test_transform.py index 513b09cac..68fe46a2d 100644 --- a/tests/test_transform.py +++ b/tests/test_transform.py @@ -384,6 +384,7 @@ async def test_pydantic_aliased_field_round_trip(use_async: bool) -> None: { "from": {"model": "model-a"}, "to": {"model": "model-b"}, + "trigger": {"type": "refusal", "category": None}, "type": "fallback", }, ) @@ -391,7 +392,12 @@ async def test_pydantic_aliased_field_round_trip(use_async: bool) -> None: message = cast("BetaMessageParam", {"role": "assistant", "content": [block]}) params = cast(Any, await transform(message, BetaMessageParam, use_async)) - assert params["content"][0] == {"from": {"model": "model-a"}, "to": {"model": "model-b"}, "type": "fallback"} + assert params["content"][0] == { + "from": {"model": "model-a"}, + "to": {"model": "model-b"}, + "trigger": {"type": "refusal", "category": None}, + "type": "fallback", + } assert "from_" not in params["content"][0] From 9473321280ebe61350ac5b70b5587a7851828531 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2026 17:10:53 +0000 Subject: [PATCH 5/5] release: 0.110.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 15 +++++++++++++++ pyproject.toml | 2 +- src/anthropic/_version.py | 2 +- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 5de7008fb..c37434e9f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.109.2" + ".": "0.110.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b75a44b02..a32dece16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 0.110.0 (2026-06-18) + +Full Changelog: [v0.109.2...v0.110.0](https://github.com/anthropics/anthropic-sdk-python/compare/v0.109.2...v0.110.0) + +### Features + +* **api:** add support for new code_execution_20260120 tool ([5e23212](https://github.com/anthropics/anthropic-sdk-python/commit/5e23212dc0883174c879b97ef8e7e33ead4e8da5)) + + +### Bug Fixes + +* append x-stainless-helper across header merges instead of clobbering ([#105](https://github.com/anthropics/anthropic-sdk-python/issues/105)) ([922558e](https://github.com/anthropics/anthropic-sdk-python/commit/922558e2ce52e18863dab27bcc04067068827364)) +* **bedrock:** preserve stream event type ([#1682](https://github.com/anthropics/anthropic-sdk-python/issues/1682)) ([b27e343](https://github.com/anthropics/anthropic-sdk-python/commit/b27e3439699174dbc41e34e2d6ef5cb1e2930c18)) +* **helpers:** single source of truth for x-stainless-helper key + closed value vocabulary ([#95](https://github.com/anthropics/anthropic-sdk-python/issues/95)) ([e6f7a56](https://github.com/anthropics/anthropic-sdk-python/commit/e6f7a56bb624f4c946cb15ba7973fd6fe052e10f)) + ## 0.109.2 (2026-06-15) Full Changelog: [v0.109.1...v0.109.2](https://github.com/anthropics/anthropic-sdk-python/compare/v0.109.1...v0.109.2) diff --git a/pyproject.toml b/pyproject.toml index 133046e6a..73c91aa6c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "anthropic" -version = "0.109.2" +version = "0.110.0" description = "The official Python library for the anthropic API" dynamic = ["readme"] license = "MIT" diff --git a/src/anthropic/_version.py b/src/anthropic/_version.py index 0115536ff..bdcc2aee6 100644 --- a/src/anthropic/_version.py +++ b/src/anthropic/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "anthropic" -__version__ = "0.109.2" # x-release-please-version +__version__ = "0.110.0" # x-release-please-version