feat(sdk): add per-call peer chat timeout (#1098)

Forward optional timeout overrides through sync and async Peer.chat while retaining client-wide defaults.

Refs #734
This commit is contained in:
steven-ji 2026-09-03 05:31:46 +08:00 committed by GitHub
parent a5fa8c3962
commit 55a0519bd2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 68 additions and 1 deletions

View File

@ -276,6 +276,9 @@ response = alice.chat("What do I know about Bob?", target="bob")
response = alice.chat("What happened in session-1?", session="session-1")
response = alice.chat("Summarize what matters most to me.", reasoning_level="high")
# Override the timeout for one non-streaming dialectic request
response = alice.chat("Give me a quick summary.", timeout=5.0)
# Add content to a session with a peer
session = honcho.session("session-1")
session.add_messages([
@ -378,6 +381,13 @@ const bobConclusions = await alice.conclusionsOf("bob").list(); // Conclusions
```
</CodeGroup>
For Python, `peer.chat(timeout=...)` and `await peer.aio.chat(timeout=...)`
accept a timeout in seconds for each HTTP attempt made by one non-streaming
request. Omit it or pass `None` to use the client-wide timeout configured on
`Honcho`. Retries still follow the client's `max_retries` setting and can extend
total elapsed time; use `max_retries=0` when a host shutdown budget permits only
one attempt.
### Peer Context
The `context()` method on peers retrieves both the working representation and peer card in a single API call:

View File

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased]
### Added
- Optional per-call `timeout` on synchronous and asynchronous `Peer.chat()`. It overrides the timeout for each HTTP attempt; when omitted or set to `None`, the client-wide timeout configured on `Honcho` remains in effect.
## [2.4.0] - 2026-08-25
### Added

View File

@ -777,6 +777,7 @@ class PeerAio(AsyncMetadataConfigMixin):
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[TResponseFormat],
timeout: float | None = None,
) -> TResponseFormat | None: ...
@overload
@ -791,6 +792,7 @@ class PeerAio(AsyncMetadataConfigMixin):
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: dict[str, Any] | None = None,
timeout: float | None = None,
) -> str | None: ...
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
@ -805,12 +807,17 @@ class PeerAio(AsyncMetadataConfigMixin):
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[BaseModel] | dict[str, Any] | None = None,
timeout: float | None = Field(
None, gt=0, description="Timeout in seconds for this chat request"
),
) -> BaseModel | str | None:
"""Query the peer's representation asynchronously.
See Peer.chat for parameter details. When response_format is a Pydantic
model class, the answer is parsed into an instance of it; when it is a
JSON Schema dict, the answer is a JSON string.
JSON Schema dict, the answer is a JSON string. When timeout is omitted,
the Honcho client's configured timeout is used; retries can extend total
elapsed time.
"""
await self._peer._honcho._ensure_workspace_async()
target_id = resolve_id(target)
@ -835,6 +842,7 @@ class PeerAio(AsyncMetadataConfigMixin):
data = await self._peer._honcho._async_http_client.post(
routes.peer_chat(self._peer.workspace_id, self._peer.id),
body=body,
timeout=timeout,
)
content = data.get("content")
if not content:

View File

@ -246,6 +246,7 @@ class Peer(PeerBase, MetadataConfigMixin):
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[TResponseFormat],
timeout: float | None = None,
) -> TResponseFormat | None: ...
@overload
@ -260,6 +261,7 @@ class Peer(PeerBase, MetadataConfigMixin):
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: dict[str, Any] | None = None,
timeout: float | None = None,
) -> str | None: ...
@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
@ -274,6 +276,9 @@ class Peer(PeerBase, MetadataConfigMixin):
reasoning_level: Literal["minimal", "low", "medium", "high", "max"]
| None = None,
response_format: type[BaseModel] | dict[str, Any] | None = None,
timeout: float | None = Field(
None, gt=0, description="Timeout in seconds for this chat request"
),
) -> BaseModel | str | None:
"""
Query the peer's representation with a natural language question.
@ -310,6 +315,9 @@ class Peer(PeerBase, MetadataConfigMixin):
model class to get a parsed instance back, or a raw
JSON Schema dict (root type "object") to get the
answer as a JSON string.
timeout: Optional timeout in seconds for each HTTP attempt made by
this request. When omitted, the Honcho client's configured
timeout is used. Retries can extend total elapsed time.
Returns:
Response string containing the answer (a JSON string when a schema
@ -342,6 +350,7 @@ class Peer(PeerBase, MetadataConfigMixin):
data = self._honcho._http.post(
routes.peer_chat(self.workspace_id, self.id),
body=body,
timeout=timeout,
)
content = data.get("content")
if not content:

View File

@ -277,6 +277,40 @@ async def test_peer_chat_non_streaming(
assert response is None or isinstance(response, str)
@pytest.mark.asyncio
@pytest.mark.parametrize("timeout", [None, 2.5])
async def test_peer_chat_forwards_per_call_timeout(
client_fixture: tuple[Honcho, str],
timeout: float | None,
) -> None:
honcho_client, client_type = client_fixture
timeout_label = "default" if timeout is None else "override"
if client_type == "async":
peer = await honcho_client.aio.peer(id=f"test-timeout-{timeout_label}-async")
async def mock_post(*args: object, **kwargs: object) -> dict[str, str]: # pyright: ignore[reportUnusedParameter]
return {"content": "ok"}
with patch.object(
peer._honcho._async_http_client, # pyright: ignore[reportPrivateUsage]
"post",
side_effect=mock_post,
) as mock:
result = await peer.aio.chat("What do I like?", timeout=timeout)
else:
peer = honcho_client.peer(id=f"test-timeout-{timeout_label}-sync")
with patch.object(
peer._honcho._http, # pyright: ignore[reportPrivateUsage]
"post",
return_value={"content": "ok"},
) as mock:
result = peer.chat("What do I like?", timeout=timeout)
assert result == "ok"
assert mock.call_args.kwargs["timeout"] == timeout
@pytest.mark.asyncio
async def test_peer_representation_no_params(
client_fixture: tuple[Honcho, str],