BerriAI/litellm · error · ValueError
user_custom_key_generate must be a coroutine
Error message
user_custom_key_generate must be a coroutine
What it means
LiteLLM Proxy supports an optional custom auth hook (custom_key_generate) loaded from your custom_auth module; before every key generation it is invoked to allow/reject the request. The proxy checks the hook with inspect.iscoroutinefunction() and raises this ValueError if the function was defined without async def, because it is always awaited. This is a server-side configuration bug, not a client input problem, and surfaces as a 500 on the first /key/generate call after startup.
Source
Thrown at litellm/proxy/management_endpoints/key_management_endpoints.py:1707
if data.max_budget is not None and (not math.isfinite(data.max_budget) or data.max_budget < 0):
raise HTTPException(
status_code=400,
detail={"error": f"max_budget must be a non-negative finite number. Received: {data.max_budget}"},
)
if data.soft_budget is not None and (not math.isfinite(data.soft_budget) or data.soft_budget < 0):
raise HTTPException(
status_code=400,
detail={"error": f"soft_budget must be a non-negative finite number. Received: {data.soft_budget}"},
)
custom_key_generate_hook: Final[Callable[..., Awaitable[Mapping[str, object]]] | None] = (
user_custom_key_generate
)
if custom_key_generate_hook is not None:
if inspect.iscoroutinefunction(custom_key_generate_hook):
result: Final = await custom_key_generate_hook(data)
else:
raise ValueError("user_custom_key_generate must be a coroutine")
decision: Final = result.get("decision", True)
message: Final = result.get("message", "Authentication Failed - Custom Auth Rule")
if not decision:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=message)
_check_allowed_routes_caller_permission(
allowed_routes=data.allowed_routes,
user_api_key_dict=user_api_key_dict,
allowed_routes_was_provided="allowed_routes" in data.model_fields_set,
)
_check_passthrough_routes_caller_permission(
data=data,
user_api_key_dict=user_api_key_dict,
)
# For non-admin internal users: auto-assign caller's user_id if not provided
# This prevents creating unbound keys with no user association (LIT-1884)
_is_proxy_admin: Final = (View on GitHub (pinned to 77b7c6c40c)
Solutions
- Change the hook to an async function: async def custom_key_generate(data: GenerateRequest) -> dict in your custom auth module
- Ensure any decorator wrapping it is itself async def and returns the coroutine intact
- Restart the LiteLLM proxy after fixing the module so the hook is re-imported
- Verify with a quick import test: python -c "import inspect, my_module; print(inspect.iscoroutinefunction(my_module.custom_key_generate))" should print True
Example fix
# custom_auth.py (before)
def custom_key_generate(data):
return {"decision": True}
# after
async def custom_key_generate(data):
# async logic, e.g. await some_check(data)
return {"decision": True} Defensive patterns
Strategy: type-guard
Validate before calling
# in custom_auth.py, fail fast at import time
import inspect
async def custom_key_generate(data):
return {"decision": True}
assert inspect.iscoroutinefunction(custom_key_generate), "custom_key_generate must be async" Type guard
import inspect
def is_async_hook(fn) -> bool:
return callable(fn) and inspect.iscoroutinefunction(fn) Try / catch
try:
r = await client.post("/key/generate", json=payload)
except httpx.HTTPStatusError as e:
if e.response.status_code == 500 and "must be a coroutine" in e.response.text:
raise RuntimeError("proxy custom_auth hook is sync -- fix module and restart proxy") from e
raise Prevention
- Write every LiteLLM custom auth hook (custom_auth, custom_key_generate, custom_key_update) as async def from the start
- Add import-time assertions using inspect.iscoroutinefunction so misconfigurations fail at startup, not first request
- Restart the proxy after any edit to the custom auth module; containers may serve stale imports otherwise
When it happens
Trigger: general_settings.custom_auth points at a module defining def custom_key_generate(data): (sync); the hook is a lambda or partial that hides its coroutine nature; the hook is defined async but wrapped in a sync decorator that returns a plain function; server started, first POST /key/generate triggers the check.
Common situations: Copy-pasting an old sync custom auth example from docs/LLM output; refactoring the auth module and dropping the async keyword; using functools.wraps around a wrapper that is not itself async; defining custom_key_update correctly as async but forgetting custom_key_generate.
Related errors
- user_custom_key_update must be a coroutine
- Invalid mode: {custom_auth_settings['mode']}
- custom_ui_sso_sign_in_handler is not configured. Please set
- LLM Router not initialized. Ensure models added to proxy.
- DB not connected. This endpoint needs a database; set DATABA
AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18).
Data as JSON: /api/errors/53e7b7fa06a8f3d4.
Report an issue: GitHub.