Skip to content

[Fix] Key Expiry Default Duration#22956

Merged
yuneng-jiang merged 1 commit intomainfrom
litellm_key_null_duration
Mar 6, 2026
Merged

[Fix] Key Expiry Default Duration#22956
yuneng-jiang merged 1 commit intomainfrom
litellm_key_null_duration

Conversation

@yuneng-jiang
Copy link
Collaborator

Summary

Failure Path (Before Fix)

To reset a key's expiry to "never expires" via /key/update, users had to pass duration: "-1" — a magic string with poor discoverability. The budget fields already use null to mean "unlimited" (e.g., max_budget: null), but duration had no equivalent.

Fix

Added support for duration: null on /key/update to reset a key's expiry to never expires. The existing "-1" magic string is preserved for backward compatibility.

  • Backend: prepare_key_update_data now treats duration=None the same as duration="-1" (sets expires=None). The upperbound validation also accepts None as infinite duration. Docstring updated to document null as preferred, "-1" as deprecated.
  • UI: Added a "Never Expire" checkbox in the key edit view (edit mode only). When checked, the duration input is disabled and duration: null is sent on submit. Initialized from the key's current expires value.

Testing

  • Added test_prepare_key_update_data_duration_none_never_expires alongside the existing test_prepare_key_update_data_duration_never_expires test — both pass.
  • Run: python3 -m pytest tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py -v -k "duration"

Type

🐛 Bug Fix
✅ Test

Support passing duration=null on /key/update to reset a key's expiry to never expires, alongside the existing "-1" magic string (kept for backward compat).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 6, 2026 4:56am

Request Review

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 6, 2026

Greptile Summary

This PR adds duration: null as a supported value on /key/update to reset a key's expiry to "never expires", improving on the existing magic string "-1" which is preserved for backward compatibility. The backend change is minimal and correct; the frontend adds a "Never Expire" checkbox in the key edit view initialized from the key's current expiry state.

Key changes:

  • prepare_key_update_data now treats duration=None identically to duration="-1" — both set expires=None in the DB
  • The upperbound duration validation accepts None (user requesting infinite duration) as float("inf") so it can be correctly compared against any configured max
  • A "Never Expire" checkbox is added to KeyLifecycleSettings (edit mode only), pre-checked when keyData.expires is falsy, and injects duration: null into the form payload on submit
  • New unit test test_prepare_key_update_data_duration_none_never_expires validates the None path alongside the existing "-1" test

Issues found:

  • The tooltip in KeyLifecycleSettings was consolidated into a single string that says "Leave empty to keep the current expiry unchanged." This message is inaccurate in create mode (used in create_key_button.tsx with isCreateMode={true}), where leaving the field empty means "never expire" — not "keep the current expiry unchanged."
  • If a user unchecks "Never Expire" but doesn't enter a duration before saving, the backend silently ignores the empty string and the key remains as never-expiring, with no validation feedback to the user.

Confidence Score: 4/5

  • Safe to merge with minor UX regressions in the frontend tooltip and a missing validation for the "Never Expire" unchecked state.
  • The backend logic is correct and well-tested with mock unit tests. The two frontend issues are UX-only: a misleading tooltip in create mode and a silent no-op when "Never Expire" is unchecked without providing a duration. Neither causes data corruption or broken API behavior.
  • ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx — tooltip regression affects create mode; ui/litellm-dashboard/src/components/templates/key_edit_view.tsx — missing validation when "Never Expire" is unchecked without a duration.

Important Files Changed

Filename Overview
litellm/proxy/management_endpoints/key_management_endpoints.py Two targeted fixes: prepare_key_update_data now treats duration=None the same as duration="-1" (never expires), and the upperbound comparison likewise accepts None as infinite duration. Both are minimal, correct changes backed by the new test.
tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py New test_prepare_key_update_data_duration_none_never_expires test mirrors the existing -1 test and correctly validates the new None path. Test is mock-only (no real network calls), consistent with the repo's test policy for this folder.
ui/litellm-dashboard/src/components/common_components/KeyLifecycleSettings.tsx Added "Never Expire" checkbox (edit-mode only) with Antd Checkbox. The tooltip was unified into a single message that is now inaccurate in create mode — it says "Leave empty to keep the current expiry unchanged" but in create mode leaving the field empty means "never expire".
ui/litellm-dashboard/src/components/templates/key_edit_view.tsx Initializes neverExpire state from !keyData.expires and injects duration: null on submit when checked. Minor UX gap: unchecking "Never Expire" without providing a duration silently leaves the key as never-expiring, with no validation feedback.

Sequence Diagram

sequenceDiagram
    participant UI as key_edit_view.tsx
    participant API as /key/update (FastAPI)
    participant FN as prepare_key_update_data()
    participant DB as Database

    Note over UI: User checks "Never Expire"
    UI->>UI: neverExpire = true<br/>values.duration = null

    UI->>API: POST /key/update {duration: null}

    API->>FN: UpdateKeyRequest(duration=None)
    Note over FN: model_dump(exclude_unset=True)<br/>→ {duration: None}
    FN->>FN: duration is None → expires = None
    FN-->>API: {expires: None, ...}

    API->>DB: UPDATE key SET expires = NULL

    DB-->>API: OK
    API-->>UI: {key updated}

    Note over UI: User leaves "-1" (legacy path)
    UI->>API: POST /key/update {duration: "-1"}
    API->>FN: UpdateKeyRequest(duration="-1")
    FN->>FN: duration == "-1" → expires = None
    FN-->>API: {expires: None, ...}
    API->>DB: UPDATE key SET expires = NULL
Loading

Last reviewed commit: e468b02

? "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to never expire."
: "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Use -1 to never expire."
}
title="Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tooltip message is inaccurate in create mode

The tooltip was previously conditional based on isCreateMode. Now it always says "Leave empty to keep the current expiry unchanged." However, in create mode there is no "current expiry" — leaving the field empty means "never expire". This is a UX regression for the key creation flow (used in create_key_button.tsx with isCreateMode={true}).

The isCreateMode prop is still passed through to KeyLifecycleSettings by callers, so it can still be used to differentiate the tooltip message.

Suggested change
title="Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged."
title={isCreateMode
? "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to never expire."
: "Set when this key should expire. Format: 30s (seconds), 30m (minutes), 30h (hours), 30d (days). Leave empty to keep the current expiry unchanged."}

);
const [autoRotationEnabled, setAutoRotationEnabled] = useState<boolean>(keyData.auto_rotate || false);
const [rotationInterval, setRotationInterval] = useState<string>(keyData.rotation_interval || "");
const [neverExpire, setNeverExpire] = useState<boolean>(!keyData.expires);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unchecking "Never Expire" without a duration silently keeps key as never-expiring

neverExpire is initialized to true when keyData.expires is falsy. If a user opens the edit view for a never-expiring key, unchecks the "Never Expire" checkbox (intending to add an expiry), but then doesn't fill in a duration string and saves — the backend receives duration: "". Since "" matches neither the None / "-1" branch nor the len(duration) > 0 branch in prepare_key_update_data, expires is never updated and the key silently remains as never-expiring.

Consider adding a validation step in handleSubmit (or inside KeyLifecycleSettings when the checkbox is unchecked) to surface an error when "Never Expire" is unchecked but no duration is provided.

@yuneng-jiang yuneng-jiang merged commit 8523bb6 into main Mar 6, 2026
28 of 38 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant