diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index e312c5e7fa..a9f327692b 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -104,6 +104,11 @@ def _extract_sdk_settings_values( return values +_SETTINGS_FROZEN_FIELDS = frozenset( + name for name, field_info in Settings.model_fields.items() if field_info.frozen +) + + def _apply_settings_payload( payload: dict[str, Any], existing_settings: Settings | None, @@ -116,7 +121,7 @@ def _apply_settings_payload( sdk_settings_values = dict(settings.sdk_settings_values) for key, value in payload.items(): - if key in Settings.model_fields: + if key in Settings.model_fields and key not in _SETTINGS_FROZEN_FIELDS: setattr(settings, key, value) if key in sdk_field_keys and key not in secret_field_keys: diff --git a/tests/unit/server/routes/test_settings_api.py b/tests/unit/server/routes/test_settings_api.py index 3a2dd58a1a..e5fc5290a5 100644 --- a/tests/unit/server/routes/test_settings_api.py +++ b/tests/unit/server/routes/test_settings_api.py @@ -270,6 +270,21 @@ async def test_settings_api_endpoints(test_client): assert response.status_code == 200 +@pytest.mark.asyncio +async def test_saving_settings_with_frozen_secrets_store(test_client): + """Regression: POSTing settings must not fail when the payload includes + ``secrets_store`` (a frozen field on the Settings model). + See https://github.com/OpenHands/OpenHands/issues/13306. + """ + settings_data = { + 'language': 'en', + 'llm_model': 'gpt-4', + 'secrets_store': {'provider_tokens': {}}, + } + response = test_client.post('/api/settings', json=settings_data) + assert response.status_code == 200 + + @pytest.mark.asyncio async def test_search_api_key_preservation(test_client): """Test that search_api_key is preserved when sending an empty string."""