Skip to content

Commit 41bf063

Browse files
refactor(settings): drop persisted diff/patch helpers
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 0ccc381 commit 41bf063

File tree

4 files changed

+44
-158
lines changed

4 files changed

+44
-158
lines changed

openhands-agent-server/openhands/agent_server/settings_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def _get_conversation_settings_schema() -> SettingsSchema:
1818
return ConversationSettings.export_schema()
1919

2020

21-
@settings_router.get("/schema", response_model=SettingsSchema)
21+
@settings_router.get("/agent-schema", response_model=SettingsSchema)
2222
async def get_agent_settings_schema() -> SettingsSchema:
2323
"""Return the schema used to render AgentSettings-based settings forms."""
2424
return _get_agent_settings_schema()

openhands-sdk/openhands/sdk/settings/model.py

Lines changed: 2 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -225,11 +225,9 @@ def _default_llm_settings() -> LLM:
225225
return LLM(model=model)
226226

227227

228-
# Canonical persisted AgentSettings payload contract:
229-
# - v1 (legacy): flat dotted OpenHands mapping or unversioned nested payload
230-
# - v2 (current): standard AgentSettings JSON including `schema_version`
228+
# Persisted settings payloads currently use schema_version 1.
231229
_LEGACY_AGENT_SETTINGS_VERSION = 1
232-
_CURRENT_AGENT_SETTINGS_VERSION = 2
230+
_CURRENT_AGENT_SETTINGS_VERSION = 1
233231
_PERSISTED_AGENT_SETTINGS_VERSION_KEY = "schema_version"
234232
_LEGACY_WRAPPED_SETTINGS_VERSION_KEY = "version"
235233
_LEGACY_WRAPPED_SETTINGS_SETTINGS_KEY = "settings"
@@ -469,38 +467,6 @@ def export_schema(cls) -> SettingsSchema:
469467
"""Export a structured schema describing configurable conversation settings."""
470468
return export_settings_schema(cls)
471469

472-
@classmethod
473-
def migrate_persisted_payload(cls, payload: Mapping[str, Any]) -> dict[str, Any]:
474-
"""Return the latest canonical persisted ConversationSettings payload."""
475-
return _migrate_persisted_conversation_settings_payload(payload)
476-
477-
@classmethod
478-
def from_persisted(cls, payload: Mapping[str, Any]) -> ConversationSettings:
479-
"""Load persisted ConversationSettings after applying SDK-owned migrations."""
480-
return cls.model_validate(cls.migrate_persisted_payload(payload))
481-
482-
def patch(self, payload: Mapping[str, Any]) -> ConversationSettings:
483-
"""Return a new settings object with a persisted patch applied."""
484-
base_payload = self.model_dump(mode="json")
485-
merged_payload = _merge_patch_payload(
486-
base_payload, _normalize_patch_payload(payload)
487-
)
488-
merged_payload[_PERSISTED_AGENT_SETTINGS_VERSION_KEY] = (
489-
self.CURRENT_PERSISTED_VERSION
490-
)
491-
return type(self).from_persisted(merged_payload)
492-
493-
def diff(self, target: ConversationSettings | Mapping[str, Any]) -> dict[str, Any]:
494-
"""Return the minimal persisted patch from these settings to ``target``."""
495-
target_settings = (
496-
target
497-
if isinstance(target, ConversationSettings)
498-
else type(self).from_persisted(target)
499-
)
500-
base_payload = self.model_dump(mode="json")
501-
target_payload = target_settings.model_dump(mode="json")
502-
return _diff_payload(base_payload, target_payload)
503-
504470
def build_confirmation_policy(self):
505471
from openhands.sdk.security.confirmation_policy import (
506472
AlwaysConfirm,
@@ -620,44 +586,6 @@ def export_schema(cls) -> SettingsSchema:
620586
"""Export a structured schema describing configurable agent settings."""
621587
return export_settings_schema(cls)
622588

623-
@classmethod
624-
def migrate_persisted_payload(cls, payload: Mapping[str, Any]) -> dict[str, Any]:
625-
"""Return the latest canonical persisted AgentSettings payload."""
626-
return _migrate_persisted_agent_settings_payload(payload)
627-
628-
@classmethod
629-
def from_persisted(cls, payload: Mapping[str, Any]) -> AgentSettings:
630-
"""Load persisted AgentSettings after applying SDK-owned migrations."""
631-
return cls.model_validate(cls.migrate_persisted_payload(payload))
632-
633-
def patch(self, payload: Mapping[str, Any]) -> AgentSettings:
634-
"""Return a new settings object with a persisted patch applied.
635-
636-
The patch payload may be sparse and may use either the current nested
637-
representation or the legacy flat dotted representation.
638-
"""
639-
base_payload = self.model_dump(mode="json", context={"expose_secrets": True})
640-
merged_payload = _merge_patch_payload(
641-
base_payload, _normalize_patch_payload(payload)
642-
)
643-
merged_payload[_PERSISTED_AGENT_SETTINGS_VERSION_KEY] = (
644-
self.CURRENT_PERSISTED_VERSION
645-
)
646-
return type(self).from_persisted(merged_payload)
647-
648-
def diff(self, target: AgentSettings | Mapping[str, Any]) -> dict[str, Any]:
649-
"""Return the minimal persisted patch from these settings to ``target``."""
650-
target_settings = (
651-
target
652-
if isinstance(target, AgentSettings)
653-
else type(self).from_persisted(target)
654-
)
655-
base_payload = self.model_dump(mode="json", context={"expose_secrets": True})
656-
target_payload = target_settings.model_dump(
657-
mode="json", context={"expose_secrets": True}
658-
)
659-
return _diff_payload(base_payload, target_payload)
660-
661589
def create_agent(self) -> Agent:
662590
"""Build an :class:`Agent` purely from these settings.
663591

tests/agent_server/test_settings_router.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
def test_get_agent_settings_schema():
88
client = TestClient(create_app(Config(static_files_path=None, session_api_keys=[])))
99

10-
response = client.get("/api/settings/schema")
10+
response = client.get("/api/settings/agent-schema")
1111

1212
assert response.status_code == 200
1313
body = response.json()

tests/sdk/test_settings.py

Lines changed: 40 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,6 @@ def test_agent_settings_export_schema_groups_sections() -> None:
114114
assert "verification.security_analyzer" not in v_fields
115115

116116

117-
118117
def test_conversation_settings_export_schema_groups_sections() -> None:
119118
schema = ConversationSettings.export_schema()
120119

@@ -140,36 +139,25 @@ def test_conversation_settings_export_schema_groups_sections() -> None:
140139
)
141140
assert verification_fields["verification.security_analyzer"].default == "llm"
142141
assert (
143-
verification_fields["verification.security_analyzer"].choices[0].value
144-
== "llm"
142+
verification_fields["verification.security_analyzer"].choices[0].value == "llm"
145143
)
146144
assert verification_fields["verification.security_analyzer"].depends_on == [
147145
"verification.confirmation_mode"
148146
]
149147

150148

151-
def test_conversation_settings_patch_and_diff_roundtrip() -> None:
152-
defaults = ConversationSettings()
153-
updated = defaults.patch(
154-
{
155-
"max_iterations": 42,
156-
"verification": {
157-
"confirmation_mode": True,
158-
"security_analyzer": "none",
159-
},
160-
}
149+
def test_conversation_settings_model_dump_roundtrip() -> None:
150+
settings = ConversationSettings(
151+
max_iterations=42,
152+
verification=ConversationVerificationSettings(
153+
confirmation_mode=True,
154+
security_analyzer="none",
155+
),
161156
)
162157

163-
patch = defaults.diff(updated)
158+
restored = ConversationSettings.model_validate(settings.model_dump(mode="json"))
164159

165-
assert patch == {
166-
"max_iterations": 42,
167-
"verification": {
168-
"confirmation_mode": True,
169-
"security_analyzer": "none",
170-
},
171-
}
172-
assert defaults.patch(patch) == updated
160+
assert restored == settings
173161

174162

175163
def test_conversation_settings_builds_runtime_helpers() -> None:
@@ -187,8 +175,12 @@ def test_conversation_settings_builds_runtime_helpers() -> None:
187175
assert isinstance(request_kwargs["confirmation_policy"], ConfirmRisky)
188176
assert isinstance(request_kwargs["security_analyzer"], LLMSecurityAnalyzer)
189177

190-
fallback_settings = settings.patch(
191-
{"verification": {"security_analyzer": "none"}}
178+
fallback_settings = ConversationSettings(
179+
max_iterations=settings.max_iterations,
180+
verification=ConversationVerificationSettings(
181+
confirmation_mode=True,
182+
security_analyzer="none",
183+
),
192184
)
193185
fallback_request_kwargs = fallback_settings.to_start_request_kwargs()
194186

@@ -331,78 +323,44 @@ def test_roundtrip_preserves_llm_model() -> None:
331323
assert restored.llm.model == "test-model"
332324

333325

334-
def test_from_persisted_agent_settings_migrates_legacy_flat_payload() -> None:
335-
legacy_payload = {
336-
"schema_version": 1,
337-
"llm.model": "legacy-model",
338-
"llm.api_key": "secret-key",
339-
"verification.critic_enabled": True,
340-
}
341-
342-
settings = AgentSettings.from_persisted(legacy_payload)
326+
def test_agent_settings_model_validate_accepts_nested_payload() -> None:
327+
settings = AgentSettings.model_validate(
328+
{
329+
"schema_version": AgentSettings.CURRENT_PERSISTED_VERSION,
330+
"llm": {
331+
"model": "test-model",
332+
"api_key": "secret-key",
333+
},
334+
"verification": {"critic_enabled": True},
335+
}
336+
)
343337

344338
assert settings.schema_version == AgentSettings.CURRENT_PERSISTED_VERSION
345-
assert settings.llm.model == "legacy-model"
339+
assert settings.llm.model == "test-model"
346340
assert isinstance(settings.llm.api_key, SecretStr)
347341
assert settings.llm.api_key.get_secret_value() == "secret-key"
348342
assert settings.verification.critic_enabled is True
349343

350344

351-
def test_from_persisted_agent_settings_accepts_current_payload() -> None:
352-
current_payload = {
353-
"schema_version": AgentSettings.CURRENT_PERSISTED_VERSION,
354-
"llm": {"model": "test-model"},
355-
}
356-
357-
settings = AgentSettings.from_persisted(current_payload)
358-
359-
assert settings.schema_version == AgentSettings.CURRENT_PERSISTED_VERSION
360-
assert settings.llm.model == "test-model"
361-
362-
363-
def test_from_persisted_agent_settings_accepts_legacy_wrapped_payload() -> None:
364-
wrapped_payload = {
365-
"version": 2,
366-
"settings": {
367-
"llm": {"model": "wrapped-model"},
368-
},
369-
}
370-
371-
settings = AgentSettings.from_persisted(wrapped_payload)
372-
373-
assert settings.schema_version == AgentSettings.CURRENT_PERSISTED_VERSION
374-
assert settings.llm.model == "wrapped-model"
375-
376-
377-
def test_agent_settings_patch_accepts_sparse_nested_payload() -> None:
378-
settings = AgentSettings.from_persisted(
345+
def test_agent_settings_model_dump_roundtrip_preserves_sparse_updates() -> None:
346+
settings = AgentSettings.model_validate(
379347
{
380348
"schema_version": AgentSettings.CURRENT_PERSISTED_VERSION,
381349
"llm": {"model": "base-model", "base_url": "https://example.com"},
382350
}
383351
)
384352

385-
patched = settings.patch({"llm": {"base_url": None}})
386-
387-
assert patched.llm.model == "base-model"
388-
assert patched.llm.base_url is None
389-
assert patched.schema_version == AgentSettings.CURRENT_PERSISTED_VERSION
390-
391-
392-
def test_agent_settings_diff_returns_sparse_persisted_patch() -> None:
393-
org_settings = AgentSettings.from_persisted(
394-
{
395-
"schema_version": AgentSettings.CURRENT_PERSISTED_VERSION,
396-
"llm": {"model": "base-model", "base_url": "https://example.com"},
353+
updated = settings.model_copy(
354+
update={
355+
"llm": settings.llm.model_copy(
356+
update={"model": "personal-model", "base_url": None}
357+
)
397358
}
398359
)
399-
personal_settings = org_settings.patch(
400-
{"llm": {"model": "personal-model", "base_url": None}}
360+
restored = AgentSettings.model_validate(
361+
updated.model_dump(mode="json", context={"expose_secrets": True})
401362
)
402363

403-
member_patch = org_settings.diff(personal_settings)
404-
405-
assert member_patch == {"llm": {"base_url": None, "model": "personal-model"}}
406-
assert org_settings.patch(member_patch).model_dump(
407-
mode="json", context={"expose_secrets": True}
408-
) == personal_settings.model_dump(mode="json", context={"expose_secrets": True})
364+
assert restored.llm.model == "personal-model"
365+
assert restored.llm.base_url is None
366+
assert restored.schema_version == AgentSettings.CURRENT_PERSISTED_VERSION

0 commit comments

Comments
 (0)