diff --git a/enterprise/server/middleware.py b/enterprise/server/middleware.py index bf8ba18706..932fbae7ac 100644 --- a/enterprise/server/middleware.py +++ b/enterprise/server/middleware.py @@ -162,6 +162,7 @@ class SetAuthCookieMiddleware: '/api/email/resend', '/oauth/device/authorize', '/oauth/device/token', + '/api/v1/web-client/config', ) if path in ignore_paths: return False diff --git a/openhands/app_server/config.py b/openhands/app_server/config.py index c549445b09..08bccc11e2 100644 --- a/openhands/app_server/config.py +++ b/openhands/app_server/config.py @@ -48,7 +48,14 @@ from openhands.app_server.services.httpx_client_injector import HttpxClientInjec from openhands.app_server.services.injector import InjectorState from openhands.app_server.services.jwt_service import JwtService, JwtServiceInjector from openhands.app_server.user.user_context import UserContext, UserContextInjector +from openhands.app_server.web_client.default_web_client_config_injector import ( + DefaultWebClientConfigInjector, +) +from openhands.app_server.web_client.web_client_config_injector import ( + WebClientConfigInjector, +) from openhands.sdk.utils.models import OpenHandsModel +from openhands.server.types import AppMode def get_default_persistence_dir() -> Path: @@ -114,9 +121,12 @@ class AppServerConfig(OpenHandsModel): persistence_dir=get_default_persistence_dir() ) ) - # Services lifespan: AppLifespanService | None = Field(default_factory=_get_default_lifespan) + app_mode: AppMode = AppMode.OPENHANDS + web_client: WebClientConfigInjector = Field( + default_factory=DefaultWebClientConfigInjector + ) def config_from_env() -> AppServerConfig: diff --git a/openhands/app_server/v1_router.py b/openhands/app_server/v1_router.py index f99ef68a6b..2a21c06abd 100644 --- a/openhands/app_server/v1_router.py +++ b/openhands/app_server/v1_router.py @@ -7,6 +7,7 @@ from openhands.app_server.event_callback import ( ) from openhands.app_server.sandbox import sandbox_router, sandbox_spec_router from openhands.app_server.user import user_router +from openhands.app_server.web_client import web_client_router # Include routers router = APIRouter(prefix='/api/v1') @@ -16,3 +17,4 @@ router.include_router(sandbox_router.router) router.include_router(sandbox_spec_router.router) router.include_router(user_router.router) router.include_router(webhook_router.router) +router.include_router(web_client_router.router) diff --git a/openhands/app_server/web_client/default_web_client_config_injector.py b/openhands/app_server/web_client/default_web_client_config_injector.py new file mode 100644 index 0000000000..8afa9008b9 --- /dev/null +++ b/openhands/app_server/web_client/default_web_client_config_injector.py @@ -0,0 +1,40 @@ +from datetime import datetime + +from pydantic import Field + +from openhands.app_server.web_client.web_client_config_injector import ( + WebClientConfigInjector, +) +from openhands.app_server.web_client.web_client_models import ( + WebClientConfig, + WebClientFeatureFlags, +) +from openhands.integrations.service_types import ProviderType + + +class DefaultWebClientConfigInjector(WebClientConfigInjector): + posthog_client_key: str | None = 'phc_3ESMmY9SgqEAGBB6sMGK5ayYHkeUuknH2vP6FmWH9RA' + feature_flags: WebClientFeatureFlags = Field(default_factory=WebClientFeatureFlags) + providers_configured: list[ProviderType] = Field(default_factory=list) + maintenance_start_time: datetime | None = None + auth_url: str | None = None + recaptcha_site_key: str | None = None + faulty_models: list[str] = Field(default_factory=list) + error_message: str | None = None + + async def get_web_client_config(self) -> WebClientConfig: + from openhands.app_server.config import get_global_config + + config = get_global_config() + result = WebClientConfig( + app_mode=config.app_mode, + posthog_client_key=self.posthog_client_key, + feature_flags=self.feature_flags, + providers_configured=self.providers_configured, + maintenance_start_time=self.maintenance_start_time, + auth_url=self.auth_url, + recaptcha_site_key=self.recaptcha_site_key, + faulty_models=self.faulty_models, + error_message=self.error_message, + ) + return result diff --git a/openhands/app_server/web_client/web_client_config_injector.py b/openhands/app_server/web_client/web_client_config_injector.py new file mode 100644 index 0000000000..c47207e384 --- /dev/null +++ b/openhands/app_server/web_client/web_client_config_injector.py @@ -0,0 +1,10 @@ +from abc import abstractmethod + +from openhands.agent_server.env_parser import ABC, DiscriminatedUnionMixin +from openhands.app_server.web_client.web_client_models import WebClientConfig + + +class WebClientConfigInjector(DiscriminatedUnionMixin, ABC): + @abstractmethod + async def get_web_client_config(self) -> WebClientConfig: + """Get the current web client configuration.""" diff --git a/openhands/app_server/web_client/web_client_models.py b/openhands/app_server/web_client/web_client_models.py new file mode 100644 index 0000000000..be0e18e7da --- /dev/null +++ b/openhands/app_server/web_client/web_client_models.py @@ -0,0 +1,27 @@ +from datetime import datetime + +from pydantic import BaseModel + +from openhands.agent_server.env_parser import DiscriminatedUnionMixin +from openhands.integrations.service_types import ProviderType +from openhands.server.types import AppMode + + +class WebClientFeatureFlags(BaseModel): + enable_billing: bool = False + hide_llm_settings: bool = False + enable_jira: bool = False + enable_jira_dc: bool = False + enable_linear: bool = False + + +class WebClientConfig(DiscriminatedUnionMixin): + app_mode: AppMode + posthog_client_key: str | None + feature_flags: WebClientFeatureFlags + providers_configured: list[ProviderType] + maintenance_start_time: datetime | None + auth_url: str | None + recaptcha_site_key: str | None + faulty_models: list[str] + error_message: str | None diff --git a/openhands/app_server/web_client/web_client_router.py b/openhands/app_server/web_client/web_client_router.py new file mode 100644 index 0000000000..3c7189597b --- /dev/null +++ b/openhands/app_server/web_client/web_client_router.py @@ -0,0 +1,16 @@ +from fastapi import APIRouter + +from openhands.app_server.config import get_global_config +from openhands.app_server.web_client.web_client_models import WebClientConfig + +router = APIRouter(prefix='/web-client', tags=['Config']) + + +@router.get('/config') +async def get_web_client_config() -> WebClientConfig: + """Get the configuration of the web client. This endpoint is typically one of the first + invoked, and does not require authentication. It provides general settings for the + web client independent of users.""" + config = get_global_config() + result = await config.web_client.get_web_client_config() + return result diff --git a/openhands/server/routes/public.py b/openhands/server/routes/public.py index 76ca3f505f..c425088f03 100644 --- a/openhands/server/routes/public.py +++ b/openhands/server/routes/public.py @@ -67,10 +67,13 @@ async def get_security_analyzers() -> list[str]: return sorted(SecurityAnalyzers.keys()) -@app.get('/config', response_model=dict[str, Any]) +@app.get('/config', response_model=dict[str, Any], deprecated=True) async def get_config() -> dict[str, Any]: """Get current config. + This method has been replaced with the /v1/web-client/config endpoint, + and will be removed as part of the V0 cleanup on 2026-04-01 + Returns: dict[str, Any]: The current server configuration. """