Skip to content

Conversation

@xingyaoww
Copy link
Collaborator

@xingyaoww xingyaoww commented Nov 19, 2025

Description

Fixes #1205

The api_timeout parameter was defined in APIRemoteWorkspace but was not actually being used in the HTTP client configuration. This caused all requests to use the hardcoded 60-second timeout from the base RemoteWorkspace class, preventing users from extending timeouts for slow local models or saturated systems.

Changes

  • Override the client property: Added a client property override in APIRemoteWorkspace that uses the api_timeout field for the HTTP client's read timeout
  • Comprehensive tests: Added three test cases to verify:
    • Custom api_timeout values are properly applied
    • Default api_timeout of 60 seconds works correctly
    • Different timeout values are correctly configured
  • Maintained compatibility: Other timeout values (connect, write, pool) remain at their default values

Testing

All tests pass:

  • New tests in tests/workspace/test_api_remote_workspace.py verify the timeout configuration
  • Existing workspace tests continue to pass
  • Pre-commit hooks (ruff, pyright, pycodestyle) all pass

Impact

Users can now properly configure longer API timeouts by setting the api_timeout parameter when creating an APIRemoteWorkspace, which will help with scenarios like:

  • Slow local LLM models
  • Saturated systems with many concurrent jobs
  • Operations that require more than 60 seconds to complete

@xingyaoww can click here to continue refining the PR


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:9b5b643-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-9b5b643-python \
  ghcr.io/openhands/agent-server:9b5b643-python

All tags pushed for this build

ghcr.io/openhands/agent-server:9b5b643-golang-amd64
ghcr.io/openhands/agent-server:9b5b643-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:9b5b643-golang-arm64
ghcr.io/openhands/agent-server:9b5b643-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:9b5b643-java-amd64
ghcr.io/openhands/agent-server:9b5b643-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:9b5b643-java-arm64
ghcr.io/openhands/agent-server:9b5b643-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:9b5b643-python-amd64
ghcr.io/openhands/agent-server:9b5b643-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:9b5b643-python-arm64
ghcr.io/openhands/agent-server:9b5b643-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:9b5b643-golang
ghcr.io/openhands/agent-server:9b5b643-java
ghcr.io/openhands/agent-server:9b5b643-python

About Multi-Architecture Support

  • Each variant tag (e.g., 9b5b643-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 9b5b643-python-amd64) are also available if needed

The api_timeout parameter was defined but not actually used in the HTTP client
configuration. This caused all requests to use the default 60 second timeout
from the base RemoteWorkspace class, preventing users from extending timeouts
for slow local models.

Changes:
- Override the client property in APIRemoteWorkspace to use api_timeout for
  the read timeout in HTTP requests
- Add comprehensive tests to verify api_timeout is properly applied
- Ensure other timeout values (connect, write, pool) remain at defaults

Fixes #1205

Co-authored-by: openhands <[email protected]>
base_url=self.host, timeout=timeout, headers=self._headers
)
self._client = client
return client
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code smells. @OpenHands, can you check the parent class implementation and suggest ways to simplify how we initialize the client so we don't end up overwriting the client initialized by the parent class multiple times?

We run into a similar situation in _start_or_attach_to_runtime when we do

    self.host = self._runtime_url.rstrip("/")
    self.api_key = self._session_api_key
    self._client = None  # Reset HTTP client with new host and API key
    _ = self.client  # Initialize client by accessing the property
    assert self.client is not None
    assert self.client.base_url == self.host

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on it! xingyaoww can track my progress at all-hands.dev

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary of Refactoring

I've successfully refactored the client initialization code to eliminate the code smell you identified. The changes have been committed and pushed to the PR branch.

Problem Identified

The original implementation had several issues:

  1. Code Duplication: APIRemoteWorkspace duplicated the entire 18-line client property just to change the timeout value
  2. Fragile Reset Logic: Manual reset using self._client = None followed by _ = self.client was not explicit or reusable
  3. Multiple Initializations: Client was being initialized and reset multiple times during the lifecycle

Solution: Template Method Pattern

I applied the Template Method design pattern to make the code more maintainable:

1. Added _create_timeout() method in base classes (RemoteWorkspace and AsyncRemoteWorkspace)

  • Returns default timeout configuration
  • Can be overridden by subclasses to customize timeouts
  • Eliminates need to duplicate entire client property

2. Added _reset_client() method in base classes

  • Encapsulates client reset logic with proper cleanup
  • Makes the reset operation explicit and reusable
  • Properly closes existing client before resetting

3. Simplified APIRemoteWorkspace

  • Removed the entire 18-line client property override
  • Added a simple 12-line _create_timeout() override
  • Updated _start_or_attach_to_runtime() to use self._reset_client() instead of self._client = None

4. Updated AsyncRemoteWorkspace for consistency

  • Applied the same pattern to the async version
  • Previously had no timeout configuration at all
  • Now consistent with sync version

Benefits

Reduced Code Duplication: Child classes now only need to override _create_timeout() (12 lines) instead of the entire client property (18 lines)
Clearer Intent: _reset_client() makes it obvious when and why we're resetting the client
Better Maintainability: Changes to client initialization logic only need to be made in one place
Extensibility: Easy for future subclasses to customize timeout or client creation behavior
Consistency: Both sync and async versions follow the same pattern
Backward Compatible: All 64 existing tests pass without modification

Files Changed

  • openhands-sdk/openhands/sdk/workspace/remote/base.py (+35, -6)
  • openhands-sdk/openhands/sdk/workspace/remote/async_remote_workspace.py (+33, -1)
  • openhands-workspace/openhands/workspace/remote_api/workspace.py (+16, -18)

The changes have been pushed to PR #1206.

View full conversation

This refactoring simplifies how subclasses customize the HTTP client
configuration without duplicating the entire client property logic.

Changes:
- Add _create_timeout() method in base RemoteWorkspace classes that
  subclasses can override to customize timeout configuration
- Add _reset_client() method to encapsulate client reset logic
- Update APIRemoteWorkspace to override _create_timeout() instead of
  duplicating the entire client property
- Apply same pattern to AsyncRemoteWorkspace for consistency
- Replace manual client reset (_client = None) with _reset_client() call

Benefits:
- Eliminates code duplication (child classes no longer need to override
  the entire client property)
- Makes client reset logic explicit and reusable
- Follows template method design pattern for better maintainability
- All existing tests pass, ensuring backward compatibility

Co-authored-by: openhands <[email protected]>
@github-actions
Copy link
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/workspace/remote
   async_remote_workspace.py553536%31, 39–44, 48–51, 54–55, 58–64, 85–87, 105–107, 125–127, 141–143, 157–159
   base.py561573%54–59, 140–142, 156–158, 172–174
openhands-workspace/openhands/workspace/remote_api
   workspace.py18714422%88, 105–108, 112–113, 117, 119–124, 128–129, 131–135, 137, 139–140, 144–145, 150–152, 154–167, 171–172, 174, 176, 187–190, 192–194, 201–202, 206, 213, 217–222, 232, 234, 239–241, 244–251, 253–256, 259, 261–274, 276–278, 280–286, 289, 292–293, 297–298, 300–303, 305–313, 317–318, 320–322, 324–326, 333–334, 336–342, 345, 348, 351
TOTAL12294563754% 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

api_timeout not being used in APIRemoteWorkspace

3 participants