fix(proxy): reject expires on key requests

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Krrish Dholakia 2026-10-20 04:42:01 +00:00
parent 8884f461af
commit 073df7b8f5
2 changed files with 43 additions and 7 deletions

View file

@ -54,6 +54,16 @@ class LiteLLMKeyType(str, enum.Enum):
DEFAULT = "default" # Uses default allowed routes
def _is_mapping(value: object) -> TypeIs[Mapping[object, object]]: # guard-ok: raw before-validator payload
return isinstance(value, Mapping)
def _reject_expires(values: object) -> object:
if _is_mapping(values) and "expires" in values:
raise ValueError('expires is not supported on key requests. Set duration instead, e.g. "3600s" or "30d"')
return values
class GenerateKeyRequest(KeyRequestBase):
soft_budget: float | None = None
send_invite_email: bool | None = None
@ -69,6 +79,11 @@ class GenerateKeyRequest(KeyRequestBase):
organization_id: str | None = None
project_id: str | None = None
@model_validator(mode="before")
@classmethod
def reject_expires(cls, values: object) -> object:
return _reject_expires(values)
@field_validator("team_id", "organization_id", "project_id", mode="before")
@classmethod
def treat_cleared_id_as_unset(cls, v: object) -> object:
@ -77,10 +92,6 @@ class GenerateKeyRequest(KeyRequestBase):
return v
def _is_mapping(value: object) -> TypeIs[Mapping[object, object]]: # guard-ok: raw before-validator payload
return isinstance(value, Mapping)
class UpdateKeyRequest(KeyRequestBase):
# Note: the defaults of all Params here MUST BE NONE
# else they will get overwritten
@ -99,6 +110,11 @@ class UpdateKeyRequest(KeyRequestBase):
description="Omit to retain the project, or send null to detach. Assigning a different project is not supported.",
)
@model_validator(mode="before")
@classmethod
def reject_expires(cls, values: object) -> object:
return _reject_expires(values)
@model_validator(mode="before")
@classmethod
def drop_blank_team_id(cls, values: object) -> object:

View file

@ -15,7 +15,7 @@ from fastapi.testclient import TestClient
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi import HTTPException
from pydantic import JsonValue
from pydantic import JsonValue, ValidationError
import inspect
@ -108,6 +108,28 @@ from litellm.types.proxy.management_endpoints.key_management_endpoints import (
client = TestClient(app)
def test_key_request_models_reject_expires() -> None:
for request_model, request_values in (
(GenerateKeyRequest, {"expires": "2026-01-01T00:00:00Z"}),
(UpdateKeyRequest, {"key": "sk-x", "expires": "2026-01-01T00:00:00Z"}),
(RegenerateKeyRequest, {"expires": "2026-01-01T00:00:00Z"}),
(GenerateKeyRequest, {"expires": None}),
):
with pytest.raises(ValidationError) as exc_info:
request_model.model_validate(request_values)
assert "duration" in str(exc_info.value)
def test_generate_key_response_preserves_expires() -> None:
expires: Final = datetime(2026, 1, 1, tzinfo=timezone.utc)
assert GenerateKeyResponse(key="sk-x", expires=expires).expires == expires
def test_generate_key_request_accepts_duration_without_expires() -> None:
assert GenerateKeyRequest.model_validate({"duration": "1h"}).duration == "1h"
@pytest.mark.asyncio
async def test_list_keys():
mock_prisma_client = AsyncMock()
@ -23305,5 +23327,3 @@ def test_key_info_database_not_connected_sanitized_500(
finally:
proxy_server.app.dependency_overrides.pop(user_api_key_auth, None)