diff --git a/.github/scripts/get_min_versions.py b/.github/scripts/get_min_versions.py index acdfa4a46f..be32b940e5 100644 --- a/.github/scripts/get_min_versions.py +++ b/.github/scripts/get_min_versions.py @@ -23,6 +23,8 @@ MIN_VERSION_LIBS = [ "langchain-text-splitters", "numpy", "SQLAlchemy", + "anthropic", + "openai", ] # some libs only get checked on release because of simultaneous changes in diff --git a/libs/partners/anthropic/Makefile b/libs/partners/anthropic/Makefile index 6f4c846a98..b1b34efc34 100644 --- a/libs/partners/anthropic/Makefile +++ b/libs/partners/anthropic/Makefile @@ -11,10 +11,16 @@ TEST_FILE ?= tests/unit_tests/ PYTEST_EXTRA ?= integration_test integration_tests: TEST_FILE=tests/integration_tests/ +SDK_SMOKE_TEST = tests/integration_tests/test_chat_models.py::test_stream + test tests: uv run --group test pytest -vvv $(PYTEST_EXTRA) --disable-socket --allow-unix-socket $(TEST_FILE) +# Smoke each supported anthropic major first: 0.x is backed by `httpx`, 1.x by +# `httpx2`, and uv.lock only ever resolves one of them. integration_test integration_tests: + uv run --with 'anthropic>=0.120.0,<1.0.0' --group test --group test_integration pytest -v --tb=short $(SDK_SMOKE_TEST) + uv run --with 'anthropic>=1.0.0,<2.0.0' --group test --group test_integration pytest -v --tb=short $(SDK_SMOKE_TEST) uv run --group test --group test_integration pytest -v --tb=short -n auto --benchmark-disable --timeout 30 --retries 3 --retry-delay 1 $(TEST_FILE) test_watch: diff --git a/libs/partners/anthropic/langchain_anthropic/_sdk_compat.py b/libs/partners/anthropic/langchain_anthropic/_sdk_compat.py new file mode 100644 index 0000000000..62e7fa3a63 --- /dev/null +++ b/libs/partners/anthropic/langchain_anthropic/_sdk_compat.py @@ -0,0 +1,124 @@ +"""Adapt to the installed `anthropic` SDK major version. + +`langchain-anthropic` supports `anthropic` 0.x and 1.x. Two differences between +them reach this package, and each is resolved here by reading the answer off the +installed SDK rather than off its version string: + +- **Sampling parameters.** `anthropic>=1` dropped `temperature`, `top_p` and + `top_k` from its message-creation methods, since current models do not use + them. The API still honors them for older models, so they are relocated to + `extra_body`, which the SDK merges into the request JSON as-is. The wire + payload is identical on both majors. + +- **Raw responses.** `.with_raw_response` on the async client returns + `AsyncAPIResponse`, whose `parse()` is a coroutine; `anthropic<1` returned + `LegacyAPIResponse` with a synchronous `parse()`. + +`anthropic>=1` also moved its HTTP layer from `httpx` to +[`httpx2`](https://github.com/pydantic/httpx2), but that does not surface here: +this package hands the SDK only primitives (a `base_url` string, a float +`timeout`, a `proxy` URL string) and subclasses `anthropic.DefaultHttpxClient`, +which already points at the right library, so no `httpx` type crosses the +boundary. Only the unit tests inject `httpx` objects, and they resolve the +matching library themselves in `tests/unit_tests/_httpx_compat.py`. +""" + +from __future__ import annotations + +import inspect +import logging +import re +from collections.abc import Mapping +from functools import lru_cache +from typing import Any + +import anthropic + +logger = logging.getLogger(__name__) + +# Sampling parameters `anthropic>=1` no longer accepts as named arguments. +_SAMPLING_PARAMS = ("temperature", "top_p", "top_k") + + +def _sdk_major_version() -> int | None: + """Major version of the installed `anthropic` SDK, or `None` if unreadable. + + Tolerates tag-derived strings such as `v1.0.0`, which build tooling + sometimes leaves unstripped, by reading the first integer run. + """ + match = re.match(r"\D*(\d+)", str(getattr(anthropic, "__version__", ""))) + return int(match.group(1)) if match else None + + +@lru_cache(maxsize=1) +def _unsupported_sampling_params() -> frozenset[str]: + """Sampling parameters the installed SDK's message methods no longer accept. + + Derived from the signature of `Messages.create`, which `beta.messages` and + the async resources track. No client is instantiated, so this is safe to + call during payload construction without resolved credentials. + """ + try: + from anthropic.resources.messages import Messages + + accepted = inspect.signature(Messages.create).parameters + except (ImportError, AttributeError, TypeError, ValueError): + # The SDK moved the resource or the signature is unreadable. Guess from + # the major version rather than failing: a wrong guess costs a TypeError + # on the first request, an exception here breaks every import. + major = _sdk_major_version() + logger.warning( + "Could not introspect the anthropic SDK's `Messages.create` " + "signature; falling back to its major version (%r) to decide " + "whether `temperature`/`top_p`/`top_k` are still accepted.", + major, + ) + if major is not None and major >= 1: + return frozenset(_SAMPLING_PARAMS) + return frozenset() + + return frozenset(p for p in _SAMPLING_PARAMS if p not in accepted) + + +def _route_unsupported_sampling_params(payload: dict[str, Any]) -> dict[str, Any]: + """Move sampling params the installed SDK rejects into `extra_body`. + + A no-op on `anthropic<1`, where the message methods still accept them as + named arguments. Values the caller already set in `extra_body` win, since + that is the SDK's documented escape hatch and a deliberate choice. + + Args: + payload: Request payload, modified in place. + + Returns: + The same payload. + """ + unsupported = _unsupported_sampling_params() + if not unsupported: + return payload + + extra_body = payload.get("extra_body") + if extra_body is not None and not isinstance(extra_body, Mapping): + # A value the SDK would reject anyway. Leave the payload untouched + # rather than relocating params into something that cannot hold them. + return payload + + relocated = {k: payload.pop(k) for k in unsupported if k in payload} + if not relocated: + return payload + + payload["extra_body"] = {**relocated, **(extra_body or {})} + return payload + + +async def _aparse(raw_response: Any) -> Any: + """Parse a raw SDK response from the async client. + + `anthropic>=1` returns `AsyncAPIResponse`, whose `parse()` is a coroutine; + `anthropic<1` returned `LegacyAPIResponse`, whose `parse()` was + synchronous. + """ + parsed = raw_response.parse() + if inspect.isawaitable(parsed): + return await parsed + return parsed diff --git a/libs/partners/anthropic/langchain_anthropic/chat_models.py b/libs/partners/anthropic/langchain_anthropic/chat_models.py index ad7f858af9..bad2789fb1 100644 --- a/libs/partners/anthropic/langchain_anthropic/chat_models.py +++ b/libs/partners/anthropic/langchain_anthropic/chat_models.py @@ -84,6 +84,10 @@ from langchain_anthropic._client_utils import ( _get_default_httpx_client, ) from langchain_anthropic._compat import _convert_from_v1_to_anthropic +from langchain_anthropic._sdk_compat import ( + _aparse, + _route_unsupported_sampling_params, +) from langchain_anthropic.data._profiles import _PROFILES from langchain_anthropic.output_parsers import extract_tool_calls @@ -1736,7 +1740,9 @@ class ChatAnthropic(BaseChatModel): else: payload["betas"] = [required_beta] - return {k: v for k, v in payload.items() if v is not None} + return _route_unsupported_sampling_params( + {k: v for k, v in payload.items() if v is not None} + ) def _create(self, payload: dict) -> Any: try: @@ -1826,7 +1832,7 @@ class ChatAnthropic(BaseChatModel): base_generation_info: dict[str, Any] = {} if self._uses_gateway: _add_gateway_metadata(base_generation_info, raw_response) - stream = raw_response.parse() + stream = await _aparse(raw_response) coerce_content_to_string = ( not _tools_in_params(payload) and not _documents_in_params(payload) @@ -2185,7 +2191,7 @@ class ChatAnthropic(BaseChatModel): if self._uses_gateway: _add_gateway_metadata(generation_info, raw_response) return self._format_output( - raw_response.parse(), generation_info=generation_info, **kwargs + await _aparse(raw_response), generation_info=generation_info, **kwargs ) def _get_llm_for_structured_output_when_thinking_is_enabled( diff --git a/libs/partners/anthropic/langchain_anthropic/llms.py b/libs/partners/anthropic/langchain_anthropic/llms.py index 39a1d4ac8c..c6d96889f8 100644 --- a/libs/partners/anthropic/langchain_anthropic/llms.py +++ b/libs/partners/anthropic/langchain_anthropic/llms.py @@ -21,6 +21,8 @@ from langchain_core.utils.utils import _build_model_kwargs, from_env, secret_fro from pydantic import ConfigDict, Field, SecretStr, model_validator from typing_extensions import Self +from langchain_anthropic._sdk_compat import _route_unsupported_sampling_params + class _AnthropicCommon(BaseLanguageModel): client: Any = None @@ -276,6 +278,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): # Remove parameters not supported by Messages API params = {k: v for k, v in params.items() if k != "max_tokens_to_sample"} + params = _route_unsupported_sampling_params(params) response = self.client.messages.create( messages=self._format_messages(prompt), @@ -312,6 +315,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): # Remove parameters not supported by Messages API params = {k: v for k, v in params.items() if k != "max_tokens_to_sample"} + params = _route_unsupported_sampling_params(params) response = await self.async_client.messages.create( messages=self._format_messages(prompt), @@ -352,6 +356,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): # Remove parameters not supported by Messages API params = {k: v for k, v in params.items() if k != "max_tokens_to_sample"} + params = _route_unsupported_sampling_params(params) with self.client.messages.stream( messages=self._format_messages(prompt), @@ -397,6 +402,7 @@ class AnthropicLLM(LLM, _AnthropicCommon): # Remove parameters not supported by Messages API params = {k: v for k, v in params.items() if k != "max_tokens_to_sample"} + params = _route_unsupported_sampling_params(params) async with self.async_client.messages.stream( messages=self._format_messages(prompt), diff --git a/libs/partners/anthropic/pyproject.toml b/libs/partners/anthropic/pyproject.toml index 7e545bd44a..44c13ee607 100644 --- a/libs/partners/anthropic/pyproject.toml +++ b/libs/partners/anthropic/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ version = "1.6.1" requires-python = ">=3.10.0,<4.0.0" dependencies = [ - "anthropic>=0.120.0,<1.0.0", + "anthropic>=0.120.0,<2.0.0", "langchain-core>=1.6.0,<2.0.0", "pydantic>=2.7.4,<3.0.0", ] @@ -52,7 +52,7 @@ test = [ "pytest-timeout>=2.3.1,<3.0.0", "pytest-socket>=0.7.0,<1.0.0", "pytest-xdist>=3.8.0,<4.0.0", - "vcrpy>=8.0.0,<9.0.0", + "vcrpy>=8.2.0,<9.0.0", "langgraph-prebuilt>=0.7.0a2", # set explicitly until we have a stable version "langchain-tests>=1.1.9,<2.0.0", "langchain>=1.0.0,<2.0.0", diff --git a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py index 9c166e5eed..905c8a3064 100644 --- a/libs/partners/anthropic/tests/integration_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/integration_tests/test_chat_models.py @@ -140,7 +140,7 @@ async def test_astream() -> None: "model": MODEL_NAME, "max_tokens": 1024, "messages": [{"role": "user", "content": "hi"}], - "temperature": 0.0, + "extra_body": {"temperature": 0.0}, } stream = await async_client.messages.create(**params, stream=True) async for event in stream: diff --git a/libs/partners/anthropic/tests/unit_tests/_httpx_compat.py b/libs/partners/anthropic/tests/unit_tests/_httpx_compat.py new file mode 100644 index 0000000000..c78baac161 --- /dev/null +++ b/libs/partners/anthropic/tests/unit_tests/_httpx_compat.py @@ -0,0 +1,47 @@ +"""Resolve the `httpx` library the installed `anthropic` SDK is built on. + +Test-only. `langchain_anthropic` hands the SDK nothing but primitives — a +`base_url` string, a float `timeout`, a `proxy` URL string — and gets its client +class by subclassing `anthropic.DefaultHttpxClient`, which already points at the +right library. So no `httpx` type crosses that boundary in library code and +nothing there needs to know which library is in play. + +These tests do cross it: they inject mock transports, and the SDK accepts a +built client only from the library it is itself built on (`httpx` on +`anthropic<1`, `httpx2` on `anthropic>=1`), rejecting the other with a +`TypeError`. `anthropic.DefaultHttpxClient` subclasses that library's client +class, so its MRO names the one to use. +""" + +from __future__ import annotations + +import importlib + +import anthropic + +_HTTPX_MODULES = ("httpx2", "httpx") + + +def _resolve_httpx_module() -> str: + """Name of the HTTP library `anthropic.DefaultHttpxClient` derives from. + + Raises: + RuntimeError: If the SDK exposes no recognizable base class. Guessing + from the version string would be wrong as often as it was right; + failing at import makes the mismatch obvious here rather than in a + confusing downstream `TypeError`. + """ + for base in anthropic.DefaultHttpxClient.__mro__: + root = base.__module__.split(".", 1)[0] + if root in _HTTPX_MODULES: + return root + + msg = ( + f"Cannot tell which httpx library anthropic " + f"{getattr(anthropic, '__version__', 'unknown')} is built on: " + f"DefaultHttpxClient derives from none of {_HTTPX_MODULES}." + ) + raise RuntimeError(msg) + + +httpx = importlib.import_module(_resolve_httpx_module()) diff --git a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py index d5316c3b8a..0f2c3802c4 100644 --- a/libs/partners/anthropic/tests/unit_tests/test_chat_models.py +++ b/libs/partners/anthropic/tests/unit_tests/test_chat_models.py @@ -12,7 +12,6 @@ from typing import Any, Literal, cast from unittest.mock import MagicMock, patch import anthropic -import httpx import pytest from anthropic.types import Message, TextBlock, Usage from blockbuster import blockbuster_ctx @@ -45,6 +44,7 @@ from pydantic import BaseModel, Field, RootModel, SecretStr, ValidationError from pytest import CaptureFixture, MonkeyPatch from langchain_anthropic import ChatAnthropic +from langchain_anthropic._sdk_compat import _unsupported_sampling_params from langchain_anthropic._version import __version__ from langchain_anthropic.chat_models import ( _TOOL_CALL_ID_PATTERN, @@ -58,6 +58,7 @@ from langchain_anthropic.chat_models import ( _thinking_in_params, convert_to_anthropic_tool, ) +from tests.unit_tests._httpx_compat import httpx os.environ["ANTHROPIC_API_KEY"] = "foo" @@ -118,8 +119,10 @@ _STREAM_EVENTS: list[dict[str, Any]] = [ def _gateway_handler( expected_beta: str | None, -) -> Callable[[httpx.Request], httpx.Response]: - def handler(request: httpx.Request) -> httpx.Response: +) -> Callable[[Any], Any]: + # Annotated `Any`: the concrete request/response classes come from `httpx` + # or `httpx2` depending on the installed anthropic SDK. + def handler(request: Any) -> Any: assert request.headers.get("anthropic-beta") == expected_beta headers = {"x-langsmith-gateway-metadata": json.dumps(_GATEWAY_METADATA)} if json.loads(request.content).get("stream"): @@ -2507,6 +2510,24 @@ def test_mcp_tracing() -> None: assert payload["mcp_servers"][0]["authorization_token"] == "PLACEHOLDER" # noqa: S105 +def test_sampling_params_reach_the_request_payload() -> None: + """`temperature`/`top_p`/`top_k` land wherever the installed SDK accepts them. + + `anthropic>=1` dropped them as named arguments, so they are relocated to + `extra_body`; the resulting wire payload is the same on both majors. + """ + llm = ChatAnthropic(model=MODEL_NAME, temperature=0, top_p=0.9, top_k=5) + payload = llm._get_request_payload([HumanMessage("foo")]) + + sampling = {"temperature": 0, "top_p": 0.9, "top_k": 5} + if _unsupported_sampling_params(): + assert payload["extra_body"] == sampling + assert not sampling.keys() & payload.keys() + else: + assert {k: payload[k] for k in sampling} == sampling + assert "extra_body" not in payload + + def test_cache_control_kwarg() -> None: llm = ChatAnthropic(model=MODEL_NAME) diff --git a/libs/partners/anthropic/tests/unit_tests/test_sdk_compat.py b/libs/partners/anthropic/tests/unit_tests/test_sdk_compat.py new file mode 100644 index 0000000000..9412d5a546 --- /dev/null +++ b/libs/partners/anthropic/tests/unit_tests/test_sdk_compat.py @@ -0,0 +1,151 @@ +"""Tests for adapting to the installed `anthropic` SDK major version.""" + +from __future__ import annotations + +import inspect +from types import MappingProxyType +from typing import Any +from unittest.mock import patch + +import anthropic +import pytest + +from langchain_anthropic import _sdk_compat +from langchain_anthropic._sdk_compat import ( + _aparse, + _route_unsupported_sampling_params, + _sdk_major_version, + _unsupported_sampling_params, +) + + +@pytest.mark.parametrize( + ("version", "expected"), + [ + ("1.0.0", 1), + ("v1.0.0", 1), # tag-derived strings build tooling leaves unstripped + ("0.125.0", 0), + ("2.0.0rc1", 2), + ("not-a-version", None), + ], +) +def test_sdk_major_version_parsing(version: str, expected: int | None) -> None: + with patch.object(anthropic, "__version__", version): + assert _sdk_major_version() == expected + + +def test_unsupported_sampling_params_matches_the_sdk_signature() -> None: + """The dropped set is exactly what `Messages.create` no longer accepts.""" + from anthropic.resources.messages import Messages + + accepted = inspect.signature(Messages.create).parameters + assert _unsupported_sampling_params() == frozenset( + p for p in ("temperature", "top_p", "top_k") if p not in accepted + ) + + +def test_sampling_params_end_up_where_the_sdk_accepts_them() -> None: + """Sampling params stay top-level on 0.x and move to `extra_body` on 1.x.""" + payload = _route_unsupported_sampling_params( + {"model": "claude-sonnet-4-5", "temperature": 0.5, "top_p": 0.9, "top_k": 5} + ) + + if _unsupported_sampling_params(): + assert payload == { + "model": "claude-sonnet-4-5", + "extra_body": {"temperature": 0.5, "top_p": 0.9, "top_k": 5}, + } + else: + assert payload == { + "model": "claude-sonnet-4-5", + "temperature": 0.5, + "top_p": 0.9, + "top_k": 5, + } + + +def test_routing_is_a_no_op_without_sampling_params() -> None: + """No `extra_body` key is invented for payloads that set no sampling params.""" + assert _route_unsupported_sampling_params({"model": "claude-sonnet-4-5"}) == { + "model": "claude-sonnet-4-5" + } + + +def test_explicit_extra_body_wins_over_relocated_params() -> None: + """`extra_body` is the SDK's documented escape hatch; don't overwrite it.""" + with patch.object( + _sdk_compat, "_unsupported_sampling_params", lambda: frozenset({"temperature"}) + ): + payload = _route_unsupported_sampling_params( + {"temperature": 0.5, "extra_body": {"temperature": 0.1, "foo": "bar"}} + ) + + assert payload == {"extra_body": {"temperature": 0.1, "foo": "bar"}} + + +def test_non_dict_mapping_extra_body_still_receives_relocated_params() -> None: + """`extra_body` accepts any Mapping, not just `dict`.""" + with patch.object( + _sdk_compat, "_unsupported_sampling_params", lambda: frozenset({"temperature"}) + ): + payload = _route_unsupported_sampling_params( + {"temperature": 0.5, "extra_body": MappingProxyType({"foo": "bar"})} + ) + + assert payload == {"extra_body": {"temperature": 0.5, "foo": "bar"}} + + +def test_non_dict_extra_body_leaves_payload_untouched() -> None: + """A malformed `extra_body` is the SDK's to reject, not ours to merge into. + + Crucially the sampling params must survive: relocating them into a value + that cannot hold them would drop them silently. + """ + with patch.object( + _sdk_compat, "_unsupported_sampling_params", lambda: frozenset({"temperature"}) + ): + payload = _route_unsupported_sampling_params( + {"temperature": 0.5, "extra_body": "not-a-dict"} + ) + + assert payload == {"temperature": 0.5, "extra_body": "not-a-dict"} + + +def test_unsupported_sampling_params_falls_back_when_introspection_fails( + caplog: pytest.LogCaptureFixture, +) -> None: + """An unreadable signature guesses from the major version instead of raising.""" + _unsupported_sampling_params.cache_clear() + try: + with ( + patch.object(inspect, "signature", side_effect=ValueError("no signature")), + patch.object(anthropic, "__version__", "1.0.0"), + ): + assert _unsupported_sampling_params() == frozenset( + {"temperature", "top_p", "top_k"} + ) + finally: + _unsupported_sampling_params.cache_clear() + + assert "Could not introspect" in caplog.text + + +class _SyncRawResponse: + def parse(self) -> str: + return "sync" + + +class _AsyncRawResponse: + async def parse(self) -> str: + return "async" + + +@pytest.mark.parametrize( + ("raw_response", "expected"), + [(_SyncRawResponse(), "sync"), (_AsyncRawResponse(), "async")], +) +async def test_aparse_handles_both_raw_response_classes( + raw_response: Any, expected: str +) -> None: + """`anthropic<1` parsed synchronously; `anthropic>=1` returns a coroutine.""" + assert await _aparse(raw_response) == expected diff --git a/libs/partners/anthropic/uv.lock b/libs/partners/anthropic/uv.lock index 01b59738e8..61f312f8f2 100644 --- a/libs/partners/anthropic/uv.lock +++ b/libs/partners/anthropic/uv.lock @@ -6,9 +6,11 @@ resolution-markers = [ "python_full_version >= '3.13' and python_full_version < '3.15' and platform_python_implementation == 'PyPy'", "python_full_version >= '3.15' and platform_python_implementation != 'PyPy'", "python_full_version >= '3.13' and python_full_version < '3.15' and platform_python_implementation != 'PyPy'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation != 'PyPy'", + "python_full_version == '3.12.*' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten')", "python_full_version < '3.11' and platform_python_implementation != 'PyPy'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation == 'PyPy'", + "python_full_version == '3.12.*' and platform_python_implementation == 'PyPy' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'emscripten')", "python_full_version < '3.11' and platform_python_implementation == 'PyPy'", ] @@ -29,21 +31,20 @@ wheels = [ [[package]] name = "anthropic" -version = "0.120.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, { name = "docstring-parser" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/92/5c/3331da4fc009d448008a50c78d86cc929e8c937cd1442245ce3f80561c4e/anthropic-0.120.0.tar.gz", hash = "sha256:6ba6007dc9b00365b20f6101a6618f5196ac1ceef81512e4b5cc0e7436d4975d", size = 1008042, upload-time = "2026-07-24T16:32:52.384Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/40/8249e272c9bba8b4b5802f90717fcdb55996da243a712dca449f2d81c95b/anthropic-1.1.0.tar.gz", hash = "sha256:03d180143ca61177772a35350c78e6ff975b636fd226d221b122b1595f284f3e", size = 1132609, upload-time = "2026-08-26T17:14:53.335Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1b/8a/8522bdf809e1f95f0d9c936540987a3f6afba01d2921a2bf488dedf836a8/anthropic-0.120.0-py3-none-any.whl", hash = "sha256:591bd531563ec7b63a1e138f5c11f14cb94edda99623b349c2ce2ece8e08b8a5", size = 1022602, upload-time = "2026-07-24T16:32:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/93/9a/37a5d1913434913ed89bef960693870a1dd9478e76fd12e7b9f2c066270a/anthropic-1.1.0-py3-none-any.whl", hash = "sha256:babf7b217a6f289059a25ba4871c167411c747bbf233a4f1fb8ececc8a22f875", size = 1289646, upload-time = "2026-08-26T17:14:51.773Z" }, ] [[package]] @@ -320,15 +321,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] -[[package]] -name = "distro" -version = "1.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fc/f8/98eea607f65de6527f8a2e8885fc8015d3e6f5775df186e443e0964a11c3/distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed", size = 60722, upload-time = "2023-12-24T09:54:32.31Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/12/b3/231ffd4ab1fc9d679809f356cebee130ac7daa00d6d6f3206dd4fd137e9e/distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2", size = 20277, upload-time = "2023-12-24T09:54:30.421Z" }, -] - [[package]] name = "docstring-parser" version = "0.17.0" @@ -399,6 +391,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, + { name = "truststore", marker = "python_full_version != '3.12.*' or sys_platform != 'emscripten'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -415,12 +420,38 @@ wheels = [ ] [[package]] -name = "idna" -version = "3.15" +name = "httpx2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + +[[package]] +name = "idna" +version = "3.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -552,7 +583,7 @@ wheels = [ [[package]] name = "langchain" -version = "1.3.16" +version = "1.3.17" source = { editable = "../../langchain_v1" } dependencies = [ { name = "langchain-core" }, @@ -657,7 +688,7 @@ typing = [ [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.120.0,<1.0.0" }, + { name = "anthropic", specifier = ">=0.120.0,<2.0.0" }, { name = "langchain-core", editable = "../../core" }, { name = "pydantic", specifier = ">=2.7.4,<3.0.0" }, ] @@ -680,7 +711,7 @@ test = [ { name = "pytest-watcher", specifier = ">=0.3.4,<1.0.0" }, { name = "pytest-xdist", specifier = ">=3.8.0,<4.0.0" }, { name = "syrupy", specifier = ">=5.0.0,<6.0.0" }, - { name = "vcrpy", specifier = ">=8.0.0,<9.0.0" }, + { name = "vcrpy", specifier = ">=8.2.0,<9.0.0" }, ] test-integration = [{ name = "requests", specifier = ">=2.32.3,<3.0.0" }] typing = [ @@ -738,7 +769,7 @@ test = [ { name = "pytest-benchmark" }, { name = "pytest-codspeed" }, { name = "pytest-mock", specifier = ">=3.10.0,<4.0.0" }, - { name = "pytest-socket", specifier = ">=0.7.0,<1.0.0" }, + { name = "pytest-socket", specifier = ">=0.7.0,<0.8.0" }, { name = "pytest-watcher", specifier = ">=0.3.4,<1.0.0" }, { name = "pytest-xdist", specifier = ">=3.6.1,<4.0.0" }, { name = "responses", specifier = ">=0.25.0,<1.0.0" }, @@ -1137,8 +1168,10 @@ resolution-markers = [ "python_full_version >= '3.13' and python_full_version < '3.15' and platform_python_implementation == 'PyPy'", "python_full_version >= '3.15' and platform_python_implementation != 'PyPy'", "python_full_version >= '3.13' and python_full_version < '3.15' and platform_python_implementation != 'PyPy'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation != 'PyPy'", - "python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation == 'PyPy'", + "python_full_version == '3.12.*' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation != 'PyPy' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and platform_python_implementation != 'PyPy' and sys_platform == 'emscripten')", + "python_full_version == '3.12.*' and platform_python_implementation == 'PyPy' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and platform_python_implementation == 'PyPy' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and platform_python_implementation == 'PyPy' and sys_platform == 'emscripten')", ] sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } wheels = [ @@ -1928,6 +1961,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "types-requests" version = "2.31.0.6"