diff --git a/docsgpt/llm/anthropic.py b/docsgpt/llm/anthropic.py index 9b5953ad..89ffd25b 100644 --- a/docsgpt/llm/anthropic.py +++ b/docsgpt/llm/anthropic.py @@ -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: diff --git a/docsgpt/llm/base.py b/docsgpt/llm/base.py index b4ef253e..e5b4c55d 100644 --- a/docsgpt/llm/base.py +++ b/docsgpt/llm/base.py @@ -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]: diff --git a/docsgpt/requirements-docling.txt b/docsgpt/requirements-docling.txt index c9907cc1..83daa7ee 100644 --- a/docsgpt/requirements-docling.txt +++ b/docsgpt/requirements-docling.txt @@ -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 diff --git a/docsgpt/requirements-milvus.txt b/docsgpt/requirements-milvus.txt index cebc7cf0..92fd8465 100644 --- a/docsgpt/requirements-milvus.txt +++ b/docsgpt/requirements-milvus.txt @@ -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 diff --git a/docsgpt/requirements.txt b/docsgpt/requirements.txt index 9d477f2c..ef73fe60 100644 --- a/docsgpt/requirements.txt +++ b/docsgpt/requirements.txt @@ -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 diff --git a/docsgpt/security/safe_url.py b/docsgpt/security/safe_url.py index 73af4c97..201b84a1 100644 --- a/docsgpt/security/safe_url.py +++ b/docsgpt/security/safe_url.py @@ -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) diff --git a/pyproject.toml b/pyproject.toml index da519792..67c8067a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/tests/core/test_byom_user_aware_helpers.py b/tests/core/test_byom_user_aware_helpers.py index 9dea16e1..28318a12 100644 --- a/tests/core/test_byom_user_aware_helpers.py +++ b/tests/core/test_byom_user_aware_helpers.py @@ -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" ) diff --git a/tests/llm/test_anthropic.py b/tests/llm/test_anthropic.py index 5532dee9..cb08c021 100644 --- a/tests/llm/test_anthropic.py +++ b/tests/llm/test_anthropic.py @@ -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 diff --git a/tests/llm/test_fallback.py b/tests/llm/test_fallback.py index a0b9d6f6..0aae3d4b 100644 --- a/tests/llm/test_fallback.py +++ b/tests/llm/test_fallback.py @@ -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 ): diff --git a/tests/security/test_safe_url.py b/tests/security/test_safe_url.py index 11f581f3..65e7810b 100644 --- a/tests/security/test_safe_url.py +++ b/tests/security/test_safe_url.py @@ -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 diff --git a/uv.lock b/uv.lock index 3586f8ea..ddd2b3ff 100644 --- a/uv.lock +++ b/uv.lock @@ -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"