mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
198 lines
7.3 KiB
Python
198 lines
7.3 KiB
Python
from fastapi import APIRouter, Depends, status
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from openhands.core.logger import openhands_logger as logger
|
|
from openhands.integrations.provider import (
|
|
PROVIDER_TOKEN_TYPE,
|
|
ProviderType,
|
|
)
|
|
from openhands.server.dependencies import get_dependencies
|
|
from openhands.server.routes.secrets import invalidate_legacy_secrets_store
|
|
from openhands.server.settings import (
|
|
GETSettingsModel,
|
|
)
|
|
from openhands.server.shared import config
|
|
from openhands.server.user_auth import (
|
|
get_provider_tokens,
|
|
get_secrets_store,
|
|
get_user_settings,
|
|
get_user_settings_store,
|
|
)
|
|
from openhands.storage.data_models.settings import Settings
|
|
from openhands.storage.secrets.secrets_store import SecretsStore
|
|
from openhands.storage.settings.settings_store import SettingsStore
|
|
|
|
app = APIRouter(prefix='/api', dependencies=get_dependencies())
|
|
|
|
|
|
@app.get(
|
|
'/settings',
|
|
response_model=GETSettingsModel,
|
|
responses={
|
|
404: {'description': 'Settings not found', 'model': dict},
|
|
401: {'description': 'Invalid token', 'model': dict},
|
|
},
|
|
)
|
|
async def load_settings(
|
|
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
|
settings_store: SettingsStore = Depends(get_user_settings_store),
|
|
settings: Settings = Depends(get_user_settings),
|
|
secrets_store: SecretsStore = Depends(get_secrets_store),
|
|
) -> GETSettingsModel | JSONResponse:
|
|
try:
|
|
if not settings:
|
|
return JSONResponse(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
content={'error': 'Settings not found'},
|
|
)
|
|
|
|
# On initial load, user secrets may not be populated with values migrated from settings store
|
|
user_secrets = await invalidate_legacy_secrets_store(
|
|
settings, settings_store, secrets_store
|
|
)
|
|
|
|
# If invalidation is successful, then the returned user secrets holds the most recent values
|
|
git_providers = (
|
|
user_secrets.provider_tokens if user_secrets else provider_tokens
|
|
)
|
|
|
|
provider_tokens_set: dict[ProviderType, str | None] = {}
|
|
if git_providers:
|
|
for provider_type, provider_token in git_providers.items():
|
|
if provider_token.token or provider_token.user_id:
|
|
provider_tokens_set[provider_type] = provider_token.host
|
|
|
|
settings_with_token_data = GETSettingsModel(
|
|
**settings.model_dump(exclude='secrets_store'),
|
|
llm_api_key_set=settings.llm_api_key is not None
|
|
and bool(settings.llm_api_key),
|
|
search_api_key_set=settings.search_api_key is not None
|
|
and bool(settings.search_api_key),
|
|
provider_tokens_set=provider_tokens_set,
|
|
)
|
|
settings_with_token_data.llm_api_key = None
|
|
settings_with_token_data.search_api_key = None
|
|
settings_with_token_data.sandbox_api_key = None
|
|
return settings_with_token_data
|
|
except Exception as e:
|
|
logger.warning(f'Invalid token: {e}')
|
|
# Get user_id from settings if available
|
|
user_id = getattr(settings, 'user_id', 'unknown') if settings else 'unknown'
|
|
logger.info(
|
|
f'Returning 401 Unauthorized - Invalid token for user_id: {user_id}'
|
|
)
|
|
return JSONResponse(
|
|
status_code=status.HTTP_401_UNAUTHORIZED,
|
|
content={'error': 'Invalid token'},
|
|
)
|
|
|
|
|
|
@app.post(
|
|
'/reset-settings',
|
|
responses={
|
|
410: {
|
|
'description': 'Reset settings functionality has been removed',
|
|
'model': dict,
|
|
}
|
|
},
|
|
)
|
|
async def reset_settings() -> JSONResponse:
|
|
"""
|
|
Resets user settings. (Deprecated)
|
|
"""
|
|
logger.warning('Deprecated endpoint /api/reset-settings called by user')
|
|
return JSONResponse(
|
|
status_code=status.HTTP_410_GONE,
|
|
content={'error': 'Reset settings functionality has been removed.'},
|
|
)
|
|
|
|
|
|
async def store_llm_settings(
|
|
settings: Settings, settings_store: SettingsStore
|
|
) -> Settings:
|
|
existing_settings = await settings_store.load()
|
|
|
|
# Convert to Settings model and merge with existing settings
|
|
if existing_settings:
|
|
# Keep existing LLM settings if not provided
|
|
if settings.llm_api_key is None:
|
|
settings.llm_api_key = existing_settings.llm_api_key
|
|
if settings.llm_model is None:
|
|
settings.llm_model = existing_settings.llm_model
|
|
if settings.llm_base_url is None:
|
|
settings.llm_base_url = existing_settings.llm_base_url
|
|
# Keep existing search API key if not provided
|
|
if settings.search_api_key is None:
|
|
settings.search_api_key = existing_settings.search_api_key
|
|
|
|
return settings
|
|
|
|
|
|
# NOTE: We use response_model=None for endpoints that return JSONResponse directly.
|
|
# This is because FastAPI's response_model expects a Pydantic model, but we're returning
|
|
# a response object directly. We document the possible responses using the 'responses'
|
|
# parameter and maintain proper type annotations for mypy.
|
|
@app.post(
|
|
'/settings',
|
|
response_model=None,
|
|
responses={
|
|
200: {'description': 'Settings stored successfully', 'model': dict},
|
|
500: {'description': 'Error storing settings', 'model': dict},
|
|
},
|
|
)
|
|
async def store_settings(
|
|
settings: Settings,
|
|
settings_store: SettingsStore = Depends(get_user_settings_store),
|
|
) -> JSONResponse:
|
|
# Check provider tokens are valid
|
|
try:
|
|
existing_settings = await settings_store.load()
|
|
|
|
# Convert to Settings model and merge with existing settings
|
|
if existing_settings:
|
|
settings = await store_llm_settings(settings, settings_store)
|
|
|
|
# Keep existing analytics consent if not provided
|
|
if settings.user_consents_to_analytics is None:
|
|
settings.user_consents_to_analytics = (
|
|
existing_settings.user_consents_to_analytics
|
|
)
|
|
|
|
# Update sandbox config with new settings
|
|
if settings.remote_runtime_resource_factor is not None:
|
|
config.sandbox.remote_runtime_resource_factor = (
|
|
settings.remote_runtime_resource_factor
|
|
)
|
|
|
|
settings = convert_to_settings(settings)
|
|
await settings_store.store(settings)
|
|
return JSONResponse(
|
|
status_code=status.HTTP_200_OK,
|
|
content={'message': 'Settings stored'},
|
|
)
|
|
except Exception as e:
|
|
logger.warning(f'Something went wrong storing settings: {e}')
|
|
return JSONResponse(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
content={'error': 'Something went wrong storing settings'},
|
|
)
|
|
|
|
|
|
def convert_to_settings(settings_with_token_data: Settings) -> Settings:
|
|
settings_data = settings_with_token_data.model_dump()
|
|
|
|
# Filter out additional fields from `SettingsWithTokenData`
|
|
filtered_settings_data = {
|
|
key: value
|
|
for key, value in settings_data.items()
|
|
if key in Settings.model_fields # Ensures only `Settings` fields are included
|
|
}
|
|
|
|
# Convert the API keys to `SecretStr` instances
|
|
filtered_settings_data['llm_api_key'] = settings_with_token_data.llm_api_key
|
|
filtered_settings_data['search_api_key'] = settings_with_token_data.search_api_key
|
|
|
|
# Create a new Settings instance
|
|
settings = Settings(**filtered_settings_data)
|
|
return settings
|