From 063a968175171c58389b8ae4a2e2d5a8cca1c398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=9C=88?= <1759519137@qq.com> Date: Sat, 10 Oct 2026 15:27:08 +0800 Subject: [PATCH 1/4] fix(proxy): honor explicit expires on key generation and update (#45236) --- litellm/proxy/_types.py | 4 + .../key_management_endpoints.py | 41 ++++- .../test_key_management_endpoints.py | 152 ++++++++++++++++++ ui/litellm-dashboard/src/lib/http/schema.d.ts | 17 ++ 4 files changed, 207 insertions(+), 7 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 477edf47c5f..97feff65dc4 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -1351,6 +1351,10 @@ class AllowedVectorStoreIndexItem(LiteLLMPydanticObjectBase): class KeyRequestBase(GenerateRequestBase): key: str | None = None + expires: datetime | None = Field( + default=None, + description="Absolute expiry timestamp for the key. Cannot be combined with `duration`.", + ) tpd_limit: int | None = None default_estimated_output_tokens: PositiveInt | None = None default_estimated_output_tokens_per_model: Mapping[str, PositiveInt] | None = None diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index ac9dfeb136d..d4856cc3082 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -440,7 +440,11 @@ def _effective_key_for_generate(data: GenerateKeyRequest, now: datetime) -> Lite folded_metadata: Final = {**metadata, **metadata_fields} columns: Final = handle_key_type(data, {**column_fields}) expires: Final = ( - now + timedelta(seconds=duration_in_seconds(duration=data.duration)) if data.duration is not None else None + data.expires + if data.expires is not None + else now + timedelta(seconds=duration_in_seconds(duration=data.duration)) + if data.duration is not None + else None ) budget_reset_at: Final = ( get_budget_reset_time(budget_duration=data.budget_duration) if data.budget_duration is not None else None @@ -1199,6 +1203,23 @@ def _enforce_upperbound_key_params( ) +def _reject_conflicting_expires_and_duration(data: GenerateKeyRequest | UpdateKeyRequest) -> None: + """`expires` is an absolute timestamp, `duration` is relative -- they are two + ways of saying when a key stops working. If a caller supplies both, honoring + either one would silently discard the other (GH#45236), so reject the request. + """ + if "expires" in data.model_fields_set and data.expires is not None and "duration" in data.model_fields_set: + raise HTTPException( + status_code=400, + detail={ + "error": ( + "`expires` and `duration` cannot be set together. " + "Use `expires` for an absolute timestamp or `duration` for a relative lifetime, not both." + ) + }, + ) + + async def _common_key_generation_helper( data: GenerateKeyRequest, user_api_key_dict: UserAPIKeyAuth, @@ -1221,6 +1242,7 @@ async def _common_key_generation_helper( ) validate_budget_duration(data.budget_duration) + _reject_conflicting_expires_and_duration(data) raise_on_invalid_key_logging_config(data.metadata) if data.throttle_on_budget_exceeded is True and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value: @@ -1989,6 +2011,7 @@ async def generate_key_fn( Parameters: - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). + - expires: Optional[datetime] - Absolute expiry timestamp for the key (ISO 8601). Use this or `duration`, not both. - key_alias: Optional[str] - User defined key alias - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you. - team_id: Optional[str] - The team id of the key @@ -2495,6 +2518,7 @@ async def prepare_key_update_data( prisma_client: PrismaClient | None = None, llm_router: Router | None = None, ): + _reject_conflicting_expires_and_duration(data) if data.router_settings is not None or ( "router_settings" not in data.model_fields_set and "team_id" in data.model_fields_set @@ -3444,6 +3468,7 @@ async def update_key_fn( - rpm_limit_type: Optional[str] - RPM rate limit type - "best_effort_throughput", "guaranteed_throughput", or "dynamic" - allowed_cache_controls: Optional[list] - List of allowed cache control values - duration: Optional[str] - Key validity duration ("30d", "1h", etc.), null to never expire, or "-1" to never expire (deprecated, use null) + - expires: Optional[datetime] - Absolute expiry timestamp for the key (ISO 8601). Use this or `duration`, not both. - permissions: Optional[dict] - Key-specific permissions - send_invite_email: Optional[bool] - Send invite email to user_id - guardrails: Optional[List[str]] - List of active guardrails for the key @@ -4590,6 +4615,7 @@ def metadata_json_with_limits( async def generate_key_helper_fn( request_type: Literal["user", "key"], # identifies if this request is from /user/new or /key/generate duration: str | None = None, + expires: datetime | None = None, # caller-supplied absolute expiry; wins over `duration` models: list = [], aliases: dict = {}, config: dict = {}, @@ -4667,12 +4693,13 @@ async def generate_key_helper_fn( else: token = f"sk-{secrets.token_urlsafe(LENGTH_OF_LITELLM_GENERATED_KEY)}" - if duration is None: # allow tokens that never expire - expires = None - else: - # Add duration to current time for exact expiration (not standardized reset time) - duration_seconds: Final = duration_in_seconds(duration) - expires = datetime.now(timezone.utc) + timedelta(seconds=duration_seconds) + if expires is None: + if duration is None: # allow tokens that never expire + expires = None + else: + # Add duration to current time for exact expiration (not standardized reset time) + duration_seconds: Final = duration_in_seconds(duration) + expires = datetime.now(timezone.utc) + timedelta(seconds=duration_seconds) if key_budget_duration is None: # one-time budget key_reset_at = None diff --git a/tests/unit/proxy/management_endpoints/test_key_management_endpoints.py b/tests/unit/proxy/management_endpoints/test_key_management_endpoints.py index 0f21f3ee3e0..c7238346a84 100644 --- a/tests/unit/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_key_management_endpoints.py @@ -484,6 +484,100 @@ async def test_key_expiration_exact_duration_hours(monkeypatch): ), f"Expected expiration to be approximately 12 hours from creation, got {hours_diff} hours" +@pytest.mark.asyncio +async def test_generate_key_honors_explicit_expires_without_duration(monkeypatch): + """GH#45236: an absolute `expires` timestamp supplied to /key/generate must be + persisted and returned as-is — not silently replaced by a duration-derived + expiry. No `duration` is given, so the supplied timestamp is the only + source of truth for the key's expiry, and the insert payload (what an + immediate GET /key/info reads back) must carry it.""" + explicit_expires = datetime(2031, 5, 4, 3, 2, 1, tzinfo=timezone.utc) + + mock_prisma_client = AsyncMock() + mock_prisma_client.insert_data = AsyncMock( + return_value=MagicMock(token="hashed_token_123", litellm_budget_table=None, object_permission=None) + ) + mock_prisma_client.db = MagicMock() + mock_prisma_client.db.litellm_verificationtoken = MagicMock() + mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + mock_prisma_client.db.litellm_verificationtoken.count = AsyncMock(return_value=0) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + response = await generate_key_fn( + data=GenerateKeyRequest(expires=explicit_expires), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-9876", user_id="admin" + ), + ) + + assert response.expires == explicit_expires + key_insert = mock_prisma_client.insert_data.await_args_list[-1].kwargs + assert key_insert["table_name"] == "key" + assert key_insert["data"]["expires"] == explicit_expires + + +@pytest.mark.asyncio +async def test_generate_key_rejects_explicit_expires_with_duration(): + """GH#45236: `expires` (absolute) and `duration` (relative) are two ways to say + when a key dies. Accepting both would require silently picking one, so the + ambiguous request must be rejected with a clear 400.""" + with pytest.raises(HTTPException) as exc: + await _common_key_generation_helper( + data=GenerateKeyRequest( + expires=datetime(2031, 5, 4, 3, 2, 1, tzinfo=timezone.utc), + duration="1d", + ), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-9876", user_id="admin" + ), + litellm_changed_by=None, + team_table=None, + ) + assert exc.value.status_code == 400 + assert "expires" in str(exc.value.detail) and "duration" in str(exc.value.detail) + + +@pytest.mark.asyncio +async def test_generate_key_expires_only_survives_config_upperbound_duration(monkeypatch): + """With `upperbound_key_generate_params.duration` configured, generate fills a + duration onto requests that don't set one. An explicit `expires` timestamp + from the caller must still win over that config-derived duration — otherwise + the timestamp is silently ignored (the exact bug in GH#45236).""" + explicit_expires = datetime(2031, 5, 4, 3, 2, 1, tzinfo=timezone.utc) + from litellm.types.proxy.management_endpoints.ui_sso import ( + LiteLLM_UpperboundKeyGenerateParams, + ) + + monkeypatch.setattr( + litellm, + "upperbound_key_generate_params", + LiteLLM_UpperboundKeyGenerateParams(duration="1d"), + ) + + mock_prisma_client = AsyncMock() + mock_prisma_client.insert_data = AsyncMock( + return_value=MagicMock(token="hashed_token_123", litellm_budget_table=None, object_permission=None) + ) + mock_prisma_client.db = MagicMock() + mock_prisma_client.db.litellm_verificationtoken = MagicMock() + mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock(return_value=None) + mock_prisma_client.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + mock_prisma_client.db.litellm_verificationtoken.count = AsyncMock(return_value=0) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + response = await generate_key_fn( + data=GenerateKeyRequest(expires=explicit_expires), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-9876", user_id="admin" + ), + ) + + assert response.expires == explicit_expires + key_insert = mock_prisma_client.insert_data.await_args_list[-1].kwargs + assert key_insert["data"]["expires"] == explicit_expires + + @pytest.mark.asyncio async def test_generate_key_persists_tpd_limit(monkeypatch): mock_prisma_client = AsyncMock() @@ -2386,6 +2480,64 @@ async def test_prepare_key_update_data_duration_none_never_expires(): assert result["expires"] is None +@pytest.mark.asyncio +async def test_prepare_key_update_data_honors_explicit_expires(): + """GH#45236: an absolute `expires` timestamp on /key/update must be persisted, + replacing the stored expiry — previously it was dropped and the stored + expiry (here, from the original duration) stayed unchanged.""" + explicit_expires = datetime(2031, 5, 4, 3, 2, 1, tzinfo=timezone.utc) + existing_key = LiteLLM_VerificationToken( + token="test-token", + expires="2030-01-01T00:00:00+00:00", + metadata={}, + ) + + result = await prepare_key_update_data( + data=UpdateKeyRequest(key="test-token", expires=explicit_expires), + existing_key_row=existing_key, + ) + + assert result["expires"] == explicit_expires + assert "duration" not in result + + +@pytest.mark.asyncio +async def test_prepare_key_update_data_rejects_explicit_expires_with_duration(): + """GH#45236: `expires` + `duration` on /key/update is ambiguous — reject with a + clear 400 instead of silently letting one of them win.""" + existing_key = LiteLLM_VerificationToken( + token="test-token", + expires="2030-01-01T00:00:00+00:00", + metadata={}, + ) + + with pytest.raises(HTTPException) as exc: + await prepare_key_update_data( + data=UpdateKeyRequest( + key="test-token", + expires=datetime(2031, 5, 4, 3, 2, 1, tzinfo=timezone.utc), + duration="1d", + ), + existing_key_row=existing_key, + ) + assert exc.value.status_code == 400 + assert "expires" in str(exc.value.detail) and "duration" in str(exc.value.detail) + + +@pytest.mark.asyncio +async def test_effective_key_for_generate_uses_explicit_expires(): + """The custom-key-policy view of a generate request must see the caller's + absolute `expires`, not a duration-derived timestamp.""" + now = datetime(2031, 5, 4, 3, 2, 1, tzinfo=timezone.utc) + explicit_expires = datetime(2031, 6, 1, 0, 0, 0, tzinfo=timezone.utc) + + effective = _effective_key_for_generate( + data=GenerateKeyRequest(expires=explicit_expires), now=now + ) + + assert effective.expires == explicit_expires + + @pytest.mark.asyncio @pytest.mark.parametrize("cleared_value", [[], None]) async def test_prepare_key_update_data_budget_limits_clears_field(cleared_value): diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index c9bf2e4aa5d..bc33ced196f 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -8330,6 +8330,7 @@ export interface paths { * * Parameters: * - duration: Optional[str] - Specify the length of time the token is valid for. You can set duration as seconds ("30s"), minutes ("30m"), hours ("30h"), days ("30d"). + * - expires: Optional[datetime] - Absolute expiry timestamp for the key (ISO 8601). Use this or `duration`, not both. * - key_alias: Optional[str] - User defined key alias * - key: Optional[str] - User defined key value. Must start with 'sk-' and be at least 16 characters long. If not set, a 16-digit unique sk-key is created for you. * - team_id: Optional[str] - The team id of the key @@ -8828,6 +8829,7 @@ export interface paths { * - rpm_limit_type: Optional[str] - RPM rate limit type - "best_effort_throughput", "guaranteed_throughput", or "dynamic" * - allowed_cache_controls: Optional[list] - List of allowed cache control values * - duration: Optional[str] - Key validity duration ("30d", "1h", etc.), null to never expire, or "-1" to never expire (deprecated, use null) + * - expires: Optional[datetime] - Absolute expiry timestamp for the key (ISO 8601). Use this or `duration`, not both. * - permissions: Optional[dict] - Key-specific permissions * - send_invite_email: Optional[bool] - Send invite email to user_id * - guardrails: Optional[List[str]] - List of active guardrails for the key @@ -32053,6 +32055,11 @@ export interface components { end_user_budget_id?: string | null; /** Enforced Params */ enforced_params?: string[] | null; + /** + * Expires + * @description Absolute expiry timestamp for the key. Cannot be combined with `duration`. + */ + expires?: string | null; /** Guardrails */ guardrails?: string[] | null; /** Key */ @@ -41035,6 +41042,11 @@ export interface components { end_user_budget_id?: string | null; /** Enforced Params */ enforced_params?: string[] | null; + /** + * Expires + * @description Absolute expiry timestamp for the key. Cannot be combined with `duration`. + */ + expires?: string | null; /** Grace Period */ grace_period?: string | null; /** Guardrails */ @@ -46953,6 +46965,11 @@ export interface components { end_user_budget_id?: string | null; /** Enforced Params */ enforced_params?: string[] | null; + /** + * Expires + * @description Absolute expiry timestamp for the key. Cannot be combined with `duration`. + */ + expires?: string | null; /** Guardrails */ guardrails?: string[] | null; /** Key */ From d24b63ae51ca3b76204625848999442a4d6b9102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=9C=88?= <1759519137@qq.com> Date: Sat, 10 Oct 2026 15:37:09 +0800 Subject: [PATCH 2/4] fix(proxy): compute the effective expiry into a fresh local With `expires` promoted to a parameter of generate_key_helper_fn, the duration-derived write to it read as parameter rebinding and tripped the type-discipline gate (LIT011 +2 past the merge-base ceiling). Compute the effective expiry into `key_expires` (Final, same chained-conditional shape as _effective_key_for_generate) and keep the parameter strictly an input. Behavior is unchanged; LIT011 count returns to the merge-base. --- .../key_management_endpoints.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index d4856cc3082..f5781381bf3 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -4693,13 +4693,14 @@ async def generate_key_helper_fn( else: token = f"sk-{secrets.token_urlsafe(LENGTH_OF_LITELLM_GENERATED_KEY)}" - if expires is None: - if duration is None: # allow tokens that never expire - expires = None - else: - # Add duration to current time for exact expiration (not standardized reset time) - duration_seconds: Final = duration_in_seconds(duration) - expires = datetime.now(timezone.utc) + timedelta(seconds=duration_seconds) + key_expires: Final = ( + expires + if expires is not None + # Add duration to current time for exact expiration (not standardized reset time) + else datetime.now(timezone.utc) + timedelta(seconds=duration_in_seconds(duration)) + if duration is not None + else None # allow tokens that never expire + ) if key_budget_duration is None: # one-time budget key_reset_at = None @@ -4776,7 +4777,7 @@ async def generate_key_helper_fn( key_data: Final = { "token": token, "key_alias": key_alias, - "expires": expires, + "expires": key_expires, "models": models, "aliases": aliases_json, "config": config_json, From b0419f0e2fd8e8e440949509cd9439ca22417f7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=9C=88?= <1759519137@qq.com> Date: Sat, 10 Oct 2026 16:26:38 +0800 Subject: [PATCH 3/4] fix(proxy): widen the helper's expires param for spread call sites `generate_key_helper_fn` is called with `**data_json` spreads whose value type is `object`; typing `expires` precisely taxed each of those call sites with a new reportArgumentType error, breaching the basedpyright delta gate. Accept `object` and validate at runtime, and pin the two dict-literal spreads to `dict[str, object]` so they add no unknown-argument noise. Signed-off-by: fengyue-xve <1759519137@qq.com> --- litellm/proxy/auth/login_utils.py | 23 ++++++++++--------- .../key_management_endpoints.py | 7 +++++- litellm/proxy/proxy_server.py | 23 ++++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/litellm/proxy/auth/login_utils.py b/litellm/proxy/auth/login_utils.py index 629b31024e2..debb6243108 100644 --- a/litellm/proxy/auth/login_utils.py +++ b/litellm/proxy/auth/login_utils.py @@ -336,20 +336,21 @@ async def _sign_in( ) if os.getenv("DATABASE_URL") is not None: + session_key_data: Final[dict[str, object]] = { + "user_role": LitellmUserRoles.PROXY_ADMIN, + "duration": LITELLM_UI_SESSION_DURATION, + "key_max_budget": litellm.max_ui_session_budget, + "models": [], + "aliases": {}, + "config": {}, + "spend": 0, + "user_id": key_user_id, + "team_id": "litellm-dashboard", + } response = await generate_key_helper_fn( llm_router=None, request_type="key", - **{ - "user_role": LitellmUserRoles.PROXY_ADMIN, - "duration": LITELLM_UI_SESSION_DURATION, - "key_max_budget": litellm.max_ui_session_budget, - "models": [], - "aliases": {}, - "config": {}, - "spend": 0, - "user_id": key_user_id, - "team_id": "litellm-dashboard", - }, + **session_key_data, ) else: raise ProxyException( diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index f5781381bf3..119bb590fea 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -4615,7 +4615,9 @@ def metadata_json_with_limits( async def generate_key_helper_fn( request_type: Literal["user", "key"], # identifies if this request is from /user/new or /key/generate duration: str | None = None, - expires: datetime | None = None, # caller-supplied absolute expiry; wins over `duration` + # Typed as `object` because this helper is called with `**data_json` spreads whose + # value type is `object`; the caller-supplied absolute expiry (a `datetime`). + expires: object = None, models: list = [], aliases: dict = {}, config: dict = {}, @@ -4693,6 +4695,9 @@ async def generate_key_helper_fn( else: token = f"sk-{secrets.token_urlsafe(LENGTH_OF_LITELLM_GENERATED_KEY)}" + if expires is not None and not isinstance(expires, datetime): + raise ValueError(f"`expires` must be a datetime, got {type(expires).__name__}") + key_expires: Final = ( expires if expires is not None diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index dac46855fd6..6e75eb3eb91 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -17561,20 +17561,21 @@ async def _rollback_onboarding_invite_claim( async def _generate_onboarding_ui_session_token(user_obj: _UserTableRow) -> str: global master_key, general_settings + onboarding_key_data: Final[dict[str, object]] = { + "user_role": user_obj.user_role, + "duration": LITELLM_UI_SESSION_DURATION, + "key_max_budget": litellm.max_ui_session_budget, + "models": [], + "aliases": {}, + "config": {}, + "spend": 0, + "user_id": user_obj.user_id, + "team_id": UI_TEAM_ID, + } response: Final = await generate_key_helper_fn( llm_router=llm_router, request_type="key", - **{ - "user_role": user_obj.user_role, - "duration": LITELLM_UI_SESSION_DURATION, - "key_max_budget": litellm.max_ui_session_budget, - "models": [], - "aliases": {}, - "config": {}, - "spend": 0, - "user_id": user_obj.user_id, - "team_id": UI_TEAM_ID, - }, + **onboarding_key_data, ) key: Final = response["token"] From e44549b8082fbd0150b9d6f4dd45ef67b9dcdf02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=9C=88?= <1759519137@qq.com> Date: Sat, 10 Oct 2026 17:18:41 +0800 Subject: [PATCH 4/4] fix(proxy): annotate the session-key payloads as read-only mappings The LIT001 type-discipline gate counts mutable collections in annotations and flagged the two `Final[dict[str, object]]` payloads, breaching the merge-base ceiling by 2. `Mapping` is the read-only view the rule asks for; per-rule basedpyright counts are unchanged. Signed-off-by: fengyue-xve <1759519137@qq.com> --- litellm/proxy/auth/login_utils.py | 2 +- litellm/proxy/proxy_server.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/auth/login_utils.py b/litellm/proxy/auth/login_utils.py index debb6243108..5a320c4ea72 100644 --- a/litellm/proxy/auth/login_utils.py +++ b/litellm/proxy/auth/login_utils.py @@ -336,7 +336,7 @@ async def _sign_in( ) if os.getenv("DATABASE_URL") is not None: - session_key_data: Final[dict[str, object]] = { + session_key_data: Final[Mapping[str, object]] = { "user_role": LitellmUserRoles.PROXY_ADMIN, "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget, diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 6e75eb3eb91..d68c4ae20a2 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -17561,7 +17561,7 @@ async def _rollback_onboarding_invite_claim( async def _generate_onboarding_ui_session_token(user_obj: _UserTableRow) -> str: global master_key, general_settings - onboarding_key_data: Final[dict[str, object]] = { + onboarding_key_data: Final[Mapping[str, object]] = { "user_role": user_obj.user_role, "duration": LITELLM_UI_SESSION_DURATION, "key_max_budget": litellm.max_ui_session_budget,