settings: expose SDK settings schema to OpenHands (#2228)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-03-08 20:10:48 +00:00
parent 8c46df6b59
commit 424f6b30d1
5 changed files with 64 additions and 9 deletions

View File

@@ -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.

View File

@@ -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;
};

View File

@@ -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

View File

@@ -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

View File

@@ -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',