Skip to content

feat(sdk): gate switch llm default tool#3190

Merged
neubig merged 1 commit into
mainfrom
switch-llm-tool-settings
May 11, 2026
Merged

feat(sdk): gate switch llm default tool#3190
neubig merged 1 commit into
mainfrom
switch-llm-tool-settings

Conversation

@neubig
Copy link
Copy Markdown
Contributor

@neubig neubig commented May 10, 2026

Summary

  • Add OpenHandsAgentSettings.enable_switch_llm_tool, defaulting to true.
  • Include SwitchLLMTool in settings-created agents only when the setting is enabled and at least one saved LLM profile is available.
  • Reuse the switch tool's profile discovery for both gating and tool descriptions so invalid/corrupted profile files are skipped consistently.

Stacked on

Validation

  • env -u LMNR_PROJECT_API_KEY -u LMNR_BASE_URL -u LMNR_FORCE_HTTP uv run pytest tests/sdk/tool/test_switch_llm.py tests/sdk/test_settings.py -q
  • env -u LMNR_PROJECT_API_KEY -u LMNR_BASE_URL -u LMNR_FORCE_HTTP uv run pre-commit run --files openhands-sdk/openhands/sdk/settings/model.py openhands-sdk/openhands/sdk/tool/builtins/switch_llm.py tests/sdk/tool/test_switch_llm.py tests/sdk/test_settings.py

This pull request was created by an AI agent (OpenHands) on behalf of the user.


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.13-nodejs22-slim 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:6dba02e-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:6dba02e-golang-amd64
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-golang-amd64
ghcr.io/openhands/agent-server:switch-llm-tool-settings-golang-amd64
ghcr.io/openhands/agent-server:6dba02e-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:6dba02e-golang-arm64
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-golang-arm64
ghcr.io/openhands/agent-server:switch-llm-tool-settings-golang-arm64
ghcr.io/openhands/agent-server:6dba02e-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:6dba02e-java-amd64
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-java-amd64
ghcr.io/openhands/agent-server:switch-llm-tool-settings-java-amd64
ghcr.io/openhands/agent-server:6dba02e-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:6dba02e-java-arm64
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-java-arm64
ghcr.io/openhands/agent-server:switch-llm-tool-settings-java-arm64
ghcr.io/openhands/agent-server:6dba02e-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:6dba02e-python-amd64
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-python-amd64
ghcr.io/openhands/agent-server:switch-llm-tool-settings-python-amd64
ghcr.io/openhands/agent-server:6dba02e-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:6dba02e-python-arm64
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-python-arm64
ghcr.io/openhands/agent-server:switch-llm-tool-settings-python-arm64
ghcr.io/openhands/agent-server:6dba02e-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:6dba02e-golang
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-golang
ghcr.io/openhands/agent-server:switch-llm-tool-settings-golang
ghcr.io/openhands/agent-server:6dba02e-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:6dba02e-java
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-java
ghcr.io/openhands/agent-server:switch-llm-tool-settings-java
ghcr.io/openhands/agent-server:6dba02e-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:6dba02e-python
ghcr.io/openhands/agent-server:6dba02e794701401b3d6adeaabafa700cd091042-python
ghcr.io/openhands/agent-server:switch-llm-tool-settings-python
ghcr.io/openhands/agent-server:6dba02e-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 6dba02e-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., 6dba02e-python-amd64) are also available if needed

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

Taste Rating: 🟢 Good taste - Clean conditional gating that solves a real UX problem.

[RISK ASSESSMENT]

  • [Overall PR] ⚠️ Risk Assessment: 🟢 LOW

Simple setting that conditionally includes SwitchLLMTool based on (1) the new enable_switch_llm_tool flag and (2) presence of saved profiles. Defaults to true for backward compatibility. Well-tested with comprehensive coverage of all scenarios. No agent behavior or eval impact.

VERDICT:
Worth merging: Pragmatic UX improvement with clean implementation.

KEY INSIGHT:
Eliminating the switch tool when no profiles exist removes a special case from the user's perspective - the tool only appears when it's actually usable.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 10, 2026

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/settings
   model.py5214990%268, 270, 282, 284, 339, 349–352, 355, 368, 372, 378, 388, 394, 399, 575, 588, 599, 609, 613, 615, 617, 619, 621, 623, 625, 869, 871, 1143, 1211, 1327, 1363–1366, 1392, 1516, 1561, 1593, 1603, 1605, 1610, 1628, 1641, 1643, 1645, 1647, 1654
openhands-sdk/openhands/sdk/tool/builtins
   switch_llm.py651281%34–40, 62, 97, 108, 125, 165
TOTAL26294758671% 

Copy link
Copy Markdown
Collaborator

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

✅ QA Report: PASS

Verified that the SwitchLLMTool gating feature works correctly across all scenarios.

Does this PR achieve its stated goal?

Yes. The PR successfully gates the SwitchLLMTool behind the new enable_switch_llm_tool setting (defaulting to true) and automatically excludes it when no saved LLM profiles exist. All four behavioral scenarios were verified through actual execution: (1) the setting exists with the correct default, (2) the tool is included when enabled with profiles, (3) the tool is excluded when disabled, and (4) the tool is excluded when no profiles exist even if the setting is enabled. The schema export also correctly includes the new field with appropriate metadata.

Phase Result
Environment Setup ✅ Clean build with make build
CI Status ✅ All checks passing (13/13 pass, qa-changes pending)
Functional Verification ✅ All 5 test scenarios passed
Functional Verification

Test 1: Setting exists with correct default

Step 1 — Verify the new setting:
Created an OpenHandsAgentSettings instance and checked for the enable_switch_llm_tool field:

settings = OpenHandsAgentSettings(llm=_make_llm("test-model", "test"))
assert hasattr(settings, 'enable_switch_llm_tool')
assert settings.enable_switch_llm_tool is True

Result:

✅ PASS: Setting exists and defaults to True

This confirms the new setting is properly defined and has the expected default value.


Test 2: SwitchLLMTool included when enabled with profiles

Step 1 — Create a temporary profile directory with profiles:

store = LLMProfileStore(base_dir=profile_dir)
store.save("fast", _make_llm("fast-model", "fast"))
store.save("slow", _make_llm("slow-model", "slow"))

Step 2 — Create settings with enable_switch_llm_tool=True:

settings = OpenHandsAgentSettings(
    llm=_make_llm("default-model", "default"),
    enable_switch_llm_tool=True
)
agent = settings.create_agent()

Step 3 — Verify SwitchLLMTool is included:

assert "SwitchLLMTool" in agent.include_default_tools
conversation = LocalConversation(agent=agent, workspace=Path.cwd())
conversation._ensure_agent_ready()
assert "switch_llm" in agent.tools_map

Result:

✅ PASS: SwitchLLMTool is included when enabled with profiles

This confirms the tool is correctly added to both include_default_tools and the runtime tools_map when conditions are met.


Test 3: SwitchLLMTool excluded when disabled

Step 1 — Create profiles (same as Test 2):
Saved LLM profiles to ensure profiles exist.

Step 2 — Create settings with enable_switch_llm_tool=False:

settings = OpenHandsAgentSettings(
    llm=_make_llm("default-model", "default"),
    enable_switch_llm_tool=False
)
agent = settings.create_agent()

Step 3 — Verify SwitchLLMTool is NOT included:

assert "SwitchLLMTool" not in agent.include_default_tools
conversation = LocalConversation(agent=agent, workspace=Path.cwd())
conversation._ensure_agent_ready()
assert "switch_llm" not in agent.tools_map

Result:

✅ PASS: SwitchLLMTool is excluded when disabled

This confirms the setting correctly gates the tool even when profiles are available.


Test 4: SwitchLLMTool excluded without profiles

Step 1 — Create an empty profile directory:

profile_dir.mkdir()  # Empty directory, no profiles

Step 2 — Create settings with enable_switch_llm_tool=True:

settings = OpenHandsAgentSettings(
    llm=_make_llm("default-model", "default"),
    enable_switch_llm_tool=True  # Enabled but no profiles
)
agent = settings.create_agent()

Step 3 — Verify SwitchLLMTool is NOT included:

assert "SwitchLLMTool" not in agent.include_default_tools
conversation = LocalConversation(agent=agent, workspace=Path.cwd())
conversation._ensure_agent_ready()
assert "switch_llm" not in agent.tools_map

Result:

✅ PASS: SwitchLLMTool is excluded when no profiles exist

This confirms the tool is correctly omitted when no profiles are available, even when the setting is enabled.


Test 5: Schema export includes the new field

Step 1 — Export the schema:

schema = OpenHandsAgentSettings.export_schema()

Step 2 — Verify the field is present with correct properties:

general_section = [s for s in schema.sections if s.key == 'general'][0]
switch_llm_field = [f for f in general_section.fields if f.key == 'enable_switch_llm_tool'][0]

Result:

General section fields: {'enable_sub_agents', 'tools', 'enable_switch_llm_tool', 'agent', 'mcp_config'}
✅ Field found with correct properties:
   - key: enable_switch_llm_tool
   - value_type: boolean
   - default: True
   - label: Enable LLM switching tool
   - prominence: SettingProminence.MINOR

This confirms the schema export correctly includes the new field with appropriate metadata for client applications.

Issues Found

None.


Verdict: This PR fully achieves its stated goal and is ready for merge. The gating logic works correctly in all scenarios, the setting is properly exported in the schema, and all CI checks pass.

@neubig neubig force-pushed the switch-llm-tool-settings branch from ac5b217 to 288639f Compare May 10, 2026 15:39
Comment thread openhands-sdk/openhands/sdk/settings/model.py
Base automatically changed from agent-switch-llm-tool to main May 11, 2026 13:55
Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig force-pushed the switch-llm-tool-settings branch from 288639f to 6dba02e Compare May 11, 2026 13:56
@github-actions
Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions
Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@neubig neubig merged commit 1a884f7 into main May 11, 2026
31 checks passed
@neubig neubig deleted the switch-llm-tool-settings branch May 11, 2026 14:10
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.

4 participants