mirror of
https://github.com/langchain-ai/langchain.git
synced 2026-10-05 09:25:14 +03:00
feat(anthropic): support 1.0 sdk (#39938)
This commit is contained in:
1 parent
b0fb33f919
commit
fd2fbd702b
11 files changed
+440
-35
No files matched your search
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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())
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
Generated
+68
-26
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user