Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ dependencies = [
"aiofiles",
"aiohttp",
"alembic",
"anthropic==0.125.0",
"anthropic==1.0.0",
# 5.0.0 split Confluence into separate Cloud (REST v2) and Server (REST v1)
# classes with different method signatures. The major ceiling keeps the next
# such split from reaching production through a routine bump.
Expand Down
15 changes: 6 additions & 9 deletions requirements-lock.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,9 @@ annotated-types==0.8.0 \
--hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \
--hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0
# via pydantic
anthropic==0.125.0 \
--hash=sha256:3486013602eca76d8b12540764e53654f02cf4951110bca86cf06e67428a9f21 \
--hash=sha256:e0cdd336580cb7411c1cdab69f80973e9bf4bff7f8e08141811d46307d45c682
anthropic==1.0.0 \
--hash=sha256:32dd52e9e1d774393b27182f451398ba4262287a4d0eab30887f89f1481b3ae4 \
--hash=sha256:42be3c97604af7252c5898413aee076ace6c46e9bca0d0d90ceb77c7d3719027
# via dispatch (pyproject.toml)
anyio==4.14.2 \
--hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
Expand Down Expand Up @@ -787,10 +787,6 @@ deprecated==1.3.1 \
# via
# atlassian-python-api
# limits
distro==1.9.0 \
--hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \
--hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2
# via anthropic
dkimpy==1.1.8 \
--hash=sha256:b5f60fb47bbf5d8d762f134bcea0c388eba6b498342a682a21f1686545094b77
# via emails
Expand Down Expand Up @@ -1093,12 +1089,13 @@ httpx==0.28.1 \
--hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
# via
# dispatch (pyproject.toml)
# anthropic
# weasel
httpx2==2.12.0 \
--hash=sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf \
--hash=sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36
# via openai
# via
# anthropic
# openai
idna==3.19 \
--hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \
--hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4
Expand Down
4 changes: 2 additions & 2 deletions tests/plugins/dispatch_anthropic/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
importer gets a second copy.
"""

import httpx
import httpx2
import pytest
from anthropic import Anthropic

Expand All @@ -22,7 +22,7 @@ def client_factory(**kwargs):
# max_retries=0 only so a failure test issues one request rather than
# three with backoff; it does not change what is sent.
return Anthropic(
http_client=httpx.Client(transport=httpx.MockTransport(api)),
http_client=httpx2.Client(transport=httpx2.MockTransport(api)),
max_retries=0,
**kwargs,
)
Expand Down
20 changes: 12 additions & 8 deletions tests/plugins/dispatch_anthropic/fake_anthropic.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
"""An in-memory Anthropic API, for driving the real plugin against (issue #79).

The fake is installed at the httpx transport, below the anthropic SDK, so the
The fake is installed at the httpx2 transport, below the anthropic SDK, so the
plugin's own client construction and the SDK's own request building, structured
output schema generation and response parsing all run for real against it.
Replacing ``client.messages`` with a mock instead would assert only that the
plugin called something -- and issue #75, the defect this plugin exists not to
repeat, is precisely a case where every mocked assertion passed while no real
request could ever be built.

httpx2, not httpx: anthropic 1.x builds its requests on httpx2 and rejects an
httpx client from its constructor outright, so a fake mounted on httpx would not
even construct.

Only the plugin's ``Anthropic`` name is patched, and only to hand the real
client a transport. Everything the plugin passes to that constructor -- the API
key above all -- still travels the real code path and lands in a request this
Expand All @@ -16,7 +20,7 @@

import json

import httpx
import httpx2
from anthropic import Anthropic # noqa: F401 (re-exported for the fixtures)

# Obviously fake, and never a real-looking credential.
Expand Down Expand Up @@ -68,7 +72,7 @@ def respond_with_text(self, content: str):
def respond_with_blocks(self, blocks: list[dict], stop_reason: str = "end_turn"):
"""An arbitrary content-block list -- several text blocks, a thinking
block, a tool_use block, or none at all."""
self._handler = lambda request: httpx.Response(
self._handler = lambda request: httpx2.Response(
200, json=self._message(request, blocks, stop_reason=stop_reason)
)

Expand All @@ -81,7 +85,7 @@ def respond_with_refusal(self, category: str = "cyber"):
"""The model declined. Anthropic sends stop_reason ``refusal`` plus a
``stop_details`` object naming the policy category; ``content`` is
empty, and ``messages.parse`` reports ``parsed_output`` as None."""
self._handler = lambda request: httpx.Response(
self._handler = lambda request: httpx2.Response(
200,
json=self._message(
request,
Expand Down Expand Up @@ -109,7 +113,7 @@ def respond_with_status(
body quotes the submitted API key back inside it -- see
``respond_with_key_echo``, which is the shape that matters.
"""
self._handler = lambda request: httpx.Response(
self._handler = lambda request: httpx2.Response(
status,
json={"type": "error", "error": {"type": error_type, "message": message}},
headers={"request-id": "req_test_0001"},
Expand All @@ -128,7 +132,7 @@ def respond_with_undocumented_refusal_category(self, category: str):
def respond_with_non_envelope(self, status: int = 502):
"""A body that is not Anthropic's error envelope at all -- what a proxy
or gateway in front of the API returns. `APIStatusError.type` is None."""
self._handler = lambda request: httpx.Response(
self._handler = lambda request: httpx2.Response(
status, text="<html><body>502 Bad Gateway</body></html>"
)

Expand All @@ -138,7 +142,7 @@ def respond_with_connection_error(self):
`APIStatusError`, so it has no status, type or request id."""

def raise_connect_error(request):
raise httpx.ConnectError("connection refused", request=request)
raise httpx2.ConnectError("connection refused", request=request)

self._handler = raise_connect_error

Expand Down Expand Up @@ -180,7 +184,7 @@ def _message(self, request, blocks, stop_reason="end_turn", stop_details=None):
"usage": {"input_tokens": 1, "output_tokens": 1},
}

def __call__(self, request: httpx.Request) -> httpx.Response:
def __call__(self, request: httpx2.Request) -> httpx2.Response:
self.requests.append(
{
"url": str(request.url),
Expand Down
12 changes: 12 additions & 0 deletions tests/plugins/dispatch_anthropic/test_anthropic_sdk_surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,15 @@ def test_the_error_hierarchy_the_plugin_catches_is_the_root_one():

for cls in (APIStatusError, APIConnectionError, AuthenticationError, RateLimitError):
assert issubclass(cls, AnthropicError), cls.__name__


def test_the_sdk_builds_on_httpx2():
"""The fixtures mount their fake at the SDK's transport, so they have to
hand it a client of the right kind -- and anthropic 1.x rejects an ``httpx``
client from the constructor outright. Pinning the HTTP layer here reports a
future swap as the dependency change it is, rather than as a TypeError in
every fixture that builds a client."""
import httpx2
from anthropic import DefaultHttpxClient

assert issubclass(DefaultHttpxClient, httpx2.Client)
2 changes: 1 addition & 1 deletion tests/plugins/dispatch_openai/fake_openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
httpx2, not httpx: openai 3.x builds its requests on httpx2 and only accepts an
httpx client through a legacy escape hatch the SDK documents as temporary. A
fake mounted on that hatch would exercise a stack production no longer uses.
The anthropic fake stays on httpx, which is what that SDK still ships against.
The anthropic fake is on httpx2 too, since anthropic 1.x.

Only the plugin's ``OpenAI`` name is patched, and only to hand the real client a
transport. Everything the plugin passes to that constructor -- the API key above
Expand Down
Loading