fix(proxy): preserve explicit key expiration through key mutations

This commit is contained in:
Tin 2026-10-27 16:28:10 -07:00
parent 969161501a
commit 694b47dbdb
10 changed files with 326 additions and 26 deletions

View file

@ -338,6 +338,7 @@ async def _sign_in(
if os.getenv("DATABASE_URL") is not None:
response = await generate_key_helper_fn(
llm_router=None,
expires=None,
request_type="key",
**{
"user_role": LitellmUserRoles.PROXY_ADMIN,

View file

@ -594,7 +594,7 @@ async def new_user(
teams = check_if_default_team_set()
organization_ids: Final = cast(list[str] | None, data_json.pop("organizations", None))
response: Final = await generate_key_helper_fn(request_type="user", **data_json, llm_router=None)
response: Final = await generate_key_helper_fn(request_type="user", **data_json, expires=None, llm_router=None)
created_user_id: Final = cast(str | None, response.get("user_id", None))
if created_user_id is not None:
forget_missing_user(created_user_id)

View file

@ -477,6 +477,12 @@ def _generate_budget_windows(
)
def _key_expiration(data: GenerateKeyRequest | UpdateKeyRequest, now: datetime) -> datetime | None:
if "expires" in data.model_fields_set:
return data.expires
return now + timedelta(seconds=duration_in_seconds(data.duration)) if data.duration is not None else None
def _effective_key_for_generate(data: GenerateKeyRequest, now: datetime) -> LiteLLM_VerificationToken:
requested: Final = _as_object_dict(data.model_dump(exclude_unset=True, exclude_none=True))
metadata_fields: Final = MappingProxyType(
@ -494,9 +500,6 @@ def _effective_key_for_generate(data: GenerateKeyRequest, now: datetime) -> Lite
if default_allowed_routes is None
else {**preset_columns, "allowed_routes": list(default_allowed_routes)}
)
expires: Final = (
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
)
@ -510,7 +513,7 @@ def _effective_key_for_generate(data: GenerateKeyRequest, now: datetime) -> Lite
{
**columns,
"metadata": encrypt_callback_vars(folded_metadata),
"expires": expires,
"expires": _key_expiration(data, now),
"budget_reset_at": budget_reset_at,
"key_rotation_at": key_rotation_at,
"budget_limits": _generate_budget_windows(data.budget_limits),
@ -1329,9 +1332,23 @@ async def validate_team_id_used_in_service_account_request(
return True
def _key_expiration_limit_error(
expires: datetime | None, max_duration: str | None, now: datetime, source: str
) -> str | None:
if max_duration is None:
return None
if expires is None:
return f"expires cannot be null when {source} sets a maximum key duration"
if expires > now + timedelta(seconds=duration_in_seconds(max_duration)):
return f"expires exceeds the maximum key duration set by {source}: {max_duration}"
return None
def _enforce_upperbound_key_params(
data: GenerateKeyRequest | UpdateKeyRequest,
fill_defaults: bool = True,
*,
now: datetime | None = None,
) -> None:
"""
Enforce upperbound limits on key parameters.
@ -1362,9 +1379,26 @@ def _enforce_upperbound_key_params(
if litellm.upperbound_key_generate_params is None:
return
expiration_error: Final = (
_key_expiration_limit_error(
data.expires,
litellm.upperbound_key_generate_params.duration,
now if now is not None else datetime.now(timezone.utc),
"upperbound_key_generate_params",
)
if "expires" in data.model_fields_set
else None
)
if expiration_error is not None:
raise HTTPException(status_code=400, detail={"error": expiration_error})
for elem in data:
key, value = elem
upperbound_value = getattr(litellm.upperbound_key_generate_params, key, None)
upperbound_value = (
None
if key == "duration" and "expires" in data.model_fields_set
else getattr(litellm.upperbound_key_generate_params, key, None)
)
if upperbound_value is not None:
if value is None:
if fill_defaults:
@ -1434,6 +1468,8 @@ async def _common_key_generation_helper(
user_api_key_dict: UserAPIKeyAuth,
litellm_changed_by: str | None,
team_table: LiteLLM_TeamTableCachedObj | None,
*,
now: datetime | None = None,
) -> GenerateKeyResponse:
from litellm.proxy import proxy_server
from litellm.proxy.proxy_server import (
@ -1519,6 +1555,7 @@ async def _common_key_generation_helper(
if (
value is None
and (key != "budget_duration" or key not in data.model_fields_set)
and (key != "duration" or "expires" not in data.model_fields_set)
and key
in [
"max_budget",
@ -1540,7 +1577,8 @@ async def _common_key_generation_helper(
setattr(data, key, litellm.default_key_generate_params.get(key, {}))
# check if user set upperbound key/generate params on config.yaml
_enforce_upperbound_key_params(data, fill_defaults=True)
generation_time: Final = now if now is not None else datetime.now(timezone.utc)
_enforce_upperbound_key_params(data, fill_defaults=True, now=generation_time)
_enforce_budget_delegation_ceiling(
requested_max_budget=_requested_max_budget,
@ -1572,13 +1610,27 @@ async def _common_key_generation_helper(
"litellm.proxy.proxy_server.generate_key_fn(): Enterprise key management params not applied - %s", e
)
generation_expires: Final = _key_expiration(data, generation_time)
if "expires" in data.model_fields_set and team_table is not None and data.user_id is not None:
team_metadata: Final = TypeAdapter[dict[str, object]](dict[str, object]).validate_python(
team_table.metadata or {} # pyright: ignore[reportUnknownMemberType] # legacy model declares metadata as bare dict
)
team_duration: Final = TypeAdapter[str | None](str | None).validate_python(
team_metadata.get("team_member_key_duration")
)
team_expiration_error: Final = _key_expiration_limit_error(
generation_expires, team_duration, generation_time, "team_member_key_duration"
)
if team_expiration_error is not None:
raise HTTPException(status_code=400, detail={"error": team_expiration_error})
await _enforce_custom_key_policy(
hook=_custom_key_policy_hook(proxy_server),
build_policy_request=lambda: CustomKeyPolicyRequest(
operation="generate",
existing_key=None,
effective_key=_effective_key_for_generate(
data=_member_key_request(data, acting_as_team_admin), now=datetime.now(timezone.utc)
data=_member_key_request(data, acting_as_team_admin), now=generation_time
),
request=data,
),
@ -1798,7 +1850,12 @@ async def _common_key_generation_helper(
try:
response = await generate_key_helper_fn(
request_type="key", **data_json, table_name="key", llm_router=llm_router
request_type="key",
**{field: value for field, value in data_json.items() if field not in {"duration", "expires"}},
duration=None,
expires=generation_expires,
table_name="key",
llm_router=llm_router,
)
except KeyProjectBindingError:
if prisma_client is not None:
@ -2304,6 +2361,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 expiration with a timezone, overriding duration when supplied. Null requests no expiration, subject to lifetime limits. Past timestamps expire the key immediately.
- 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
@ -2870,13 +2928,13 @@ async def prepare_key_update_data(
if "duration" in non_default_values:
duration: Final = non_default_values.pop("duration")
if duration is None or duration == "-1":
# Set expires to None to indicate the key never expires
non_default_values["expires"] = None
elif duration and (isinstance(duration, str)) and len(duration) > 0:
duration_s: Final = duration_in_seconds(duration=duration)
expires: Final = datetime.now(timezone.utc) + timedelta(seconds=duration_s)
non_default_values["expires"] = expires
if "expires" not in non_default_values:
if duration is None or duration == "-1":
non_default_values["expires"] = None
elif duration and (isinstance(duration, str)) and len(duration) > 0:
duration_s: Final = duration_in_seconds(duration=duration)
expires: Final = datetime.now(timezone.utc) + timedelta(seconds=duration_s)
non_default_values["expires"] = expires
if "budget_duration" in non_default_values:
budget_duration: Final = non_default_values.pop("budget_duration")
@ -3874,6 +3932,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 expiration with a timezone, overriding duration when supplied. Omit to retain expiration, or use null to clear it when lifetime limits allow. Past timestamps expire the key immediately.
- 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
@ -5103,6 +5162,7 @@ async def generate_key_helper_fn(
access_group_ids: list[str] | None = None,
budget_limits: list | None = None, # multiple concurrent budget windows
*,
expires: datetime | None = None,
llm_router: Router | None = None,
):
from litellm.proxy.proxy_server import premium_user, prisma_client
@ -5123,12 +5183,11 @@ 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)
effective_expires: Final = (
expires
if expires is not None or duration is None
else datetime.now(timezone.utc) + timedelta(seconds=duration_in_seconds(duration))
)
if key_budget_duration is None: # one-time budget
key_reset_at = None
@ -5205,7 +5264,7 @@ async def generate_key_helper_fn(
key_data: Final = {
"token": token,
"key_alias": key_alias,
"expires": expires,
"expires": effective_expires,
"models": models,
"aliases": aliases_json,
"config": config_json,
@ -6315,6 +6374,7 @@ async def regenerate_key_fn(
- model_tpm_limit: Optional[dict] - Model-specific TPM limits {"gpt-4": 100000, "claude-v1": 200000}
- allowed_cache_controls: Optional[list] - List of allowed cache control values
- duration: Optional[str] - Key validity duration ("30d", "1h", etc.)
- expires: Optional[datetime] - Absolute expiration with a timezone, overriding duration when supplied. Omit to retain expiration, or use null to clear it when lifetime limits allow. Past timestamps expire the key immediately.
- permissions: Optional[dict] - Key-specific permissions
- guardrails: Optional[List[str]] - List of active guardrails for the key
- blocked: Optional[bool] - Whether the key is blocked

View file

@ -18942,6 +18942,7 @@ async def _generate_onboarding_ui_session_token(user_obj: _UserTableRow) -> str:
response: Final = await generate_key_helper_fn(
llm_router=llm_router,
expires=None,
request_type="key",
**{
"user_role": user_obj.user_role,

View file

@ -1,3 +1,4 @@
from collections.abc import Mapping
from datetime import datetime
from typing import Any, Final, Literal, TypeAlias
@ -39,6 +40,14 @@ class BulkUpdateKeyRequestItem(LiteLLMBaseModel):
tags: list[str] | None = None # Tags for organizing keys
object_permission: LiteLLM_ObjectPermissionBase | None = None
@model_validator(mode="before")
@classmethod
def reject_expiration(cls, values: object) -> object:
original: Final[object] = values
if "expires" in (values if isinstance(values, Mapping) else {}):
raise ValueError("Use /key/update to change expires; /key/bulk_update does not support expiration changes")
return original
class BulkUpdateKeyRequest(LiteLLMBaseModel):
"""Request for bulk key updates"""

View file

@ -2,9 +2,9 @@ import enum
from collections.abc import Mapping
from datetime import datetime
from types import MappingProxyType
from typing import Any, Literal # noqa: TID251 # allowed_routes and metadata stay Any for existing callers
from typing import Annotated, Any, Literal # noqa: TID251 # allowed_routes and metadata stay Any for existing callers
from pydantic import Field, PositiveInt, field_validator, model_validator
from pydantic import AwareDatetime, Field, PositiveInt, field_validator, model_validator
from typing_extensions import TypeIs # noqa: TID251 # narrows the raw payload a before-validator receives
from litellm.types.proxy.management_endpoints.request_base import GenerateRequestBase
@ -55,6 +55,10 @@ class LiteLLMKeyType(str, enum.Enum):
class GenerateKeyRequest(KeyRequestBase):
expires: Annotated[datetime, AwareDatetime] | None = Field(
default=None,
description="Absolute expiration with a timezone. When supplied, overrides duration. Null means no expiration, subject to lifetime limits. Past timestamps expire the key immediately.",
)
tpm_limit: int | None = Field(default=None, ge=0)
rpm_limit: int | None = Field(default=None, ge=0)
soft_budget: float | None = None
@ -84,6 +88,10 @@ def _is_mapping(value: object) -> TypeIs[Mapping[object, object]]: # guard-ok:
class UpdateKeyRequest(KeyRequestBase):
expires: Annotated[datetime, AwareDatetime] | None = Field(
default=None,
description="Absolute expiration with a timezone. When supplied, overrides duration. Omit to retain expiration, or use null to clear it when lifetime limits allow. Past timestamps expire the key immediately.",
)
tpm_limit: int | None = Field(default=None, ge=0)
rpm_limit: int | None = Field(default=None, ge=0)
# Note: the defaults of all Params here MUST BE NONE

View file

@ -13,7 +13,7 @@ import pytest
import yaml
from fastapi import HTTPException
from fastapi.testclient import TestClient
from pydantic import JsonValue
from pydantic import JsonValue, ValidationError
import litellm
from litellm.constants import PROXY_DB_LOOKUP_STALL_WINDOW_SECONDS
@ -101,7 +101,9 @@ from litellm.types.proxy.management_endpoints.key_management_endpoints import (
BulkUpdateKeyRequest,
BulkUpdateKeyResponse,
CustomKeyPolicyRequest,
KeyUpdateFields,
)
from litellm.types.proxy.management_endpoints.ui_sso import LiteLLM_UpperboundKeyGenerateParams
client = TestClient(app)
@ -23372,3 +23374,185 @@ def test_key_info_database_not_connected_sanitized_500(
assert "Database not connected" in caplog.text
finally:
proxy_server.app.dependency_overrides.pop(user_api_key_auth, None)
_EXPIRATION_NOW: Final = datetime(2030, 1, 1, tzinfo=timezone.utc)
_EXPIRATION_OFFSET: Final = "2030-01-01T05:30:00.123456+02:00"
_EXPIRATION_INSTANT: Final = datetime.fromisoformat(_EXPIRATION_OFFSET).astimezone(timezone.utc)
_EXPIRATION_BOUNDARY: Final = _EXPIRATION_NOW + timedelta(days=1)
@pytest.mark.parametrize("request_type", (GenerateKeyRequest, UpdateKeyRequest, RegenerateKeyRequest))
@pytest.mark.parametrize("expires", ("not-a-date", "2030-01-01T05:30:00", datetime(2030, 1, 1)))
def test_key_expiration_rejects_malformed_or_naive_input(
request_type: type[GenerateKeyRequest] | type[UpdateKeyRequest], expires: str | datetime
) -> None:
with pytest.raises(ValidationError, match="expires"):
request_type.model_validate({"key": "sk-expiration", "expires": expires})
def test_key_expiration_response_still_accepts_legacy_naive_datetime() -> None:
naive: Final = datetime(2030, 1, 1)
response: Final = GenerateKeyResponse(key="sk-expiration", expires=naive)
assert response.expires == naive
@pytest.mark.asyncio
@pytest.mark.parametrize(
("payload", "default_duration", "member", "expected", "rejected"),
(
({"expires": _EXPIRATION_OFFSET, "duration": "30d"}, "1d", None, _EXPIRATION_INSTANT, False),
({"expires": None, "duration": "30d"}, "1d", None, None, False),
({}, "1d", None, _EXPIRATION_BOUNDARY, False),
({"duration": "1h"}, None, None, _EXPIRATION_NOW + timedelta(hours=1), False),
({}, None, None, None, False),
({"expires": _EXPIRATION_NOW - timedelta(seconds=1)}, None, None,
_EXPIRATION_NOW - timedelta(seconds=1), False),
({"expires": _EXPIRATION_BOUNDARY}, None, True, _EXPIRATION_BOUNDARY, False),
({"duration": "2h"}, None, True, _EXPIRATION_BOUNDARY, False),
({"expires": _EXPIRATION_BOUNDARY + timedelta(microseconds=1)}, None, True, None, True),
({"expires": None}, None, True, None, True),
({"expires": None}, None, False, None, False),
({"expires": _EXPIRATION_NOW + timedelta(days=2)}, None, False,
_EXPIRATION_NOW + timedelta(days=2), False),
),
ids=("absolute-overrides-duration-and-default", "null-overrides-duration-and-default",
"default-duration", "relative-duration", "no-expiration", "already-expired",
"member-at-limit", "legacy-member-duration", "member-over-limit", "member-cannot-clear",
"service-account-can-clear", "service-account-exempt"),
)
async def test_key_expiration_generation_policy_storage_and_response_agree(
monkeypatch: pytest.MonkeyPatch,
payload: dict[str, object],
default_duration: str | None,
member: bool | None,
expected: datetime | None,
rejected: bool,
) -> None:
insert_data: Final = _wire_key_generation_prisma(monkeypatch)
policy: Final = AsyncMock(return_value={"decision": True})
monkeypatch.setattr(
litellm, "default_key_generate_params",
{"duration": default_duration} if default_duration is not None else None,
)
monkeypatch.setattr(
litellm, "upperbound_key_generate_params", LiteLLM_UpperboundKeyGenerateParams(max_budget=10)
)
monkeypatch.setattr("litellm.proxy.proxy_server.user_custom_key_policy", policy)
team: Final = (
LiteLLM_TeamTableCachedObj(team_id="expiry-team", metadata={"team_member_key_duration": "1d"})
if member is not None else None
)
request: Final = GenerateKeyRequest.model_validate(
{
**payload,
"user_id": "expiry-member" if member else None,
"team_id": team.team_id if team is not None else None,
}
)
caller: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, user_id="expiry-admin")
if rejected:
with pytest.raises(HTTPException, match="team_member_key_duration") as exc:
await _common_key_generation_helper(
data=request,
user_api_key_dict=caller,
litellm_changed_by=None,
team_table=team,
now=_EXPIRATION_NOW,
)
assert exc.value.status_code == 400
insert_data.assert_not_awaited()
policy.assert_not_awaited()
return
response: Final = await _common_key_generation_helper(
data=request,
user_api_key_dict=caller,
litellm_changed_by=None,
team_table=team,
now=_EXPIRATION_NOW,
)
saved: Final = next(
call.kwargs["data"] for call in insert_data.await_args_list if call.kwargs["table_name"] == "key"
)
policy.assert_awaited_once()
policy_request: Final = CustomKeyPolicyRequest.model_validate(policy.await_args.args[0])
assert policy_request.effective_key.expires == saved["expires"] == response.expires == expected
assert policy_request.effective_key.max_budget == saved["max_budget"] == response.max_budget == 10
@pytest.mark.parametrize("request_type", (UpdateKeyRequest, RegenerateKeyRequest))
@pytest.mark.parametrize(
("payload", "expected", "writes_expiration"),
(
({"expires": _EXPIRATION_OFFSET, "duration": "not-a-duration"}, _EXPIRATION_INSTANT, True),
({"expires": None, "duration": "1h"}, None, True),
({}, _EXPIRATION_BOUNDARY, False),
({"duration": None}, None, True),
({"duration": ""}, _EXPIRATION_BOUNDARY, False),
),
ids=("absolute-precedence", "null-precedence", "omitted", "relative-clear", "blank-duration"),
)
@pytest.mark.asyncio
async def test_key_expiration_update_and_regenerate_preserve_intent(
request_type: type[UpdateKeyRequest] | type[RegenerateKeyRequest],
payload: dict[str, object],
expected: datetime | None,
writes_expiration: bool,
) -> None:
existing: Final = LiteLLM_VerificationToken(token="expiry-token", expires=_EXPIRATION_BOUNDARY)
request: Final = request_type.model_validate({"key": "expiry-token", **payload})
prepared: Final = await prepare_key_update_data(data=request, existing_key_row=existing)
effective: Final = _effective_key_after_update(existing_key_row=existing, non_default_values=prepared)
assert ("expires" in prepared) is writes_expiration
assert "duration" not in prepared
assert effective.expires == expected
assert existing.expires == _EXPIRATION_BOUNDARY
@pytest.mark.parametrize("request_type", (GenerateKeyRequest, UpdateKeyRequest, RegenerateKeyRequest))
@pytest.mark.parametrize(
("payload", "rejected"),
(
({"expires": _EXPIRATION_BOUNDARY.astimezone(timezone(timedelta(hours=5, minutes=30))),
"duration": "30d"}, False),
({"expires": _EXPIRATION_BOUNDARY + timedelta(microseconds=1)}, True),
({"expires": None, "duration": "1h"}, True),
({}, False),
({"duration": None}, False),
),
ids=("same-instant-boundary-and-precedence", "over-limit", "explicit-clear", "omitted", "relative-clear"),
)
def test_key_expiration_upperbound_checks_effective_deadline(
monkeypatch: pytest.MonkeyPatch,
request_type: type[GenerateKeyRequest] | type[UpdateKeyRequest],
payload: dict[str, object],
rejected: bool,
) -> None:
monkeypatch.setattr(
litellm, "upperbound_key_generate_params", LiteLLM_UpperboundKeyGenerateParams(duration="1d")
)
request: Final = request_type.model_validate({"key": "sk-expiration", **payload})
fill_defaults: Final = request_type is GenerateKeyRequest
if rejected:
with pytest.raises(HTTPException) as exc:
_enforce_upperbound_key_params(request, fill_defaults=fill_defaults, now=_EXPIRATION_NOW)
assert exc.value.status_code == 400
return
_enforce_upperbound_key_params(request, fill_defaults=fill_defaults, now=_EXPIRATION_NOW)
if "expires" in payload:
assert request.expires == _EXPIRATION_BOUNDARY
elif fill_defaults:
assert request.duration == "1d"
elif "duration" in payload:
assert "duration" in request.model_fields_set
assert request.duration is None
else:
assert "duration" not in request.model_fields_set
@pytest.mark.parametrize("expires", (_EXPIRATION_OFFSET, None))
def test_key_expiration_bulk_requests_do_not_silently_discard_input(expires: str | None) -> None:
with pytest.raises(ValidationError, match="/key/update"):
BulkUpdateKeyRequest.model_validate({"keys": [{"key": "expiry-token", "expires": expires}]})
with pytest.raises(ValidationError, match="expires"):
KeyUpdateFields.model_validate({"expires": expires})

View file

@ -131,6 +131,23 @@ describe("duration", () => {
});
});
describe("absolute expiration", () => {
it.each(["", " "])("omits a cleared expiration from the request (%j)", (expires) => {
const payload = payloadOf(build({ key_alias: "my-key", expires }));
expect(JSON.parse(JSON.stringify(payload))).toStrictEqual(aliasOnly());
});
it.each(["2030-01-01T10:30:00+05:30", null])("preserves an explicit expiration (%j)", (expires) => {
const payload = payloadOf(build({ key_alias: "my-key", expires }));
expect(JSON.parse(JSON.stringify(payload))).toStrictEqual(aliasOnly({ expires }));
});
it("omits an untouched expiration without turning it into an explicit null", () => {
const payload = payloadOf(build({ key_alias: "my-key", expires: undefined }));
expect(JSON.parse(JSON.stringify(payload))).toStrictEqual(aliasOnly());
});
});
describe("key ownership", () => {
it("overwrites user_id with the signed-in user when the key is owned by you", () => {
expect(payloadOf(build({ key_alias: "my-key", user_id: "someone-else" }))).toStrictEqual(

View file

@ -202,6 +202,7 @@ export const buildKeyCreatePayload = (input: KeyCreateInput): KeyPayloadResult =
endpoint: input.keyOwner === "service_account" ? "service_account" : "standard",
payload: {
...withoutKeys(values, dropped),
...(typeof values.expires === "string" && values.expires.trim() === "" && { expires: undefined }),
...(values.organization_id === null && { organization_id: undefined }),
...(values.project_id === null && { project_id: undefined }),
...(input.keyOwner === "you" && { user_id: input.userID }),

View file

@ -8451,6 +8451,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 expiration with a timezone, overriding duration when supplied. Null requests no expiration, subject to lifetime limits. Past timestamps expire the key immediately.
* - 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
@ -8733,6 +8734,7 @@ export interface paths {
* - model_tpm_limit: Optional[dict] - Model-specific TPM limits {"gpt-4": 100000, "claude-v1": 200000}
* - allowed_cache_controls: Optional[list] - List of allowed cache control values
* - duration: Optional[str] - Key validity duration ("30d", "1h", etc.)
* - expires: Optional[datetime] - Absolute expiration with a timezone, overriding duration when supplied. Omit to retain expiration, or use null to clear it when lifetime limits allow. Past timestamps expire the key immediately.
* - permissions: Optional[dict] - Key-specific permissions
* - guardrails: Optional[List[str]] - List of active guardrails for the key
* - blocked: Optional[bool] - Whether the key is blocked
@ -8950,6 +8952,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 expiration with a timezone, overriding duration when supplied. Omit to retain expiration, or use null to clear it when lifetime limits allow. Past timestamps expire the key immediately.
* - 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
@ -9032,6 +9035,7 @@ export interface paths {
* - model_tpm_limit: Optional[dict] - Model-specific TPM limits {"gpt-4": 100000, "claude-v1": 200000}
* - allowed_cache_controls: Optional[list] - List of allowed cache control values
* - duration: Optional[str] - Key validity duration ("30d", "1h", etc.)
* - expires: Optional[datetime] - Absolute expiration with a timezone, overriding duration when supplied. Omit to retain expiration, or use null to clear it when lifetime limits allow. Past timestamps expire the key immediately.
* - permissions: Optional[dict] - Key-specific permissions
* - guardrails: Optional[List[str]] - List of active guardrails for the key
* - blocked: Optional[bool] - Whether the key is blocked
@ -32817,6 +32821,11 @@ export interface components {
end_user_budget_id?: string | null;
/** Enforced Params */
enforced_params?: string[] | null;
/**
* Expires
* @description Absolute expiration with a timezone. When supplied, overrides duration. Null means no expiration, subject to lifetime limits. Past timestamps expire the key immediately.
*/
expires?: string | null;
/** Guardrails */
guardrails?: string[] | null;
/** Key */
@ -42152,6 +42161,11 @@ export interface components {
end_user_budget_id?: string | null;
/** Enforced Params */
enforced_params?: string[] | null;
/**
* Expires
* @description Absolute expiration with a timezone. When supplied, overrides duration. Null means no expiration, subject to lifetime limits. Past timestamps expire the key immediately.
*/
expires?: string | null;
/** Grace Period */
grace_period?: string | null;
/** Guardrails */
@ -48294,6 +48308,11 @@ export interface components {
end_user_budget_id?: string | null;
/** Enforced Params */
enforced_params?: string[] | null;
/**
* Expires
* @description Absolute expiration with a timezone. When supplied, overrides duration. Omit to retain expiration, or use null to clear it when lifetime limits allow. Past timestamps expire the key immediately.
*/
expires?: string | null;
/** Guardrails */
guardrails?: string[] | null;
/** Key */