diff --git a/enterprise/storage/saas_settings_store.py b/enterprise/storage/saas_settings_store.py index 78925a9f72..2853c16d71 100644 --- a/enterprise/storage/saas_settings_store.py +++ b/enterprise/storage/saas_settings_store.py @@ -160,17 +160,11 @@ class SaasSettingsStore(SettingsStore): ) return None - llm_base_url = ( - org_member.llm_base_url - if org_member.llm_base_url - else org.default_llm_base_url - ) - - # Check if provider is OpenHands and generate API key if needed - if self._is_openhands_provider(item): - await self._ensure_api_key(item, str(org_id), openhands_type=True) - elif llm_base_url == LITE_LLM_API_URL: - await self._ensure_api_key(item, str(org_id)) + # Check if we need to generate an LLM key. + if item.llm_base_url == LITE_LLM_API_URL: + await self._ensure_api_key( + item, str(org_id), openhands_type=self._is_openhands_provider(item) + ) kwargs = item.model_dump(context={'expose_secrets': True}) for model in (user, org, org_member): diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index 6063752dea..2a5d27f83d 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -6,6 +6,8 @@ # Unless you are working on deprecation, please avoid extending this legacy file and consult the V1 codepaths above. # Tag: Legacy-V0 # This module belongs to the old V0 web server. The V1 application server lives under openhands/app_server/. +import os + from fastapi import APIRouter, Depends, status from fastapi.responses import JSONResponse @@ -30,6 +32,10 @@ from openhands.storage.data_models.settings import Settings from openhands.storage.secrets.secrets_store import SecretsStore from openhands.storage.settings.settings_store import SettingsStore +LITE_LLM_API_URL = os.environ.get( + 'LITE_LLM_API_URL', 'https://llm-proxy.app.all-hands.dev' +) + app = APIRouter(prefix='/api', dependencies=get_dependencies()) @@ -123,8 +129,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 settings.llm_base_url is None: - settings.llm_base_url = existing_settings.llm_base_url + # if llm_base_url is missing or empty, set to default as this only happens for "basic" settings + if not settings.llm_base_url: + settings.llm_base_url = LITE_LLM_API_URL # Keep search API key if missing or empty if not settings.search_api_key: settings.search_api_key = existing_settings.search_api_key diff --git a/tests/unit/server/routes/test_settings_store_functions.py b/tests/unit/server/routes/test_settings_store_functions.py index 25c23a7daf..78dd3f92f7 100644 --- a/tests/unit/server/routes/test_settings_store_functions.py +++ b/tests/unit/server/routes/test_settings_store_functions.py @@ -186,7 +186,12 @@ async def test_store_llm_settings_update_existing(): @pytest.mark.asyncio async def test_store_llm_settings_partial_update(): - """Test store_llm_settings with partial update.""" + """Test store_llm_settings with partial update. + + Note: When llm_base_url is not provided in the update, it gets set to the default + LiteLLM proxy URL. This is intentional behavior for "basic" settings where users + don't specify a custom base URL. + """ settings = Settings( llm_model='gpt-4' # Only updating model ) @@ -200,11 +205,17 @@ async def test_store_llm_settings_partial_update(): result = await store_llm_settings(settings, existing_settings) - # Should return settings with updated model but keep other values + # Should return settings with updated model but keep API key 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' - assert result.llm_base_url == 'https://existing.example.com' + # When llm_base_url is not provided, it defaults to the LiteLLM proxy URL + import os + + expected_base_url = os.environ.get( + 'LITE_LLM_API_URL', 'https://llm-proxy.app.all-hands.dev' + ) + assert result.llm_base_url == expected_base_url # Tests for store_provider_tokens