mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-05 08:07:05 +00:00
fix(openapi): correct budget limits documentation
This commit is contained in:
parent
68a4ca7247
commit
253e5bbbf5
5 changed files with 65 additions and 57 deletions
|
|
@ -388,7 +388,7 @@ async def new_user(
|
|||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
||||
- prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts.
|
||||
- organizations: List[str] - List of organization id's the user is a member of
|
||||
- budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
- budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
Returns:
|
||||
- key: (str) The generated api key for the user
|
||||
- expires: (datetime) Datetime object for when key expires.
|
||||
|
|
@ -1389,7 +1389,7 @@ async def user_update(
|
|||
- key_alias: Optional[str] - [NOT IMPLEMENTED].
|
||||
- object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
||||
- prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts.
|
||||
- budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
- budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
|
||||
"""
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1519,7 +1519,7 @@ async def generate_key_fn(
|
|||
- allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint.
|
||||
- router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings.
|
||||
- access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"].
|
||||
- budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
- budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
|
||||
Examples:
|
||||
|
||||
|
|
@ -2543,7 +2543,7 @@ async def update_key_fn(
|
|||
- allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint.
|
||||
- router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings.
|
||||
- access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"].
|
||||
- budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
- budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
|
||||
Example:
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -955,7 +955,7 @@ async def new_team(
|
|||
- access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"].
|
||||
- enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}.
|
||||
- enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}.
|
||||
- budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
- budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
- default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1627,7 +1627,7 @@ async def update_team(
|
|||
- access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"].
|
||||
- enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}.
|
||||
- enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}.
|
||||
- budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
- budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
- default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models.
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -11,6 +11,43 @@ Related issue: https://github.com/BerriAI/litellm/issues/21305
|
|||
import pytest
|
||||
|
||||
|
||||
class TestBudgetLimitsOpenAPISchema:
|
||||
def test_descriptions_use_budget_limit_entry_field_names(self):
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
from litellm.proxy.management_endpoints.internal_user_endpoints import (
|
||||
router as user_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.key_management_endpoints import (
|
||||
router as key_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.team_endpoints import (
|
||||
router as team_router,
|
||||
)
|
||||
|
||||
schema = get_openapi(
|
||||
title="LiteLLM",
|
||||
version="test",
|
||||
routes=[*key_router.routes, *user_router.routes, *team_router.routes],
|
||||
)
|
||||
|
||||
for path in (
|
||||
"/key/generate",
|
||||
"/key/update",
|
||||
"/user/new",
|
||||
"/user/update",
|
||||
"/team/new",
|
||||
"/team/update",
|
||||
):
|
||||
description = schema["paths"][path]["post"]["description"]
|
||||
budget_limits_line = next(line for line in description.splitlines() if "budget_limits:" in line)
|
||||
|
||||
assert "max_budget and budget_duration" in budget_limits_line
|
||||
assert '{"budget_duration": "24h", "max_budget": 10.0}' in budget_limits_line
|
||||
assert '"budget_limit"' not in budget_limits_line
|
||||
assert '"time_period"' not in budget_limits_line
|
||||
|
||||
|
||||
class TestSpendCalculateOpenAPISchema:
|
||||
"""Test /spend/calculate response schema is valid OpenAPI 3.x."""
|
||||
|
||||
|
|
@ -22,9 +59,7 @@ class TestSpendCalculateOpenAPISchema:
|
|||
if hasattr(route, "path") and route.path == "/spend/calculate":
|
||||
responses = route.responses or {}
|
||||
response_200 = responses.get(200, {})
|
||||
assert (
|
||||
"description" in response_200
|
||||
), "/spend/calculate 200 response must have a 'description' field"
|
||||
assert "description" in response_200, "/spend/calculate 200 response must have a 'description' field"
|
||||
break
|
||||
else:
|
||||
pytest.fail("/spend/calculate route not found in router")
|
||||
|
|
@ -43,9 +78,7 @@ class TestSpendCalculateOpenAPISchema:
|
|||
"top-level property - use 'content' wrapper instead"
|
||||
)
|
||||
# Must have 'content' wrapper
|
||||
assert (
|
||||
"content" in response_200
|
||||
), "/spend/calculate 200 response must have a 'content' field"
|
||||
assert "content" in response_200, "/spend/calculate 200 response must have a 'content' field"
|
||||
content = response_200["content"]
|
||||
assert "application/json" in content
|
||||
assert "schema" in content["application/json"]
|
||||
|
|
@ -81,8 +114,7 @@ class TestCredentialEndpointsOpenAPISchema:
|
|||
by_name_endpoint = by_name_routes[0].endpoint
|
||||
by_model_endpoint = by_model_routes[0].endpoint
|
||||
assert by_name_endpoint is not by_model_endpoint, (
|
||||
"by_name and by_model must be separate handler functions "
|
||||
"to avoid path parameter conflicts in OpenAPI spec"
|
||||
"by_name and by_model must be separate handler functions to avoid path parameter conflicts in OpenAPI spec"
|
||||
)
|
||||
|
||||
def test_by_model_route_does_not_require_credential_name(self):
|
||||
|
|
@ -97,9 +129,7 @@ class TestCredentialEndpointsOpenAPISchema:
|
|||
|
||||
sig = inspect.signature(get_credential_by_model)
|
||||
param_names = list(sig.parameters.keys())
|
||||
assert (
|
||||
"credential_name" not in param_names
|
||||
), "get_credential_by_model must not have a credential_name parameter"
|
||||
assert "credential_name" not in param_names, "get_credential_by_model must not have a credential_name parameter"
|
||||
|
||||
def test_by_name_route_does_not_require_model_id(self):
|
||||
"""
|
||||
|
|
@ -113,9 +143,7 @@ class TestCredentialEndpointsOpenAPISchema:
|
|||
|
||||
sig = inspect.signature(get_credential_by_name)
|
||||
param_names = list(sig.parameters.keys())
|
||||
assert (
|
||||
"model_id" not in param_names
|
||||
), "get_credential_by_name must not have a model_id parameter"
|
||||
assert "model_id" not in param_names, "get_credential_by_name must not have a model_id parameter"
|
||||
|
||||
def test_by_model_has_model_id_path_param(self):
|
||||
"""The by_model handler must accept model_id as a path parameter."""
|
||||
|
|
@ -125,9 +153,7 @@ class TestCredentialEndpointsOpenAPISchema:
|
|||
)
|
||||
|
||||
sig = inspect.signature(get_credential_by_model)
|
||||
assert (
|
||||
"model_id" in sig.parameters
|
||||
), "get_credential_by_model must have a model_id parameter"
|
||||
assert "model_id" in sig.parameters, "get_credential_by_model must have a model_id parameter"
|
||||
|
||||
def test_by_name_has_credential_name_path_param(self):
|
||||
"""The by_name handler must accept credential_name as a path parameter."""
|
||||
|
|
@ -137,9 +163,7 @@ class TestCredentialEndpointsOpenAPISchema:
|
|||
)
|
||||
|
||||
sig = inspect.signature(get_credential_by_name)
|
||||
assert (
|
||||
"credential_name" in sig.parameters
|
||||
), "get_credential_by_name must have a credential_name parameter"
|
||||
assert "credential_name" in sig.parameters, "get_credential_by_name must have a credential_name parameter"
|
||||
|
||||
|
||||
class TestWebSocketStubInjection:
|
||||
|
|
@ -166,26 +190,16 @@ class TestWebSocketStubInjection:
|
|||
_inject_websocket_stubs_into_openapi_schema,
|
||||
)
|
||||
|
||||
schema = {
|
||||
"paths": {
|
||||
"/v1/responses": {
|
||||
"post": {"summary": "responses_api", "operationId": "responses_api"}
|
||||
}
|
||||
}
|
||||
}
|
||||
schema = {"paths": {"/v1/responses": {"post": {"summary": "responses_api", "operationId": "responses_api"}}}}
|
||||
ws_routes = [self._make_fake_ws_route("/v1/responses", name="responses_ws")]
|
||||
|
||||
result = _inject_websocket_stubs_into_openapi_schema(schema, ws_routes)
|
||||
|
||||
assert (
|
||||
"post" in result["paths"]["/v1/responses"]
|
||||
), "POST operation must be preserved when a WebSocket route shares the path"
|
||||
assert (
|
||||
result["paths"]["/v1/responses"]["post"]["operationId"] == "responses_api"
|
||||
assert "post" in result["paths"]["/v1/responses"], (
|
||||
"POST operation must be preserved when a WebSocket route shares the path"
|
||||
)
|
||||
assert (
|
||||
"get" in result["paths"]["/v1/responses"]
|
||||
), "WebSocket stub should also be added under 'get'"
|
||||
assert result["paths"]["/v1/responses"]["post"]["operationId"] == "responses_api"
|
||||
assert "get" in result["paths"]["/v1/responses"], "WebSocket stub should also be added under 'get'"
|
||||
assert result["paths"]["/v1/responses"]["get"]["tags"] == ["WebSocket"]
|
||||
|
||||
def test_websocket_stub_added_when_path_is_new(self):
|
||||
|
|
@ -217,20 +231,14 @@ class TestWebSocketStubInjection:
|
|||
_inject_websocket_stubs_into_openapi_schema,
|
||||
)
|
||||
|
||||
schema = {
|
||||
"paths": {
|
||||
"/health": {
|
||||
"get": {"summary": "health_check", "operationId": "real_get"}
|
||||
}
|
||||
}
|
||||
}
|
||||
schema = {"paths": {"/health": {"get": {"summary": "health_check", "operationId": "real_get"}}}}
|
||||
ws_routes = [self._make_fake_ws_route("/health", name="health_ws")]
|
||||
|
||||
result = _inject_websocket_stubs_into_openapi_schema(schema, ws_routes)
|
||||
|
||||
assert (
|
||||
result["paths"]["/health"]["get"]["operationId"] == "real_get"
|
||||
), "Real GET must take precedence over WebSocket stub"
|
||||
assert result["paths"]["/health"]["get"]["operationId"] == "real_get", (
|
||||
"Real GET must take precedence over WebSocket stub"
|
||||
)
|
||||
|
||||
def test_responses_post_routes_registered_on_router(self):
|
||||
"""
|
||||
|
|
|
|||
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
12
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -6531,7 +6531,7 @@ export interface paths {
|
|||
* - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint.
|
||||
* - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings.
|
||||
* - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"].
|
||||
* - budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
* - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
|
|
@ -6924,7 +6924,7 @@ export interface paths {
|
|||
* - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint.
|
||||
* - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"gpt-4": {"RateLimitErrorRetries": 5}}}. IF null or {} then no router settings.
|
||||
* - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the key. Access groups define which models a key can access. Example - ["access_group_1", "access_group_2"].
|
||||
* - budget_limits: Optional[list] - List of concurrent budget windows for the key. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
* - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the key. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
*
|
||||
* Example:
|
||||
* ```bash
|
||||
|
|
@ -13576,7 +13576,7 @@ export interface paths {
|
|||
* - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"].
|
||||
* - enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}.
|
||||
* - enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}.
|
||||
* - budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
* - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
* - default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models.
|
||||
*
|
||||
* Returns:
|
||||
|
|
@ -13756,7 +13756,7 @@ export interface paths {
|
|||
* - access_group_ids: Optional[List[str]] - List of access group IDs to associate with the team. Access groups define which models the team can access. Example - ["access_group_1", "access_group_2"].
|
||||
* - enforced_file_expires_after: Optional[dict] - Enforced file expiration policy for the team. Keys created under this team will inherit this policy for file uploads. Example - {"anchor": "created_at", "days": 30}.
|
||||
* - enforced_batch_output_expires_after: Optional[dict] - Enforced batch output file expiration policy for the team. Keys created under this team will inherit this policy for batch output files. Example - {"anchor": "created_at", "days": 30}.
|
||||
* - budget_limits: Optional[list] - List of concurrent budget windows for the team. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
* - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the team. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
* - default_team_member_models: Optional[List[str]] - Default models assigned to new team members when they join this team. Must be a subset of the team's models.
|
||||
*
|
||||
* ```
|
||||
|
|
@ -14636,7 +14636,7 @@ export interface paths {
|
|||
* - object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
||||
* - prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts.
|
||||
* - organizations: List[str] - List of organization id's the user is a member of
|
||||
* - budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
* - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
* Returns:
|
||||
* - key: (str) The generated api key for the user
|
||||
* - expires: (datetime) Datetime object for when key expires.
|
||||
|
|
@ -14716,7 +14716,7 @@ export interface paths {
|
|||
* - key_alias: Optional[str] - [NOT IMPLEMENTED].
|
||||
* - object_permission: Optional[LiteLLM_ObjectPermissionBase] - internal user-specific object permission. Example - {"vector_stores": ["vector_store_1", "vector_store_2"]}. IF null or {} then no object permission.
|
||||
* - prompts: Optional[List[str]] - List of allowed prompts for the user. If specified, the user will only be able to use these specific prompts.
|
||||
* - budget_limits: Optional[list] - List of concurrent budget windows for the user. Each window specifies a budget_limit, time_period, and optional budget_duration. Example - [{"budget_limit": 10.0, "time_period": "1d"}, {"budget_limit": 50.0, "time_period": "7d"}].
|
||||
* - budget_limits: Optional[List[BudgetLimitEntry]] - List of concurrent budget windows for the user. Each window specifies max_budget and budget_duration. Example - [{"budget_duration": "24h", "max_budget": 10.0}, {"budget_duration": "30d", "max_budget": 100.0}].
|
||||
*/
|
||||
post: operations["user_update_user_update_post"];
|
||||
delete?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue