mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
refactor(proxy): move bulk user creation to POST /management/v1/users/bulk
Follows the Management API modernization design: plural resource under
/management/v1, {data, meta} response envelope, unknown request fields
rejected with 422, and RFC 9457 problem+json for request-level errors.
Body validation failures under /management/v1 now answer 422 instead of
the 400 query-parameter problem. /user/bulk_new is removed.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
0c9e0c407e
commit
96332f75e8
14 changed files with 462 additions and 234 deletions
|
|
@ -656,7 +656,7 @@ class LiteLLMRoutes(enum.Enum):
|
|||
[
|
||||
# user
|
||||
"/user/new",
|
||||
"/user/bulk_new",
|
||||
"/management/v1/users/bulk",
|
||||
"/user/update",
|
||||
"/user/bulk_update",
|
||||
"/user/delete",
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ _PROXY_ADMIN_VIEW_ONLY_BLOCKED_ROUTES: Final = frozenset(
|
|||
[
|
||||
# user
|
||||
"/user/new",
|
||||
"/user/bulk_new",
|
||||
"/management/v1/users/bulk",
|
||||
"/user/delete",
|
||||
"/user/bulk_update",
|
||||
# team
|
||||
|
|
@ -756,7 +756,7 @@ class RouteChecks:
|
|||
_ADMIN_VIEWER_BLOCKED_WRITE_ROUTES = frozenset(
|
||||
[
|
||||
"/user/new",
|
||||
"/user/bulk_new",
|
||||
"/management/v1/users/bulk",
|
||||
"/user/delete",
|
||||
"/user/bulk_update",
|
||||
"/team/new",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Contract machinery shared by every LiteLLM-defined list route, on any surface."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
from typing import Final
|
||||
from urllib.parse import urlencode
|
||||
|
||||
|
|
@ -7,6 +8,7 @@ from fastapi import Request
|
|||
from fastapi.dependencies.utils import get_flat_params
|
||||
from fastapi.params import ParamTypes
|
||||
from fastapi.responses import JSONResponse
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
from litellm.types.proxy.management_endpoints.management_v1 import (
|
||||
ListLinks,
|
||||
|
|
@ -56,6 +58,31 @@ def escape_like(value: str) -> str:
|
|||
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
|
||||
|
||||
|
||||
class ValidationErrorDetail(TypedDict):
|
||||
"""The two keys of a pydantic/FastAPI validation error a problem document needs."""
|
||||
|
||||
loc: ReadOnly[tuple[int | str, ...]]
|
||||
msg: ReadOnly[str]
|
||||
|
||||
|
||||
def request_validation_problem(errors: Sequence[ValidationErrorDetail]) -> ProblemDetail:
|
||||
"""A body that fails validation (an unknown field included) is 422; a bad query parameter is 400."""
|
||||
detail: Final = "; ".join(f"{'.'.join(str(part) for part in error['loc'][1:])}: {error['msg']}" for error in errors)
|
||||
if any(error["loc"] and error["loc"][0] == "body" for error in errors):
|
||||
return ProblemDetail(
|
||||
type=f"{PROBLEM_TYPE_BASE}invalid-request-body",
|
||||
title="Invalid request body",
|
||||
status=422,
|
||||
detail=detail or "The request body is invalid.",
|
||||
)
|
||||
return ProblemDetail(
|
||||
type=f"{PROBLEM_TYPE_BASE}invalid-query-parameter",
|
||||
title="Invalid query parameter",
|
||||
status=400,
|
||||
detail=detail or "The request query parameters are invalid.",
|
||||
)
|
||||
|
||||
|
||||
def unknown_query_param_problem(unknown: tuple[str, ...], allowed: tuple[str, ...]) -> ProblemDetail:
|
||||
return ProblemDetail(
|
||||
type=f"{PROBLEM_TYPE_BASE}unknown-query-parameter",
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ Internal User Management Endpoints
|
|||
These are members of a Team on LiteLLM
|
||||
|
||||
/user/new
|
||||
/user/bulk_new
|
||||
/user/update
|
||||
/user/bulk_update
|
||||
/user/delete
|
||||
|
|
@ -78,8 +77,6 @@ from litellm.types.proxy.management_endpoints.common_daily_activity import (
|
|||
SpendAnalyticsPaginatedResponse,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.internal_user_endpoints import (
|
||||
BulkNewUserRequest,
|
||||
BulkNewUserResponse,
|
||||
BulkUpdateUserRequest,
|
||||
BulkUpdateUserResponse,
|
||||
UserListResponse,
|
||||
|
|
@ -640,71 +637,6 @@ async def new_user(
|
|||
raise handle_exception_on_proxy(e)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/user/bulk_new",
|
||||
tags=["Internal User management"],
|
||||
dependencies=[Depends(user_api_key_auth)],
|
||||
response_model=BulkNewUserResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def bulk_new_user(
|
||||
data: BulkNewUserRequest,
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), # noqa: B008 # FastAPI dependency injection
|
||||
) -> BulkNewUserResponse:
|
||||
"""
|
||||
Create up to 500 internal users in one request, optionally adding each one to teams.
|
||||
|
||||
Every entry in `users` takes the same fields as `/user/new`, with two differences: `auto_create_key`
|
||||
defaults to `false` (opt in per user to also get a virtual key back) and `send_invite_email` is not
|
||||
supported. Rows are validated together (duplicate ids or emails, unknown teams, roles the caller may not
|
||||
grant), inserted in one statement, and each referenced team is written once for all of its new members.
|
||||
|
||||
Rows fail independently: a bad row is reported in `results` with `success: false` and an `error`, and the
|
||||
other rows still get created. A user that was created but could not be added to one of its teams is
|
||||
reported with `success: true`, `teams` listing where they did land, and `error` naming the failed team.
|
||||
The whole request is rejected with 403 only if creating the valid rows would exceed the license seat limit.
|
||||
|
||||
Usage Example
|
||||
|
||||
```shell
|
||||
curl -X POST "http://localhost:4000/user/bulk_new" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer sk-1234" \\
|
||||
-d '{
|
||||
"users": [
|
||||
{"user_email": "a@example.com", "user_role": "internal_user", "teams": ["team-1"]},
|
||||
{"user_email": "b@example.com", "user_role": "internal_user", "auto_create_key": true}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Returns `results` (one entry per input row, in order, with `user_id`, `user_email`, `success`, `teams`,
|
||||
`key`, `error`), `total_requested`, `successful_creations` and `failed_creations`.
|
||||
"""
|
||||
from litellm.proxy.management_helpers.bulk_user_creation import bulk_create_users
|
||||
from litellm.proxy.proxy_server import (
|
||||
_license_check, # pyright: ignore[reportPrivateUsage] # same proxy license singleton /user/new reads
|
||||
litellm_proxy_admin_name,
|
||||
prisma_client,
|
||||
user_api_key_cache,
|
||||
)
|
||||
|
||||
if prisma_client is None:
|
||||
raise HTTPException(status_code=400, detail=CommonProxyErrors.db_not_connected_error.value)
|
||||
try:
|
||||
return await bulk_create_users(
|
||||
users=data.users,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
prisma_client=prisma_client,
|
||||
license_check=_license_check,
|
||||
litellm_proxy_admin_name=litellm_proxy_admin_name,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
)
|
||||
except Exception as e: # noqa: BLE001 # normalize every failure to the proxy exception contract
|
||||
verbose_proxy_logger.exception("/user/bulk_new: Exception occured")
|
||||
raise handle_exception_on_proxy(e)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/user/available_roles",
|
||||
tags=["Internal User management"],
|
||||
|
|
|
|||
|
|
@ -10,9 +10,13 @@ from litellm.proxy.management_endpoints.management_v1.budgets import (
|
|||
from litellm.proxy.management_endpoints.management_v1.spend_logs import (
|
||||
router as spend_logs_router,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.management_v1.users import (
|
||||
router as users_router,
|
||||
)
|
||||
|
||||
router: Final = APIRouter()
|
||||
router.include_router(budgets_router)
|
||||
router.include_router(spend_logs_router)
|
||||
router.include_router(users_router)
|
||||
|
||||
__all__ = ["router"]
|
||||
|
|
|
|||
105
litellm/proxy/management_endpoints/management_v1/users.py
Normal file
105
litellm/proxy/management_endpoints/management_v1/users.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
"""`POST /management/v1/users/bulk`."""
|
||||
|
||||
from typing import Annotated, Final
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
|
||||
from litellm._logging import verbose_proxy_logger
|
||||
from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
from litellm.proxy.list_api.common import PROBLEM_TYPE_BASE, ManagementProblem
|
||||
from litellm.proxy.management_endpoints.management_v1.common import MANAGEMENT_V1_PREFIX
|
||||
from litellm.proxy.management_helpers.bulk_user_creation import bulk_create_users
|
||||
from litellm.proxy.management_helpers.utils import (
|
||||
management_endpoint_wrapper, # pyright: ignore[reportUnknownVariableType] # legacy untyped decorator
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.internal_user_endpoints import (
|
||||
BulkNewUserRequest,
|
||||
BulkNewUserResponse,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.management_v1 import ProblemDetail
|
||||
|
||||
router: Final = APIRouter(prefix=MANAGEMENT_V1_PREFIX)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/users/bulk",
|
||||
tags=["Internal User management"], # mutable-ok: fastapi types tags as list[str | Enum]
|
||||
dependencies=(Depends(user_api_key_auth),),
|
||||
response_model=BulkNewUserResponse,
|
||||
)
|
||||
@management_endpoint_wrapper
|
||||
async def bulk_create_users_route(
|
||||
data: BulkNewUserRequest,
|
||||
user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)],
|
||||
) -> BulkNewUserResponse:
|
||||
"""
|
||||
Create up to 500 internal users in one request, optionally adding each one to teams.
|
||||
|
||||
Every entry in `users` takes the same fields as `/user/new`, with two differences: `auto_create_key`
|
||||
defaults to `false` (opt in per user to also get a virtual key back) and `send_invite_email` is not
|
||||
supported. Unknown fields are rejected with 422. Rows are validated together (duplicate ids or emails,
|
||||
unknown teams, roles the caller may not grant), inserted in one statement, and each referenced team is
|
||||
written once for all of its new members.
|
||||
|
||||
Rows fail independently: a bad row is reported in `data` with `success: false` and an `error`, and the
|
||||
other rows still get created. A user that was created but could not be added to one of its teams is
|
||||
reported with `success: true`, `teams` listing where they did land, and `error` naming the failed team.
|
||||
The whole request is refused with a 403 problem document only if creating the valid rows would exceed
|
||||
the license seat limit.
|
||||
|
||||
Example curl:
|
||||
```
|
||||
curl -X POST "http://localhost:4000/management/v1/users/bulk" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-H "Authorization: Bearer sk-1234" \\
|
||||
-d '{
|
||||
"users": [
|
||||
{"user_email": "a@example.com", "user_role": "internal_user", "teams": ["team-1"]},
|
||||
{"user_email": "b@example.com", "user_role": "internal_user", "auto_create_key": true}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Returns `data` (one entry per input row, in order, with `user_id`, `user_email`, `success`, `teams`,
|
||||
`key`, `error`) and `meta` with `total_requested`, `created` and `failed`.
|
||||
"""
|
||||
try:
|
||||
from litellm.proxy.proxy_server import (
|
||||
_license_check, # pyright: ignore[reportPrivateUsage] # same proxy license singleton /user/new reads
|
||||
litellm_proxy_admin_name,
|
||||
prisma_client,
|
||||
user_api_key_cache,
|
||||
)
|
||||
|
||||
if prisma_client is None:
|
||||
raise ManagementProblem(
|
||||
ProblemDetail(
|
||||
type=f"{PROBLEM_TYPE_BASE}database-not-connected",
|
||||
title="Database not connected",
|
||||
status=503,
|
||||
detail=CommonProxyErrors.db_not_connected_error.value,
|
||||
)
|
||||
)
|
||||
|
||||
return await bulk_create_users(
|
||||
users=data.users,
|
||||
user_api_key_dict=user_api_key_dict,
|
||||
prisma_client=prisma_client,
|
||||
license_check=_license_check,
|
||||
litellm_proxy_admin_name=litellm_proxy_admin_name,
|
||||
user_api_key_cache=user_api_key_cache,
|
||||
)
|
||||
|
||||
except ManagementProblem:
|
||||
raise
|
||||
except Exception: # noqa: BLE001 # a driver error answers as a problem document, not the OpenAI error shape
|
||||
verbose_proxy_logger.exception("/management/v1/users/bulk: Exception occurred")
|
||||
raise ManagementProblem(
|
||||
ProblemDetail(
|
||||
type=f"{PROBLEM_TYPE_BASE}internal-server-error",
|
||||
title="Internal server error",
|
||||
status=500,
|
||||
detail="Failed to create users.",
|
||||
)
|
||||
)
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
"""Batched internal user creation behind `/user/bulk_new`.
|
||||
"""Batched internal user creation behind `POST /management/v1/users/bulk`.
|
||||
|
||||
The batch is validated with set queries, user rows land in one `create_many`, and every
|
||||
referenced team is written once under its advisory lock instead of once per user.
|
||||
|
|
@ -33,6 +33,7 @@ from litellm.proxy.auth.litellm_license import LicenseCheck
|
|||
from litellm.proxy.common_utils.timezone_utils import get_budget_reset_time
|
||||
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
|
||||
from litellm.proxy.hooks.user_management_event_hooks import UserManagementEventHooks
|
||||
from litellm.proxy.list_api.common import PROBLEM_TYPE_BASE, ManagementProblem
|
||||
from litellm.proxy.management_endpoints.common_utils import (
|
||||
_is_user_org_admin_for_team, # pyright: ignore[reportPrivateUsage] # same team-admin check /user/new uses
|
||||
_is_user_team_admin, # pyright: ignore[reportPrivateUsage] # same team-admin check /user/new uses
|
||||
|
|
@ -61,9 +62,11 @@ from litellm.repositories.team_repository import TeamRepository
|
|||
from litellm.repositories.user_repository import UserRepository
|
||||
from litellm.types.proxy.management_endpoints.internal_user_endpoints import (
|
||||
BulkNewUserItem,
|
||||
BulkNewUserMeta,
|
||||
BulkNewUserResponse,
|
||||
UserCreateResult,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.management_v1 import ProblemDetail
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from prisma import Prisma
|
||||
|
|
@ -776,7 +779,8 @@ async def bulk_create_users(
|
|||
) -> BulkNewUserResponse:
|
||||
"""Create every valid row in `users`; rows that fail validation or a write are reported, not raised.
|
||||
|
||||
Raises `HTTPException(403)` only when the whole batch would push the deployment over its license seat limit.
|
||||
Raises a 403 `ManagementProblem` only when the whole batch would push the deployment over its license seat
|
||||
limit.
|
||||
"""
|
||||
pending, request_failures = _partition_rows(users, user_api_key_dict)
|
||||
existing_ids, existing_emails = await _existing_user_conflicts(prisma_client, pending)
|
||||
|
|
@ -791,9 +795,13 @@ async def bulk_create_users(
|
|||
|
||||
billable_users: Final = await UserRepository(prisma_client).count_billable_users()
|
||||
if creatable and license_check.is_over_limit(total_users=billable_users + len(creatable)):
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail="License is over limit. Please contact support@berri.ai to upgrade your license.",
|
||||
raise ManagementProblem(
|
||||
ProblemDetail(
|
||||
type=f"{PROBLEM_TYPE_BASE}license-limit-exceeded",
|
||||
title="License limit exceeded",
|
||||
status=403,
|
||||
detail="License is over limit. Please contact support@berri.ai to upgrade your license.",
|
||||
)
|
||||
)
|
||||
|
||||
prepared_outcomes: Final = tuple([await _prepare_user(user, prisma_client) for user in creatable])
|
||||
|
|
@ -858,8 +866,6 @@ async def bulk_create_users(
|
|||
)
|
||||
successes: Final = sum(1 for result in results if result.success)
|
||||
return BulkNewUserResponse(
|
||||
results=results,
|
||||
total_requested=len(users),
|
||||
successful_creations=successes,
|
||||
failed_creations=len(users) - successes,
|
||||
data=results,
|
||||
meta=BulkNewUserMeta(total_requested=len(users), created=successes, failed=len(users) - successes),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -473,9 +473,10 @@ from litellm.proxy.hooks.prompt_injection_detection import (
|
|||
from litellm.proxy.hooks.proxy_track_cost_callback import _ProxyDBLogger, run_spend_event
|
||||
from litellm.proxy.image_endpoints.endpoints import router as image_router
|
||||
from litellm.proxy.list_api.common import (
|
||||
PROBLEM_TYPE_BASE,
|
||||
ManagementProblem,
|
||||
ValidationErrorDetail,
|
||||
problem_response,
|
||||
request_validation_problem,
|
||||
)
|
||||
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
|
||||
from litellm.proxy.logging_endpoints.callback_logs_endpoints import (
|
||||
|
|
@ -598,7 +599,6 @@ from litellm.proxy.spend_tracking.spend_event_producer import (
|
|||
SpendEventProducer,
|
||||
build_spend_event_producer,
|
||||
)
|
||||
from litellm.types.proxy.management_endpoints.management_v1 import ProblemDetail
|
||||
|
||||
try:
|
||||
from litellm.proxy.enterprise_billing.billing_metrics import (
|
||||
|
|
@ -1784,27 +1784,13 @@ class _ExceptionRow(TypedDict, total=False):
|
|||
exception_counts: Mapping[str, int]
|
||||
|
||||
|
||||
class _ValidationErrorDetail(TypedDict):
|
||||
loc: tuple[int | str, ...]
|
||||
msg: str
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def otel_request_validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
if request.url.path.startswith(MANAGEMENT_V1_PREFIX):
|
||||
_close_dangling_otel_server_span(request, 400, exc=exc)
|
||||
validation_errors: Final[Sequence[_ValidationErrorDetail]] = exc.errors()
|
||||
return problem_response(
|
||||
ProblemDetail(
|
||||
type=f"{PROBLEM_TYPE_BASE}invalid-query-parameter",
|
||||
title="Invalid query parameter",
|
||||
status=400,
|
||||
detail="; ".join(
|
||||
f"{'.'.join(str(part) for part in error['loc'][1:])}: {error['msg']}" for error in validation_errors
|
||||
)
|
||||
or "The request query parameters are invalid.",
|
||||
)
|
||||
)
|
||||
validation_errors: Final[Sequence[ValidationErrorDetail]] = exc.errors()
|
||||
problem: Final = request_validation_problem(validation_errors)
|
||||
_close_dangling_otel_server_span(request, problem.status, exc=exc)
|
||||
return problem_response(problem)
|
||||
_close_dangling_otel_server_span(request, 422, exc=exc)
|
||||
return JSONResponse(
|
||||
status_code=422,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
from collections.abc import Mapping
|
||||
from typing import Any, Final, Literal
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
from typing_extensions import ReadOnly, TypedDict
|
||||
|
||||
from litellm.proxy._types import (
|
||||
|
|
@ -89,7 +89,10 @@ class BulkUpdateUserResponse(BaseModel):
|
|||
|
||||
|
||||
class BulkNewUserItem(NewUserRequest):
|
||||
"""One row of `/user/bulk_new`: the `/user/new` body, with keys opt-in and invite emails unsupported."""
|
||||
"""One row of `POST /management/v1/users/bulk`: the `/user/new` body, with keys opt-in and invite emails
|
||||
unsupported. Unknown fields are rejected, as on every `/management/v1` request body."""
|
||||
|
||||
model_config = ConfigDict(extra="forbid", protected_namespaces=())
|
||||
|
||||
auto_create_key: bool = False
|
||||
|
||||
|
|
@ -97,16 +100,19 @@ class BulkNewUserItem(NewUserRequest):
|
|||
@classmethod
|
||||
def reject_invite_email(cls, value: bool | None) -> bool | None:
|
||||
if value:
|
||||
raise ValueError("send_invite_email is not supported on /user/bulk_new; invite users separately")
|
||||
raise ValueError("send_invite_email is not supported on /management/v1/users/bulk; invite users separately")
|
||||
return value
|
||||
|
||||
|
||||
class BulkNewUserRequest(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
users: tuple[BulkNewUserItem, ...] = Field(min_length=1, max_length=MAX_BULK_NEW_USERS)
|
||||
|
||||
|
||||
class UserCreateResult(BaseModel):
|
||||
"""Outcome for one row of `/user/bulk_new`. `teams` lists the teams the user was actually added to."""
|
||||
"""Outcome for one row of `POST /management/v1/users/bulk`. `teams` lists the teams the user was actually
|
||||
added to."""
|
||||
|
||||
user_id: str | None = None
|
||||
user_email: str | None = None
|
||||
|
|
@ -116,8 +122,14 @@ class UserCreateResult(BaseModel):
|
|||
error: str | None = None
|
||||
|
||||
|
||||
class BulkNewUserResponse(BaseModel):
|
||||
results: tuple[UserCreateResult, ...]
|
||||
class BulkNewUserMeta(BaseModel):
|
||||
total_requested: int
|
||||
successful_creations: int
|
||||
failed_creations: int
|
||||
created: int
|
||||
failed: int
|
||||
|
||||
|
||||
class BulkNewUserResponse(BaseModel):
|
||||
"""`data` holds one result per input row, in input order."""
|
||||
|
||||
data: tuple[UserCreateResult, ...]
|
||||
meta: BulkNewUserMeta
|
||||
|
|
|
|||
|
|
@ -84,7 +84,6 @@ POST /team/{team_id}/member/{user_id}/reset_spend
|
|||
POST /team/key/bulk_update
|
||||
POST /team/permissions_bulk_update
|
||||
POST /team/{team_id}/disable_logging
|
||||
POST /user/bulk_new
|
||||
POST /user/bulk_update
|
||||
|
||||
# Alternate method or path for functionality the provider already manages elsewhere
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
"""The HTTP contract of `POST /management/v1/users/bulk`: envelope, problem documents and strict bodies.
|
||||
|
||||
The batching behaviour itself is covered next to the helper, in
|
||||
`tests/test_litellm/proxy/management_helpers/test_bulk_user_creation.py`, whose in-memory Prisma this reuses.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.exceptions import RequestValidationError
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from litellm.proxy._types import LitellmUserRoles, Member
|
||||
from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth
|
||||
from litellm.proxy.list_api.common import ManagementProblem, problem_response, request_validation_problem
|
||||
from litellm.proxy.management_endpoints.management_v1 import router
|
||||
from litellm.proxy.management_endpoints.management_v1.common import MANAGEMENT_V1_PREFIX
|
||||
from tests.test_litellm.proxy.management_helpers.test_bulk_user_creation import _FakePrisma, _License, _team
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
|
||||
@app.exception_handler(ManagementProblem)
|
||||
async def management_problem_exception_handler(request: Request, exc: ManagementProblem):
|
||||
return problem_response(exc.problem)
|
||||
|
||||
|
||||
@app.exception_handler(RequestValidationError)
|
||||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||||
return problem_response(request_validation_problem(exc.errors()))
|
||||
|
||||
|
||||
app.include_router(router)
|
||||
client = TestClient(app)
|
||||
|
||||
USERS_BULK_PATH = f"{MANAGEMENT_V1_PREFIX}/users/bulk"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def as_proxy_admin():
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
yield
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def prisma(monkeypatch):
|
||||
fake = _FakePrisma(teams=[_team("t1", [Member(user_id="existing", role="admin")])])
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", fake)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server._license_check", _License())
|
||||
return fake
|
||||
|
||||
|
||||
def _post(body: object):
|
||||
return client.post(USERS_BULK_PATH, json=body, headers={"Authorization": "Bearer k"})
|
||||
|
||||
|
||||
def test_returns_one_result_per_row_in_order_inside_the_data_meta_envelope(prisma, as_proxy_admin):
|
||||
response = _post(
|
||||
{
|
||||
"users": [
|
||||
{"user_id": "u1", "user_email": "a@example.com", "teams": ["t1"]},
|
||||
{"user_id": "u2", "teams": ["missing-team"]},
|
||||
{"user_id": "u3"},
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert set(body) == {"data", "meta"}
|
||||
assert body["meta"] == {"total_requested": 3, "created": 2, "failed": 1}
|
||||
assert [row["user_id"] for row in body["data"]] == ["u1", "u2", "u3"]
|
||||
assert [row["success"] for row in body["data"]] == [True, False, True]
|
||||
assert body["data"][0]["teams"] == ["t1"]
|
||||
assert "missing-team" in body["data"][1]["error"]
|
||||
assert [m.user_id for m in prisma.db.litellm_teamtable.rows["t1"].members_with_roles] == ["existing", "u1"]
|
||||
|
||||
|
||||
def test_an_unknown_field_anywhere_in_the_body_is_a_422_problem(prisma, as_proxy_admin):
|
||||
for body in (
|
||||
{"users": [{"user_email": "a@example.com", "user_emial": "typo"}]},
|
||||
{"users": [{"user_email": "a@example.com"}], "dry_run": True},
|
||||
):
|
||||
response = _post(body)
|
||||
|
||||
assert response.status_code == 422, body
|
||||
assert response.headers["content-type"] == "application/problem+json"
|
||||
assert response.json()["type"] == "urn:litellm:error:invalid-request-body"
|
||||
assert "Extra inputs are not permitted" in response.json()["detail"]
|
||||
assert prisma.db.litellm_usertable.rows == {}
|
||||
|
||||
|
||||
def test_empty_and_oversized_batches_are_422_problems(prisma, as_proxy_admin):
|
||||
for users in ([], [{"user_email": f"{i}@example.com"} for i in range(501)]):
|
||||
response = _post({"users": users})
|
||||
|
||||
assert response.status_code == 422, len(users)
|
||||
assert response.json()["type"] == "urn:litellm:error:invalid-request-body"
|
||||
assert prisma.db.litellm_usertable.rows == {}
|
||||
|
||||
|
||||
def test_license_limit_is_a_403_problem_and_creates_nothing(prisma, as_proxy_admin, monkeypatch):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server._license_check", _License(max_users=1))
|
||||
|
||||
response = _post({"users": [{"user_id": "u1"}, {"user_id": "u2"}]})
|
||||
|
||||
assert response.status_code == 403
|
||||
assert response.headers["content-type"] == "application/problem+json"
|
||||
assert response.json()["type"] == "urn:litellm:error:license-limit-exceeded"
|
||||
assert prisma.db.litellm_usertable.rows == {}
|
||||
|
||||
|
||||
def test_no_database_is_a_503_problem(as_proxy_admin, monkeypatch):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
|
||||
|
||||
response = _post({"users": [{"user_id": "u1"}]})
|
||||
|
||||
assert response.status_code == 503
|
||||
assert response.headers["content-type"] == "application/problem+json"
|
||||
assert response.json()["type"] == "urn:litellm:error:database-not-connected"
|
||||
|
|
@ -4,12 +4,12 @@ from typing import Final
|
|||
|
||||
import httpx
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
from prisma.errors import UniqueViolationError
|
||||
from pydantic import BaseModel, ConfigDict, ValidationError
|
||||
|
||||
from litellm.caching.caching import DualCache
|
||||
from litellm.proxy._types import LiteLLM_TeamTable, LitellmUserRoles, Member, UserAPIKeyAuth
|
||||
from litellm.proxy.list_api.common import ManagementProblem
|
||||
from litellm.proxy.management_helpers.bulk_user_creation import bulk_create_users
|
||||
from litellm.types.proxy.management_endpoints.internal_user_endpoints import (
|
||||
BulkNewUserItem,
|
||||
|
|
@ -203,10 +203,10 @@ async def test_creates_users_and_team_membership_in_every_store():
|
|||
],
|
||||
)
|
||||
|
||||
assert (response.total_requested, response.successful_creations, response.failed_creations) == (3, 3, 0)
|
||||
assert [r.user_id for r in response.results] == ["u1", "u2", "u3"]
|
||||
assert all(r.success and r.key is None and r.error is None for r in response.results)
|
||||
assert [r.teams for r in response.results] == [("t1", "t2"), ("t1",), ()]
|
||||
assert (response.meta.total_requested, response.meta.created, response.meta.failed) == (3, 3, 0)
|
||||
assert [r.user_id for r in response.data] == ["u1", "u2", "u3"]
|
||||
assert all(r.success and r.key is None and r.error is None for r in response.data)
|
||||
assert [r.teams for r in response.data] == [("t1", "t2"), ("t1",), ()]
|
||||
|
||||
users = prisma.db.litellm_usertable.rows
|
||||
assert users["u1"].teams == ["t1", "t2"] and users["u1"].max_budget == 50
|
||||
|
|
@ -225,9 +225,9 @@ async def test_user_id_already_on_the_roster_keeps_the_team_and_is_not_added_twi
|
|||
prisma = _FakePrisma(teams=[_team("t1", [Member(user_id="u1", role="user")])])
|
||||
response = await _run(prisma, [{"user_id": "u1", "teams": ["t1"]}, {"user_id": "u2", "teams": ["t1"]}])
|
||||
|
||||
assert [r.success for r in response.results] == [True, True]
|
||||
assert [r.teams for r in response.results] == [("t1",), ("t1",)]
|
||||
assert [r.error for r in response.results] == [None, None]
|
||||
assert [r.success for r in response.data] == [True, True]
|
||||
assert [r.teams for r in response.data] == [("t1",), ("t1",)]
|
||||
assert [r.error for r in response.data] == [None, None]
|
||||
assert prisma.db.litellm_usertable.rows["u1"].teams == ["t1"]
|
||||
assert [m.user_id for m in prisma.db.litellm_teamtable.rows["t1"].members_with_roles] == ["u1", "u2"]
|
||||
|
||||
|
|
@ -267,9 +267,9 @@ async def test_bad_rows_fail_alone_and_good_rows_still_land():
|
|||
],
|
||||
)
|
||||
|
||||
assert [r.success for r in response.results] == [True, False, False, False, False, False, False, False, True]
|
||||
assert (response.successful_creations, response.failed_creations) == (2, 7)
|
||||
errors = [r.error for r in response.results]
|
||||
assert [r.success for r in response.data] == [True, False, False, False, False, False, False, False, True]
|
||||
assert (response.meta.created, response.meta.failed) == (2, 7)
|
||||
errors = [r.error for r in response.data]
|
||||
assert "Duplicate user_email" in errors[1]
|
||||
assert "Duplicate user_id" in errors[2]
|
||||
assert "already exists" in errors[3] and "already exists" in errors[4]
|
||||
|
|
@ -289,8 +289,8 @@ async def test_insert_failure_falls_back_to_per_row_and_reports_only_that_row():
|
|||
[{"user_id": "u1", "teams": ["t1"]}, {"user_id": "u2", "teams": ["t1"]}, {"user_id": "u3"}],
|
||||
)
|
||||
|
||||
assert [r.success for r in response.results] == [True, False, True]
|
||||
assert "insert failed for u2" in (response.results[1].error or "")
|
||||
assert [r.success for r in response.data] == [True, False, True]
|
||||
assert "insert failed for u2" in (response.data[1].error or "")
|
||||
assert set(prisma.db.litellm_usertable.rows) == {"u1", "u3"}
|
||||
assert [m.user_id for m in prisma.db.litellm_teamtable.rows["t1"].members_with_roles] == ["u1"]
|
||||
|
||||
|
|
@ -300,8 +300,8 @@ async def test_insert_that_committed_but_lost_its_response_still_counts_as_creat
|
|||
prisma = _FakePrisma(teams=[_team("t1")], commit_then_drop=True)
|
||||
response = await _run(prisma, [{"user_id": "u1", "teams": ["t1"]}, {"user_id": "u2"}])
|
||||
|
||||
assert [r.success for r in response.results] == [True, True]
|
||||
assert [r.error for r in response.results] == [None, None]
|
||||
assert [r.success for r in response.data] == [True, True]
|
||||
assert [r.error for r in response.data] == [None, None]
|
||||
assert set(prisma.db.litellm_usertable.rows) == {"u1", "u2"}
|
||||
assert [m.user_id for m in prisma.db.litellm_teamtable.rows["t1"].members_with_roles] == ["u1"]
|
||||
|
||||
|
|
@ -311,8 +311,8 @@ async def test_user_id_taken_by_a_concurrent_request_is_not_claimed_by_this_batc
|
|||
prisma = _FakePrisma(teams=[_team("t1")], raced_ids=frozenset({"u1"}))
|
||||
response = await _run(prisma, [{"user_id": "u1", "teams": ["t1"]}, {"user_id": "u2", "teams": ["t1"]}])
|
||||
|
||||
assert [r.success for r in response.results] == [False, True]
|
||||
assert "User id=u1 already exists" in (response.results[0].error or "")
|
||||
assert [r.success for r in response.data] == [False, True]
|
||||
assert "User id=u1 already exists" in (response.data[0].error or "")
|
||||
assert prisma.db.litellm_usertable.rows["u1"].user_email == "u1@other-request.example"
|
||||
assert [m.user_id for m in prisma.db.litellm_teamtable.rows["t1"].members_with_roles] == ["u2"]
|
||||
|
||||
|
|
@ -327,12 +327,12 @@ async def test_team_write_failure_keeps_user_and_reports_it_on_the_row():
|
|||
prisma.db.litellm_teamtable.update = explode
|
||||
response = await _run(prisma, [{"user_id": "u1", "teams": ["t1", "t2"]}])
|
||||
|
||||
result = response.results[0]
|
||||
result = response.data[0]
|
||||
assert result.success is True
|
||||
assert result.teams == ()
|
||||
assert "t1" in (result.error or "") and "roster write failed" in (result.error or "")
|
||||
assert prisma.db.litellm_usertable.rows["u1"].teams == []
|
||||
assert (response.successful_creations, response.failed_creations) == (1, 0)
|
||||
assert (response.meta.created, response.meta.failed) == (1, 0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -364,7 +364,7 @@ async def test_keys_are_opt_in_per_row():
|
|||
generate_key=generate_key,
|
||||
)
|
||||
|
||||
assert [r.key for r in response.results] == [None, "sk-u2", None]
|
||||
assert [r.key for r in response.data] == [None, "sk-u2", None]
|
||||
assert len(calls) == 1
|
||||
assert calls[0]["user_id"] == "u2" and calls[0]["table_name"] == "key"
|
||||
assert calls[0]["models"] == ("gpt-4o",) and calls[0]["key_alias"] == "u2-key"
|
||||
|
|
@ -385,8 +385,8 @@ async def test_non_admin_cannot_create_admin_users_but_other_rows_proceed():
|
|||
caller=INTERNAL,
|
||||
)
|
||||
|
||||
assert [r.success for r in response.results] == [False, True]
|
||||
assert "Only proxy admins" in (response.results[0].error or "")
|
||||
assert [r.success for r in response.data] == [False, True]
|
||||
assert "Only proxy admins" in (response.data[0].error or "")
|
||||
assert set(prisma.db.litellm_usertable.rows) == {"u2"}
|
||||
|
||||
|
||||
|
|
@ -396,19 +396,19 @@ async def test_license_is_checked_once_against_the_whole_batch():
|
|||
prisma.db.litellm_usertable.rows["existing"] = _UserRow(user_id="existing")
|
||||
license = _License(max_users=3)
|
||||
|
||||
with pytest.raises(HTTPException) as exc:
|
||||
with pytest.raises(ManagementProblem) as exc:
|
||||
await _run(prisma, [{"user_id": f"u{i}"} for i in range(3)], license=license)
|
||||
|
||||
assert exc.value.status_code == 403
|
||||
assert (exc.value.problem.status, exc.value.problem.type) == (403, "urn:litellm:error:license-limit-exceeded")
|
||||
assert license.seen == [4]
|
||||
assert set(prisma.db.litellm_usertable.rows) == {"existing"}
|
||||
|
||||
ok = await _run(prisma, [{"user_id": f"u{i}"} for i in range(2)], license=license)
|
||||
assert ok.successful_creations == 2
|
||||
assert ok.meta.created == 2
|
||||
|
||||
resend = await _run(prisma, [{"user_id": f"u{i}"} for i in range(2)], license=license)
|
||||
assert [r.success for r in resend.results] == [False, False]
|
||||
assert all("already exists" in (r.error or "") for r in resend.results)
|
||||
assert [r.success for r in resend.data] == [False, False]
|
||||
assert all("already exists" in (r.error or "") for r in resend.data)
|
||||
assert license.seen == [4, 3]
|
||||
assert set(prisma.db.litellm_usertable.rows) == {"existing", "u0", "u1"}
|
||||
|
||||
|
|
@ -422,3 +422,10 @@ def test_request_rejects_empty_oversized_and_invite_rows():
|
|||
BulkNewUserItem(user_email="a@example.com", send_invite_email=True)
|
||||
assert len(BulkNewUserRequest(users=[{"user_email": f"{i}@example.com"} for i in range(500)]).users) == 500
|
||||
assert BulkNewUserItem(user_email="a@example.com").auto_create_key is False
|
||||
|
||||
|
||||
def test_request_rejects_unknown_fields_at_both_levels():
|
||||
with pytest.raises(ValidationError, match="extra_forbidden"):
|
||||
BulkNewUserRequest(users=[{"user_email": "a@example.com", "user_emial": "typo"}])
|
||||
with pytest.raises(ValidationError, match="extra_forbidden"):
|
||||
BulkNewUserRequest(users=[{"user_email": "a@example.com"}], dry_run=True)
|
||||
|
|
|
|||
|
|
@ -227,7 +227,9 @@ async def test_otel_request_validation_exception_handler_empty_errors_invalid_pa
|
|||
async def test_otel_request_validation_exception_handler_returns_a_problem_on_the_control_plane():
|
||||
"""`/management/v1` answers validation errors as RFC 9457, so a caller there gets a
|
||||
400 problem document rather than the proxy-wide 422 `{"detail": [...]}` shape."""
|
||||
errors = [{"loc": ["query", "page_size"], "msg": "Input should be less than or equal to 100", "type": "less_than_equal"}]
|
||||
errors = [
|
||||
{"loc": ["query", "page_size"], "msg": "Input should be less than or equal to 100", "type": "less_than_equal"}
|
||||
]
|
||||
exc = RequestValidationError(errors)
|
||||
request = _make_request(path="/management/v1/spend_logs/end_users")
|
||||
|
||||
|
|
@ -242,6 +244,26 @@ async def test_otel_request_validation_exception_handler_returns_a_problem_on_th
|
|||
assert "detail" in body and not isinstance(body["detail"], list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_otel_request_validation_exception_handler_answers_a_bad_control_plane_body_with_422():
|
||||
"""A request body that fails validation, an unknown field included, is 422 on
|
||||
`/management/v1`; only query parameter problems are 400."""
|
||||
errors = [
|
||||
{"loc": ["body", "users", 0, "user_emial"], "msg": "Extra inputs are not permitted", "type": "extra_forbidden"}
|
||||
]
|
||||
exc = RequestValidationError(errors)
|
||||
request = _make_request(path="/management/v1/users/bulk")
|
||||
|
||||
response = await otel_request_validation_exception_handler(request=request, exc=exc)
|
||||
body = json.loads(response.body)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert response.media_type == "application/problem+json"
|
||||
assert body["type"] == "urn:litellm:error:invalid-request-body"
|
||||
assert body["status"] == 422
|
||||
assert "users.0.user_emial: Extra inputs are not permitted" in body["detail"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_otel_request_validation_exception_handler_leaves_other_routes_on_422():
|
||||
"""The problem+json branch is scoped by path prefix. A route that merely contains
|
||||
|
|
@ -249,9 +271,7 @@ async def test_otel_request_validation_exception_handler_leaves_other_routes_on_
|
|||
exc = RequestValidationError([])
|
||||
|
||||
for path in ("/management", "/v1/management/foo", "/customer/list"):
|
||||
response = await otel_request_validation_exception_handler(
|
||||
request=_make_request(path=path), exc=exc
|
||||
)
|
||||
response = await otel_request_validation_exception_handler(request=_make_request(path=path), exc=exc)
|
||||
|
||||
assert response.status_code == 422, path
|
||||
assert json.loads(response.body) == {"detail": []}, path
|
||||
|
|
@ -294,6 +314,4 @@ async def test_otel_unhandled_exception_handler_reraises_proxy_exception_error()
|
|||
async def test_otel_unhandled_exception_handler_reraises_http_exception_invalid():
|
||||
request = _make_request()
|
||||
with pytest.raises(HTTPException):
|
||||
await otel_unhandled_exception_handler(
|
||||
request=request, exc=HTTPException(status_code=418, detail="teapot")
|
||||
)
|
||||
await otel_unhandled_exception_handler(request=request, exc=HTTPException(status_code=418, detail="teapot"))
|
||||
|
|
|
|||
192
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
192
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -8470,6 +8470,54 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/management/v1/users/bulk": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Bulk Create Users Route
|
||||
* @description Create up to 500 internal users in one request, optionally adding each one to teams.
|
||||
*
|
||||
* Every entry in `users` takes the same fields as `/user/new`, with two differences: `auto_create_key`
|
||||
* defaults to `false` (opt in per user to also get a virtual key back) and `send_invite_email` is not
|
||||
* supported. Unknown fields are rejected with 422. Rows are validated together (duplicate ids or emails,
|
||||
* unknown teams, roles the caller may not grant), inserted in one statement, and each referenced team is
|
||||
* written once for all of its new members.
|
||||
*
|
||||
* Rows fail independently: a bad row is reported in `data` with `success: false` and an `error`, and the
|
||||
* other rows still get created. A user that was created but could not be added to one of its teams is
|
||||
* reported with `success: true`, `teams` listing where they did land, and `error` naming the failed team.
|
||||
* The whole request is refused with a 403 problem document only if creating the valid rows would exceed
|
||||
* the license seat limit.
|
||||
*
|
||||
* Example curl:
|
||||
* ```
|
||||
* curl -X POST "http://localhost:4000/management/v1/users/bulk" \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -H "Authorization: Bearer sk-1234" \
|
||||
* -d '{
|
||||
* "users": [
|
||||
* {"user_email": "a@example.com", "user_role": "internal_user", "teams": ["team-1"]},
|
||||
* {"user_email": "b@example.com", "user_role": "internal_user", "auto_create_key": true}
|
||||
* ]
|
||||
* }'
|
||||
* ```
|
||||
*
|
||||
* Returns `data` (one entry per input row, in order, with `user_id`, `user_email`, `success`, `teams`,
|
||||
* `key`, `error`) and `meta` with `total_requested`, `created` and `failed`.
|
||||
*/
|
||||
post: operations["bulk_create_users_route_management_v1_users_bulk_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/mcp": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -16478,53 +16526,6 @@ export interface paths {
|
|||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/user/bulk_new": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Bulk New User
|
||||
* @description Create up to 500 internal users in one request, optionally adding each one to teams.
|
||||
*
|
||||
* Every entry in `users` takes the same fields as `/user/new`, with two differences: `auto_create_key`
|
||||
* defaults to `false` (opt in per user to also get a virtual key back) and `send_invite_email` is not
|
||||
* supported. Rows are validated together (duplicate ids or emails, unknown teams, roles the caller may not
|
||||
* grant), inserted in one statement, and each referenced team is written once for all of its new members.
|
||||
*
|
||||
* Rows fail independently: a bad row is reported in `results` with `success: false` and an `error`, and the
|
||||
* other rows still get created. A user that was created but could not be added to one of its teams is
|
||||
* reported with `success: true`, `teams` listing where they did land, and `error` naming the failed team.
|
||||
* The whole request is rejected with 403 only if creating the valid rows would exceed the license seat limit.
|
||||
*
|
||||
* Usage Example
|
||||
*
|
||||
* ```shell
|
||||
* curl -X POST "http://localhost:4000/user/bulk_new" \
|
||||
* -H "Content-Type: application/json" \
|
||||
* -H "Authorization: Bearer sk-1234" \
|
||||
* -d '{
|
||||
* "users": [
|
||||
* {"user_email": "a@example.com", "user_role": "internal_user", "teams": ["team-1"]},
|
||||
* {"user_email": "b@example.com", "user_role": "internal_user", "auto_create_key": true}
|
||||
* ]
|
||||
* }'
|
||||
* ```
|
||||
*
|
||||
* Returns `results` (one entry per input row, in order, with `user_id`, `user_email`, `success`, `teams`,
|
||||
* `key`, `error`), `total_requested`, `successful_creations` and `failed_creations`.
|
||||
*/
|
||||
post: operations["bulk_new_user_user_bulk_new_post"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/user/bulk_update": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -24552,7 +24553,8 @@ export interface components {
|
|||
};
|
||||
/**
|
||||
* BulkNewUserItem
|
||||
* @description One row of `/user/bulk_new`: the `/user/new` body, with keys opt-in and invite emails unsupported.
|
||||
* @description One row of `POST /management/v1/users/bulk`: the `/user/new` body, with keys opt-in and invite emails
|
||||
* unsupported. Unknown fields are rejected, as on every `/management/v1` request body.
|
||||
*/
|
||||
BulkNewUserItem: {
|
||||
/** Agent Id */
|
||||
|
|
@ -24676,21 +24678,28 @@ export interface components {
|
|||
/** User Role */
|
||||
user_role?: ("proxy_admin" | "proxy_admin_viewer" | "internal_user" | "internal_user_viewer") | null;
|
||||
};
|
||||
/** BulkNewUserMeta */
|
||||
BulkNewUserMeta: {
|
||||
/** Created */
|
||||
created: number;
|
||||
/** Failed */
|
||||
failed: number;
|
||||
/** Total Requested */
|
||||
total_requested: number;
|
||||
};
|
||||
/** BulkNewUserRequest */
|
||||
BulkNewUserRequest: {
|
||||
/** Users */
|
||||
users: components["schemas"]["BulkNewUserItem"][];
|
||||
};
|
||||
/** BulkNewUserResponse */
|
||||
/**
|
||||
* BulkNewUserResponse
|
||||
* @description `data` holds one result per input row, in input order.
|
||||
*/
|
||||
BulkNewUserResponse: {
|
||||
/** Failed Creations */
|
||||
failed_creations: number;
|
||||
/** Results */
|
||||
results: components["schemas"]["UserCreateResult"][];
|
||||
/** Successful Creations */
|
||||
successful_creations: number;
|
||||
/** Total Requested */
|
||||
total_requested: number;
|
||||
/** Data */
|
||||
data: components["schemas"]["UserCreateResult"][];
|
||||
meta: components["schemas"]["BulkNewUserMeta"];
|
||||
};
|
||||
/**
|
||||
* BulkTeamMemberAddRequest
|
||||
|
|
@ -39534,7 +39543,8 @@ export interface components {
|
|||
};
|
||||
/**
|
||||
* UserCreateResult
|
||||
* @description Outcome for one row of `/user/bulk_new`. `teams` lists the teams the user was actually added to.
|
||||
* @description Outcome for one row of `POST /management/v1/users/bulk`. `teams` lists the teams the user was actually
|
||||
* added to.
|
||||
*/
|
||||
UserCreateResult: {
|
||||
/** Error */
|
||||
|
|
@ -51322,6 +51332,39 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
bulk_create_users_route_management_v1_users_bulk_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["BulkNewUserRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["BulkNewUserResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
aggregate_mcp_route_mcp_get: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
@ -60836,39 +60879,6 @@ export interface operations {
|
|||
};
|
||||
};
|
||||
};
|
||||
bulk_new_user_user_bulk_new_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["BulkNewUserRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["BulkNewUserResponse"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
422: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["HTTPValidationError"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
bulk_user_update_user_bulk_update_post: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue