diff --git a/dev-notes/architecture/codex-runtime-model-limits.md b/dev-notes/architecture/codex-runtime-model-limits.md index 4f7dc7aff..493af0abe 100644 --- a/dev-notes/architecture/codex-runtime-model-limits.md +++ b/dev-notes/architecture/codex-runtime-model-limits.md @@ -118,7 +118,7 @@ rare. ## Testing -Deterministic tests use `httpx.MockTransport` and a fake model-limit provider. +Deterministic tests use `httpx2.MockTransport` and a fake model-limit provider. They cover: - authenticated Codex model-catalog URL and headers diff --git a/dev-notes/architecture/oauth-provider-parity.md b/dev-notes/architecture/oauth-provider-parity.md index e3769b286..bbd8b036c 100644 --- a/dev-notes/architecture/oauth-provider-parity.md +++ b/dev-notes/architecture/oauth-provider-parity.md @@ -98,7 +98,7 @@ the short-lived token. ## Validation and release process -Deterministic tests use `httpx.MockTransport` and fake credentials. They cover: +Deterministic tests use `httpx2.MockTransport` and fake credentials. They cover: - Anthropic refresh success and error redaction - Copilot device login, token exchange, Enterprise routing, untrusted URL diff --git a/dev-notes/architecture/phase-18-provider-config-foundation.md b/dev-notes/architecture/phase-18-provider-config-foundation.md index 251475d45..ea9e958f8 100644 --- a/dev-notes/architecture/phase-18-provider-config-foundation.md +++ b/dev-notes/architecture/phase-18-provider-config-foundation.md @@ -109,7 +109,7 @@ variable is not currently set. Provider HTTP timeouts are configurable through `timeout_seconds` in `~/.tau/providers.json`. The default OpenAI-compatible provider can also read -`OPENAI_TIMEOUT_SECONDS`. The configured value is passed to the HTTPX streaming +`OPENAI_TIMEOUT_SECONDS`. The configured value is passed to the HTTPX2 streaming client instead of keeping timeout behavior hardcoded in the provider adapter. Transient retry behavior is configurable through `max_retries` and diff --git a/dev-notes/architecture/socks-proxy-support.md b/dev-notes/architecture/socks-proxy-support.md index 83cdb6aa1..9cecd35d0 100644 --- a/dev-notes/architecture/socks-proxy-support.md +++ b/dev-notes/architecture/socks-proxy-support.md @@ -1,6 +1,6 @@ # SOCKS proxy support -Tau uses `httpx` for provider requests, OAuth token refreshes, and startup update checks. `httpx` reads standard proxy environment variables such as `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY`. +Tau uses `httpx2` for provider requests, OAuth token refreshes, and startup update checks. `httpx2` reads standard proxy environment variables such as `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY`. ## What changed @@ -10,33 +10,33 @@ Issue #221 reported failures when the environment contained a generic SOCKS prox ALL_PROXY=socks://127.0.0.1:1080 ``` -`httpx` does not accept the generic `socks://` scheme. It accepts explicit SOCKS schemes such as `socks5://` and `socks5h://`, and those require the optional SOCKS dependency. +`httpx2` does not accept the generic `socks://` scheme. It accepts explicit SOCKS schemes such as `socks5://` and `socks5h://`, and those require the optional SOCKS dependency. Tau now: -- installs `httpx[socks]` in the base package so `socksio` is available; +- installs `httpx2[socks]` in the base package so `socksio` is available; - normalizes `socks://...` to `socks5://...` before constructing Tau-owned HTTP clients; - routes provider clients, OAuth token refresh clients, and update-check fetches through shared helpers in `tau_ai.http`. ## Why `socks://` maps to `socks5://` -The generic scheme does not specify whether DNS lookup should happen locally or through the proxy. Tau treats it as SOCKS5 with local DNS resolution because that is the closest explicit `httpx` scheme and avoids silently changing DNS behavior beyond making the previously invalid URL usable. +The generic scheme does not specify whether DNS lookup should happen locally or through the proxy. Tau treats it as SOCKS5 with local DNS resolution because that is the closest explicit `httpx2` scheme and avoids silently changing DNS behavior beyond making the previously invalid URL usable. Users who need proxy-side DNS resolution should set an explicit `socks5h://` URL. ## Future improvement: avoid temporary environment mutation -The current helper temporarily normalizes proxy environment variables while constructing Tau-owned `httpx` clients. For the synchronous update-check helper, the normalization currently wraps the full `httpx.get(...)` call because `httpx.get` constructs and uses a short-lived client internally. +The current helper temporarily normalizes proxy environment variables while constructing Tau-owned `httpx2` clients. For the synchronous update-check helper, the normalization currently wraps the full `httpx2.get(...)` call because `httpx2.get` constructs and uses a short-lived client internally. This is acceptable for the current low-concurrency startup update-check path, but environment variables are process-global state. If Tau later performs more concurrent networking around this helper, another thread or task could observe the normalized proxy value while the request is in progress. If this becomes a concern, prefer avoiding process environment mutation for request execution: 1. normalize proxy values into local data; -2. construct an explicit `httpx.Client` or `httpx.AsyncClient` with equivalent proxy configuration; +2. construct an explicit `httpx2.Client` or `httpx2.AsyncClient` with equivalent proxy configuration; 3. perform requests through that client without changing `os.environ` during request execution. -When implementing that, preserve `NO_PROXY` semantics. `httpx` currently handles environment proxy discovery and no-proxy matching internally, so replacing it with explicit mounts/proxy configuration should include tests for: +When implementing that, preserve `NO_PROXY` semantics. `httpx2` currently handles environment proxy discovery and no-proxy matching internally, so replacing it with explicit mounts/proxy configuration should include tests for: - `ALL_PROXY=socks://...` normalization; - `HTTP_PROXY` and `HTTPS_PROXY` handling; diff --git a/dev-notes/hugging-face-response-provider-metadata.md b/dev-notes/hugging-face-response-provider-metadata.md index cf29185c1..4f3ca4e2a 100644 --- a/dev-notes/hugging-face-response-provider-metadata.md +++ b/dev-notes/hugging-face-response-provider-metadata.md @@ -24,7 +24,7 @@ not expose provider selection or pinning controls in this change. ## Validation -Focused tests use an `httpx.MockTransport` to simulate one failed response from +Focused tests use an `httpx2.MockTransport` to simulate one failed response from one Inference Provider followed by a successful response from another. They verify that only the provider from the successful request becomes the final message's `response_provider`, while `provider` remains `huggingface`. diff --git a/dev-notes/llama-cpp-phase-5.md b/dev-notes/llama-cpp-phase-5.md index 13510ce2c..45597385f 100644 --- a/dev-notes/llama-cpp-phase-5.md +++ b/dev-notes/llama-cpp-phase-5.md @@ -63,7 +63,7 @@ stale; Tau does not silently select the remaining model. ## How to test -The deterministic suite uses `httpx.MockTransport` and fake credential/state +The deterministic suite uses `httpx2.MockTransport` and fake credential/state stores. It covers endpoint safety, auth headers, cache/offline behavior, malformed discovery, metadata allowlisting, stale model handling, atomic state writes, orphan cleanup, reset, Doctor, real runtime registration, generation diff --git a/dev-notes/llama-cpp-phase-7.md b/dev-notes/llama-cpp-phase-7.md index 8859924ed..784c32299 100644 --- a/dev-notes/llama-cpp-phase-7.md +++ b/dev-notes/llama-cpp-phase-7.md @@ -74,7 +74,7 @@ search metadata. ## How to test -All HTTP behavior is deterministic through `httpx.MockTransport`: +All HTTP behavior is deterministic through `httpx2.MockTransport`: ```bash uv run pytest tests/test_llama_cpp_extension.py -q diff --git a/plans/602-built-in-local-inference.md b/plans/602-built-in-local-inference.md index 57606017e..7083f9918 100644 --- a/plans/602-built-in-local-inference.md +++ b/plans/602-built-in-local-inference.md @@ -1333,7 +1333,7 @@ Test: ### 15.5 llama.cpp fake HTTP server -Use `httpx.MockTransport` or the transport injection already used by Tau. Cover: +Use `httpx2.MockTransport` or the transport injection already used by Tau. Cover: - root URL normalization; - `/v1` normalization; diff --git a/pyproject.toml b/pyproject.toml index c11413408..c3dec07b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ license-files = ["LICENSE"] requires-python = ">=3.12" dependencies = [ "anyio>=4.0", - "httpx[socks]>=0.27", + "httpx2[socks]>=2.12.0", "packaging>=24.0", "pillow>=11.0", "pydantic>=2.11", diff --git a/src/tau_ai/anthropic.py b/src/tau_ai/anthropic.py index 8614cd1ee..e435ec143 100644 --- a/src/tau_ai/anthropic.py +++ b/src/tau_ai/anthropic.py @@ -6,7 +6,7 @@ from json import loads from typing import Any, cast -import httpx +import httpx2 from tau_agent.messages import ( AgentMessage, @@ -77,7 +77,7 @@ def __init__( self, config: AnthropicConfig, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> None: self._config = config self._client = client @@ -341,7 +341,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: finish_reason=finish_reason, ) return - except httpx.HTTPError as exc: + except httpx2.HTTPError as exc: if not emitted_content and self._should_retry(attempt): delay = retry_delay_seconds( attempt, @@ -369,7 +369,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: return iterator() - def _get_client(self) -> httpx.AsyncClient: + def _get_client(self) -> httpx2.AsyncClient: if self._client is None: self._client = create_async_client(timeout=self._config.timeout_seconds) return self._client diff --git a/src/tau_ai/google.py b/src/tau_ai/google.py index 859be362f..c7ab11a4d 100644 --- a/src/tau_ai/google.py +++ b/src/tau_ai/google.py @@ -5,7 +5,7 @@ from collections.abc import AsyncIterator, Mapping from json import JSONDecodeError, loads -import httpx +import httpx2 from tau_agent.messages import ( AgentMessage, @@ -51,7 +51,7 @@ def __init__( self, config: OpenAICompatibleConfig, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> None: self._config = config self._client = client @@ -184,7 +184,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: for parser_event in parser.finalize(): yield parser_event return - except httpx.HTTPError as exc: + except httpx2.HTTPError as exc: if not parser.emitted_content and self._should_retry(attempt): delay = retry_delay_seconds( attempt, @@ -206,7 +206,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: return iterator() - def _get_client(self) -> httpx.AsyncClient: + def _get_client(self) -> httpx2.AsyncClient: if self._client is None: self._client = create_async_client(timeout=self._config.timeout_seconds) return self._client diff --git a/src/tau_ai/http.py b/src/tau_ai/http.py index 121bc0111..fc2529d87 100644 --- a/src/tau_ai/http.py +++ b/src/tau_ai/http.py @@ -7,7 +7,7 @@ from contextlib import contextmanager from typing import Any -import httpx +import httpx2 _PROXY_ENV_VARS = ( "HTTP_PROXY", @@ -20,9 +20,9 @@ def normalize_proxy_url(proxy_url: str) -> str: - """Return an httpx-compatible proxy URL. + """Return an httpx2-compatible proxy URL. - Some environments use ``socks://`` as a generic SOCKS proxy scheme. httpx + Some environments use ``socks://`` as a generic SOCKS proxy scheme. httpx2 accepts explicit SOCKS versions (for example ``socks5://`` and ``socks5h://``), but rejects the generic scheme before it can make a request. Treat the generic form as SOCKS5 so Tau can honor these proxy @@ -36,7 +36,7 @@ def normalize_proxy_url(proxy_url: str) -> str: @contextmanager def normalized_proxy_environment() -> Iterator[None]: - """Temporarily normalize proxy environment variables for httpx construction.""" + """Temporarily normalize proxy environment variables for httpx2 construction.""" original: dict[str, str | None] = {} changed = False @@ -62,18 +62,18 @@ def normalized_proxy_environment() -> Iterator[None]: os.environ[name] = value -def create_async_client(**kwargs: Any) -> httpx.AsyncClient: - """Create an ``httpx.AsyncClient`` with Tau's proxy normalization applied.""" +def create_async_client(**kwargs: Any) -> httpx2.AsyncClient: + """Create an ``httpx2.AsyncClient`` with Tau's proxy normalization applied.""" with normalized_proxy_environment(): - return httpx.AsyncClient(**kwargs) + return httpx2.AsyncClient(**kwargs) def get_json(url: str, *, timeout: float, follow_redirects: bool = False) -> dict[str, object]: """Fetch a JSON object with Tau's proxy normalization applied.""" with normalized_proxy_environment(): - response = httpx.get(url, timeout=timeout, follow_redirects=follow_redirects) + response = httpx2.get(url, timeout=timeout, follow_redirects=follow_redirects) response.raise_for_status() data = response.json() if not isinstance(data, dict): diff --git a/src/tau_ai/mistral.py b/src/tau_ai/mistral.py index e6ee689c5..e07f083e3 100644 --- a/src/tau_ai/mistral.py +++ b/src/tau_ai/mistral.py @@ -6,7 +6,7 @@ from json import JSONDecodeError, dumps, loads from typing import Any, Protocol -import httpx +import httpx2 from tau_agent.messages import ( AgentMessage, @@ -51,7 +51,7 @@ def __init__( self, config: OpenAICompatibleConfig, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> None: self._config = config self._client = client @@ -174,7 +174,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: for parser_event in parser.finalize(): yield parser_event return - except httpx.HTTPError as exc: + except httpx2.HTTPError as exc: if not parser.emitted_content and self._should_retry(attempt): delay = retry_delay_seconds( attempt, @@ -196,7 +196,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: return iterator() - def _get_client(self) -> httpx.AsyncClient: + def _get_client(self) -> httpx2.AsyncClient: if self._client is None: self._client = create_async_client(timeout=self._config.timeout_seconds) return self._client diff --git a/src/tau_ai/openai_codex.py b/src/tau_ai/openai_codex.py index 32f47c9b7..87f8aad94 100644 --- a/src/tau_ai/openai_codex.py +++ b/src/tau_ai/openai_codex.py @@ -8,7 +8,7 @@ from platform import machine, release, system from typing import Any -import httpx +import httpx2 from tau_agent.messages import ( AgentMessage, @@ -93,7 +93,7 @@ def __init__( self, config: OpenAICodexConfig, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> None: self._config = config self._client = client @@ -280,7 +280,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: if not await wait_for_retry(delay, signal=signal): return continue - except httpx.HTTPError as exc: + except httpx2.HTTPError as exc: if not emitted_content and self._should_retry(attempt): delay = retry_delay_seconds( attempt, @@ -311,7 +311,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: return iterator() - def _get_client(self) -> httpx.AsyncClient: + def _get_client(self) -> httpx2.AsyncClient: if self._client is None: self._client = create_async_client(timeout=self._config.timeout_seconds) return self._client @@ -502,7 +502,7 @@ def _tool_to_codex(tool: AgentTool) -> dict[str, JSONValue]: async def _codex_provider_events( - response: httpx.Response, + response: httpx2.Response, *, signal: CancellationToken | None, ) -> AsyncIterator[ProviderEvent]: @@ -677,7 +677,7 @@ async def _codex_provider_events( ) -async def _iter_sse_objects(response: httpx.Response) -> AsyncIterator[dict[str, JSONValue]]: +async def _iter_sse_objects(response: httpx2.Response) -> AsyncIterator[dict[str, JSONValue]]: data_lines: list[str] = [] async for line in response.aiter_lines(): stripped = line.strip() diff --git a/src/tau_ai/openai_compatible.py b/src/tau_ai/openai_compatible.py index 47e01e598..297b3f5a0 100644 --- a/src/tau_ai/openai_compatible.py +++ b/src/tau_ai/openai_compatible.py @@ -15,7 +15,7 @@ from json import JSONDecodeError, dumps, loads from typing import Any, Protocol -import httpx +import httpx2 from tau_agent.messages import ( AgentMessage, @@ -79,7 +79,7 @@ def __init__( self, config: OpenAICompatibleConfig, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> None: self._config = config self._client = client @@ -347,7 +347,7 @@ async def iterator() -> AsyncIterator[ProviderEvent]: for parser_event in final_events: yield parser_event return - except httpx.HTTPError as exc: + except httpx2.HTTPError as exc: if not parser.emitted_content and self._should_retry(attempt): delay = retry_delay_seconds( attempt, @@ -392,7 +392,7 @@ def _session_affinity_format(self, *, responses: bool) -> str | None: value = self._config.compat.get("sessionAffinityFormat") return value if isinstance(value, str) else "openai" - def _get_client(self) -> httpx.AsyncClient: + def _get_client(self) -> httpx2.AsyncClient: if self._client is None: self._client = create_async_client(timeout=self._config.timeout_seconds) return self._client @@ -403,7 +403,7 @@ def _should_retry(self, attempt: int, *, status_code: int | None = None) -> bool return status_code is None or _is_transient_status(status_code) -def _response_header_value(response: httpx.Response, header_name: str | None) -> str | None: +def _response_header_value(response: httpx2.Response, header_name: str | None) -> str | None: """Return one normalized response metadata header when configured.""" if header_name is None: return None diff --git a/src/tau_coding/built_in_extensions.py b/src/tau_coding/built_in_extensions.py index c6d1f9e9b..0f28cfa6b 100644 --- a/src/tau_coding/built_in_extensions.py +++ b/src/tau_coding/built_in_extensions.py @@ -7,7 +7,7 @@ from inspect import iscoroutinefunction from typing import TYPE_CHECKING -import httpx +import httpx2 from tau_coding.credentials import CredentialStore from tau_coding.paths import TauPaths @@ -29,7 +29,7 @@ class BuiltInExtensionContext: paths: TauPaths credential_store: CredentialStore environment: Mapping[str, str] - http_client: httpx.AsyncClient | None = None + http_client: httpx2.AsyncClient | None = None BuiltInExtensionSetup = Callable[["ExtensionAPI"], None] diff --git a/src/tau_coding/extensions/builtins/llama_cpp/huggingface.py b/src/tau_coding/extensions/builtins/llama_cpp/huggingface.py index 80fa3f755..b77eee42f 100644 --- a/src/tau_coding/extensions/builtins/llama_cpp/huggingface.py +++ b/src/tau_coding/extensions/builtins/llama_cpp/huggingface.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from pathlib import Path -import httpx +import httpx2 HF_API_ROOT = "https://huggingface.co/api" HF_TOKEN_ENV = "HF_TOKEN" @@ -67,7 +67,7 @@ def discover_hf_token( async def search_gguf_repositories( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, query: str, *, token: str | None, @@ -105,7 +105,7 @@ async def search_gguf_repositories( async def repository_details( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, repository: str, *, token: str | None, @@ -167,14 +167,14 @@ def _headers(token: str | None) -> dict[str, str]: return {"Authorization": f"Bearer {token}"} if token else {} -def _json(response: httpx.Response) -> object: +def _json(response: httpx2.Response) -> object: try: return response.json() except ValueError as exc: raise HuggingFaceSearchError("Hugging Face returned malformed JSON.") from exc -def _raise_http(response: httpx.Response, operation: str) -> None: +def _raise_http(response: httpx2.Response, operation: str) -> None: if response.status_code in {401, 403}: raise HuggingFaceSearchError( "Hugging Face denied access. Accept the repository terms and provide HF_TOKEN " diff --git a/src/tau_coding/extensions/builtins/llama_cpp/router.py b/src/tau_coding/extensions/builtins/llama_cpp/router.py index eea659d3f..034a73da4 100644 --- a/src/tau_coding/extensions/builtins/llama_cpp/router.py +++ b/src/tau_coding/extensions/builtins/llama_cpp/router.py @@ -14,7 +14,7 @@ from dataclasses import dataclass from typing import Literal, TypeGuard, cast -import httpx +import httpx2 LLAMA_CPP_ROUTER_MIN_BUILD = 9688 LLAMA_CPP_ROUTER_MAX_BUILD = 10595 @@ -51,7 +51,7 @@ class RouterModel: async def detect_router( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, server_root: str, headers: Mapping[str, str], ) -> RouterCapability: @@ -88,7 +88,7 @@ async def detect_router( async def list_router_models( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, server_root: str, headers: Mapping[str, str], *, @@ -190,7 +190,7 @@ def _is_non_negative_number(value: object) -> TypeGuard[int | float]: async def watch_router_download_progress( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, server_root: str, headers: Mapping[str, str], model_id: str, @@ -241,7 +241,7 @@ def _forward_download_progress( async def mutate_router_model( - client: httpx.AsyncClient, + client: httpx2.AsyncClient, server_root: str, headers: Mapping[str, str], *, @@ -260,7 +260,7 @@ async def mutate_router_model( raise LlamaCppRouterError(f"llama.cpp did not accept the model {action} request.") -def _object(response: httpx.Response, endpoint: str) -> Mapping[str, object]: +def _object(response: httpx2.Response, endpoint: str) -> Mapping[str, object]: try: payload = response.json() except ValueError as exc: @@ -270,7 +270,7 @@ def _object(response: httpx.Response, endpoint: str) -> Mapping[str, object]: return payload -def _raise_http(response: httpx.Response, operation: str) -> None: +def _raise_http(response: httpx2.Response, operation: str) -> None: if response.status_code in {401, 403}: raise LlamaCppRouterError( "llama.cpp rejected the router request. Check the optional API key or LLAMA_API_KEY." @@ -283,7 +283,7 @@ def _raise_http(response: httpx.Response, operation: str) -> None: ) -def _server_error_detail(response: httpx.Response) -> str | None: +def _server_error_detail(response: httpx2.Response) -> str | None: """Extract one bounded, user-actionable message from a router error.""" try: payload = response.json() diff --git a/src/tau_coding/extensions/builtins/llama_cpp/service.py b/src/tau_coding/extensions/builtins/llama_cpp/service.py index 54d266955..4d033a3bf 100644 --- a/src/tau_coding/extensions/builtins/llama_cpp/service.py +++ b/src/tau_coding/extensions/builtins/llama_cpp/service.py @@ -12,7 +12,7 @@ from typing import Literal, cast from urllib.parse import urlsplit, urlunsplit -import httpx +import httpx2 from tau_agent.harness import SimpleCancellationToken from tau_agent.messages import TextContent, UserMessage @@ -163,7 +163,7 @@ def __init__( state_store: LlamaCppStateStore | None = None, credential_store: CredentialStore | None = None, environment: Mapping[str, str] | None = None, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, timeout_seconds: float = DEFAULT_LLAMA_CPP_TIMEOUT_SECONDS, register_provider: Callable[[DynamicProvider], None] | None = None, update_provider: Callable[[DynamicProvider], bool] | None = None, @@ -337,14 +337,14 @@ async def discover( error = LlamaCppError(str(exc)) self._last_error = error raise error from exc - except httpx.TimeoutException as exc: + except httpx2.TimeoutException as exc: error = LlamaCppError( f"Timed out connecting to llama.cpp at {self.endpoint.server_root}. " f"Check the server and retry. Router setup: {LLAMA_CPP_SERVER_GUIDE_URL}" ) self._last_error = error raise error from exc - except httpx.HTTPError as exc: + except httpx2.HTTPError as exc: error = LlamaCppError( f"Could not connect to llama.cpp at {self.endpoint.server_root}. " f"Start llama-server and retry. Router setup: {LLAMA_CPP_SERVER_GUIDE_URL}" @@ -650,7 +650,7 @@ async def load_model( if cancel_target is not None: return await self._cancel_router_mutation(client, headers, cancel_target) return await self._reconcile_cancelled(client, headers) - except (httpx.HTTPError, LlamaCppRouterError, TimeoutError) as exc: + except (httpx2.HTTPError, LlamaCppRouterError, TimeoutError) as exc: return await self._reconcile_after_router_failure(client, headers, exc) finally: if owned: @@ -696,7 +696,7 @@ async def unload_model( return await self._publish_router_models(models, message=f"Unloaded {model_id}.") except asyncio.CancelledError: return await self._reconcile_cancelled(client, headers) - except (httpx.HTTPError, LlamaCppRouterError, TimeoutError) as exc: + except (httpx2.HTTPError, LlamaCppRouterError, TimeoutError) as exc: return await self._reconcile_after_router_failure(client, headers, exc) finally: if owned: @@ -780,7 +780,7 @@ async def download_model( if mutation_started: return await self._cancel_router_mutation(client, headers, model_id) return await self._reconcile_cancelled(client, headers) - except (httpx.HTTPError, LlamaCppRouterError, TimeoutError) as exc: + except (httpx2.HTTPError, LlamaCppRouterError, TimeoutError) as exc: return await self._reconcile_after_router_failure(client, headers, exc) finally: if progress_task is not None: @@ -800,7 +800,7 @@ async def search_models( token = discover_hf_token(self.environment) try: repositories = await search_gguf_repositories(client, query, token=token) - except (httpx.HTTPError, HuggingFaceSearchError) as exc: + except (httpx2.HTTPError, HuggingFaceSearchError) as exc: return LocalOperationResult(diagnostics=(LocalDiagnostic(str(exc), "error", "search"),)) finally: if owned: @@ -846,7 +846,7 @@ async def search_models( async def _prepare_router_operation( self, context: LocalOperationContext - ) -> tuple[httpx.AsyncClient, bool, Mapping[str, str]] | LocalOperationResult: + ) -> tuple[httpx2.AsyncClient, bool, Mapping[str, str]] | LocalOperationResult: if context.cancelled: return LocalOperationResult(cancelled=True) if not self._router_capability.compatible: @@ -860,7 +860,7 @@ async def _prepare_router_operation( async def _wait_for_router_state( self, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, headers: Mapping[str, str], model_id: str, terminal: set[str], @@ -910,7 +910,7 @@ async def _publish_router_models( async def _cancel_router_mutation( self, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, headers: Mapping[str, str], model_id: str, ) -> LocalOperationResult: @@ -927,7 +927,7 @@ async def _cancel_router_mutation( return await self._reconcile_cancelled(client, headers) async def _reconcile_cancelled( - self, client: httpx.AsyncClient, headers: Mapping[str, str] + self, client: httpx2.AsyncClient, headers: Mapping[str, str] ) -> LocalOperationResult: try: models = await asyncio.shield( @@ -953,7 +953,7 @@ async def _reconcile_cancelled( async def _reconcile_after_router_failure( self, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, headers: Mapping[str, str], error: BaseException, ) -> LocalOperationResult: @@ -1358,7 +1358,7 @@ async def _probe_stream( return True, "Streaming chat completions accepted." if isinstance(event, AssistantErrorEvent): return False, f"Streaming chat completions failed: {event.error.text}" - except httpx.HTTPError: + except httpx2.HTTPError: return False, "Streaming chat completions could not be reached." finally: if owned_client: @@ -1413,7 +1413,7 @@ async def executor( return True, "Tool calls supported." if isinstance(event, AssistantErrorEvent): return False, f"Tool-call probe failed: {event.error.text}" - except httpx.HTTPError: + except httpx2.HTTPError: return False, "Tool-call compatibility could not be checked." finally: if owned_client: @@ -1425,7 +1425,7 @@ async def executor( async def _detect_router_safely( self, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, headers: Mapping[str, str], ) -> RouterCapability: try: @@ -1441,18 +1441,18 @@ async def _detect_router_safely( async def _get( self, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, url: str, headers: Mapping[str, str], signal: CancellationToken | None, - ) -> httpx.Response: + ) -> httpx2.Response: if signal is not None and signal.is_cancelled(): raise asyncio.CancelledError try: response = await client.get(url, headers=dict(headers)) except asyncio.CancelledError: raise - except httpx.TimeoutException: + except httpx2.TimeoutException: raise if response.status_code in {401, 403}: raise _HttpFailure( @@ -1518,14 +1518,14 @@ async def _resolve_auth_for_backend(service: LlamaCppService) -> ResolvedProvide ) -def _safe_json(response: httpx.Response) -> object: +def _safe_json(response: httpx2.Response) -> object: try: return response.json() except ValueError: return None -def _json_object(response: httpx.Response, endpoint: str) -> Mapping[str, object]: +def _json_object(response: httpx2.Response, endpoint: str) -> Mapping[str, object]: payload = _safe_json(response) if not isinstance(payload, Mapping): raise LlamaCppError(f"llama.cpp {endpoint} returned malformed JSON.") diff --git a/src/tau_coding/extensions/providers.py b/src/tau_coding/extensions/providers.py index 504d8dcc9..9ca61322a 100644 --- a/src/tau_coding/extensions/providers.py +++ b/src/tau_coding/extensions/providers.py @@ -9,7 +9,7 @@ from types import MappingProxyType from typing import Protocol -import httpx +import httpx2 from tau_agent.provider import CancellationToken, ModelProvider from tau_agent.types import JSONPrimitive, JSONValue @@ -255,7 +255,7 @@ class OpenAICompatibleTransport: timeout_seconds: float = DEFAULT_OPENAI_COMPATIBLE_TIMEOUT_SECONDS max_retries: int = DEFAULT_OPENAI_COMPATIBLE_MAX_RETRIES max_retry_delay_seconds: float = DEFAULT_OPENAI_COMPATIBLE_MAX_RETRY_DELAY_SECONDS - client: httpx.AsyncClient | None = field(default=None, repr=False, compare=False) + client: httpx2.AsyncClient | None = field(default=None, repr=False, compare=False) def __post_init__(self) -> None: _require_non_empty(self.base_url, "Transport base URL") diff --git a/src/tau_coding/extensions/runtime.py b/src/tau_coding/extensions/runtime.py index adbc4c590..47e22da67 100644 --- a/src/tau_coding/extensions/runtime.py +++ b/src/tau_coding/extensions/runtime.py @@ -11,7 +11,7 @@ from time import time_ns from typing import Literal, Protocol, cast -import httpx +import httpx2 import tau_coding.built_in_extensions as built_in_extension_registry from tau_agent.events import AgentEvent, AgentStartEvent @@ -195,7 +195,7 @@ def __init__( built_in_extensions: Sequence[BuiltInExtension] | None = None, paths: TauPaths | None = None, built_in_credentials: CredentialStore | None = None, - built_in_http_client: httpx.AsyncClient | None = None, + built_in_http_client: httpx2.AsyncClient | None = None, ) -> None: self._generation = ExtensionGeneration() self._built_in_extensions = tuple( @@ -798,7 +798,7 @@ def built_in_credentials(self) -> CredentialStore: return self._built_in_context.credential_store @property - def built_in_http_client(self) -> httpx.AsyncClient | None: + def built_in_http_client(self) -> httpx2.AsyncClient | None: """Return the externally owned HTTP client used by trusted built-ins.""" return self._built_in_context.http_client diff --git a/src/tau_coding/models_dev_store.py b/src/tau_coding/models_dev_store.py index 974ac3384..e6b25f684 100644 --- a/src/tau_coding/models_dev_store.py +++ b/src/tau_coding/models_dev_store.py @@ -11,7 +11,7 @@ from tempfile import NamedTemporaryFile from typing import Any, cast -import httpx +import httpx2 from tau_coding.models_dev import ( MODELS_DEV_URL, @@ -68,7 +68,7 @@ async def refresh_models_dev_catalog( *, paths: TauPaths | None = None, force: bool = False, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, now: float | None = None, ) -> ModelsDevRefreshResult: """Refresh models.dev plus Pi's NVIDIA filter and atomically cache the result.""" @@ -97,7 +97,7 @@ async def refresh_models_dev_catalog( ) owned_client = client is None - http = client or httpx.AsyncClient(timeout=MODELS_REFRESH_TIMEOUT_SECONDS) + http = client or httpx2.AsyncClient(timeout=MODELS_REFRESH_TIMEOUT_SECONDS) try: headers = {"Accept": "application/json", "User-Agent": "tau-model-catalog-refresh"} if cache is not None and cache.get("etag"): @@ -146,7 +146,7 @@ async def refresh_models_dev_catalog( model_count=_model_count(document), cache_path=path, ) - except (httpx.HTTPError, TypeError, ValueError, json.JSONDecodeError) as error: + except (httpx2.HTTPError, TypeError, ValueError, json.JSONDecodeError) as error: raise ModelsDevRefreshError(f"Could not refresh model catalogs: {error}") from error finally: if owned_client: diff --git a/src/tau_coding/oauth.py b/src/tau_coding/oauth.py index 53b34c4b0..fa7d93c25 100644 --- a/src/tau_coding/oauth.py +++ b/src/tau_coding/oauth.py @@ -17,7 +17,7 @@ from typing import Any from urllib.parse import parse_qs, urlencode, urlparse -import httpx +import httpx2 from tau_ai.http import create_async_client from tau_coding.credentials import OAuthCredential @@ -203,7 +203,7 @@ async def login_openai_codex( on_progress: ProgressCallback | None = None, open_browser: bool = True, originator: str = "tau", - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> OAuthCredential: """Run OpenAI Codex OAuth and return refreshable credentials.""" flow = create_openai_codex_authorization_flow(originator=originator) @@ -257,7 +257,7 @@ async def exchange_openai_codex_authorization_code( code: str, verifier: str, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> TokenResponse: """Exchange an OpenAI Codex authorization code for OAuth tokens.""" raw = await _post_openai_codex_token( @@ -283,7 +283,7 @@ async def exchange_openai_codex_authorization_code( async def refresh_openai_codex_token( refresh_token: str, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> OAuthCredential: """Refresh OpenAI Codex OAuth credentials.""" raw = await _post_openai_codex_token( @@ -348,7 +348,7 @@ def _access_token_payload(access_token: str) -> dict[str, Any] | None: async def _post_openai_codex_token( data: dict[str, str], *, - client: httpx.AsyncClient | None, + client: httpx2.AsyncClient | None, action: str, ) -> dict[str, Any]: owns_client = client is None diff --git a/src/tau_coding/oauth_anthropic.py b/src/tau_coding/oauth_anthropic.py index 35803515a..af03ba193 100644 --- a/src/tau_coding/oauth_anthropic.py +++ b/src/tau_coding/oauth_anthropic.py @@ -9,7 +9,7 @@ from typing import Any from urllib.parse import urlencode -import httpx +import httpx2 from tau_ai.http import create_async_client from tau_coding.credentials import OAuthCredential @@ -50,7 +50,7 @@ async def login_anthropic( on_manual_code_input: Callable[[], Awaitable[str]] | None = None, on_progress: Callable[[str], None] | None = None, open_browser: bool = True, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> OAuthCredential: """Run Anthropic's authorization-code + PKCE login flow.""" verifier, challenge = create_pkce_pair() @@ -118,7 +118,7 @@ async def login_anthropic( async def refresh_anthropic_token( refresh_token: str, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> OAuthCredential: """Refresh Anthropic OAuth credentials.""" return await _anthropic_token_request( @@ -136,7 +136,7 @@ async def refresh_anthropic_token( async def _anthropic_token_request( data: dict[str, str], *, - client: httpx.AsyncClient | None, + client: httpx2.AsyncClient | None, action: str, previous_refresh: str | None = None, ) -> OAuthCredential: @@ -174,7 +174,7 @@ async def _anthropic_token_request( ) -def _error_detail(response: httpx.Response, *, secrets: Iterable[str] = ()) -> str: +def _error_detail(response: httpx2.Response, *, secrets: Iterable[str] = ()) -> str: """Summarize a token-endpoint failure body. The endpoint explains itself ("invalid_grant: Refresh token not found or diff --git a/src/tau_coding/oauth_github_copilot.py b/src/tau_coding/oauth_github_copilot.py index 159166149..1ee144bec 100644 --- a/src/tau_coding/oauth_github_copilot.py +++ b/src/tau_coding/oauth_github_copilot.py @@ -7,7 +7,7 @@ from typing import Any from urllib.parse import urlparse -import httpx +import httpx2 from tau_ai.http import create_async_client from tau_coding.credentials import OAuthCredential @@ -72,7 +72,7 @@ def github_copilot_base_url(token: str | None, enterprise_domain: str | None = N async def login_github_copilot( callbacks: OAuthLoginCallbacks, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, cancel_event: asyncio.Event | None = None, ) -> OAuthCredential: """Run GitHub's device flow and exchange its token for Copilot auth.""" @@ -127,7 +127,7 @@ async def login_github_copilot( async def refresh_github_copilot_token( credential: OAuthCredential, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, ) -> OAuthCredential: """Exchange a long-lived GitHub token for a short-lived Copilot token.""" enterprise_domain = oauth_metadata_string(credential.metadata, "enterprise_domain") @@ -160,7 +160,7 @@ async def refresh_github_copilot_token( ) -async def _start_device_flow(domain: str, client: httpx.AsyncClient) -> GitHubDeviceCode: +async def _start_device_flow(domain: str, client: httpx2.AsyncClient) -> GitHubDeviceCode: response = await client.post( f"https://{domain}/login/device/code", data={"client_id": GITHUB_COPILOT_CLIENT_ID, "scope": "read:user"}, @@ -193,7 +193,7 @@ async def _start_device_flow(domain: str, client: httpx.AsyncClient) -> GitHubDe async def _poll_github_access_token( domain: str, device: GitHubDeviceCode, - client: httpx.AsyncClient, + client: httpx2.AsyncClient, *, cancel_event: asyncio.Event | None, ) -> str: @@ -238,7 +238,7 @@ async def poll() -> DevicePollResult[str]: def _response_object( - response: httpx.Response, + response: httpx2.Response, label: str, *, accept_oauth_error: bool = False, diff --git a/src/tau_coding/provider_config.py b/src/tau_coding/provider_config.py index 950736146..163583c75 100644 --- a/src/tau_coding/provider_config.py +++ b/src/tau_coding/provider_config.py @@ -10,7 +10,7 @@ from pathlib import Path from shutil import copy2 from tempfile import NamedTemporaryFile -from typing import Any, Protocol, cast +from typing import Any, Protocol from urllib.parse import urlsplit from tau_ai.env import ( @@ -2326,7 +2326,7 @@ def _optional_provider_api(value: object, field_name: str) -> ProviderApi | None "google-generative-ai", "mistral-conversations", }: - return cast(ProviderApi, value) + return value raise ProviderConfigError(f"Provider field has unsupported API: {field_name}") diff --git a/src/tau_coding/tui/local_backends.py b/src/tau_coding/tui/local_backends.py index b6844c041..81c7ab447 100644 --- a/src/tau_coding/tui/local_backends.py +++ b/src/tau_coding/tui/local_backends.py @@ -9,7 +9,7 @@ import asyncio from collections.abc import Awaitable, Callable, Mapping from contextlib import suppress -from typing import ClassVar, Literal, cast +from typing import ClassVar, cast from rich.console import Console, ConsoleOptions, RenderResult from rich.style import StyleType @@ -639,9 +639,7 @@ def progress(item: LocalProgress) -> None: ) elif action in {"load_model", "unload_model", "download_model"}: assert model_id is not None - manage_action = cast( - Literal["load_model", "unload_model", "download_model"], action - ) + manage_action = action result = await self.registry.manage_model( self.backend_id, manage_action, diff --git a/tests/fixtures/ollama_adapter_spike.py b/tests/fixtures/ollama_adapter_spike.py index b99f3ac15..4583c018c 100644 --- a/tests/fixtures/ollama_adapter_spike.py +++ b/tests/fixtures/ollama_adapter_spike.py @@ -10,7 +10,7 @@ from collections.abc import Mapping from dataclasses import dataclass -import httpx +import httpx2 from tau_agent.harness import SimpleCancellationToken from tau_coding.extensions import ( @@ -34,7 +34,7 @@ class OllamaAdapterSpike: """Small adapter exercising only generic provider/backend seams.""" - client: httpx.AsyncClient + client: httpx2.AsyncClient endpoint: str = "http://ollama.test:11434" def __post_init__(self) -> None: diff --git a/tests/test_extension_providers.py b/tests/test_extension_providers.py index 1330d3199..acd2f8176 100644 --- a/tests/test_extension_providers.py +++ b/tests/test_extension_providers.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import cast -import httpx +import httpx2 import pytest from tau_agent import UserMessage @@ -1462,17 +1462,17 @@ async def test_dynamic_transport_merges_headers_and_supports_custom_auth() -> No credential_store=MemoryCredentials(), # type: ignore[arg-type] environment={}, ) - observed: list[httpx.Request] = [] + observed: list[httpx2.Request] = [] - def handle(request: httpx.Request) -> httpx.Response: + def handle(request: httpx2.Request) -> httpx2.Response: observed.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n', headers={"content-type": "text/event-stream"}, ) - runtime._client = httpx.AsyncClient(transport=httpx.MockTransport(handle)) # type: ignore[attr-defined] + runtime._client = httpx2.AsyncClient(transport=httpx2.MockTransport(handle)) # type: ignore[attr-defined] runtime._owns_client = True # type: ignore[attr-defined] _ = [ event @@ -1522,11 +1522,11 @@ async def test_dynamic_openai_transport_emits_conditional_authorization(with_key environment={}, ) assert isinstance(runtime, OpenAICompatibleProvider) - observed: list[httpx.Request] = [] + observed: list[httpx2.Request] = [] - def handle(request: httpx.Request) -> httpx.Response: + def handle(request: httpx2.Request) -> httpx2.Response: observed.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"}}]}\n\n' 'data: {"choices":[{"finish_reason":"stop"}]}\n\n' @@ -1534,7 +1534,7 @@ def handle(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - runtime._client = httpx.AsyncClient(transport=httpx.MockTransport(handle)) + runtime._client = httpx2.AsyncClient(transport=httpx2.MockTransport(handle)) runtime._owns_client = True events = [ event @@ -1564,15 +1564,15 @@ async def test_dynamic_transport_does_not_infer_endpoint_from_model_name(model_i assert isinstance(runtime, OpenAICompatibleProvider) urls: list[str] = [] - def handle(request: httpx.Request) -> httpx.Response: + def handle(request: httpx2.Request) -> httpx2.Response: urls.append(str(request.url)) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"finish_reason":"stop"}]}\n\ndata: [DONE]\n\n', headers={"content-type": "text/event-stream"}, ) - runtime._client = httpx.AsyncClient(transport=httpx.MockTransport(handle)) + runtime._client = httpx2.AsyncClient(transport=httpx2.MockTransport(handle)) runtime._owns_client = True _ = [ event diff --git a/tests/test_http.py b/tests/test_http.py index c90b0b5df..99ccb6499 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,6 +1,6 @@ import os -import httpx +import httpx2 import pytest from tau_ai.http import create_async_client, normalize_proxy_url, normalized_proxy_environment @@ -38,7 +38,7 @@ async def test_create_async_client_accepts_generic_socks_proxy_env( client = create_async_client(timeout=1) try: - assert isinstance(client, httpx.AsyncClient) + assert isinstance(client, httpx2.AsyncClient) finally: await client.aclose() diff --git a/tests/test_llama_cpp_extension.py b/tests/test_llama_cpp_extension.py index 9ca7dc290..ce6a201dd 100644 --- a/tests/test_llama_cpp_extension.py +++ b/tests/test_llama_cpp_extension.py @@ -7,7 +7,7 @@ from collections.abc import Callable, Mapping from pathlib import Path -import httpx +import httpx2 import pytest from conftest import isolate_home @@ -89,31 +89,31 @@ def _state( def _client( - handler: Callable[[httpx.Request], httpx.Response | Exception], -) -> tuple[httpx.AsyncClient, list[httpx.Request]]: - requests: list[httpx.Request] = [] + handler: Callable[[httpx2.Request], httpx2.Response | Exception], +) -> tuple[httpx2.AsyncClient, list[httpx2.Request]]: + requests: list[httpx2.Request] = [] - def dispatch(request: httpx.Request) -> httpx.Response: + def dispatch(request: httpx2.Request) -> httpx2.Response: requests.append(request) result = handler(request) if isinstance(result, Exception): raise result return result - return httpx.AsyncClient(transport=httpx.MockTransport(dispatch)), requests + return httpx2.AsyncClient(transport=httpx2.MockTransport(dispatch)), requests def _healthy_handler( models: list[Mapping[str, object]] | None = None, -) -> Callable[[httpx.Request], httpx.Response]: +) -> Callable[[httpx2.Request], httpx2.Response]: payload = models if models is not None else [{"id": "qwen-local", "name": "Qwen local"}] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/v1/models": - return httpx.Response(200, json={"object": "list", "data": payload}) - return httpx.Response(404) + return httpx2.Response(200, json={"object": "list", "data": payload}) + return httpx2.Response(404) return handler @@ -121,7 +121,7 @@ def handler(request: httpx.Request) -> httpx.Response: def _service( tmp_path: Path, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, credentials: object | None = None, environment: Mapping[str, str] | None = None, ) -> tuple[LlamaCppService, LlamaCppStateStore, FileCredentialStore]: @@ -140,7 +140,7 @@ def _service( def _runtime( tmp_path: Path, *, - client: httpx.AsyncClient | None = None, + client: httpx2.AsyncClient | None = None, environment: Mapping[str, str] | None = None, ) -> tuple[ExtensionRuntime, TauPaths, FileCredentialStore]: paths = TauPaths(home=tmp_path / "tau", agents_home=tmp_path / "agents") @@ -291,7 +291,7 @@ async def test_auth_headers_cover_stored_environment_fallback_and_no_auth(tmp_pa async def test_discovery_reports_authentication_http_failures( tmp_path: Path, status_code: int ) -> None: - client, _ = _client(lambda request: httpx.Response(status_code, text="denied")) + client, _ = _client(lambda request: httpx2.Response(status_code, text="denied")) service, state_store, _ = _service(tmp_path, client=client) state_store.save(_state()) service = LlamaCppService( @@ -308,8 +308,8 @@ async def test_discovery_reports_authentication_http_failures( @pytest.mark.anyio async def test_discovery_timeout_and_health_loading_are_actionable(tmp_path: Path) -> None: - def timeout(request: httpx.Request) -> Exception: - return httpx.ReadTimeout("timed out", request=request) + def timeout(request: httpx2.Request) -> Exception: + return httpx2.ReadTimeout("timed out", request=request) client, _ = _client(timeout) service, state_store, _ = _service(tmp_path, client=client) @@ -327,7 +327,7 @@ def timeout(request: httpx.Request) -> Exception: assert "github.com/ggml-org/llama.cpp" in result.backend_status.diagnostics[0].message await client.aclose() - loading_client, _ = _client(lambda request: httpx.Response(503, json={"status": "loading"})) + loading_client, _ = _client(lambda request: httpx2.Response(503, json={"status": "loading"})) loading_service, _, _ = _service(tmp_path / "loading", client=loading_client) with pytest.raises(LlamaCppError, match="still loading"): await loading_service.discover(ResolvedProviderAuth()) @@ -348,12 +348,12 @@ def timeout(request: httpx.Request) -> Exception: async def test_malformed_empty_and_multiple_model_responses( tmp_path: Path, payload: object ) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if payload is None: - return httpx.Response(200, text="") - return httpx.Response(200, json=payload) + return httpx2.Response(200, text="") + return httpx2.Response(200, json=payload) client, _ = _client(handler) service, _, _ = _service(tmp_path, client=client) @@ -368,10 +368,10 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_malformed_model_payloads_raise_without_guessing_metadata(tmp_path: Path) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) - return httpx.Response( + return httpx2.Response(200, json={"status": "ok"}) + return httpx2.Response( 200, json={ "data": [ @@ -413,7 +413,7 @@ async def test_dormant_cache_and_offline_refresh_are_network_free(tmp_path: Path assert dormant.provider().models == () assert (await dormant.status(_context("refresh"))).state == "unconfigured" - client, requests = _client(lambda request: httpx.Response(500)) + client, requests = _client(lambda request: httpx2.Response(500)) cached, state_store, credentials = _service(tmp_path / "cached", client=client) state_store.save(_state()) cached = LlamaCppService( @@ -573,11 +573,11 @@ async def test_setup_status_refresh_use_doctor_and_reset_through_real_runtime( ) -> None: current_models = [{"id": "qwen-local", "name": "Qwen local"}] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/v1/models": - return httpx.Response(200, json={"data": current_models}) + return httpx2.Response(200, json={"data": current_models}) if request.method == "POST" and request.url.path == "/v1/chat/completions": payload = json.loads(request.content) if payload.get("tools"): @@ -592,8 +592,8 @@ def handler(request: httpx.Request) -> httpx.Response: 'data: {"choices":[{"delta":{"content":"OK"},"finish_reason":"stop"}]}' "\n\ndata: [DONE]\n\n" ) - return httpx.Response(200, text=text, headers={"content-type": "text/event-stream"}) - return httpx.Response(404) + return httpx2.Response(200, text=text, headers={"content-type": "text/event-stream"}) + return httpx2.Response(404) client, requests = _client(handler) runtime, paths, credentials = _runtime(tmp_path, client=client) @@ -749,13 +749,13 @@ async def test_print_mode_explicit_dynamic_startup_uses_cached_state( LlamaCppStateStore(paths=paths).save(_state()) project = tmp_path / "project" project.mkdir() - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response | Exception: + def handler(request: httpx2.Request) -> httpx2.Response | Exception: requests.append(request) if request.method == "GET": - return httpx.ConnectError("server is down", request=request) - return httpx.Response( + return httpx2.ConnectError("server is down", request=request) + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"hello"},"finish_reason":"stop"}]}\n' @@ -764,8 +764,8 @@ def handler(request: httpx.Request) -> httpx.Response | Exception: headers={"content-type": "text/event-stream"}, ) - def fake_client(*, timeout: float) -> httpx.AsyncClient: - return httpx.AsyncClient(transport=httpx.MockTransport(handler), timeout=timeout) + def fake_client(*, timeout: float) -> httpx2.AsyncClient: + return httpx2.AsyncClient(transport=httpx2.MockTransport(handler), timeout=timeout) monkeypatch.setattr(llama_service, "create_async_client", fake_client) import tau_ai.openai_compatible as compatible @@ -813,11 +813,11 @@ def __init__(self, session: object, **kwargs: object) -> None: async def run_async(self) -> None: return None - def unavailable_client(*, timeout: float) -> httpx.AsyncClient: - def handler(request: httpx.Request) -> Exception: - raise httpx.ConnectError("server is down", request=request) + def unavailable_client(*, timeout: float) -> httpx2.AsyncClient: + def handler(request: httpx2.Request) -> Exception: + raise httpx2.ConnectError("server is down", request=request) - return httpx.AsyncClient(transport=httpx.MockTransport(handler), timeout=timeout) + return httpx2.AsyncClient(transport=httpx2.MockTransport(handler), timeout=timeout) monkeypatch.setattr(tui_app, "TauTuiApp", HeadlessTui) monkeypatch.setattr(llama_service, "create_async_client", unavailable_client) @@ -856,19 +856,19 @@ async def test_builtin_source_lifecycle_is_generation_local(tmp_path: Path) -> N @pytest.mark.anyio async def test_router_capability_version_gate_falls_back_without_mutations(tmp_path: Path) -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b20000-new"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b20000-new"}) if request.url.path == "/v1/models": - return httpx.Response(200, json={"data": [{"id": "fallback"}]}) - return httpx.Response(500) + return httpx2.Response(200, json={"data": [{"id": "fallback"}]}) + return httpx2.Response(500) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) service = LlamaCppService( @@ -897,24 +897,24 @@ async def test_router_load_requires_explicit_confirmation_without_active_peers( state = "unloaded" posts = 0 - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: nonlocal state, posts if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b10000-test"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b10000-test"}) if request.url.path == "/models" and request.method == "GET": - return httpx.Response( + return httpx2.Response( 200, json={"data": [{"id": "target", "status": {"value": state}}]}, ) if request.url.path == "/models/load": posts += 1 state = "loaded" - return httpx.Response(200, json={"success": True}) - return httpx.Response(404) + return httpx2.Response(200, json={"success": True}) + return httpx2.Response(404) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) service = LlamaCppService( @@ -946,11 +946,11 @@ async def test_router_load_unload_download_confirm_reconcile_and_publish(tmp_pat transitions: dict[str, int] = {} mutations: list[tuple[str, str]] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b10000-test"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b10000-test"}) if request.url.path == "/models" and request.method == "GET": for model_id, remaining in tuple(transitions.items()): if remaining <= 0: @@ -958,7 +958,7 @@ def handler(request: httpx.Request) -> httpx.Response: transitions.pop(model_id) else: transitions[model_id] = remaining - 1 - return httpx.Response( + return httpx2.Response( 200, json={ "data": [ @@ -984,10 +984,10 @@ def handler(request: httpx.Request) -> httpx.Response: mutations.append(("download", model_id)) states[model_id] = "downloading" transitions[model_id] = 1 - return httpx.Response(200, json={"success": True}) - return httpx.Response(404) + return httpx2.Response(200, json={"success": True}) + return httpx2.Response(404) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) published = [] service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) @@ -1045,23 +1045,23 @@ async def test_fast_router_download_finishes_without_observing_intermediate_stat ) -> None: downloaded = False - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: nonlocal downloaded if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b10000-test"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b10000-test"}) if request.url.path == "/models" and request.method == "GET": models = ( [{"id": "owner/repo:Q4_K_M", "status": {"value": "unloaded"}}] if downloaded else [] ) - return httpx.Response(200, json={"data": models}) + return httpx2.Response(200, json={"data": models}) if request.url.path == "/models" and request.method == "POST": downloaded = True - return httpx.Response(200, json={"success": True}) - return httpx.Response(404) + return httpx2.Response(200, json={"success": True}) + return httpx2.Response(404) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) service = LlamaCppService( @@ -1096,9 +1096,9 @@ async def test_router_sse_reports_aggregate_download_progress() -> None: "", ) ) - client = httpx.AsyncClient( - transport=httpx.MockTransport( - lambda request: httpx.Response(200, text=payload, request=request) + client = httpx2.AsyncClient( + transport=httpx2.MockTransport( + lambda request: httpx2.Response(200, text=payload, request=request) ) ) progress: list[tuple[int, int]] = [] @@ -1120,21 +1120,21 @@ async def test_router_download_reports_aggregate_byte_progress(tmp_path: Path) - download_poll = 0 started = False - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: nonlocal download_poll, started if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b10000-test"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b10000-test"}) if request.url.path == "/models" and request.method == "POST": started = True - return httpx.Response(200, json={"success": True}) + return httpx2.Response(200, json={"success": True}) if request.url.path == "/models" and request.method == "GET": if not started: - return httpx.Response(200, json={"data": []}) + return httpx2.Response(200, json={"data": []}) download_poll += 1 if download_poll >= 3: - return httpx.Response( + return httpx2.Response( 200, json={ "data": [ @@ -1146,7 +1146,7 @@ def handler(request: httpx.Request) -> httpx.Response: }, ) done = 25 if download_poll == 1 else 75 - return httpx.Response( + return httpx2.Response( 200, json={ "data": [ @@ -1163,9 +1163,9 @@ def handler(request: httpx.Request) -> httpx.Response: ] }, ) - return httpx.Response(404) + return httpx2.Response(404) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) service = LlamaCppService( @@ -1196,15 +1196,15 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_router_download_rejection_surfaces_server_message(tmp_path: Path) -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b10000-test"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b10000-test"}) if request.url.path == "/models" and request.method == "GET": - return httpx.Response(200, json={"data": []}) + return httpx2.Response(200, json={"data": []}) if request.url.path == "/models" and request.method == "POST": - return httpx.Response( + return httpx2.Response( 500, json={ "error": { @@ -1214,9 +1214,9 @@ def handler(request: httpx.Request) -> httpx.Response: } }, ) - return httpx.Response(404) + return httpx2.Response(404) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) service = LlamaCppService( @@ -1245,25 +1245,25 @@ async def test_router_connection_loss_reconciles_without_replaying_mutation(tmp_ posts = 0 list_calls = 0 - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: nonlocal posts, list_calls if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b10000-test"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b10000-test"}) if request.url.path == "/models" and request.method == "GET": list_calls += 1 if list_calls > 2: - raise httpx.ConnectError("lost", request=request) - return httpx.Response( + raise httpx2.ConnectError("lost", request=request) + return httpx2.Response( 200, json={"data": [{"id": "target", "status": {"value": "unloaded"}}]} ) if request.url.path == "/models/load": posts += 1 - return httpx.Response(200, json={"success": True}) - return httpx.Response(404) + return httpx2.Response(200, json={"success": True}) + return httpx2.Response(404) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) service = LlamaCppService( @@ -1289,28 +1289,28 @@ async def test_router_cancellation_requests_documented_cancel_and_reconciles( load_started = asyncio.Event() mutations: list[str] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: nonlocal state if request.url.path == "/health": - return httpx.Response(200, json={"status": "ok"}) + return httpx2.Response(200, json={"status": "ok"}) if request.url.path == "/props": - return httpx.Response(200, json={"role": "router", "build_info": "b10000-test"}) + return httpx2.Response(200, json={"role": "router", "build_info": "b10000-test"}) if request.url.path == "/models" and request.method == "GET": - return httpx.Response( + return httpx2.Response( 200, json={"data": [{"id": "target", "status": {"value": state}}]} ) if request.url.path == "/models/load": mutations.append("load") state = "loading" load_started.set() - return httpx.Response(200, json={"success": True}) + return httpx2.Response(200, json={"success": True}) if request.url.path == "/models/unload": mutations.append("unload") state = "unloaded" - return httpx.Response(200, json={"success": True}) - return httpx.Response(404) + return httpx2.Response(200, json={"success": True}) + return httpx2.Response(404) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, state_store, credentials = _service(tmp_path, client=client) state_store.save(_state(selected_model=None, models=())) service = LlamaCppService( @@ -1338,14 +1338,14 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_hugging_face_search_details_token_and_gating_are_search_only(tmp_path: Path) -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) if request.url.path == "/api/models": - return httpx.Response(200, json=[{"id": "owner/repo-GGUF"}]) + return httpx2.Response(200, json=[{"id": "owner/repo-GGUF"}]) if request.url.path == "/api/models/owner/repo-GGUF": - return httpx.Response( + return httpx2.Response( 200, json={ "id": "owner/repo-GGUF", @@ -1360,7 +1360,7 @@ def handler(request: httpx.Request) -> httpx.Response: ) return _healthy_handler()(request) - client = httpx.AsyncClient(transport=httpx.MockTransport(handler)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) service, _, _ = _service( tmp_path, client=client, diff --git a/tests/test_models_dev_store.py b/tests/test_models_dev_store.py index ecae5c018..6c3a1cd9a 100644 --- a/tests/test_models_dev_store.py +++ b/tests/test_models_dev_store.py @@ -3,7 +3,7 @@ import json from pathlib import Path -import httpx +import httpx2 import pytest import tau_coding.models_dev_store as store @@ -25,15 +25,15 @@ def _source() -> dict[str, object]: return source -def _client(source: dict[str, object]) -> httpx.AsyncClient: - def handler(request: httpx.Request) -> httpx.Response: +def _client(source: dict[str, object]) -> httpx2.AsyncClient: + def handler(request: httpx2.Request) -> httpx2.Response: if str(request.url) == MODELS_DEV_URL: - return httpx.Response(200, json=source, headers={"etag": '"fixture"'}) + return httpx2.Response(200, json=source, headers={"etag": '"fixture"'}) if str(request.url) == NVIDIA_MODELS_URL: - return httpx.Response(200, json={"data": []}) + return httpx2.Response(200, json={"data": []}) raise AssertionError(f"unexpected request: {request.url}") - return httpx.AsyncClient(transport=httpx.MockTransport(handler)) + return httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) @pytest.mark.anyio @@ -74,11 +74,11 @@ async def test_refresh_revalidates_with_etag_and_preserves_cached_body(tmp_path: result_path = paths.home / "models-store.json" before_catalog = json.loads(result_path.read_text(encoding="utf-8"))["catalog"] - def not_modified(request: httpx.Request) -> httpx.Response: + def not_modified(request: httpx2.Request) -> httpx2.Response: assert request.headers["if-none-match"] == '"fixture"' - return httpx.Response(304) + return httpx2.Response(304) - async with httpx.AsyncClient(transport=httpx.MockTransport(not_modified)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(not_modified)) as client: result = await refresh_models_dev_catalog( paths=paths, force=True, @@ -98,10 +98,10 @@ async def test_offline_mode_uses_bundled_catalog_without_network( ) -> None: monkeypatch.setenv("TAU_OFFLINE", "1") - def no_network(_request: httpx.Request) -> httpx.Response: + def no_network(_request: httpx2.Request) -> httpx2.Response: raise AssertionError("offline mode should avoid network") - async with httpx.AsyncClient(transport=httpx.MockTransport(no_network)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(no_network)) as client: result = await refresh_models_dev_catalog( paths=TauPaths(home=tmp_path / ".tau"), force=True, @@ -119,10 +119,10 @@ async def test_fresh_cache_skips_network_and_failed_force_preserves_it(tmp_path: first = await refresh_models_dev_catalog(paths=paths, force=True, client=client, now=1000.0) cached_text = first.cache_path.read_text(encoding="utf-8") - def no_network(_request: httpx.Request) -> httpx.Response: + def no_network(_request: httpx2.Request) -> httpx2.Response: raise AssertionError("fresh cache should avoid network") - async with httpx.AsyncClient(transport=httpx.MockTransport(no_network)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(no_network)) as client: skipped = await refresh_models_dev_catalog( paths=paths, client=client, @@ -130,10 +130,10 @@ def no_network(_request: httpx.Request) -> httpx.Response: ) assert not skipped.refreshed - def failure(_request: httpx.Request) -> httpx.Response: - return httpx.Response(503) + def failure(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(503) - async with httpx.AsyncClient(transport=httpx.MockTransport(failure)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(failure)) as client: with pytest.raises(ModelsDevRefreshError, match="503"): await refresh_models_dev_catalog( paths=paths, diff --git a/tests/test_oauth.py b/tests/test_oauth.py index f683cef49..cddb75b57 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -3,7 +3,7 @@ from time import time from urllib.parse import parse_qs, urlparse -import httpx +import httpx2 import pytest from tau_coding.oauth import ( @@ -52,11 +52,11 @@ def test_account_id_from_access_token_reads_openai_auth_claim() -> None: @pytest.mark.anyio async def test_refresh_openai_codex_token_returns_oauth_credential() -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: body = request.content.decode() assert "grant_type=refresh_token" in body assert "client_id=" in body - return httpx.Response( + return httpx2.Response( 200, json={ "access_token": _jwt("account-2"), @@ -65,7 +65,7 @@ def handler(request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: credential = await refresh_openai_codex_token("old-refresh", client=client) assert credential.access == _jwt("account-2") @@ -79,12 +79,12 @@ async def test_refresh_openai_codex_token_preserves_refresh_and_reads_jwt_expiry expires = int(time()) + 3600 access_token = _jwt("account-3", expires=expires) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: body = request.content.decode() assert "grant_type=refresh_token" in body - return httpx.Response(200, json={"access_token": access_token}) + return httpx2.Response(200, json={"access_token": access_token}) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: credential = await refresh_openai_codex_token("old-refresh", client=client) assert credential.access == access_token diff --git a/tests/test_oauth_providers.py b/tests/test_oauth_providers.py index ca105eee9..51b57831c 100644 --- a/tests/test_oauth_providers.py +++ b/tests/test_oauth_providers.py @@ -1,7 +1,7 @@ import asyncio from typing import cast -import httpx +import httpx2 import pytest from tau_coding.credentials import FileCredentialStore, OAuthCredential @@ -59,14 +59,14 @@ async def on_select(_prompt: OAuthSelectPrompt) -> str | None: @pytest.mark.anyio async def test_refresh_anthropic_token_uses_json_and_redacts_failed_response() -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url == ANTHROPIC_TOKEN_URL assert request.headers["content-type"] == "application/json" assert request.content assert ANTHROPIC_CLIENT_ID.encode() in request.content - return httpx.Response(401, text="secret-token-body") + return httpx2.Response(401, text="secret-token-body") - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: with pytest.raises(OAuthError) as error: await refresh_anthropic_token("refresh-secret", client=client) @@ -79,8 +79,8 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_refresh_anthropic_token_reports_structured_oauth_error() -> None: """A dead refresh token should say so, not just report a status code.""" - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 400, json={ "error": "invalid_grant", @@ -88,7 +88,7 @@ def handler(_request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: with pytest.raises(OAuthError) as error: await refresh_anthropic_token("refresh-secret", client=client) @@ -100,8 +100,8 @@ def handler(_request: httpx.Request) -> httpx.Response: async def test_refresh_anthropic_token_reports_nested_error_without_echoing_token() -> None: """Anthropic's nested envelope still yields detail, minus anything we sent.""" - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 400, json={ "type": "error", @@ -112,7 +112,7 @@ def handler(_request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: with pytest.raises(OAuthError) as error: await refresh_anthropic_token("refresh-secret", client=client) @@ -127,14 +127,14 @@ async def test_refresh_anthropic_token_scrubs_a_token_before_truncating() -> Non """Scrub then truncate: the other order leaks the surviving prefix.""" secret = "refresh-" + "s" * 40 - def handler(_request: httpx.Request) -> httpx.Response: + def handler(_request: httpx2.Request) -> httpx2.Response: # Place the token so it straddles the 200-character truncation point. - return httpx.Response( + return httpx2.Response( 400, json={"error": "invalid_grant", "error_description": "y" * 175 + secret}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: with pytest.raises(OAuthError) as error: await refresh_anthropic_token(secret, client=client) @@ -146,8 +146,8 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_refresh_anthropic_token_returns_provider_neutral_credential() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, json={ "access_token": "anthropic-access", @@ -156,7 +156,7 @@ def handler(_request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: credential = await refresh_anthropic_token("old-refresh", client=client) assert credential.access == "anthropic-access" @@ -169,10 +169,10 @@ def handler(_request: httpx.Request) -> httpx.Response: async def test_github_copilot_device_login_and_token_exchange() -> None: device_codes: list[OAuthDeviceCodeInfo] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: if request.url.path == "/login/device/code": assert f"client_id={GITHUB_COPILOT_CLIENT_ID}" in request.content.decode() - return httpx.Response( + return httpx2.Response( 200, json={ "device_code": "device-secret", @@ -183,10 +183,10 @@ def handler(request: httpx.Request) -> httpx.Response: }, ) if request.url.path == "/login/oauth/access_token": - return httpx.Response(200, json={"access_token": "github-token"}) + return httpx2.Response(200, json={"access_token": "github-token"}) if request.url.path == "/copilot_internal/v2/token": assert request.headers["authorization"] == "Bearer github-token" - return httpx.Response( + return httpx2.Response( 200, json={ "token": "tid=1;exp=9999999999;proxy-ep=proxy.business.githubcopilot.com", @@ -195,7 +195,7 @@ def handler(request: httpx.Request) -> httpx.Response: ) raise AssertionError(f"Unexpected request: {request.url}") - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: credential = await login_github_copilot( _callbacks(device_codes=device_codes), client=client, @@ -216,8 +216,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_github_copilot_rejects_untrusted_device_verification_uri() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, json={ "device_code": "device", @@ -228,16 +228,16 @@ def handler(_request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: with pytest.raises(OAuthError, match="Untrusted verification_uri"): await login_github_copilot(_callbacks(), client=client) @pytest.mark.anyio async def test_refresh_github_copilot_preserves_enterprise_metadata() -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: assert request.url.host == "api.ghe.example.com" - return httpx.Response(200, json={"token": "copilot", "expires_at": 9999999999}) + return httpx2.Response(200, json={"token": "copilot", "expires_at": 9999999999}) original = OAuthCredential( access="old", @@ -245,7 +245,7 @@ def handler(request: httpx.Request) -> httpx.Response: expires=1, metadata={"enterprise_domain": "ghe.example.com"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: refreshed = await refresh_github_copilot_token(original, client=client) assert refreshed.metadata == original.metadata diff --git a/tests/test_ollama_adapter_spike.py b/tests/test_ollama_adapter_spike.py index dc211dd96..67a02285c 100644 --- a/tests/test_ollama_adapter_spike.py +++ b/tests/test_ollama_adapter_spike.py @@ -4,7 +4,7 @@ import json -import httpx +import httpx2 import pytest from fixtures.ollama_adapter_spike import OllamaAdapterSpike, context @@ -18,13 +18,13 @@ def anyio_backend() -> str: return "asyncio" -def _client() -> tuple[httpx.AsyncClient, list[httpx.Request]]: - requests: list[httpx.Request] = [] +def _client() -> tuple[httpx2.AsyncClient, list[httpx2.Request]]: + requests: list[httpx2.Request] = [] - def dispatch(request: httpx.Request) -> httpx.Response: + def dispatch(request: httpx2.Request) -> httpx2.Response: requests.append(request) if request.url.path == "/v1/models": - return httpx.Response( + return httpx2.Response( 200, json={ "object": "list", @@ -35,15 +35,15 @@ def dispatch(request: httpx.Request) -> httpx.Response: }, ) if request.url.path == "/api/tags": - return httpx.Response( + return httpx2.Response( 200, json={"models": [{"name": "qwen3:8b"}, {"name": "code:latest"}]}, ) if request.url.path == "/api/ps": - return httpx.Response(200, json={"models": [{"name": "qwen3:8b"}]}) + return httpx2.Response(200, json={"models": [{"name": "qwen3:8b"}]}) raise AssertionError(f"unexpected Ollama spike request: {request.url}") - return httpx.AsyncClient(transport=httpx.MockTransport(dispatch)), requests + return httpx2.AsyncClient(transport=httpx2.MockTransport(dispatch)), requests async def test_official_ollama_shapes_fit_without_llama_concepts() -> None: @@ -92,11 +92,11 @@ async def test_ollama_openai_compatibility_uses_no_auth_and_offline_cache() -> N async def test_ollama_spike_rejects_malformed_native_state() -> None: - async def dispatch(request: httpx.Request) -> httpx.Response: + async def dispatch(request: httpx2.Request) -> httpx2.Response: del request - return httpx.Response(200, content=json.dumps({"models": {}}).encode()) + return httpx2.Response(200, content=json.dumps({"models": {}}).encode()) - client = httpx.AsyncClient(transport=httpx.MockTransport(dispatch)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(dispatch)) adapter = OllamaAdapterSpike(client) with pytest.raises(ValueError, match="models list"): await adapter.status(context()) diff --git a/tests/test_phase6_hardening.py b/tests/test_phase6_hardening.py index 24e12ec2f..8cb846e6e 100644 --- a/tests/test_phase6_hardening.py +++ b/tests/test_phase6_hardening.py @@ -6,7 +6,7 @@ import json from pathlib import Path -import httpx +import httpx2 import pytest from tau_agent.harness import SimpleCancellationToken @@ -238,9 +238,9 @@ async def test_missing_model_reference_survives_refresh_and_restart(tmp_path: Pa ) ) credentials = FileCredentialStore(paths.home / "credentials.json") - client = httpx.AsyncClient( - transport=httpx.MockTransport( - lambda request: httpx.Response( + client = httpx2.AsyncClient( + transport=httpx2.MockTransport( + lambda request: httpx2.Response( 200, json={"status": "ok"} if request.url.path == "/health" @@ -285,10 +285,10 @@ async def test_secret_state_and_diagnostics_never_contain_key(tmp_path: Path) -> state_store = LlamaCppStateStore(paths=paths) credentials = FileCredentialStore(paths.home / "credentials.json") - async def dispatch(request: httpx.Request) -> httpx.Response: - raise httpx.ConnectError("offline", request=request) + async def dispatch(request: httpx2.Request) -> httpx2.Response: + raise httpx2.ConnectError("offline", request=request) - client = httpx.AsyncClient(transport=httpx.MockTransport(dispatch)) + client = httpx2.AsyncClient(transport=httpx2.MockTransport(dispatch)) service = LlamaCppService( state_store=state_store, credential_store=credentials, diff --git a/tests/test_tau_ai.py b/tests/test_tau_ai.py index 5cec6c206..63499e6b1 100644 --- a/tests/test_tau_ai.py +++ b/tests/test_tau_ai.py @@ -1,7 +1,7 @@ from collections.abc import AsyncIterator, Mapping from json import loads -import httpx +import httpx2 import pytest from tau_agent import ( @@ -148,11 +148,11 @@ async def test_openai_compatible_provider_uses_configured_timeout() -> None: @pytest.mark.anyio async def test_openai_compatible_provider_formats_request_and_streams_text() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"Hel"}}]}\n\n' @@ -162,7 +162,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -214,17 +214,17 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_uses_wire_model_alias_but_reports_logical_model() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -250,16 +250,16 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_observes_headers_after_success() -> None: observed: list[dict[str, str]] = [] - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) if len(requests) == 1: - return httpx.Response( + return httpx2.Response( 500, headers={"x-inference-provider": "failed-provider"}, ) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n', headers={ @@ -268,7 +268,7 @@ def handler(request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -299,9 +299,9 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_observer_failure_keeps_completed_response() -> None: - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: del request - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n', headers={ @@ -314,7 +314,7 @@ def fail_observer(headers: Mapping[str, str]) -> None: del headers raise PermissionError("session index is read-only") - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -343,22 +343,22 @@ def fail_observer(headers: Mapping[str, str]) -> None: @pytest.mark.anyio async def test_openai_compatible_provider_does_not_retry_after_partial_output() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - class FailingStream(httpx.AsyncByteStream): + class FailingStream(httpx2.AsyncByteStream): async def __aiter__(self) -> AsyncIterator[bytes]: yield b'data: {"choices":[{"delta":{"content":"partial"}}]}\n\n' - raise httpx.ReadError("stream dropped") + raise httpx2.ReadError("stream dropped") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, stream=FailingStream(), headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -385,17 +385,17 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_chat_completions_sends_prompt_cache_key_without_affinity_headers() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -420,17 +420,17 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_includes_configured_reasoning_effort() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -455,17 +455,17 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_includes_openrouter_provider_routing() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -489,17 +489,17 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_supports_nested_reasoning_effort_parameter() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -528,11 +528,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_sends_system_instruction_at_top_level() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"candidates":[{"content":{"parts":[{"text":"ok"}]},' @@ -541,7 +541,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -571,11 +571,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_round_trips_thought_signature() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"candidates":[{"content":{"parts":[{"functionCall":' @@ -585,7 +585,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -628,11 +628,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_strips_unsupported_schema_keywords_from_tools() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"candidates":[{"content":{"parts":[{"text":"ok"}]},' @@ -657,7 +657,7 @@ def handler(request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -684,11 +684,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_errors_when_stream_ends_during_thinking() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"candidates":[{"content":{"parts":[' @@ -697,7 +697,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -745,8 +745,8 @@ async def execute( execute_fn=execute, # type: ignore[arg-type] ) - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"candidates":[{"content":{"parts":[' @@ -758,7 +758,7 @@ def handler(_request: httpx.Request) -> httpx.Response: ) messages = [UserMessage(content="Read README.md")] - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -785,17 +785,17 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_retries_empty_clean_close_then_errors() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text="", headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -821,8 +821,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_accepts_explicit_stop_finish_reason() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"candidates":[{"content":{"parts":[{"text":"ok"}]},' @@ -831,7 +831,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -855,8 +855,8 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_maps_max_tokens_finish_reason_to_length() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"candidates":[{"content":{"parts":[{"text":"partial"}]},' @@ -865,7 +865,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -889,17 +889,17 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_google_provider_errors_on_truncated_json_chunk() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"candidates":[\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = GoogleGenerativeAIProvider( OpenAICompatibleConfig( api_key="test-key", @@ -925,8 +925,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_streams_reasoning_content() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"reasoning_content":"plan "}}]}\n\n' @@ -937,7 +937,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -976,11 +976,11 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_replays_persisted_reasoning() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"next"},"finish_reason":"stop"}]}\n\n' @@ -995,7 +995,7 @@ def handler(request: httpx.Request) -> httpx.Response: TextContent(text="prior answer"), ] ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -1023,7 +1023,7 @@ async def test_openai_compatible_provider_streams_tool_calls() -> None: {"type": "object", "properties": {"path": {"type": "string"}}}, ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: payload = loads(request.content) assert payload["tools"] == [ { @@ -1035,7 +1035,7 @@ def handler(request: httpx.Request) -> httpx.Response: }, } ] - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call-1",' @@ -1047,7 +1047,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -1078,16 +1078,16 @@ def handler(request: httpx.Request) -> httpx.Response: async def test_openai_compatible_provider_reports_resolved_response_provider() -> None: attempts = 0 - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: nonlocal attempts attempts += 1 if attempts == 1: - return httpx.Response( + return httpx2.Response( 503, text="temporarily unavailable", headers={"x-inference-provider": "provider-before-failover"}, ) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n' @@ -1099,7 +1099,7 @@ def handler(request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -1130,13 +1130,13 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_retries_transient_status() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) if len(requests) == 1: - return httpx.Response(500, text="try again") - return httpx.Response( + return httpx2.Response(500, text="try again") + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n' @@ -1145,7 +1145,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -1177,14 +1177,14 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_cancellation_stops_retry_backoff() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] signal = SimpleCancellationToken() - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response(503, text="try later") + return httpx2.Response(503, text="try later") - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -1214,16 +1214,16 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_does_not_retry_non_transient_status() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 400, json={"error": {"message": "The selected model is unavailable."}}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -1259,10 +1259,10 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_includes_plain_http_error_body_in_message() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response(400, text="bad request details") + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(400, text="bad request details") - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -1295,14 +1295,14 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_discovers_and_caches_live_model_limits() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, json={ "models": [ @@ -1319,7 +1319,7 @@ def handler(request: httpx.Request) -> httpx.Response: }, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1353,13 +1353,13 @@ async def test_openai_codex_provider_includes_http_error_detail_in_message() -> async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 400, json={"error": {"message": "The requested model does not exist."}}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1396,10 +1396,10 @@ async def test_openai_codex_provider_includes_plain_http_error_body_in_message() async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response(400, text="bad request details") + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response(400, text="bad request details") - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1447,14 +1447,14 @@ async def test_openai_codex_provider_surfaces_nested_stream_error_message() -> N async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=_CODEX_OVERLOAD_ERROR_SSE, headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1494,21 +1494,21 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_retries_transient_stream_error() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) body = _CODEX_OVERLOAD_ERROR_SSE if len(requests) == 1 else _CODEX_TEXT_SSE - return httpx.Response( + return httpx2.Response( 200, text=body, headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1541,12 +1541,12 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_retries_transient_response_failed() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) body = ( 'data: {"type":"response.failed","response":{"status":"failed",' @@ -1555,13 +1555,13 @@ def handler(request: httpx.Request) -> httpx.Response: if len(requests) == 1 else _CODEX_TEXT_SSE ) - return httpx.Response( + return httpx2.Response( 200, text=body, headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1594,20 +1594,20 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_surfaces_stream_error_after_retry_exhaustion() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=_CODEX_OVERLOAD_ERROR_SSE, headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1637,14 +1637,14 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_does_not_retry_non_transient_stream_error() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"error","error":{"type":"invalid_request_error",' @@ -1654,7 +1654,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1682,12 +1682,12 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_formats_request_and_streams_text() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) payload = loads(request.content) assert payload["model"] == "gpt-5.5" @@ -1700,7 +1700,7 @@ def handler(request: httpx.Request) -> httpx.Response: "content": [{"type": "input_text", "text": "Say hello"}], } ] - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"response.output_text.delta","delta":"Hel"}\n\n' @@ -1710,7 +1710,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1752,20 +1752,20 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_includes_configured_reasoning_effort() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"response.completed","response":{"status":"completed"}}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1792,20 +1792,20 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_omits_reasoning_when_unset() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"response.completed","response":{"status":"completed"}}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1831,8 +1831,8 @@ async def test_openai_codex_provider_streams_reasoning_deltas() -> None: async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.reasoning.delta","delta":"trace "}\n\n' @@ -1843,7 +1843,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1883,8 +1883,8 @@ async def test_openai_codex_provider_preserves_reasoning_summary_part_boundaries async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.reasoning_summary_text.delta",' @@ -1899,7 +1899,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -1942,7 +1942,7 @@ async def credentials() -> OpenAICodexCredentials: {"type": "object", "properties": {"path": {"type": "string"}}}, ) - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: payload = loads(request.content) assert payload["tools"] == [ { @@ -1953,7 +1953,7 @@ def handler(request: httpx.Request) -> httpx.Response: "strict": None, } ] - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"response.output_item.added",' @@ -1969,7 +1969,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -2003,8 +2003,8 @@ async def test_openai_codex_provider_routes_parallel_tool_argument_streams() -> async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.output_item.added","output_index":0,' @@ -2028,7 +2028,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -2061,11 +2061,11 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_formats_request_and_streams_text() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"message_start","message":{"content":[]}}\n\n' @@ -2079,7 +2079,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2149,17 +2149,17 @@ async def test_anthropic_provider_sends_configured_max_tokens( configured_max_tokens: int | None, expected_max_tokens: int, ) -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"message_stop"}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2184,17 +2184,17 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_includes_configured_thinking_budget() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"message_stop"}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2220,17 +2220,17 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_explicitly_disables_default_adaptive_thinking() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"message_stop"}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2256,8 +2256,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_streams_thinking_deltas() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"message_start","message":{"content":[]}}\n\n' @@ -2273,7 +2273,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig(api_key="test-key", base_url="https://api.anthropic.test/v1"), client=client, @@ -2307,13 +2307,13 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_retries_transient_status_with_event() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) if len(requests) == 1: - return httpx.Response(503, text="overloaded") - return httpx.Response( + return httpx2.Response(503, text="overloaded") + return httpx2.Response( 200, text=( 'data: {"type":"content_block_delta","index":0,' @@ -2324,7 +2324,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2356,13 +2356,13 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_retries_overloaded_529() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) if len(requests) == 1: - return httpx.Response(529, text="overloaded") - return httpx.Response( + return httpx2.Response(529, text="overloaded") + return httpx2.Response( 200, text=( 'data: {"type":"content_block_delta","index":0,' @@ -2373,7 +2373,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2405,12 +2405,12 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_retries_transient_stream_error() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) if len(requests) == 1: - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"error","error":{"type":"overloaded_error",' @@ -2418,7 +2418,7 @@ def handler(request: httpx.Request) -> httpx.Response: ), headers={"content-type": "text/event-stream"}, ) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"content_block_delta","index":0,' @@ -2429,7 +2429,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2460,15 +2460,15 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_surfaces_stream_error_after_retry_exhaustion() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] error_event = { "type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}, } - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"error","error":{"type":"overloaded_error",' @@ -2477,7 +2477,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2507,11 +2507,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_does_not_retry_non_transient_stream_error() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"error","error":{"type":"authentication_error",' @@ -2520,7 +2520,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2546,11 +2546,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_does_not_retry_stream_error_after_content() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"content_block_delta","index":0,' @@ -2561,7 +2561,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2588,7 +2588,7 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_cancellation_stops_stream_error_retry_backoff() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] class CancelDuringBackoff(SimpleCancellationToken): def __init__(self) -> None: @@ -2601,9 +2601,9 @@ def is_cancelled(self) -> bool: signal = CancelDuringBackoff() - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"error","error":{"type":"overloaded_error",' @@ -2612,7 +2612,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2639,13 +2639,13 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_includes_http_error_detail_in_message() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 400, json={"error": {"message": "model: invalid-model is not supported"}}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -2701,11 +2701,11 @@ def test_use_responses_api_routes_only_restricted_models() -> None: @pytest.mark.anyio async def test_responses_api_formats_request_for_restricted_model() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"response.output_text.delta","delta":"Sun"}\n\n' @@ -2730,7 +2730,7 @@ def handler(request: httpx.Request) -> httpx.Response: UserMessage(content="summarize"), ] - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -2799,8 +2799,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_parses_streamed_tool_call() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.output_item.added","output_index":0,' @@ -2820,7 +2820,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -2851,8 +2851,8 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_streams_refusal_as_text() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.refusal.delta","delta":"I can"}\n\n' @@ -2864,7 +2864,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -2897,8 +2897,8 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_streams_reasoning_summary_as_thinking() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.reasoning_summary_text.delta",' @@ -2909,7 +2909,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -2944,8 +2944,8 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_preserves_reasoning_summary_part_boundaries() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.reasoning_summary_text.delta",' @@ -2960,7 +2960,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -2995,17 +2995,17 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_omits_reasoning_when_effort_is_none() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"response.completed","response":{"status":"completed"}}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3033,8 +3033,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_surfaces_stream_failure() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.failed","response":{"status":"failed",' @@ -3043,7 +3043,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -3068,8 +3068,8 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_orders_parallel_tool_calls_by_output_index() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.output_item.added","output_index":0,' @@ -3090,7 +3090,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -3113,14 +3113,14 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_surfaces_top_level_error_event() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text='data: {"type":"error","message":"rate limited"}\n\n', headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -3142,8 +3142,8 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_responses_api_maps_incomplete_status_to_length() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.output_text.delta","delta":"partial"}\n\n' @@ -3152,7 +3152,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig(api_key="test-key", base_url="https://example.test/v1"), client=client, @@ -3175,11 +3175,11 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_reports_usage() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"Hi"},"finish_reason":"stop"}]}\n\n' @@ -3191,7 +3191,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3226,14 +3226,14 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_codex_provider_reports_usage_and_sends_cache_affinity() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"response.output_text.delta","delta":"Hi"}\n\n' @@ -3245,7 +3245,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -3282,11 +3282,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_responses_reports_usage_and_sends_cache_affinity() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"type":"response.output_text.delta","delta":"Hi"}\n\n' @@ -3298,7 +3298,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3334,8 +3334,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_anthropic_provider_reports_usage() -> None: - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"message_start","message":{"content":[],"usage":' @@ -3351,7 +3351,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -3383,11 +3383,11 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_can_disable_usage_in_streaming() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"ok"},"finish_reason":"stop"}]}\n\n' @@ -3396,7 +3396,7 @@ def handler(request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3423,8 +3423,8 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_openai_compatible_provider_reads_usage_from_choice_fallback() -> None: # Moonshot-style: usage lives on the choice, not at the chunk top level. - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"Hi"},"finish_reason":"stop",' @@ -3434,7 +3434,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3465,8 +3465,8 @@ def handler(_request: httpx.Request) -> httpx.Response: async def test_openai_compatible_provider_falls_back_to_prompt_cache_hit_tokens() -> None: # DeepSeek-style: cache reads come via prompt_cache_hit_tokens, and there # is no prompt_tokens_details.cached_tokens to prefer. - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"Hi"},"finish_reason":"stop"}]}\n\n' @@ -3477,7 +3477,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3509,8 +3509,8 @@ async def test_openai_compatible_provider_reported_zero_cached_tokens_wins() -> # Nullish semantics (Pi: cached_tokens ?? prompt_cache_hit_tokens ?? 0): # an explicitly reported cached_tokens of 0 must not fall through to # prompt_cache_hit_tokens. - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"choices":[{"delta":{"content":"Hi"},"finish_reason":"stop"}]}\n\n' @@ -3522,7 +3522,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3551,8 +3551,8 @@ def handler(_request: httpx.Request) -> httpx.Response: async def test_anthropic_provider_reports_usage_from_message_delta_only() -> None: # No usage on message_start: the message_delta usage alone must still # produce a Usage (the `usage or Usage()` branch). - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"message_start","message":{"content":[]}}\n\n' @@ -3565,7 +3565,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", @@ -3599,8 +3599,8 @@ async def test_openai_codex_provider_leaves_reasoning_none_when_unreported() -> async def credentials() -> OpenAICodexCredentials: return OpenAICodexCredentials(access_token="access-token", account_id="account-1") - def handler(_request: httpx.Request) -> httpx.Response: - return httpx.Response( + def handler(_request: httpx2.Request) -> httpx2.Response: + return httpx2.Response( 200, text=( 'data: {"type":"response.output_text.delta","delta":"Hi"}\n\n' @@ -3610,7 +3610,7 @@ def handler(_request: httpx.Request) -> httpx.Response: headers={"content-type": "text/event-stream"}, ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICodexProvider( OpenAICodexConfig( credential_resolver=credentials, @@ -3638,11 +3638,11 @@ def handler(_request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_github_copilot_sends_vision_header_for_tool_result_images() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"response.completed","response":{"status":"completed"}}\n\n', headers={"content-type": "text/event-stream"}, @@ -3653,7 +3653,7 @@ def handler(request: httpx.Request) -> httpx.Response: tool_name="read", content=[ImageContent(data="aW1hZ2U=", mime_type="image/png")], ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = OpenAICompatibleProvider( OpenAICompatibleConfig( api_key="test-key", @@ -3678,11 +3678,11 @@ def handler(request: httpx.Request) -> httpx.Response: @pytest.mark.anyio async def test_github_copilot_anthropic_sends_vision_header() -> None: - requests: list[httpx.Request] = [] + requests: list[httpx2.Request] = [] - def handler(request: httpx.Request) -> httpx.Response: + def handler(request: httpx2.Request) -> httpx2.Response: requests.append(request) - return httpx.Response( + return httpx2.Response( 200, text='data: {"type":"message_stop"}\n\n', headers={"content-type": "text/event-stream"}, @@ -3693,7 +3693,7 @@ def handler(request: httpx.Request) -> httpx.Response: tool_name="read", content=[ImageContent(data="aW1hZ2U=", mime_type="image/png")], ) - async with httpx.AsyncClient(transport=httpx.MockTransport(handler)) as client: + async with httpx2.AsyncClient(transport=httpx2.MockTransport(handler)) as client: provider = AnthropicProvider( AnthropicConfig( api_key="test-key", diff --git a/uv.lock b/uv.lock index 57edf2bcc..85a8d21a3 100644 --- a/uv.lock +++ b/uv.lock @@ -77,15 +77,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, ] -[[package]] -name = "certifi" -version = "2026.5.20" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, -] - [[package]] name = "colorama" version = "0.4.6" @@ -105,31 +96,33 @@ wheels = [ ] [[package]] -name = "httpcore" -version = "1.0.9" +name = "httpcore2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "certifi" }, { name = "h11" }, + { name = "truststore" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +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/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, + { 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" +name = "httpx2" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, + { 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/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +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/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, + { 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.optional-dependencies] @@ -137,6 +130,15 @@ socks = [ { name = "socksio" }, ] +[[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.18" @@ -602,7 +604,7 @@ version = "0.4.0" source = { editable = "." } dependencies = [ { name = "anyio" }, - { name = "httpx", extra = ["socks"] }, + { name = "httpx2", extra = ["socks"] }, { name = "packaging" }, { name = "pillow" }, { name = "pydantic" }, @@ -622,7 +624,7 @@ dev = [ [package.metadata] requires-dist = [ { name = "anyio", specifier = ">=4.0" }, - { name = "httpx", extras = ["socks"], specifier = ">=0.27" }, + { name = "httpx2", extras = ["socks"], specifier = ">=2.12.0" }, { name = "packaging", specifier = ">=24.0" }, { name = "pillow", specifier = ">=11.0" }, { name = "pydantic", specifier = ">=2.11" }, @@ -656,6 +658,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/be/35261223d9416a0751cdff1c7b4a6f881387218a12d439fe22fefebc8c04/textual-8.2.8-py3-none-any.whl", hash = "sha256:267375fd402dc8d981457212efa71f0e3365fd17bba144ba9bb3ed7563cb374a", size = 731418, upload-time = "2026-06-30T06:51:26.364Z" }, ] +[[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.7" diff --git a/website/content/reference/configuration.md b/website/content/reference/configuration.md index 4900840e5..69eef3dc4 100644 --- a/website/content/reference/configuration.md +++ b/website/content/reference/configuration.md @@ -82,7 +82,7 @@ can replace or extend the model's highest-priority instructions. ## Network proxies -Tau uses `httpx` for provider requests, OAuth token refreshes, and startup update +Tau uses `httpx2` for provider requests, OAuth token refreshes, and startup update checks, so it honors standard proxy environment variables such as `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY`. @@ -97,7 +97,7 @@ export ALL_PROXY=socks5h://127.0.0.1:1080 Tau also accepts the generic `socks://` form that some systems and tools set in the environment. Before creating its own HTTP clients, Tau normalizes -`socks://...` to `socks5://...` because `httpx` does not recognize the generic +`socks://...` to `socks5://...` because `httpx2` does not recognize the generic scheme directly. This matters for users behind corporate proxies, VPNs, local tunnels, or