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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dev-notes/architecture/codex-runtime-model-limits.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dev-notes/architecture/oauth-provider-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions dev-notes/architecture/socks-proxy-support.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion dev-notes/hugging-face-response-provider-metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
2 changes: 1 addition & 1 deletion dev-notes/llama-cpp-phase-5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion dev-notes/llama-cpp-phase-7.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion plans/602-built-in-local-inference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions src/tau_ai/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from json import loads
from typing import Any, cast

import httpx
import httpx2

from tau_agent.messages import (
AgentMessage,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions src/tau_ai/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
16 changes: 8 additions & 8 deletions src/tau_ai/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from contextlib import contextmanager
from typing import Any

import httpx
import httpx2

_PROXY_ENV_VARS = (
"HTTP_PROXY",
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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):
Expand Down
8 changes: 4 additions & 4 deletions src/tau_ai/mistral.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions src/tau_ai/openai_codex.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from platform import machine, release, system
from typing import Any

import httpx
import httpx2

from tau_agent.messages import (
AgentMessage,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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()
Expand Down
10 changes: 5 additions & 5 deletions src/tau_ai/openai_compatible.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/tau_coding/built_in_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down
Loading