Files
OpenHands/openhands/server/settings.py
2025-03-16 16:32:27 -04:00

149 lines
4.9 KiB
Python

from __future__ import annotations
from pydantic import (
BaseModel,
SecretStr,
SerializationInfo,
field_serializer,
model_validator,
)
from pydantic.json import pydantic_encoder
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.utils import load_app_config
from openhands.integrations.provider import ProviderToken, ProviderType, SecretStore
class Settings(BaseModel):
"""
Persisted settings for OpenHands sessions
"""
language: str | None = None
agent: str | None = None
max_iterations: int | None = None
security_analyzer: str | None = None
confirmation_mode: bool | None = None
llm_model: str | None = None
llm_api_key: SecretStr | None = None
llm_base_url: str | None = None
remote_runtime_resource_factor: int | None = None
secrets_store: SecretStore = SecretStore()
enable_default_condenser: bool = False
enable_sound_notifications: bool = False
user_consents_to_analytics: bool | None = None
@field_serializer('llm_api_key')
def llm_api_key_serializer(self, llm_api_key: SecretStr, info: SerializationInfo):
"""Custom serializer for the LLM API key.
To serialize the API key instead of ********, set expose_secrets to True in the serialization context.
"""
context = info.context
if context and context.get('expose_secrets', False):
return llm_api_key.get_secret_value()
return pydantic_encoder(llm_api_key) if llm_api_key else None
@staticmethod
def _convert_token_value(
token_type: ProviderType, token_value: str | dict
) -> ProviderToken | None:
"""Convert a token value to a ProviderToken object."""
if isinstance(token_value, dict):
token_str = token_value.get('token')
if not token_str:
return None
return ProviderToken(
token=SecretStr(token_str),
user_id=token_value.get('user_id'),
)
if isinstance(token_value, str) and token_value:
return ProviderToken(token=SecretStr(token_value), user_id=None)
return None
@model_validator(mode='before')
@classmethod
def convert_provider_tokens(cls, data: dict | object) -> dict | object:
"""Convert provider tokens from JSON format to SecretStore format."""
if not isinstance(data, dict):
return data
secrets_store = data.get('secrets_store')
if not isinstance(secrets_store, dict):
return data
tokens = secrets_store.get('provider_tokens')
if not isinstance(tokens, dict):
return data
converted_tokens = {}
for token_type_str, token_value in tokens.items():
if not token_value:
continue
try:
token_type = ProviderType(token_type_str)
except ValueError:
continue
provider_token = cls._convert_token_value(token_type, token_value)
if provider_token:
converted_tokens[token_type] = provider_token
data['secrets_store'] = SecretStore(provider_tokens=converted_tokens)
return data
@field_serializer('secrets_store')
def secrets_store_serializer(self, secrets: SecretStore, info: SerializationInfo):
"""Custom serializer for secrets store."""
return {
'provider_tokens': secrets.provider_tokens_serializer(
secrets.provider_tokens, info
)
}
@staticmethod
def from_config() -> Settings | None:
app_config = load_app_config()
llm_config: LLMConfig = app_config.get_llm_config()
if llm_config.api_key is None:
# If no api key has been set, we take this to mean that there is no reasonable default
return None
security = app_config.security
settings = Settings(
language='en',
agent=app_config.default_agent,
max_iterations=app_config.max_iterations,
security_analyzer=security.security_analyzer,
confirmation_mode=security.confirmation_mode,
llm_model=llm_config.model,
llm_api_key=llm_config.api_key,
llm_base_url=llm_config.base_url,
remote_runtime_resource_factor=app_config.sandbox.remote_runtime_resource_factor,
provider_tokens={},
)
return settings
class POSTSettingsModel(Settings):
"""
Settings for POST requests
"""
unset_github_token: bool | None = None
# Override provider_tokens to accept string tokens from frontend
provider_tokens: dict[str, str] = {}
@field_serializer('provider_tokens')
def provider_tokens_serializer(self, provider_tokens: dict[str, str]):
return provider_tokens
class GETSettingsModel(Settings):
"""
Settings with additional token data for the frontend
"""
github_token_is_set: bool | None = None