Spaces:
Build error
Build error
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()) | |
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 | |
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'}, | |
) | |
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. | |
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 | |