Merge pull request #22182 from BerriAI/litellm_make_session_duration_configurable

[Feat] Make UI login session duration configurable via LITELLM_UI_SESSION_DURATION
This commit is contained in:
yuneng-jiang 2026-02-28 20:31:31 -08:00 committed by GitHub
commit 8053be60df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 72 additions and 12 deletions

View File

@ -52,6 +52,10 @@ LITELLM_CLI_JWT_EXPIRATION_HOURS=48 EXPERIMENTAL_UI_LOGIN="True" litellm --confi
- `LITELLM_CLI_JWT_EXPIRATION_HOURS=168` - Tokens expire after 7 days (168 hours)
- `LITELLM_CLI_JWT_EXPIRATION_HOURS=720` - Tokens expire after 30 days (720 hours)
:::note[Experimental UI Session]
When `EXPERIMENTAL_UI_LOGIN` is enabled, the **browser UI login** session uses a fixed 10-minute expiry (not configurable). `LITELLM_UI_SESSION_DURATION` applies only to non-experimental flows.
:::
:::tip
You can check your current token's age and expiration status using:
```bash

View File

@ -777,6 +777,7 @@ router_settings:
| LITELLM_HOSTED_UI | URL of the hosted UI for LiteLLM
| LITELLM_UI_API_DOC_BASE_URL | Optional override for the API Reference base URL (used in sample code/docs) when the admin UI runs on a different host than the proxy. Defaults to `PROXY_BASE_URL` when unset.
| LITELLM_UI_PATH | Path to directory for Admin UI files. Used when running with read-only filesystem (e.g., Kubernetes). Default is `/var/lib/litellm/ui` in Docker.
| LITELLM_UI_SESSION_DURATION | Duration for UI login session (username/password, SSO, invitation links). Format: "30s", "30m", "24h", "7d". Does not apply to EXPERIMENTAL_UI_LOGIN flow, which uses a fixed 10-minute expiry for security. Default is "24h"
| LITELM_ENVIRONMENT | Environment of LiteLLM Instance, used by logging services. Currently only used by DeepEval.
| LITELLM_KEY_ROTATION_ENABLED | Enable auto-key rotation for LiteLLM (boolean). Default is false.
| LITELLM_KEY_ROTATION_CHECK_INTERVAL_SECONDS | Interval in seconds for how often to run job that auto-rotates keys. Default is 86400 (24 hours).

View File

@ -12,6 +12,13 @@ warnings.filterwarnings(
### INIT VARIABLES #########################
import threading
import os
# Load .env before any other litellm imports so env vars (e.g. LITELLM_UI_SESSION_DURATION) are available
import dotenv as _dotenv
if os.getenv("LITELLM_MODE", "DEV") == "DEV":
_dotenv.load_dotenv()
from typing import (
Callable,
List,
@ -74,12 +81,9 @@ from litellm.constants import (
DEFAULT_ALLOWED_FAILS,
)
import httpx
import dotenv
# register_async_client_cleanup is lazy-loaded and called on first access
litellm_mode = os.getenv("LITELLM_MODE", "DEV") # "PRODUCTION", "DEV"
if litellm_mode == "DEV":
dotenv.load_dotenv()
####################################################

View File

@ -1322,6 +1322,11 @@ CLI_JWT_EXPIRATION_HOURS = int(
or 24
)
########################### UI SESSION DURATION ###########################
# Duration for UI login session (username/password, SSO, invitation links). Format: "30s", "30m", "24h", "7d"
# Does NOT apply to EXPERIMENTAL_UI_LOGIN flow, which intentionally uses a fixed 10-minute expiry for security.
LITELLM_UI_SESSION_DURATION = os.getenv("LITELLM_UI_SESSION_DURATION", "24h")
########################### DB CRON JOB NAMES ###########################
DB_SPEND_UPDATE_JOB_NAME = "db_spend_update_job"
PROMETHEUS_EMIT_BUDGET_METRICS_JOB_NAME = "prometheus_emit_budget_metrics"

View File

@ -1884,7 +1884,7 @@ class ExperimentalUIJWTToken:
if user_info.user_role is None:
raise Exception("User role is required for experimental UI login")
# Calculate expiration time (10 minutes from now)
# Experimental UI flow uses fixed 10-min expiry for security (does not use LITELLM_UI_SESSION_DURATION)
expiration_time = get_utc_datetime() + timedelta(minutes=10)
# Format the expiration time as ISO 8601 string

View File

@ -12,7 +12,7 @@ from typing import Literal, Optional, cast
from fastapi import HTTPException
import litellm
from litellm.constants import LITELLM_PROXY_ADMIN_NAME
from litellm.constants import LITELLM_PROXY_ADMIN_NAME, LITELLM_UI_SESSION_DURATION
from litellm.proxy._types import (
LiteLLM_UserTable,
LitellmUserRoles,
@ -178,7 +178,7 @@ async def authenticate_user( # noqa: PLR0915
request_type="key",
**{
"user_role": LitellmUserRoles.PROXY_ADMIN,
"duration": "24hr",
"duration": LITELLM_UI_SESSION_DURATION,
"key_max_budget": litellm.max_ui_session_budget,
"models": [],
"aliases": {},
@ -264,7 +264,7 @@ async def authenticate_user( # noqa: PLR0915
request_type="key",
**{ # type: ignore
"user_role": user_role,
"duration": "24hr",
"duration": LITELLM_UI_SESSION_DURATION,
"key_max_budget": litellm.max_ui_session_budget,
"models": [],
"aliases": {},

View File

@ -25,6 +25,7 @@ from litellm._logging import verbose_proxy_logger
from litellm._uuid import uuid
from litellm.caching import DualCache
from litellm.constants import (
LITELLM_UI_SESSION_DURATION,
MAX_SPENDLOG_ROWS_TO_QUERY,
MICROSOFT_USER_DISPLAY_NAME_ATTRIBUTE,
MICROSOFT_USER_EMAIL_ATTRIBUTE,
@ -2237,7 +2238,7 @@ class SSOAuthenticationHandler:
# User might not be already created on first generation of key
# But if it is, we want their models preferences
default_ui_key_values: Dict[str, Any] = {
"duration": "24hr",
"duration": LITELLM_UI_SESSION_DURATION,
"key_max_budget": litellm.max_ui_session_budget,
"aliases": {},
"config": {},

View File

@ -52,6 +52,7 @@ from litellm.constants import (
LITELLM_EMBEDDING_PROVIDERS_SUPPORTING_INPUT_ARRAY_OF_TOKENS,
LITELLM_SETTINGS_SAFE_DB_OVERRIDES,
LITELLM_UI_ALLOW_HEADERS,
LITELLM_UI_SESSION_DURATION,
)
from litellm.litellm_core_utils.litellm_logging import (
_init_custom_logger_compatible_class,
@ -10847,7 +10848,7 @@ async def onboarding(invite_link: str, request: Request):
request_type="key",
**{
"user_role": user_obj.user_role,
"duration": "24hr",
"duration": LITELLM_UI_SESSION_DURATION,
"key_max_budget": litellm.max_ui_session_budget,
"models": [],
"aliases": {},

View File

@ -108,11 +108,55 @@ def test_get_experimental_ui_login_jwt_auth_token_valid(valid_sso_user_defined_v
assert token_data["models"] == ["gpt-3.5-turbo"]
assert token_data["max_budget"] == litellm.max_ui_session_budget
# Verify expiration time is set and valid
# Verify expiration time is set and valid (Experimental UI uses fixed 10-min expiry)
assert "expires" in token_data
expires = datetime.fromisoformat(token_data["expires"].replace("Z", "+00:00"))
assert expires > get_utc_datetime()
assert expires <= get_utc_datetime() + timedelta(minutes=10)
now = get_utc_datetime()
# Allow 2 second buffer for test execution timing
assert expires > now
assert expires <= now + timedelta(minutes=10, seconds=2)
def test_get_experimental_ui_login_jwt_auth_token_uses_10_min_expiry(
valid_sso_user_defined_values,
):
"""Test that Experimental UI token uses fixed 10-minute expiry (does not use LITELLM_UI_SESSION_DURATION)."""
token = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token(
valid_sso_user_defined_values
)
decrypted_token = decrypt_value_helper(
token, key="ui_hash_key", exception_type="debug"
)
assert decrypted_token is not None
token_data = json.loads(decrypted_token)
expires = datetime.fromisoformat(token_data["expires"].replace("Z", "+00:00"))
now = get_utc_datetime()
# Should expire in ~10 minutes (allow 2 second buffer)
assert expires > now + timedelta(minutes=9)
assert expires <= now + timedelta(minutes=10, seconds=2)
def test_experimental_ui_token_ignores_litellm_ui_session_duration(
valid_sso_user_defined_values,
):
"""Regression test: LITELLM_UI_SESSION_DURATION must NOT affect Experimental UI token expiry.
Experimental UI intentionally uses fixed 10-min expiry. If this test fails, the constant
was incorrectly wired to the experimental flow."""
# Default LITELLM_UI_SESSION_DURATION is "24h" - token must still expire in ~10 min
token = ExperimentalUIJWTToken.get_experimental_ui_login_jwt_auth_token(
valid_sso_user_defined_values
)
decrypted_token = decrypt_value_helper(
token, key="ui_hash_key", exception_type="debug"
)
assert decrypted_token is not None
token_data = json.loads(decrypted_token)
expires = datetime.fromisoformat(token_data["expires"].replace("Z", "+00:00"))
now = get_utc_datetime()
# Must be ~10 min, NOT 24h. If LITELLM_UI_SESSION_DURATION were incorrectly used, this would fail.
assert expires <= now + timedelta(minutes=11), (
"Experimental UI must use 10-min expiry, not LITELLM_UI_SESSION_DURATION"
)
def test_get_experimental_ui_login_jwt_auth_token_invalid(