diff --git a/AGENTS.md b/AGENTS.md index 878a26e884..be8f6e820c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,10 @@ This repository contains the code for OpenHands, an automated AI software engineer. It has a Python backend (in the `openhands` directory) and React frontend (in the `frontend` directory). + +## Repository Memory +- Legacy `/api/settings` responses can bridge to the SDK by returning `sdk_settings_schema` from `openhands.sdk.settings` when that package is available. Use this as the compatibility handoff while V1 settings work moves into the SDK and newer clients. + ## General Setup: To set up the entire repo, including frontend and backend, run `make build`. You don't need to do this unless the user asks you to, or if you're trying to run the entire application. diff --git a/frontend/src/types/settings.ts b/frontend/src/types/settings.ts index 891b830af3..5ff35ac04e 100644 --- a/frontend/src/types/settings.ts +++ b/frontend/src/types/settings.ts @@ -39,6 +39,39 @@ export type MCPConfig = { shttp_servers: (string | MCPSHTTPServer)[]; }; +export type SettingsChoice = { + label: string; + value: string; +}; + +export type SettingsFieldSchema = { + key: string; + label: string; + description?: string | null; + widget: string; + section: string; + section_label: string; + order: number; + choices: SettingsChoice[]; + depends_on: string[]; + advanced: boolean; + secret: boolean; + required: boolean; + slash_command?: string | null; +}; + +export type SettingsSectionSchema = { + key: string; + label: string; + order: number; + fields: SettingsFieldSchema[]; +}; + +export type SettingsSchema = { + model_name: string; + sections: SettingsSectionSchema[]; +}; + export type Settings = { llm_model: string; llm_base_url: string; @@ -67,4 +100,5 @@ export type Settings = { git_user_name?: string; git_user_email?: string; v1_enabled?: boolean; + sdk_settings_schema?: SettingsSchema | null; }; diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index 62944d11ce..f6489b75c6 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -6,7 +6,9 @@ # 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 importlib import os +from typing import Any from fastapi import APIRouter, Depends, status from fastapi.responses import JSONResponse @@ -37,6 +39,16 @@ LITE_LLM_API_URL = os.environ.get( 'LITE_LLM_API_URL', 'https://llm-proxy.app.all-hands.dev' ) + +def _get_sdk_settings_schema() -> dict[str, Any] | None: + try: + settings_module = importlib.import_module('openhands.sdk.settings') + except ModuleNotFoundError: + return None + + return settings_module.SDKSettings.export_schema().model_dump(mode='json') + + app = APIRouter(prefix='/api', dependencies=get_dependencies()) @@ -84,6 +96,7 @@ async def load_settings( search_api_key_set=settings.search_api_key is not None and bool(settings.search_api_key), provider_tokens_set=provider_tokens_set, + sdk_settings_schema=_get_sdk_settings_schema(), ) # If the base url matches the default for the provider, we don't send it diff --git a/openhands/server/settings.py b/openhands/server/settings.py index bc799af438..40f59916a8 100644 --- a/openhands/server/settings.py +++ b/openhands/server/settings.py @@ -8,6 +8,8 @@ # This module belongs to the old V0 web server. The V1 application server lives under openhands/app_server/. from __future__ import annotations +from typing import Any + from pydantic import ( BaseModel, ConfigDict, @@ -21,44 +23,45 @@ from openhands.storage.data_models.settings import Settings class POSTProviderModel(BaseModel): - """Settings for POST requests""" + """Settings for POST requests.""" mcp_config: MCPConfig | None = None provider_tokens: dict[ProviderType, ProviderToken] = {} class POSTCustomSecrets(BaseModel): - """Adding new custom secret""" + """Add a new custom secret.""" custom_secrets: dict[str, CustomSecret] = {} class GETSettingsModel(Settings): - """Settings with additional token data for the frontend""" + """Settings with additional token data for the frontend.""" provider_tokens_set: dict[ProviderType, str | None] | None = ( None # provider + base_domain key-value pair ) llm_api_key_set: bool search_api_key_set: bool = False + sdk_settings_schema: dict[str, Any] | None = None model_config = ConfigDict(use_enum_values=True) class CustomSecretWithoutValueModel(BaseModel): - """Custom secret model without value""" + """Custom secret model without a value.""" name: str description: str | None = None class CustomSecretModel(CustomSecretWithoutValueModel): - """Custom secret model with value""" + """Custom secret model with a value.""" value: SecretStr class GETCustomSecrets(BaseModel): - """Custom secrets names""" + """Custom secret names.""" custom_secrets: list[CustomSecretWithoutValueModel] | None = None diff --git a/tests/unit/server/routes/test_settings_api.py b/tests/unit/server/routes/test_settings_api.py index ce0922996a..6ebc426e79 100644 --- a/tests/unit/server/routes/test_settings_api.py +++ b/tests/unit/server/routes/test_settings_api.py @@ -17,7 +17,7 @@ from openhands.storage.settings.settings_store import SettingsStore class MockUserAuth(UserAuth): - """Mock implementation of UserAuth for testing""" + """Mock implementation of UserAuth for testing.""" def __init__(self): self._settings = None @@ -81,7 +81,7 @@ def test_client(): @pytest.mark.asyncio async def test_settings_api_endpoints(test_client): - """Test that the settings API endpoints work with the new auth system""" + """Test that the settings API endpoints work with the new auth system.""" # Test data with remote_runtime_resource_factor settings_data = { 'language': 'en', @@ -104,6 +104,7 @@ async def test_settings_api_endpoints(test_client): # Test the GET settings endpoint response = test_client.get('/api/settings') assert response.status_code == 200 + assert response.json()['sdk_settings_schema']['model_name'] == 'SDKSettings' # Test updating with partial settings partial_settings = { @@ -122,7 +123,7 @@ async def test_settings_api_endpoints(test_client): @pytest.mark.asyncio async def test_search_api_key_preservation(test_client): - """Test that search_api_key is preserved when sending empty string""" + """Test that search_api_key is preserved when sending an empty string.""" # 1. Set initial settings with a search API key initial_settings = { 'search_api_key': 'initial-secret-key',