Skip to content

fix: treat llm_base_url="" as explicit clear in store_llm_settings#13471

Merged
enyst merged 2 commits intoOpenHands:mainfrom
LarytheLord:fix/llm-base-url-clear-13420
Mar 20, 2026
Merged

fix: treat llm_base_url="" as explicit clear in store_llm_settings#13471
enyst merged 2 commits intoOpenHands:mainfrom
LarytheLord:fix/llm-base-url-clear-13420

Conversation

@LarytheLord
Copy link
Copy Markdown
Contributor

Summary

Fixes #13420 — it is currently impossible to persist an empty llm_base_url from the settings UI.

Root cause: store_llm_settings used if not settings.llm_base_url: which catches both None (field not provided at all) and "" (explicitly cleared by the user). When the user:

  • saves from Basic view (which intentionally sends llm_base_url: "" to clear advanced settings), or
  • deletes the Base URL field in Advanced view and saves,

the backend treated "" the same as "not provided", triggering auto-detection via get_provider_api_base() and overwriting the user's intent. The old base URL kept coming back.

Fix: Split the condition into two explicit branches:

# Before
if not settings.llm_base_url:
    if settings.llm_base_url is None and existing_settings.llm_base_url:
        ...  # preserve existing

# After
if settings.llm_base_url is None:
    ...  # not provided — preserve existing or auto-detect (no behavior change)
elif settings.llm_base_url == "":
    settings.llm_base_url = None  # explicit clear — honor the intent

The None path has identical behavior to before. Only "" changes: instead of triggering auto-detection, it now normalizes to None and returns.

Test plan

  • Updated test_store_llm_settings_partial_update to assert None (the correct post-fix behavior) instead of the auto-detected URL
  • Added test_store_llm_settings_advanced_view_clear_removes_base_url as a named regression test for this issue
  • All 16 tests in test_settings_store_functions.py pass
  • Existing MCP save test (test_store_llm_settings_mcp_update_preserves_base_url) continues to pass — the None path (field not sent) is unchanged

🤖 Generated with Claude Code

Previously, `if not settings.llm_base_url:` caught both `None` (field
not provided) and `""` (explicitly cleared by the user). When a user
deleted the Base URL in Advanced view, or saved from Basic view (which
sends `llm_base_url: ""`), the backend would overwrite their intent by
running auto-detection via `get_provider_api_base()` or preserving the
old URL — making it impossible to clear a previously-set base URL.

Fix: split the condition into two explicit branches:
- `llm_base_url is None` → not provided at all; preserve existing or
  auto-detect as before (no behavior change for partial saves/MCP saves)
- `llm_base_url == ""` → explicit clear; normalize to None without
  running auto-detection

Adds a regression test `test_store_llm_settings_advanced_view_clear_removes_base_url`
and updates `test_store_llm_settings_partial_update` to assert the now-
correct behavior (None instead of auto-detected URL).

Fixes OpenHands#13420

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
Fixes lint CI failure by converting double quotes to single quotes
per the project's ruff.toml config (inline-quotes = "single").

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@enyst
Copy link
Copy Markdown
Collaborator

enyst commented Mar 19, 2026

Hey @LarytheLord thank you for the contribution!

Could you maybe show a video that it works, or logs (though video would be nice I think), and that is still works for the previous issue?

@LarytheLord
Copy link
Copy Markdown
Contributor Author

Thanks @enyst — here's the test evidence:

Regression test (test_store_llm_settings_advanced_view_clear_removes_base_url):

settings = Settings(llm_model='gpt-4', llm_base_url='')     # user clears field
existing = Settings(llm_model='gpt-4', llm_base_url='https://my-custom-proxy.example.com')

result = await store_llm_settings(settings, existing)
assert result.llm_base_url is None   # ✅ old URL is gone

Existing behavior preserved (test_store_llm_settings_partial_update and test_store_llm_settings_mcp_update_preserves_base_url):

  • When llm_base_url is None (not sent at all, e.g. MCP save), the existing URL is kept — no behavior change.
  • When llm_base_url is "" (explicitly cleared by the user), it now normalizes to None instead of re-running auto-detection.

All 3 tests pass:

tests/.../test_settings_store_functions.py::test_store_llm_settings_advanced_view_clear_removes_base_url PASSED
tests/.../test_settings_store_functions.py::test_store_llm_settings_partial_update PASSED
tests/.../test_settings_store_functions.py::test_store_llm_settings_mcp_update_preserves_base_url PASSED

Full suite (16/16): pytest tests/unit/server/routes/test_settings_store_functions.py — all green.

I can record a UI walkthrough too if you'd like, but the logic is fully covered by these tests. The key distinction is the is None vs == "" split in store_llm_settings — the diff is small and focused.

Copy link
Copy Markdown
Collaborator

@enyst enyst left a comment

Choose a reason for hiding this comment

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

Thank you!

@enyst enyst merged commit a75b576 into OpenHands:main Mar 20, 2026
19 checks passed
@mamoodi mamoodi added the release:cloud-1.18.0 Released in Cloud 1.18.0 label Mar 26, 2026 — with OpenHands AI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

release:cloud-1.18.0 Released in Cloud 1.18.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: LLM settings cannot persist an empty base URL

3 participants