{"record":{"id":"5d614f2fcc94166b","repo":"BerriAI/litellm","slug":"user-custom-key-update-must-be-a-coroutine","errorCode":null,"errorMessage":"user_custom_key_update must be a coroutine","messagePattern":"user_custom_key_update must be a coroutine","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/management_endpoints/key_management_endpoints.py","lineNumber":2288,"sourceCode":"            prisma_client=prisma_client,\n        )\n\n    # Check team member permissions\n    if prisma_client is not None:\n        await TeamMemberPermissionChecks.can_team_member_execute_key_management_endpoint(\n            user_api_key_dict=user_api_key_dict,\n            route=KeyManagementRoutes.KEY_UPDATE,\n            prisma_client=prisma_client,\n            existing_key_row=existing_key_row,\n            user_api_key_cache=user_api_key_cache,\n        )\n\n    # Custom key update hook\n    if user_custom_key_update is not None:\n        if inspect.iscoroutinefunction(user_custom_key_update):\n            result: Final = await user_custom_key_update(update_key_request)\n        else:\n            raise ValueError(\"user_custom_key_update must be a coroutine\")\n        decision: Final = result.get(\"decision\", True)\n        message: Final = result.get(\"message\", \"Authentication Failed - Custom Auth Rule\")\n        if not decision:\n            raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=message)\n\n    # Enforce upperbound key params on update (don't fill defaults)\n    _enforce_upperbound_key_params(update_key_request, fill_defaults=False)\n\n    # Get team object and check team limits if team_id is provided\n    team_obj: LiteLLM_TeamTableCachedObj | None = None\n    if update_key_request.team_id is not None:\n        team_obj = await get_team_object(\n            team_id=update_key_request.team_id,\n            prisma_client=prisma_client,\n            user_api_key_cache=user_api_key_cache,\n            check_db_only=True,\n        )\n","sourceCodeStart":2270,"sourceCodeEnd":2306,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/key_management_endpoints.py#L2270-L2306","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Change it to async def custom_key_update(update_key_request) -> dict","Keep any decorators async through the chain so iscoroutinefunction() sees True","Restart the proxy to reload the module","Add a startup assertion in your auth module: assert inspect.iscoroutinefunction(custom_key_update)"],"exampleFix":"# custom_auth.py (before)\ndef custom_key_update(data):\n    return {\"decision\": True}\n\n# after\nasync def custom_key_update(data):\n    return {\"decision\": True}","handlingStrategy":"type-guard","validationCode":"import inspect, custom_auth\n\nfn = getattr(custom_auth, \"custom_key_update\", None)\nif fn is not None:\n    assert inspect.iscoroutinefunction(fn), \"custom_key_update must be async def\"","typeGuard":"import inspect\n\ndef is_async_hook(fn) -> bool:\n    return callable(fn) and inspect.iscoroutinefunction(fn)","tryCatchPattern":"try:\n    r = await client.post(\"/key/update\", json=payload)\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 500 and \"user_custom_key_update must be a coroutine\" in e.response.text:\n        raise RuntimeError(\"convert custom_key_update to async def and restart proxy\") from e\n    raise","preventionTips":["Define every hook async: custom_auth, custom_key_generate, custom_key_update","Assert iscoroutinefunction for all hooks at module import / proxy startup","Smoke-test /key/update (not just /key/generate) after deploying custom auth changes"],"tags":["litellm","custom-auth","hooks","async","key-update"],"backgroundTag":"invalid-callback-signature","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}