Add get_llm(), get_secrets(), and get_mcp_config() methods to RemoteWorkspace#3077
Add get_llm(), get_secrets(), and get_mcp_config() methods to RemoteWorkspace#3077
Conversation
…orkspace Add default implementations of settings methods to RemoteWorkspace base class that fetch configuration from the agent-server's persisted settings endpoints. This enables DockerWorkspace, APIRemoteWorkspace, and other RemoteWorkspace subclasses to retrieve LLM config, secrets, and MCP config from the agent-server. - get_llm(): Fetches LLM settings from /api/settings with X-Expose-Secrets: plaintext - get_secrets(): Returns LookupSecret references for lazy secret resolution - get_mcp_config(): Returns MCP configuration in MCPConfig-compatible format OpenHandsCloudWorkspace already overrides these methods to use Cloud API endpoints. Closes #3076 Co-authored-by: openhands <openhands@all-hands.dev>
Python API breakage checks — ✅ PASSEDResult: ✅ PASSED |
REST API breakage checks (OpenAPI) — ✅ PASSEDResult: ✅ PASSED |
Coverage Report •
|
||||||||||||||||||||||||||||||||||||||||||||||||||
Add example 13 showing how to: 1. Spin up an agent-server 2. Configure LLM settings via the Settings API 3. Use workspace.get_llm() to retrieve a configured LLM 4. Start a conversation using the retrieved LLM 5. Demonstrate get_secrets() and get_mcp_config() methods Co-authored-by: openhands <openhands@all-hands.dev>
- Generate random session API key for agent-server - Demonstrate 401 rejection without API key - Show RemoteWorkspace.api_key usage for authenticated requests - Verify LookupSecrets include auth headers for resolution Co-authored-by: openhands <openhands@all-hands.dev>
all-hands-bot
left a comment
There was a problem hiding this comment.
🟢 Good taste - Clean implementation of settings retrieval methods with proper authentication, retry logic, and comprehensive tests.
[IMPROVEMENT OPPORTUNITIES]
-
[openhands-sdk/openhands/sdk/workspace/remote/base.py, Lines 307-313] Defensive Programming: The conditional dict building (
if llm_config.get("model")) could hide configuration errors. If the server returns empty strings, they're silently skipped. Consider preserving all values and letting the LLM constructor validate them. -
[openhands-sdk/openhands/sdk/workspace/remote/base.py, Line 432] Unclear Comment: The comment mentions "legacy format" but doesn't explain what it is. If no legacy format exists, this is dead code that should be removed. If there is a legacy format, document it.
[RISK ASSESSMENT]
- [Overall PR]
⚠️ Risk Assessment: 🟢 LOW
Additive feature that provides convenience methods for fetching settings from agent-server. Well-tested with proper authentication and retry logic. No existing behavior modified.
VERDICT:
✅ Worth merging: Core logic is sound, minor improvements suggested
KEY INSIGHT:
Provides a clean abstraction for remote workspaces to fetch LLM/secrets/MCP configuration from agent-server settings, following the existing pattern where OpenHandsCloudWorkspace can override for Cloud API endpoints.
🔄 Running Examples with
|
| Example | Status | Duration | Cost |
|---|---|---|---|
| 01_standalone_sdk/02_custom_tools.py | ✅ PASS | 24.9s | $0.03 |
| 01_standalone_sdk/03_activate_skill.py | ✅ PASS | 22.1s | $0.03 |
| 01_standalone_sdk/05_use_llm_registry.py | ✅ PASS | 11.5s | $0.01 |
| 01_standalone_sdk/07_mcp_integration.py | ✅ PASS | 37.0s | $0.03 |
| 01_standalone_sdk/09_pause_example.py | ✅ PASS | 14.5s | $0.01 |
| 01_standalone_sdk/10_persistence.py | ✅ PASS | 47.2s | $0.04 |
| 01_standalone_sdk/11_async.py | ✅ PASS | 33.0s | $0.04 |
| 01_standalone_sdk/12_custom_secrets.py | ✅ PASS | 9.3s | $0.00 |
| 01_standalone_sdk/13_get_llm_metrics.py | ✅ PASS | 30.6s | $0.02 |
| 01_standalone_sdk/14_context_condenser.py | ✅ PASS | 3m 7s | $0.20 |
| 01_standalone_sdk/17_image_input.py | ✅ PASS | 22.6s | $0.02 |
| 01_standalone_sdk/18_send_message_while_processing.py | ✅ PASS | 25.8s | $0.02 |
| 01_standalone_sdk/19_llm_routing.py | ✅ PASS | 18.0s | $0.02 |
| 01_standalone_sdk/20_stuck_detector.py | ✅ PASS | 14.9s | $0.02 |
| 01_standalone_sdk/21_generate_extraneous_conversation_costs.py | ✅ PASS | 10.4s | $0.00 |
| 01_standalone_sdk/22_anthropic_thinking.py | ✅ PASS | 15.7s | $0.01 |
| 01_standalone_sdk/23_responses_reasoning.py | ✅ PASS | 1m 48s | $0.02 |
| 01_standalone_sdk/24_planning_agent_workflow.py | ✅ PASS | 5m 22s | $0.35 |
| 01_standalone_sdk/25_agent_delegation.py | ✅ PASS | 54.4s | $0.06 |
| 01_standalone_sdk/26_custom_visualizer.py | ✅ PASS | 17.6s | $0.02 |
| 01_standalone_sdk/28_ask_agent_example.py | ✅ PASS | 44.1s | $0.05 |
| 01_standalone_sdk/29_llm_streaming.py | ✅ PASS | 38.9s | $0.03 |
| 01_standalone_sdk/30_tom_agent.py | ✅ PASS | 9.4s | $0.01 |
| 01_standalone_sdk/31_iterative_refinement.py | ✅ PASS | 4m 56s | $0.37 |
| 01_standalone_sdk/32_configurable_security_policy.py | ✅ PASS | 17.4s | $0.02 |
| 01_standalone_sdk/34_critic_example.py | ✅ PASS | 6m 24s | $0.58 |
| 01_standalone_sdk/36_event_json_to_openai_messages.py | ✅ PASS | 9.8s | $0.01 |
| 01_standalone_sdk/37_llm_profile_store/main.py | ✅ PASS | 4.0s | $0.00 |
| 01_standalone_sdk/38_browser_session_recording.py | ✅ PASS | 38.3s | $0.03 |
| 01_standalone_sdk/39_llm_fallback.py | ✅ PASS | 10.2s | $0.01 |
| 01_standalone_sdk/40_acp_agent_example.py | ✅ PASS | 45.4s | $0.13 |
| 01_standalone_sdk/41_task_tool_set.py | ✅ PASS | 31.5s | $0.03 |
| 01_standalone_sdk/42_file_based_subagents.py | ✅ PASS | 1m 25s | $0.08 |
| 01_standalone_sdk/43_mixed_marketplace_skills/main.py | ✅ PASS | 6.9s | $0.00 |
| 01_standalone_sdk/44_model_switching_in_convo.py | ✅ PASS | 8.5s | $0.01 |
| 01_standalone_sdk/45_parallel_tool_execution.py | ✅ PASS | 3m 25s | $0.57 |
| 01_standalone_sdk/46_agent_settings.py | ✅ PASS | 11.6s | $0.01 |
| 01_standalone_sdk/47_defense_in_depth_security.py | ✅ PASS | 3.3s | $0.00 |
| 01_standalone_sdk/48_conversation_fork.py | ✅ PASS | 12.4s | $0.00 |
| 02_remote_agent_server/01_convo_with_local_agent_server.py | ✅ PASS | 35.2s | $0.02 |
| 02_remote_agent_server/02_convo_with_docker_sandboxed_server.py | ✅ PASS | 1m 33s | $0.05 |
| 02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py | ✅ PASS | 1m 42s | -- |
| 02_remote_agent_server/04_convo_with_api_sandboxed_server.py | ✅ PASS | 1m 21s | $0.04 |
| 02_remote_agent_server/07_convo_with_cloud_workspace.py | ✅ PASS | 29.5s | $0.04 |
| 02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py | ✅ PASS | 3m 22s | $0.02 |
| 02_remote_agent_server/09_acp_agent_with_remote_runtime.py | ✅ PASS | 1m 11s | $0.12 |
| 02_remote_agent_server/10_cloud_workspace_share_credentials.py | ✅ PASS | 1m 23s | $0.06 |
| 02_remote_agent_server/11_conversation_fork.py | ✅ PASS | 32.9s | $0.00 |
| 02_remote_agent_server/12_settings_and_secrets_api.py | ✅ PASS | 2m 10s | $0.02 |
| 02_remote_agent_server/13_workspace_get_llm.py | ✅ PASS | 19.4s | $0.01 |
| 04_llm_specific_tools/01_gpt5_apply_patch_preset.py | ✅ PASS | 20.5s | $0.01 |
| 04_llm_specific_tools/02_gemini_file_tools.py | ✅ PASS | 43.8s | $0.08 |
| 05_skills_and_plugins/01_loading_agentskills/main.py | ✅ PASS | 16.0s | $0.02 |
| 05_skills_and_plugins/02_loading_plugins/main.py | ✅ PASS | 22.2s | $0.02 |
✅ All tests passed!
Total: 54 | Passed: 54 | Failed: 0 | Total Cost: $3.44
Address review feedback: 1. Pass all llm_config fields to LLM constructor, not just model/api_key/base_url 2. Move response models from agent-server to SDK for sharing: - SettingsResponse, SettingsUpdateRequest - SecretsListResponse, SecretItemResponse, SecretCreateRequest 3. Use Pydantic model_validate() for proper response validation 4. Agent-server now imports these models from SDK This enables SDK clients (RemoteWorkspace) to validate responses using the same models used by agent-server endpoints, eliminating duplication and ensuring contract consistency. Co-authored-by: openhands <openhands@all-hands.dev>
Build on the previous commit's SettingsResponse validation by also validating the agent_settings dict through validate_agent_settings(), which produces a fully typed AgentSettingsConfig (OpenHandsAgentSettings or ACPAgentSettings). This gives: - get_llm(): returns settings.llm directly (a real LLM instance with all persisted fields, not just model/api_key/base_url) - get_mcp_config(): accesses settings.mcp_config (typed MCPConfig), uses isinstance check to correctly handle ACPAgentSettings which has no mcp_config field - _fetch_agent_settings(): shared helper that calls GET /api/settings, validates the outer SettingsResponse, then validates the inner agent_settings dict through the SDK's discriminated union Co-authored-by: openhands <openhands@all-hands.dev>
…afety - Add get_agent_settings() method that validates and returns AgentSettingsConfig - Add get_conversation_settings() method that validates and returns ConversationSettings - Keep dict[str, Any] fields since server needs to control secret serialization via context - Document why typed fields aren't used (FastAPI serialization loses context) - Provide examples in docstrings for client usage Co-authored-by: openhands <openhands@all-hands.dev>
xingyaoww
left a comment
There was a problem hiding this comment.
LGTM once you've confirmed example 02-13 works!
🔄 Running Examples with
|
| Example | Status | Duration | Cost |
|---|---|---|---|
| 01_standalone_sdk/02_custom_tools.py | ✅ PASS | 23.0s | $0.03 |
| 01_standalone_sdk/03_activate_skill.py | ✅ PASS | 23.6s | $0.03 |
| 01_standalone_sdk/05_use_llm_registry.py | ✅ PASS | 12.9s | $0.01 |
| 01_standalone_sdk/07_mcp_integration.py | ✅ PASS | 33.8s | $0.03 |
| 01_standalone_sdk/09_pause_example.py | ✅ PASS | 13.2s | $0.01 |
| 01_standalone_sdk/10_persistence.py | ✅ PASS | 31.9s | $0.03 |
| 01_standalone_sdk/11_async.py | ✅ PASS | 34.7s | $0.04 |
| 01_standalone_sdk/12_custom_secrets.py | ✅ PASS | 9.9s | $0.00 |
| 01_standalone_sdk/13_get_llm_metrics.py | ✅ PASS | 41.4s | $0.04 |
| 01_standalone_sdk/14_context_condenser.py | ✅ PASS | 3m 29s | $0.22 |
| 01_standalone_sdk/17_image_input.py | ✅ PASS | 20.8s | $0.02 |
| 01_standalone_sdk/18_send_message_while_processing.py | ✅ PASS | 16.5s | $0.01 |
| 01_standalone_sdk/19_llm_routing.py | ✅ PASS | 16.0s | $0.02 |
| 01_standalone_sdk/20_stuck_detector.py | ✅ PASS | 15.4s | $0.02 |
| 01_standalone_sdk/21_generate_extraneous_conversation_costs.py | ✅ PASS | 10.7s | $0.00 |
| 01_standalone_sdk/22_anthropic_thinking.py | ✅ PASS | 21.4s | $0.01 |
| 01_standalone_sdk/23_responses_reasoning.py | ✅ PASS | 1m 26s | $0.01 |
| 01_standalone_sdk/24_planning_agent_workflow.py | ✅ PASS | 4m 3s | $0.31 |
| 01_standalone_sdk/25_agent_delegation.py | ✅ PASS | 1m 4s | $0.07 |
| 01_standalone_sdk/26_custom_visualizer.py | ✅ PASS | 19.5s | $0.02 |
| 01_standalone_sdk/28_ask_agent_example.py | ✅ PASS | 32.9s | $0.04 |
| 01_standalone_sdk/29_llm_streaming.py | ✅ PASS | 37.4s | $0.03 |
| 01_standalone_sdk/30_tom_agent.py | ✅ PASS | 9.1s | $0.01 |
| 01_standalone_sdk/31_iterative_refinement.py | ✅ PASS | 5m 20s | $0.36 |
| 01_standalone_sdk/32_configurable_security_policy.py | ✅ PASS | 18.1s | $0.02 |
| 01_standalone_sdk/34_critic_example.py | ✅ PASS | 1m 25s | $0.11 |
| 01_standalone_sdk/36_event_json_to_openai_messages.py | ✅ PASS | 12.8s | $0.01 |
| 01_standalone_sdk/37_llm_profile_store/main.py | ✅ PASS | 7.6s | $0.00 |
| 01_standalone_sdk/38_browser_session_recording.py | ✅ PASS | 34.2s | $0.03 |
| 01_standalone_sdk/39_llm_fallback.py | ✅ PASS | 9.8s | $0.01 |
| 01_standalone_sdk/40_acp_agent_example.py | ✅ PASS | 27.0s | $0.13 |
| 01_standalone_sdk/41_task_tool_set.py | ✅ PASS | 20.2s | $0.02 |
| 01_standalone_sdk/42_file_based_subagents.py | ✅ PASS | 1m 26s | $0.08 |
| 01_standalone_sdk/43_mixed_marketplace_skills/main.py | ✅ PASS | 6.2s | $0.00 |
| 01_standalone_sdk/44_model_switching_in_convo.py | ✅ PASS | 7.8s | $0.01 |
| 01_standalone_sdk/45_parallel_tool_execution.py | ✅ PASS | 3m 35s | $0.56 |
| 01_standalone_sdk/46_agent_settings.py | ✅ PASS | 10.2s | $0.01 |
| 01_standalone_sdk/47_defense_in_depth_security.py | ✅ PASS | 3.2s | $0.00 |
| 01_standalone_sdk/48_conversation_fork.py | ✅ PASS | 13.3s | $0.00 |
| 02_remote_agent_server/01_convo_with_local_agent_server.py | ✅ PASS | 32.0s | $0.03 |
| 02_remote_agent_server/02_convo_with_docker_sandboxed_server.py | ✅ PASS | 1m 38s | $0.07 |
| 02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py | ✅ PASS | 1m 1s | $0.06 |
| 02_remote_agent_server/04_convo_with_api_sandboxed_server.py | ✅ PASS | 1m 11s | $0.04 |
| 02_remote_agent_server/07_convo_with_cloud_workspace.py | ✅ PASS | 26.4s | $0.03 |
| 02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py | ✅ PASS | 3m 34s | $0.02 |
| 02_remote_agent_server/09_acp_agent_with_remote_runtime.py | ✅ PASS | 1m 12s | $0.14 |
| 02_remote_agent_server/10_cloud_workspace_share_credentials.py | ✅ PASS | 42.2s | $0.06 |
| 02_remote_agent_server/11_conversation_fork.py | ✅ PASS | 42.5s | $0.00 |
| 02_remote_agent_server/12_settings_and_secrets_api.py | ✅ PASS | 2m 9s | $0.02 |
| 02_remote_agent_server/13_workspace_get_llm.py | ✅ PASS | 24.8s | $0.01 |
| 04_llm_specific_tools/01_gpt5_apply_patch_preset.py | ✅ PASS | 19.9s | $0.02 |
| 04_llm_specific_tools/02_gemini_file_tools.py | ✅ PASS | 37.6s | $0.08 |
| 05_skills_and_plugins/01_loading_agentskills/main.py | ✅ PASS | 21.4s | $0.02 |
| 05_skills_and_plugins/02_loading_plugins/main.py | ✅ PASS | 20.3s | $0.02 |
✅ All tests passed!
Total: 54 | Passed: 54 | Failed: 0 | Total Cost: $2.99
Summary
Add default implementations of
get_llm(),get_secrets(), andget_mcp_config()methods to the baseRemoteWorkspaceclass that fetch configuration from the agent-server's persisted settings endpoints.Closes #3076
Fixes AGE-1451
Related
Changes
New Methods on
RemoteWorkspaceget_llm(**llm_kwargs) -> LLMGET /api/settingswithX-Expose-Secrets: plaintextheaderagent_settings.llmLLMinstanceget_secrets(names: list[str] | None = None) -> dict[str, LookupSecret]GET /api/settings/secretsto list available secretsLookupSecretreferences pointing to/api/settings/secrets/{name}X-Session-API-Keyheader for authenticationget_mcp_config() -> dict[str, Any]GET /api/settingswithX-Expose-Secrets: plaintextheaderagent_settings.mcp_configMCPConfig.model_validate()New Example
Added
examples/02_remote_agent_server/13_workspace_get_llm.pydemonstrating:workspace.get_llm()to retrieve configured LLMworkspace.get_secrets()andworkspace.get_mcp_config()Design Decision
These methods are placed on
RemoteWorkspace(not a specific subclass likeAPIRemoteWorkspace) because:/api/settingsendpointsDockerWorkspace,ApptainerWorkspace,APIRemoteWorkspaceall inherit automaticallyOpenHandsCloudWorkspacealready overrides with Cloud API callsUsage Examples
Testing
All 36 tests in
test_remote_workspace.pypass.This PR was created by an AI agent (OpenHands) on behalf of a user.
Agent Server images for this PR
• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server
Variants & Base Images
eclipse-temurin:17-jdknikolaik/python-nodejs:python3.13-nodejs22-slimgolang:1.21-bookwormPull (multi-arch manifest)
# Each variant is a multi-arch manifest supporting both amd64 and arm64 docker pull ghcr.io/openhands/agent-server:270c233-pythonRun
All tags pushed for this build
About Multi-Architecture Support
270c233-python) is a multi-arch manifest supporting both amd64 and arm64270c233-python-amd64) are also available if needed