mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-03-22 13:47:19 +08:00
settings: expose SDK settings schema to OpenHands (#2228)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user