mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-11 03:12:55 +00:00
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:
1 parent
b3326b3c33
commit
f7baa1952f
12 files changed
+237
-73
No files matched your search
@@ -23,15 +23,23 @@ DEFAULT_MAX_TOKENS = 4096
|
||||
# OpenAI-only key (``frequency_penalty``, ``response_format``,
|
||||
# ``reasoning_effort``, ...) from 400-ing an Anthropic request.
|
||||
_PASSTHROUGH_PARAMS = (
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
"stop_sequences",
|
||||
"tool_choice",
|
||||
"thinking",
|
||||
"metadata",
|
||||
)
|
||||
|
||||
# Sampling params the Messages API still honours on the models that accept
|
||||
# them, but which anthropic 1.x removed from ``messages.create``'s signature
|
||||
# (passing one is a TypeError). They go through ``extra_body``, which the SDK
|
||||
# merges into the request JSON as-is, so the wire request is unchanged and a
|
||||
# model that rejects them 400s exactly as it does today.
|
||||
_SAMPLING_PARAMS = (
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
)
|
||||
|
||||
# ``tool_choice`` values in OpenAI's vocabulary mapped to Anthropic's. OpenAI
|
||||
# sends a bare string; Anthropic requires an object and 400s on the string.
|
||||
# ``any``/``auto``/``none`` are also accepted spelled Anthropic-style, for a
|
||||
@@ -448,7 +456,8 @@ class AnthropicLLM(BaseLLM):
|
||||
messages: Internal conversation history.
|
||||
tools: OpenAI-shaped tool definitions, or ``None``.
|
||||
kwargs: Caller kwargs; the params in ``_PASSTHROUGH_PARAMS`` are
|
||||
forwarded verbatim, and the OpenAI aliases ``max_tokens`` /
|
||||
forwarded verbatim, those in ``_SAMPLING_PARAMS`` through
|
||||
``extra_body``, and the OpenAI aliases ``max_tokens`` /
|
||||
``max_completion_tokens``, ``stop`` and ``tool_choice`` are
|
||||
translated. Everything else is dropped.
|
||||
|
||||
@@ -483,6 +492,12 @@ class AnthropicLLM(BaseLLM):
|
||||
if kwargs.get(key) is not None:
|
||||
params[key] = kwargs[key]
|
||||
|
||||
sampling = {
|
||||
key: kwargs[key] for key in _SAMPLING_PARAMS if kwargs.get(key) is not None
|
||||
}
|
||||
if sampling:
|
||||
params["extra_body"] = {**(params.get("extra_body") or {}), **sampling}
|
||||
|
||||
# OpenAI's ``stop`` is Anthropic's ``stop_sequences``. A caller that
|
||||
# already speaks Anthropic wins.
|
||||
if "stop_sequences" not in params:
|
||||
|
||||
+20
-10
@@ -3,6 +3,7 @@ from abc import ABC, abstractmethod
|
||||
from typing import ClassVar, Dict, Optional, Tuple
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import openai
|
||||
|
||||
from docsgpt.cache import gen_cache, stream_cache
|
||||
@@ -20,17 +21,26 @@ logger = logging.getLogger(__name__)
|
||||
# Excludes API-level errors (4xx / 5xx status) which are not transport
|
||||
# retries — RateLimitError needs backoff, BadRequestError won't get any
|
||||
# better on retry, and the existing fallback handles both.
|
||||
_STREAM_RETRYABLE_TRANSPORT_ERRORS = (
|
||||
httpx.RemoteProtocolError,
|
||||
httpx.ReadError,
|
||||
httpx.ReadTimeout,
|
||||
httpx.WriteError,
|
||||
httpx.WriteTimeout,
|
||||
httpx.ConnectError,
|
||||
httpx.ConnectTimeout,
|
||||
httpx.PoolTimeout,
|
||||
openai.APIConnectionError,
|
||||
# Both HTTP stacks are listed because the providers are split across them and
|
||||
# the two libraries' exception classes are unrelated types: openai and
|
||||
# anthropic run on httpx2, while google-genai, elevenlabs, qdrant-client and
|
||||
# the MCP client are still on httpx. Naming only one silently stops matching
|
||||
# for half the providers — the retry just never fires.
|
||||
_TRANSPORT_ERROR_NAMES = (
|
||||
"RemoteProtocolError",
|
||||
"ReadError",
|
||||
"ReadTimeout",
|
||||
"WriteError",
|
||||
"WriteTimeout",
|
||||
"ConnectError",
|
||||
"ConnectTimeout",
|
||||
"PoolTimeout",
|
||||
)
|
||||
_STREAM_RETRYABLE_TRANSPORT_ERRORS = tuple(
|
||||
getattr(module, name)
|
||||
for module in (httpx2, httpx)
|
||||
for name in _TRANSPORT_ERROR_NAMES
|
||||
) + (openai.APIConnectionError,)
|
||||
|
||||
|
||||
def optional_int(value) -> Optional[int]:
|
||||
|
||||
@@ -51,7 +51,7 @@ annotated-doc==0.0.5
|
||||
# via typer
|
||||
annotated-types==0.8.0
|
||||
# via pydantic
|
||||
anthropic==0.121.0
|
||||
anthropic==1.5.0
|
||||
# via docsgpt
|
||||
antlr4-python3-runtime==4.9.3
|
||||
# via omegaconf
|
||||
@@ -61,6 +61,7 @@ anyio==4.15.1
|
||||
# google-genai
|
||||
# httpx
|
||||
# httpx-ws
|
||||
# httpx2
|
||||
# mcp
|
||||
# openai
|
||||
# py-key-value-aio
|
||||
@@ -189,10 +190,7 @@ deprecated==1.3.1
|
||||
dill==0.4.1
|
||||
# via multiprocess
|
||||
distro==1.9.0
|
||||
# via
|
||||
# anthropic
|
||||
# google-genai
|
||||
# openai
|
||||
# via google-genai
|
||||
dnspython==2.8.0
|
||||
# via email-validator
|
||||
doclang==0.7.3
|
||||
@@ -303,6 +301,7 @@ gunicorn==26.2.0
|
||||
h11==0.16.0
|
||||
# via
|
||||
# httpcore
|
||||
# httpcore2
|
||||
# uvicorn
|
||||
# wsproto
|
||||
h2==4.4.1
|
||||
@@ -315,6 +314,8 @@ httpcore==1.0.9
|
||||
# via
|
||||
# httpx
|
||||
# httpx-ws
|
||||
httpcore2==2.12.0 ; sys_platform != 'emscripten'
|
||||
# via httpx2
|
||||
httplib2==0.32.0
|
||||
# via
|
||||
# google-api-python-client
|
||||
@@ -323,7 +324,6 @@ httptools==0.8.0
|
||||
# via uvicorn
|
||||
httpx==0.28.1
|
||||
# via
|
||||
# anthropic
|
||||
# daytona
|
||||
# docling-slim
|
||||
# elevenlabs
|
||||
@@ -332,12 +332,18 @@ httpx==0.28.1
|
||||
# httpx-ws
|
||||
# huggingface-hub
|
||||
# mcp
|
||||
# openai
|
||||
# qdrant-client
|
||||
httpx-sse==0.4.3
|
||||
# via mcp
|
||||
httpx-ws==0.9.0
|
||||
# via daytona
|
||||
httpx2==2.12.0
|
||||
# via
|
||||
# anthropic
|
||||
# docsgpt
|
||||
# openai
|
||||
httpx2-jsfetch==1.0 ; sys_platform == 'emscripten'
|
||||
# via httpx2
|
||||
huggingface-hub==1.16.1
|
||||
# via
|
||||
# accelerate
|
||||
@@ -353,6 +359,7 @@ idna==3.19
|
||||
# anyio
|
||||
# email-validator
|
||||
# httpx
|
||||
# httpx2
|
||||
# requests
|
||||
# tldextract
|
||||
# yarl
|
||||
@@ -495,7 +502,7 @@ onnxruntime==1.30.0
|
||||
# via
|
||||
# docsgpt
|
||||
# fastembed
|
||||
openai==2.54.0
|
||||
openai==3.13.0
|
||||
# via docsgpt
|
||||
openapi-pydantic==0.5.1
|
||||
# via fastmcp-slim
|
||||
@@ -989,7 +996,6 @@ tqdm==4.70.1
|
||||
# fastembed
|
||||
# huggingface-hub
|
||||
# mpire
|
||||
# openai
|
||||
# rapidocr
|
||||
# semchunk
|
||||
# transformers
|
||||
@@ -1008,6 +1014,10 @@ tree-sitter-python==0.25.0
|
||||
# via docling-core
|
||||
tree-sitter-typescript==0.23.2
|
||||
# via docling-core
|
||||
truststore==0.10.4 ; sys_platform != 'emscripten'
|
||||
# via
|
||||
# httpcore2
|
||||
# httpx2
|
||||
typer==0.26.8
|
||||
# via
|
||||
# doclang
|
||||
@@ -1036,6 +1046,7 @@ typing-extensions==4.16.0
|
||||
# fastmcp-slim
|
||||
# google-genai
|
||||
# grpcio
|
||||
# httpx2
|
||||
# huggingface-hub
|
||||
# mcp
|
||||
# obstore
|
||||
|
||||
@@ -37,7 +37,7 @@ annotated-doc==0.0.5
|
||||
# via typer
|
||||
annotated-types==0.8.0
|
||||
# via pydantic
|
||||
anthropic==0.121.0
|
||||
anthropic==1.5.0
|
||||
# via docsgpt
|
||||
anyio==4.15.1
|
||||
# via
|
||||
@@ -45,6 +45,7 @@ anyio==4.15.1
|
||||
# google-genai
|
||||
# httpx
|
||||
# httpx-ws
|
||||
# httpx2
|
||||
# mcp
|
||||
# openai
|
||||
# py-key-value-aio
|
||||
@@ -165,10 +166,7 @@ defusedxml==0.7.1
|
||||
deprecated==1.3.1
|
||||
# via daytona
|
||||
distro==1.9.0
|
||||
# via
|
||||
# anthropic
|
||||
# google-genai
|
||||
# openai
|
||||
# via google-genai
|
||||
dnspython==2.8.0
|
||||
# via email-validator
|
||||
docstring-parser==0.18.0
|
||||
@@ -262,6 +260,7 @@ gunicorn==26.2.0
|
||||
h11==0.16.0
|
||||
# via
|
||||
# httpcore
|
||||
# httpcore2
|
||||
# uvicorn
|
||||
# wsproto
|
||||
h2==4.4.1
|
||||
@@ -274,6 +273,8 @@ httpcore==1.0.9
|
||||
# via
|
||||
# httpx
|
||||
# httpx-ws
|
||||
httpcore2==2.12.0 ; sys_platform != 'emscripten'
|
||||
# via httpx2
|
||||
httplib2==0.32.0
|
||||
# via
|
||||
# google-api-python-client
|
||||
@@ -282,7 +283,6 @@ httptools==0.8.0
|
||||
# via uvicorn
|
||||
httpx==0.28.1
|
||||
# via
|
||||
# anthropic
|
||||
# daytona
|
||||
# elevenlabs
|
||||
# fastmcp-slim
|
||||
@@ -290,12 +290,18 @@ httpx==0.28.1
|
||||
# httpx-ws
|
||||
# huggingface-hub
|
||||
# mcp
|
||||
# openai
|
||||
# qdrant-client
|
||||
httpx-sse==0.4.3
|
||||
# via mcp
|
||||
httpx-ws==0.9.0
|
||||
# via daytona
|
||||
httpx2==2.12.0
|
||||
# via
|
||||
# anthropic
|
||||
# docsgpt
|
||||
# openai
|
||||
httpx2-jsfetch==1.0 ; sys_platform == 'emscripten'
|
||||
# via httpx2
|
||||
huggingface-hub==1.16.1
|
||||
# via
|
||||
# fastembed
|
||||
@@ -307,6 +313,7 @@ idna==3.19
|
||||
# anyio
|
||||
# email-validator
|
||||
# httpx
|
||||
# httpx2
|
||||
# requests
|
||||
# tldextract
|
||||
# yarl
|
||||
@@ -418,7 +425,7 @@ onnxruntime==1.30.0
|
||||
# via
|
||||
# docsgpt
|
||||
# fastembed
|
||||
openai==2.54.0
|
||||
openai==3.13.0
|
||||
# via docsgpt
|
||||
openapi-pydantic==0.5.1
|
||||
# via fastmcp-slim
|
||||
@@ -819,7 +826,10 @@ tqdm==4.70.1
|
||||
# docsgpt
|
||||
# fastembed
|
||||
# huggingface-hub
|
||||
# openai
|
||||
truststore==0.10.4 ; sys_platform != 'emscripten'
|
||||
# via
|
||||
# httpcore2
|
||||
# httpx2
|
||||
typer==0.26.8
|
||||
# via huggingface-hub
|
||||
typing-extensions==4.16.0
|
||||
@@ -842,6 +852,7 @@ typing-extensions==4.16.0
|
||||
# fastmcp-slim
|
||||
# google-genai
|
||||
# grpcio
|
||||
# httpx2
|
||||
# huggingface-hub
|
||||
# mcp
|
||||
# obstore
|
||||
|
||||
@@ -37,7 +37,7 @@ annotated-doc==0.0.5
|
||||
# via typer
|
||||
annotated-types==0.8.0
|
||||
# via pydantic
|
||||
anthropic==0.121.0
|
||||
anthropic==1.5.0
|
||||
# via docsgpt
|
||||
anyio==4.15.1
|
||||
# via
|
||||
@@ -45,6 +45,7 @@ anyio==4.15.1
|
||||
# google-genai
|
||||
# httpx
|
||||
# httpx-ws
|
||||
# httpx2
|
||||
# mcp
|
||||
# openai
|
||||
# py-key-value-aio
|
||||
@@ -163,10 +164,7 @@ defusedxml==0.7.1
|
||||
deprecated==1.3.1
|
||||
# via daytona
|
||||
distro==1.9.0
|
||||
# via
|
||||
# anthropic
|
||||
# google-genai
|
||||
# openai
|
||||
# via google-genai
|
||||
dnspython==2.8.0
|
||||
# via email-validator
|
||||
docstring-parser==0.18.0
|
||||
@@ -256,6 +254,7 @@ gunicorn==26.2.0
|
||||
h11==0.16.0
|
||||
# via
|
||||
# httpcore
|
||||
# httpcore2
|
||||
# uvicorn
|
||||
# wsproto
|
||||
h2==4.4.1
|
||||
@@ -268,6 +267,8 @@ httpcore==1.0.9
|
||||
# via
|
||||
# httpx
|
||||
# httpx-ws
|
||||
httpcore2==2.12.0 ; sys_platform != 'emscripten'
|
||||
# via httpx2
|
||||
httplib2==0.32.0
|
||||
# via
|
||||
# google-api-python-client
|
||||
@@ -276,7 +277,6 @@ httptools==0.8.0
|
||||
# via uvicorn
|
||||
httpx==0.28.1
|
||||
# via
|
||||
# anthropic
|
||||
# daytona
|
||||
# elevenlabs
|
||||
# fastmcp-slim
|
||||
@@ -284,12 +284,18 @@ httpx==0.28.1
|
||||
# httpx-ws
|
||||
# huggingface-hub
|
||||
# mcp
|
||||
# openai
|
||||
# qdrant-client
|
||||
httpx-sse==0.4.3
|
||||
# via mcp
|
||||
httpx-ws==0.9.0
|
||||
# via daytona
|
||||
httpx2==2.12.0
|
||||
# via
|
||||
# anthropic
|
||||
# docsgpt
|
||||
# openai
|
||||
httpx2-jsfetch==1.0 ; sys_platform == 'emscripten'
|
||||
# via httpx2
|
||||
huggingface-hub==1.16.1
|
||||
# via
|
||||
# fastembed
|
||||
@@ -301,6 +307,7 @@ idna==3.19
|
||||
# anyio
|
||||
# email-validator
|
||||
# httpx
|
||||
# httpx2
|
||||
# requests
|
||||
# tldextract
|
||||
# yarl
|
||||
@@ -409,7 +416,7 @@ onnxruntime==1.30.0
|
||||
# via
|
||||
# docsgpt
|
||||
# fastembed
|
||||
openai==2.54.0
|
||||
openai==3.13.0
|
||||
# via docsgpt
|
||||
openapi-pydantic==0.5.1
|
||||
# via fastmcp-slim
|
||||
@@ -799,7 +806,10 @@ tqdm==4.70.1
|
||||
# docsgpt
|
||||
# fastembed
|
||||
# huggingface-hub
|
||||
# openai
|
||||
truststore==0.10.4 ; sys_platform != 'emscripten'
|
||||
# via
|
||||
# httpcore2
|
||||
# httpx2
|
||||
typer==0.26.8
|
||||
# via huggingface-hub
|
||||
typing-extensions==4.16.0
|
||||
@@ -822,6 +832,7 @@ typing-extensions==4.16.0
|
||||
# fastmcp-slim
|
||||
# google-genai
|
||||
# grpcio
|
||||
# httpx2
|
||||
# huggingface-hub
|
||||
# mcp
|
||||
# obstore
|
||||
|
||||
@@ -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)
|
||||
|
||||
+5
-2
@@ -35,7 +35,7 @@ classifiers = [
|
||||
dependencies = [
|
||||
"a2wsgi>=1.10.10,<2",
|
||||
"alembic>=1.13,<2",
|
||||
"anthropic>=0.121.0,<0.122",
|
||||
"anthropic>=1.5.0,<2",
|
||||
"beautifulsoup4>=4.15.0,<5",
|
||||
"boto3>=1.43.67,<2",
|
||||
"cel-python>=0.5.0,<0.6",
|
||||
@@ -63,6 +63,9 @@ dependencies = [
|
||||
"google-genai>=2.17.0,<3",
|
||||
"gTTS>=2.5.4,<3",
|
||||
"gunicorn>=26.0.0,<27",
|
||||
# Imported directly by the BYOM DNS-pinning transport and the LLM
|
||||
# stream-retry error tuple; it is what anthropic and openai run on.
|
||||
"httpx2>=2.7.0,<3",
|
||||
"jinja2>=3.1.6,<4",
|
||||
"kombu>=5.6.2,<6",
|
||||
"markdownify>=1.2.3,<2",
|
||||
@@ -71,7 +74,7 @@ dependencies = [
|
||||
"numpy>=2.5.1,<3",
|
||||
# fastembed's runtime: local embeddings execute on it.
|
||||
"onnxruntime>=1.28.0,<2",
|
||||
"openai>=2.53.0,<3",
|
||||
"openai>=3.13.0,<4",
|
||||
"openapi3-parser>=2.0.0,<3",
|
||||
# pandas reads .xlsx through openpyxl but does not depend on it.
|
||||
"openpyxl>=3.1.5,<4",
|
||||
|
||||
@@ -416,12 +416,15 @@ class TestSecurityDispatchSSRFGuard:
|
||||
def test_dispatch_injects_pinned_http_client_for_user_model(
|
||||
self, pg_conn
|
||||
):
|
||||
"""LLMCreator must build a DNS-rebinding-safe httpx.Client and
|
||||
"""LLMCreator must build a DNS-rebinding-safe httpx2.Client and
|
||||
forward it to the OpenAI SDK so the SDK's request-time DNS
|
||||
lookup cannot escape the create-time SSRF guard. ``validate_
|
||||
user_base_url`` alone is TOCTOU and does not close the
|
||||
rebinding window — the pinned client is what does."""
|
||||
import httpx
|
||||
rebinding window — the pinned client is what does. The client
|
||||
must be httpx2's: the OpenAI SDK rejects an old-httpx client
|
||||
at construction, which would take the guard offline.
|
||||
"""
|
||||
import httpx2
|
||||
|
||||
from docsgpt.core.model_settings import (
|
||||
AvailableModel,
|
||||
@@ -474,7 +477,7 @@ class TestSecurityDispatchSSRFGuard:
|
||||
|
||||
client = captured["http_client"]
|
||||
try:
|
||||
assert isinstance(client, httpx.Client), (
|
||||
assert isinstance(client, httpx2.Client), (
|
||||
"http_client must be set for user-source models so the "
|
||||
"OpenAI SDK doesn't re-resolve DNS at request time"
|
||||
)
|
||||
|
||||
@@ -585,6 +585,10 @@ class TestRawGen:
|
||||
assert _sent(llm)["max_tokens"] == 1234
|
||||
|
||||
def test_sampling_params_forwarded(self, llm):
|
||||
"""anthropic 1.x dropped the sampling params from
|
||||
``messages.create``'s signature (passing one is a TypeError), so they
|
||||
ride in ``extra_body``, which the SDK merges into the request JSON
|
||||
as-is. The wire request is unchanged."""
|
||||
llm._raw_gen(
|
||||
llm,
|
||||
model="m",
|
||||
@@ -592,8 +596,10 @@ class TestRawGen:
|
||||
temperature=0.3,
|
||||
top_p=0.9,
|
||||
)
|
||||
assert _sent(llm)["temperature"] == 0.3
|
||||
assert _sent(llm)["top_p"] == 0.9
|
||||
sent = _sent(llm)
|
||||
assert sent["extra_body"] == {"temperature": 0.3, "top_p": 0.9}
|
||||
assert "temperature" not in sent
|
||||
assert "top_p" not in sent
|
||||
|
||||
def test_openai_only_params_not_forwarded(self, llm):
|
||||
"""OpenAI-shaped request params reach every provider via
|
||||
|
||||
@@ -10,6 +10,7 @@ import types
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
import pytest
|
||||
|
||||
from docsgpt.llm.anthropic import AnthropicLLM
|
||||
@@ -362,6 +363,36 @@ class TestStreamingFallback:
|
||||
assert primary.stream_calls == 2
|
||||
assert not backup.gen_stream_called
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error",
|
||||
[httpx2.RemoteProtocolError, httpx.RemoteProtocolError],
|
||||
ids=["httpx2", "httpx"],
|
||||
)
|
||||
def test_stream_transport_error_retries_on_either_http_stack(
|
||||
self, patch_model_utils, error
|
||||
):
|
||||
"""The providers are split across two HTTP stacks whose exception
|
||||
classes are unrelated types: openai and anthropic raise from httpx2,
|
||||
google-genai and elevenlabs still from httpx. Naming one stack makes
|
||||
the retry silently stop firing for the other half.
|
||||
"""
|
||||
backup = FakeLLM(stream_chunks=["fallback"])
|
||||
patch_model_utils(
|
||||
get_provider=lambda m, **_kwargs: "openai",
|
||||
get_api_key=lambda p: "k",
|
||||
create_llm=lambda type, **kw: backup,
|
||||
)
|
||||
primary = FakeLLM(
|
||||
stream_chunks=["ok1", "ok2"],
|
||||
backup_models=["backup-model"],
|
||||
)
|
||||
primary.fail_schedule = [0, None]
|
||||
primary.error_schedule = [error, RuntimeError]
|
||||
|
||||
assert list(primary.gen_stream(**CALL_ARGS)) == ["ok1", "ok2"]
|
||||
assert primary.stream_calls == 2
|
||||
assert not backup.gen_stream_called
|
||||
|
||||
def test_stream_transport_error_retry_then_fails_uses_fallback(
|
||||
self, patch_model_utils
|
||||
):
|
||||
|
||||
@@ -537,7 +537,7 @@ def _capture_httpx_handle_request(monkeypatch):
|
||||
opening a real socket.
|
||||
"""
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
@@ -545,10 +545,10 @@ def _capture_httpx_handle_request(monkeypatch):
|
||||
captured["url"] = request.url
|
||||
captured["sni"] = request.extensions.get("sni_hostname")
|
||||
captured["host_header"] = request.headers.get("host")
|
||||
return httpx.Response(200, content=b"ok")
|
||||
return httpx2.Response(200, content=b"ok")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"httpx.HTTPTransport.handle_request", fake_handle
|
||||
"httpx2.HTTPTransport.handle_request", fake_handle
|
||||
)
|
||||
return captured
|
||||
|
||||
@@ -610,7 +610,14 @@ def test_pinned_httpx_transport_rewrites_url_to_validated_ip(monkeypatch):
|
||||
def test_pinned_httpx_transport_sets_sni_for_original_hostname(monkeypatch):
|
||||
"""TLS SNI / cert verification must use the original hostname; the
|
||||
transport sets it via the ``sni_hostname`` extension that
|
||||
httpcore forwards to ``start_tls``'s ``server_hostname``."""
|
||||
httpcore forwards to ``start_tls``'s ``server_hostname``.
|
||||
|
||||
The value must be a ``str``, not ascii bytes: httpcore passes it
|
||||
straight to ``ssl.SSLContext.wrap_socket``, and the ``truststore``
|
||||
backend httpx2 uses for the default system trust store encodes it
|
||||
rather than accepting bytes — so bytes fail every real handshake
|
||||
while passing any test that stubs the transport out.
|
||||
"""
|
||||
|
||||
captured = _capture_httpx_handle_request(monkeypatch)
|
||||
|
||||
@@ -623,7 +630,8 @@ def test_pinned_httpx_transport_sets_sni_for_original_hostname(monkeypatch):
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
assert captured["sni"] == b"api.example.com"
|
||||
assert captured["sni"] == "api.example.com"
|
||||
assert isinstance(captured["sni"], str)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
@@ -712,7 +720,7 @@ def test_pinned_httpx_transport_refuses_unexpected_host(monkeypatch):
|
||||
transport refuses rather than silently dialing the validated IP
|
||||
with a different host's credentials."""
|
||||
|
||||
import httpx
|
||||
import httpx2
|
||||
|
||||
with mock.patch(
|
||||
"socket.getaddrinfo", return_value=_addrinfo("104.18.6.192")
|
||||
@@ -721,7 +729,7 @@ def test_pinned_httpx_transport_refuses_unexpected_host(monkeypatch):
|
||||
try:
|
||||
with pytest.raises(UnsafeUserUrlError, match="refused request"):
|
||||
client.get("https://other.example.com/v1/test")
|
||||
except httpx.RequestError:
|
||||
except httpx2.RequestError:
|
||||
# Some httpx versions may wrap the transport error; accept
|
||||
# either path so long as the request didn't succeed.
|
||||
pass
|
||||
|
||||
@@ -254,21 +254,20 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.121.0"
|
||||
version = "1.5.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/0f/ca/3cb2c20ee729736fbd4546d5d8b67e818288529fe70cb7a80dbf80aef70b/anthropic-0.121.0.tar.gz", hash = "sha256:e79d6e08ab3376602fc9a70d4d5ea3540817c76cf7e16658bed790834e1833d6", size = 1013292, upload-time = "2026-08-07T17:11:07.241Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/43/6f3f6006f5216d43a059a1a856d275ef6536cdfa94883b64cc04d7873cb7/anthropic-1.5.0.tar.gz", hash = "sha256:b25f87f5758861f25993383a5c9bf274eb6e0f1b010c84019ad91854cb4e1bc6", size = 1156657, upload-time = "2026-09-10T17:45:35.93Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/91/b3d41643f1f639927e8c5fb02c3bd8bffe6f1f29e219b3bd4c61e267b15c/anthropic-0.121.0-py3-none-any.whl", hash = "sha256:6048713fa441e59e1cba8363171cd2a86273b25bd213e9c7ac70a523af88b011", size = 1035493, upload-time = "2026-08-07T17:11:08.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/1c/c32fce35ca0be0205f2377d2238e3ed73dea5abc2be09d15fbd032091d60/anthropic-1.5.0-py3-none-any.whl", hash = "sha256:d9ce04b29ad1f7025dda3e3bc478cde8d2924d2de5417d2d4a4bb0378ecbc2a6", size = 1235236, upload-time = "2026-09-10T17:45:34.216Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1365,6 +1364,7 @@ dependencies = [
|
||||
{ name = "google-genai" },
|
||||
{ name = "gtts" },
|
||||
{ name = "gunicorn" },
|
||||
{ name = "httpx2" },
|
||||
{ name = "jinja2" },
|
||||
{ name = "kombu" },
|
||||
{ name = "markdownify" },
|
||||
@@ -1449,7 +1449,7 @@ dev = [
|
||||
requires-dist = [
|
||||
{ name = "a2wsgi", specifier = ">=1.10.10,<2" },
|
||||
{ name = "alembic", specifier = ">=1.13,<2" },
|
||||
{ name = "anthropic", specifier = ">=0.121.0,<0.122" },
|
||||
{ name = "anthropic", specifier = ">=1.5.0,<2" },
|
||||
{ name = "beautifulsoup4", specifier = ">=4.15.0,<5" },
|
||||
{ name = "boto3", specifier = ">=1.43.67,<2" },
|
||||
{ name = "cel-python", specifier = ">=0.5.0,<0.6" },
|
||||
@@ -1476,6 +1476,7 @@ requires-dist = [
|
||||
{ name = "google-genai", specifier = ">=2.17.0,<3" },
|
||||
{ name = "gtts", specifier = ">=2.5.4,<3" },
|
||||
{ name = "gunicorn", specifier = ">=26.0.0,<27" },
|
||||
{ name = "httpx2", specifier = ">=2.7.0,<3" },
|
||||
{ name = "jinja2", specifier = ">=3.1.6,<4" },
|
||||
{ name = "kombu", specifier = ">=5.6.2,<6" },
|
||||
{ name = "markdownify", specifier = ">=1.2.3,<2" },
|
||||
@@ -1484,7 +1485,7 @@ requires-dist = [
|
||||
{ name = "networkx", specifier = ">=3.6.1,<4" },
|
||||
{ name = "numpy", specifier = ">=2.5.1,<3" },
|
||||
{ name = "onnxruntime", specifier = ">=1.28.0,<2" },
|
||||
{ name = "openai", specifier = ">=2.53.0,<3" },
|
||||
{ name = "openai", specifier = ">=3.13.0,<4" },
|
||||
{ name = "openapi3-parser", specifier = ">=2.0.0,<3" },
|
||||
{ name = "openpyxl", specifier = ">=3.1.5,<4" },
|
||||
{ name = "opentelemetry-distro", specifier = ">=0.50b0,<1" },
|
||||
@@ -2298,6 +2299,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" },
|
||||
{ name = "truststore" },
|
||||
]
|
||||
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 = "httplib2"
|
||||
version = "0.32.0"
|
||||
@@ -2390,6 +2404,32 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/f8/a6bc80313a9e93c888fa10534dfce2ad76ff86911b6f485777ce6de6a073/httpx_ws-0.9.0-py3-none-any.whl", hash = "sha256:71640d2fb1bf9a225775015b33cd755cfd4c5f7e21c885192fe3adc4c387b248", size = 15759, upload-time = "2026-03-28T14:11:11.887Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx2"
|
||||
version = "2.12.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpcore2", marker = "sys_platform != 'emscripten'" },
|
||||
{ name = "httpx2-jsfetch", marker = "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/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 = "huggingface-hub"
|
||||
version = "1.16.1"
|
||||
@@ -3588,21 +3628,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "2.54.0"
|
||||
version = "3.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "distro" },
|
||||
{ name = "httpx" },
|
||||
{ name = "httpx2" },
|
||||
{ name = "jiter" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tqdm" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/50/9a/8c75e8c8a5b407a0586faeb2afac91674ff955c191ecc1d6d3b6669f6788/openai-2.54.0.tar.gz", hash = "sha256:e3e6f8bc1ba30ddf381ace1a14340eed381cb984a1a59bd0f34b5be3b5d49cfa", size = 1100285, upload-time = "2026-08-11T18:46:59.035Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/4874c4f7db30121885e6a763e2670b2fe1d76c79ed4732f8acbb51c8d96e/openai-3.13.0.tar.gz", hash = "sha256:a8f87a9b3b9c08eb446d68bd0a80e8ec907c4c35fdea63f4265c7b34b2de3a60", size = 1624217, upload-time = "2026-09-10T19:38:27.597Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/64/a8/bb76c7356de8ad57f59d5ff993d434df0607f07f08bcc9c9a5c275e399c0/openai-2.54.0-py3-none-any.whl", hash = "sha256:89089789197ccdb87f173a03145ed1598d00795220c93e96cf712b1cbf5e5f2b", size = 1660351, upload-time = "2026-08-11T18:46:56.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/7f/ee9ebb7c5ab7abf016969faec7c748a91c1fd893078bfe1e5ba5d82e96c2/openai-3.13.0-py3-none-any.whl", hash = "sha256:e35b1f6fe99245e86e37504d9fad1ad2a363807307c424232c1d849bd0666c8e", size = 2009683, upload-time = "2026-09-10T19:38:25.855Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6426,6 +6464,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/e4/81f9a935789233cf412a0ed5fe04c883841d2c8fb0b7e075958a35c65032/tree_sitter_typescript-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:05db58f70b95ef0ea126db5560f3775692f609589ed6f8dd0af84b7f19f1cbb7", size = 274052, upload-time = "2024-11-11T02:36:09.514Z" },
|
||||
]
|
||||
|
||||
[[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 = "typer"
|
||||
version = "0.26.8"
|
||||
|
||||
Reference in new issue
Block a user