Skip to content

User Settings

Persisted user preferences exposed over the /erudi/user_settings routes: the global web-search default, the interface language, the automatic-update preference, the inference backend, the default reasoning effort and the default compaction threshold.

default_reasoning_effort (none, low, medium, high, xhigh, medium by default) is what a NEW conversation copies at creation and what an arena turn runs at; an existing conversation keeps the level it copied. See Conversations.

default_compaction_threshold (a whole percent from 50 to 90 in steps of 5, 80 by default) is the share of the working context window at which a NEW conversation compacts; the conversation copies it at creation and keeps it, so a later change never touches an existing conversation. Arena turns never compact and do not read it. A PUT carrying only this field is a valid update; a value off the scale is a 422. See Conversations.

inference_backend (auto or cpu) is the one setting the backend reads outside a request: the FastAPI lifespan reads it once per boot, after the migrations, and swaps CUDA_Engine for CPU_Engine when it says cpu. Changing it therefore takes effect only after a backend restart, which the frontend triggers when the user saves. The value is inert everywhere but the CUDA leg, so the Settings page only shows the control on a machine running the CUDA engine, or one already pinned back to the processor from it. See Hardware Detection.

src.domains.user_settings.endpoints

FastAPI endpoints for the user-settings singleton (issue #310).

GET/PUT /erudi/user_settings/: the app-wide settings the frontend's Settings page binds to: the global web-search default (#310), the interface language (#385), the automatic-update preference, the inference backend and the defaults new conversations copy (reasoning effort, compaction threshold). Follows the startup domain's layering (endpoints -> repository) — the resource is a one-row singleton with no business logic beyond get-or-create. The inference backend rides here too: it is read once per boot by the lifespan, so changing it needs a backend restart, which the frontend triggers explicitly.

get_user_settings(settings_repo=Depends(get_user_settings_repository), db=Depends(get_db)) async

Fetch the user-settings singleton (created with defaults on first read).

Example

GET /erudi/user_settings/ -> {"web_search_enabled": false, "language": "en", "auto_update_enabled": true, "inference_backend": "auto", "default_reasoning_effort": "medium", "default_compaction_threshold": 80}

Source code in backend/src/domains/user_settings/endpoints.py
@router.get("/", response_model=UserSettingsResponse)
async def get_user_settings(
    settings_repo: User_Settings_Repository = Depends(get_user_settings_repository),
    db: Session = Depends(get_db),
):
    """Fetch the user-settings singleton (created with defaults on first read).

    Example:
        GET /erudi/user_settings/
        -> {"web_search_enabled": false, "language": "en", "auto_update_enabled": true,
            "inference_backend": "auto", "default_reasoning_effort": "medium",
            "default_compaction_threshold": 80}
    """
    try:
        settings = settings_repo.get_or_create()
        db.commit()
        return settings
    except Exception as e:
        db.rollback()
        raise DatabaseException("Failed to get user settings", trace=str(e))

get_user_settings_repository(db=Depends(get_db))

FastAPI dependency injection factory for User_Settings_Repository.

Source code in backend/src/domains/user_settings/endpoints.py
def get_user_settings_repository(
    db: Session = Depends(get_db),
) -> User_Settings_Repository:
    """FastAPI dependency injection factory for User_Settings_Repository."""
    return User_Settings_Repository(db)

update_user_settings(payload, settings_repo=Depends(get_user_settings_repository), db=Depends(get_db)) async

Update the user-settings singleton (partial: omitted fields are kept).

Example

PUT /erudi/user_settings/ {"language": "fr"} -> {"web_search_enabled": false, "language": "fr", "auto_update_enabled": true, "inference_backend": "auto", "default_reasoning_effort": "medium", "default_compaction_threshold": 80}

Source code in backend/src/domains/user_settings/endpoints.py
@router.put("/", response_model=UserSettingsResponse)
async def update_user_settings(
    payload: UserSettingsUpdate,
    settings_repo: User_Settings_Repository = Depends(get_user_settings_repository),
    db: Session = Depends(get_db),
):
    """Update the user-settings singleton (partial: omitted fields are kept).

    Example:
        PUT /erudi/user_settings/ {"language": "fr"}
        -> {"web_search_enabled": false, "language": "fr", "auto_update_enabled": true,
            "inference_backend": "auto", "default_reasoning_effort": "medium",
            "default_compaction_threshold": 80}
    """
    try:
        settings = settings_repo.get_or_create()
        if payload.web_search_enabled is not None:
            settings_repo.set_web_search_enabled(settings, payload.web_search_enabled)
        if payload.language is not None:
            settings_repo.set_language(settings, payload.language)
        if payload.auto_update_enabled is not None:
            settings_repo.set_auto_update_enabled(settings, payload.auto_update_enabled)
        if payload.inference_backend is not None:
            settings_repo.set_inference_backend(settings, payload.inference_backend)
        if payload.default_reasoning_effort is not None:
            settings_repo.set_default_reasoning_effort(settings, payload.default_reasoning_effort)
        if payload.default_compaction_threshold is not None:
            settings_repo.set_default_compaction_threshold(
                settings, payload.default_compaction_threshold
            )
        db.commit()
        logger.info(
            "User settings updated: "
            f"web_search_enabled={settings.web_search_enabled} language={settings.language} "
            f"auto_update_enabled={settings.auto_update_enabled} "
            f"inference_backend={settings.inference_backend} "
            f"default_reasoning_effort={settings.default_reasoning_effort} "
            f"default_compaction_threshold={settings.default_compaction_threshold}"
        )
        return settings
    except Exception as e:
        db.rollback()
        raise DatabaseException("Failed to update user settings", trace=str(e))

src.domains.user_settings.repository

Data access layer for the UserSettings singleton (issue #310).

Mirrors Startup_Variables_Repository: get-or-create on a one-row table, mutations flushed (not committed) so the caller controls the transaction.

User_Settings_Repository(db)

Repository for the UserSettings singleton entity.

Attributes:

Name Type Description
db

SQLAlchemy database session (injected by FastAPI).

Initialize repository with database session.

Source code in backend/src/domains/user_settings/repository.py
def __init__(self, db: Session):
    """Initialize repository with database session."""
    self.db = db
    logger.debug("Initializing User_Settings_Repository")

get_default_compaction_threshold()

The global default compaction threshold, in percent (80 until the user changes it).

Read-only, like get_default_reasoning_effort: conversation creation must not write, and a missing singleton row IS the default.

Source code in backend/src/domains/user_settings/repository.py
def get_default_compaction_threshold(self) -> int:
    """The global default compaction threshold, in percent (80 until the
    user changes it).

    Read-only, like ``get_default_reasoning_effort``: conversation
    creation must not write, and a missing singleton row IS the default.
    """
    settings = self.db.query(UserSettings).first()
    value = settings.default_compaction_threshold if settings else None
    return value or DEFAULT_COMPACTION_THRESHOLD

get_default_reasoning_effort()

The global default reasoning effort ("medium" until the user changes it).

Read-only, like get_web_search_enabled: callers on hot paths (conversation creation, arena turns) must not write, and a missing singleton row IS the default.

Source code in backend/src/domains/user_settings/repository.py
def get_default_reasoning_effort(self) -> str:
    """The global default reasoning effort ("medium" until the user changes it).

    Read-only, like ``get_web_search_enabled``: callers on hot paths
    (conversation creation, arena turns) must not write, and a missing
    singleton row IS the default.
    """
    settings = self.db.query(UserSettings).first()
    value = settings.default_reasoning_effort if settings else None
    return value or DEFAULT_REASONING_EFFORT

get_or_create()

Retrieve the singleton UserSettings record, creating if absent.

Returns:

Name Type Description
UserSettings UserSettings

The singleton settings entity (defaults applied on

UserSettings

first creation: web_search_enabled=False, language="en",

UserSettings

auto_update_enabled=True, inference_backend="auto").

Source code in backend/src/domains/user_settings/repository.py
def get_or_create(self) -> UserSettings:
    """Retrieve the singleton UserSettings record, creating if absent.

    Returns:
        UserSettings: The singleton settings entity (defaults applied on
        first creation: web_search_enabled=False, language="en",
        auto_update_enabled=True, inference_backend="auto").
    """
    settings = self.db.query(UserSettings).first()
    if not settings:
        logger.info("UserSettings not found, creating singleton with defaults")
        settings = UserSettings(web_search_enabled=False, language=DEFAULT_LANGUAGE)
        self.db.add(settings)
        self.db.flush()
        self.db.refresh(settings)
    return settings

get_web_search_enabled()

The global web-search default (False until the user opts in).

Read-only: callers on hot paths (conversation creation, arena turns) must not write; a missing singleton row IS the default.

Source code in backend/src/domains/user_settings/repository.py
def get_web_search_enabled(self) -> bool:
    """The global web-search default (False until the user opts in).

    Read-only: callers on hot paths (conversation creation, arena turns)
    must not write; a missing singleton row IS the default.
    """
    settings = self.db.query(UserSettings).first()
    return bool(settings.web_search_enabled) if settings else False

set_auto_update_enabled(settings, value)

Update the automatic-update preference (flushed, not committed).

Parameters:

Name Type Description Default
settings UserSettings

The singleton entity to update.

required
value bool

New Boolean value (False refuses update checks entirely).

required

Returns:

Name Type Description
UserSettings UserSettings

Updated entity.

Source code in backend/src/domains/user_settings/repository.py
def set_auto_update_enabled(self, settings: UserSettings, value: bool) -> UserSettings:
    """Update the automatic-update preference (flushed, not committed).

    Args:
        settings: The singleton entity to update.
        value: New Boolean value (False refuses update checks entirely).

    Returns:
        UserSettings: Updated entity.
    """
    logger.info(f"Updating UserSettings.auto_update_enabled = {value}")
    settings.auto_update_enabled = value
    self.db.flush()
    self.db.refresh(settings)
    return settings

set_default_compaction_threshold(settings, value)

Update the global default compaction threshold (flushed, not committed).

Parameters:

Name Type Description Default
settings UserSettings

The singleton entity to update.

required
value int

One of COMPACTION_THRESHOLDS (validated by the entity).

required

Returns:

Name Type Description
UserSettings UserSettings

Updated entity.

Note

Existing conversations keep the threshold they copied at creation -- this only changes what the NEXT conversation inherits.

Source code in backend/src/domains/user_settings/repository.py
def set_default_compaction_threshold(self, settings: UserSettings, value: int) -> UserSettings:
    """Update the global default compaction threshold (flushed, not committed).

    Args:
        settings: The singleton entity to update.
        value: One of COMPACTION_THRESHOLDS (validated by the entity).

    Returns:
        UserSettings: Updated entity.

    Note:
        Existing conversations keep the threshold they copied at
        creation -- this only changes what the NEXT conversation inherits.
    """
    logger.info(f"Updating UserSettings.default_compaction_threshold = {value}")
    settings.default_compaction_threshold = value
    self.db.flush()
    self.db.refresh(settings)
    return settings

set_default_reasoning_effort(settings, value)

Update the global default reasoning effort (flushed, not committed).

Parameters:

Name Type Description Default
settings UserSettings

The singleton entity to update.

required
value str

One of REASONING_EFFORT_LEVELS (validated by the entity).

required

Returns:

Name Type Description
UserSettings UserSettings

Updated entity.

Note

Existing conversations keep the level they copied at creation -- this only changes what the NEXT conversation inherits (and what arena turns, which have no conversation row, run at).

Source code in backend/src/domains/user_settings/repository.py
def set_default_reasoning_effort(self, settings: UserSettings, value: str) -> UserSettings:
    """Update the global default reasoning effort (flushed, not committed).

    Args:
        settings: The singleton entity to update.
        value: One of REASONING_EFFORT_LEVELS (validated by the entity).

    Returns:
        UserSettings: Updated entity.

    Note:
        Existing conversations keep the level they copied at creation --
        this only changes what the NEXT conversation inherits (and what
        arena turns, which have no conversation row, run at).
    """
    logger.info(f"Updating UserSettings.default_reasoning_effort = {value}")
    settings.default_reasoning_effort = value
    self.db.flush()
    self.db.refresh(settings)
    return settings

set_inference_backend(settings, value)

Update the inference-backend preference (flushed, not committed).

Parameters:

Name Type Description Default
settings UserSettings

The singleton entity to update.

required
value str

One of INFERENCE_BACKENDS (validated by the entity).

required

Returns:

Name Type Description
UserSettings UserSettings

Updated entity.

Note

The engine is a process-level singleton read once per boot, so a change here only takes effect on the next backend start.

Source code in backend/src/domains/user_settings/repository.py
def set_inference_backend(self, settings: UserSettings, value: str) -> UserSettings:
    """Update the inference-backend preference (flushed, not committed).

    Args:
        settings: The singleton entity to update.
        value: One of INFERENCE_BACKENDS (validated by the entity).

    Returns:
        UserSettings: Updated entity.

    Note:
        The engine is a process-level singleton read once per boot, so a
        change here only takes effect on the next backend start.
    """
    logger.info(f"Updating UserSettings.inference_backend = {value}")
    settings.inference_backend = value
    self.db.flush()
    self.db.refresh(settings)
    return settings

set_language(settings, value)

Update the interface language (flushed, not committed).

Parameters:

Name Type Description Default
settings UserSettings

The singleton entity to update.

required
value str

One of the supported language codes (validated by the entity).

required

Returns:

Name Type Description
UserSettings UserSettings

Updated entity.

Source code in backend/src/domains/user_settings/repository.py
def set_language(self, settings: UserSettings, value: str) -> UserSettings:
    """Update the interface language (flushed, not committed).

    Args:
        settings: The singleton entity to update.
        value: One of the supported language codes (validated by the entity).

    Returns:
        UserSettings: Updated entity.
    """
    logger.info(f"Updating UserSettings.language = {value}")
    settings.language = value
    self.db.flush()
    self.db.refresh(settings)
    return settings

set_web_search_enabled(settings, value)

Update the global web-search default (flushed, not committed).

Parameters:

Name Type Description Default
settings UserSettings

The singleton entity to update.

required
value bool

New Boolean value.

required

Returns:

Name Type Description
UserSettings UserSettings

Updated entity.

Source code in backend/src/domains/user_settings/repository.py
def set_web_search_enabled(self, settings: UserSettings, value: bool) -> UserSettings:
    """Update the global web-search default (flushed, not committed).

    Args:
        settings: The singleton entity to update.
        value: New Boolean value.

    Returns:
        UserSettings: Updated entity.
    """
    logger.info(f"Updating UserSettings.web_search_enabled = {value}")
    settings.web_search_enabled = value
    self.db.flush()
    self.db.refresh(settings)
    return settings

read_inference_backend()

The persisted inference-backend preference, on a session of its own.

Used by the FastAPI lifespan, which has no request-scoped session and runs BEFORE the app serves anything. A missing row, an unreadable value or a database that is not ready all mean "auto": the preference exists to let someone REFUSE the GPU, so anything short of an explicit refusal must keep the hardware detection intact.

Source code in backend/src/domains/user_settings/repository.py
def read_inference_backend() -> str:
    """The persisted inference-backend preference, on a session of its own.

    Used by the FastAPI lifespan, which has no request-scoped session and runs
    BEFORE the app serves anything. A missing row, an unreadable value or a
    database that is not ready all mean "auto": the preference exists to let
    someone REFUSE the GPU, so anything short of an explicit refusal must keep
    the hardware detection intact.
    """
    from src.database.core import SessionLocal

    db = SessionLocal()
    try:
        settings = db.query(UserSettings).first()
        value = settings.inference_backend if settings else None
        return value or DEFAULT_INFERENCE_BACKEND
    except Exception as e:
        logger.warning(f"Could not read the inference backend preference: {e}")
        return DEFAULT_INFERENCE_BACKEND
    finally:
        db.close()

src.domains.user_settings.schemas

Pydantic validation schemas for the user-settings domain (issue #310).

One singleton resource: the app-wide user settings. It carries the global web-search default (#310), the interface language (#385), the automatic-update preference, the inference backend, the default reasoning effort and the default compaction threshold (#607); new settings slot in as additional fields.

UserSettingsResponse

Bases: BaseModel

Response schema for the user-settings singleton.

Attributes:

Name Type Description
web_search_enabled bool

Global default for the web_search agent tool. New conversations copy this value at creation; the per-conversation toggle owns it afterwards.

language LanguageCode

Interface language code the frontend renders in.

auto_update_enabled bool

Whether the Electron main process may check for, download and install a new version on its own.

inference_backend InferenceBackend

"auto" to let hardware detection pick the engine, "cpu" to run models on the CPU build regardless of the GPU.

default_reasoning_effort ReasoningEffort

Global default for how much a model may deliberate before answering. New conversations copy it at creation; arena turns read it directly.

default_compaction_threshold CompactionThreshold

Global default for the share of the working window, in percent, at which a conversation compacts. New conversations copy it at creation.

UserSettingsUpdate

Bases: BaseModel

Request schema for updating the user-settings singleton (PUT).

Partial update: every field is optional and an omitted field is left untouched, but an empty payload is rejected (nothing to update).