BerriAI/litellm · error · ValueError

user_custom_key_update must be a coroutine

Error message

user_custom_key_update must be a coroutine

What it means

Key updates support a separate custom hook (custom_key_update, wired as user_custom_key_update) that runs after permission checks and before the update is applied. LiteLLM requires it to be a coroutine function; a sync def triggers this ValueError when the first /key/update call reaches the hook, surfacing as a 500. Note it is distinct from custom_key_generate -- one can be async while the other is sync by mistake.

Source

Thrown at litellm/proxy/management_endpoints/key_management_endpoints.py:2288

            prisma_client=prisma_client,
        )

    # Check team member permissions
    if prisma_client is not None:
        await TeamMemberPermissionChecks.can_team_member_execute_key_management_endpoint(
            user_api_key_dict=user_api_key_dict,
            route=KeyManagementRoutes.KEY_UPDATE,
            prisma_client=prisma_client,
            existing_key_row=existing_key_row,
            user_api_key_cache=user_api_key_cache,
        )

    # Custom key update hook
    if user_custom_key_update is not None:
        if inspect.iscoroutinefunction(user_custom_key_update):
            result: Final = await user_custom_key_update(update_key_request)
        else:
            raise ValueError("user_custom_key_update 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)

    # Enforce upperbound key params on update (don't fill defaults)
    _enforce_upperbound_key_params(update_key_request, fill_defaults=False)

    # Get team object and check team limits if team_id is provided
    team_obj: LiteLLM_TeamTableCachedObj | None = None
    if update_key_request.team_id is not None:
        team_obj = await get_team_object(
            team_id=update_key_request.team_id,
            prisma_client=prisma_client,
            user_api_key_cache=user_api_key_cache,
            check_db_only=True,
        )

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Change it to async def custom_key_update(update_key_request) -> dict
  2. Keep any decorators async through the chain so iscoroutinefunction() sees True
  3. Restart the proxy to reload the module
  4. Add a startup assertion in your auth module: assert inspect.iscoroutinefunction(custom_key_update)

Example fix

# custom_auth.py (before)
def custom_key_update(data):
    return {"decision": True}

# after
async def custom_key_update(data):
    return {"decision": True}
Defensive patterns

Strategy: type-guard

Validate before calling

import inspect, custom_auth

fn = getattr(custom_auth, "custom_key_update", None)
if fn is not None:
    assert inspect.iscoroutinefunction(fn), "custom_key_update must be async def"

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/update", json=payload)
except httpx.HTTPStatusError as e:
    if e.response.status_code == 500 and "user_custom_key_update must be a coroutine" in e.response.text:
        raise RuntimeError("convert custom_key_update to async def and restart proxy") from e
    raise

Prevention

When it happens

Trigger: custom_auth module defines def custom_key_update(request): (no async); the hook is a staticmethod/reference whose coroutine nature was lost through wrapping; you added the update hook recently and only tested /key/generate, so the bug first appears on update calls.

Common situations: Asymmetric refactor: custom_key_generate migrated to async, custom_key_update forgotten; example code copied from an outdated blog answer showing sync hooks; decorator-based hooks where the outer wrapper isn't async.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/5d614f2fcc94166b. Report an issue: GitHub.