chore(deps): anthropic 1.5, openai 3.13, and the move to httpx2

Both SDKs' 1.x/3.x majors run on httpx2 (the maintained fork of httpx by its
original author, published by Pydantic at github.com/pydantic/httpx2, version
line 2.x) instead of httpx, which is what makes these two bumps one change.
httpx2 is now a declared dependency because two modules import it directly.
Everything else in the 0.x -> 1.x / 2.x -> 3.x change lists is absent here:
no Text Completions, no `with_raw_response`, no raw `output_format` dicts, no
Bedrock client, and `requires-python` is already 3.12.

Three code changes, all forced by the bump:

The BYOM DNS-pinning client in `docsgpt/security/safe_url.py` is handed to
`OpenAI(http_client=...)`, and the SDK rejects an old-httpx client at
construction — which would have taken the SSRF guard offline. It is built on
httpx2 now, and its `sni_hostname` extension carries a `str` rather than
ascii bytes: httpcore passes the value straight to
`ssl.SSLContext.wrap_socket`, and the truststore backend httpx2 uses for the
default system trust store encodes it instead of accepting bytes. Bytes
therefore failed every real handshake while passing the existing tests, which
stub the transport out; the test now pins the type and says why.

The stream-retry error tuple in `docsgpt/llm/base.py` named `httpx`
exceptions only. The two libraries' exception classes are unrelated types, so
after the bump the retry silently stopped firing for openai and anthropic
while still working for google-genai and elevenlabs. It now covers both
stacks, with a parametrized test over each.

anthropic 1.x dropped temperature/top_p/top_k from `messages.create`'s
signature (passing one raises TypeError) without dropping them from the API,
so the provider forwards them through `extra_body`. The wire request is
unchanged and a model that rejects them 400s exactly as before.
This commit is contained in:
Alex committed 2026-09-12 17:44:18 +01:00
1 parent b3326b3c33
commit f7baa1952f
12 files changed
+237 -73

No files matched your search

+12 -4
View File
@@ -16,7 +16,7 @@ Three entry points:
Resolves once, dials the IP literal, preserves the original hostname
in the ``Host`` header and via SNI / cert verification for HTTPS.
* :func:`pinned_httpx_client` — called at dispatch time when the caller
hands an ``httpx.Client`` to a third-party SDK (e.g. the OpenAI
hands an ``httpx2.Client`` to a third-party SDK (e.g. the OpenAI
Python SDK via ``OpenAI(http_client=...)``). Same DNS-rebinding
closure on the httpx transport layer.
@@ -35,7 +35,12 @@ import socket
from typing import Any, Iterable
from urllib.parse import urlsplit, urlunsplit
import httpx
# httpx2 (the maintained fork of httpx, by its original author, published by
# Pydantic at github.com/pydantic/httpx2) is what the OpenAI and Anthropic
# SDKs run on; a client built on the old ``httpx`` is rejected at their
# construction. This module uses it only for the pinned client below — its
# own fetches go through ``requests``.
import httpx2 as httpx
import requests
from requests.adapters import HTTPAdapter
@@ -507,7 +512,10 @@ class _PinnedHTTPSTransport(httpx.HTTPTransport):
``httpcore`` feeds into ``start_tls``'s ``server_hostname``
parameter. Without this, ``urllib3``-equivalent code would use
the IP literal as SNI and cert verification would fail (the
cert is for the original hostname, not the IP).
cert is for the original hostname, not the IP). It must be a
``str``: ``httpcore`` passes it through to
``ssl.SSLContext.wrap_socket``, and the ``truststore`` backend
httpx2 uses on macOS encodes it rather than accepting bytes.
"""
def __init__(
@@ -544,7 +552,7 @@ class _PinnedHTTPSTransport(httpx.HTTPTransport):
# hostname even though TCP dials the IP literal.
request.extensions = {
**request.extensions,
"sni_hostname": self._host.encode("ascii"),
"sni_hostname": self._host,
}
request.url = request.url.copy_with(host=self._ip_netloc)
return super().handle_request(request)