Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions openhands/server/routes/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,9 @@ async def store_llm_settings(
settings.llm_api_key = existing_settings.llm_api_key
if settings.llm_model is None:
settings.llm_model = existing_settings.llm_model
# if llm_base_url is missing or empty, try to preserve existing or determine appropriate URL
if not settings.llm_base_url:
if settings.llm_base_url is None and existing_settings.llm_base_url:
# Not provided at all (e.g. MCP config save) - preserve existing
if settings.llm_base_url is None:
# Not provided at all (e.g. MCP config save) - preserve existing or auto-detect
if existing_settings.llm_base_url:
settings.llm_base_url = existing_settings.llm_base_url
elif is_openhands_model(settings.llm_model):
# OpenHands models use the LiteLLM proxy
Expand All @@ -145,6 +144,9 @@ async def store_llm_settings(
logger.error(
f'Failed to get api_base from litellm for model {settings.llm_model}: {e}'
)
elif settings.llm_base_url == '':
# Explicitly cleared by the user (basic view save or advanced view clear)
settings.llm_base_url = None
# Keep search API key if missing or empty
if not settings.search_api_key:
settings.search_api_key = existing_settings.search_api_key
Expand Down
29 changes: 26 additions & 3 deletions tests/unit/server/routes/test_settings_store_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,32 @@ async def test_store_llm_settings_partial_update():
assert result.llm_model == 'gpt-4'
# For SecretStr objects, we need to compare the secret value
assert result.llm_api_key.get_secret_value() == 'existing-api-key'
# llm_base_url was explicitly cleared (""), so auto-detection runs
# OpenAI models: litellm.get_api_base() returns https://api.openai.com
assert result.llm_base_url == 'https://api.openai.com'
# llm_base_url="" is an explicit clear — must not be repopulated via auto-detection
assert result.llm_base_url is None


@pytest.mark.asyncio
async def test_store_llm_settings_advanced_view_clear_removes_base_url():
"""Regression test for #13420: clearing Base URL in Advanced view must persist.

Before the fix, llm_base_url="" was treated identically to llm_base_url=None,
causing the backend to re-run auto-detection and overwrite the user's intent.
"""
settings = Settings(
llm_model='gpt-4',
llm_base_url='', # User deleted the field in Advanced view
)

existing_settings = Settings(
llm_model='gpt-4',
llm_api_key=SecretStr('my-api-key'),
llm_base_url='https://my-custom-proxy.example.com',
)

result = await store_llm_settings(settings, existing_settings)

# The old custom URL must not come back
assert result.llm_base_url is None


@pytest.mark.asyncio
Expand Down
Loading