{"record":{"id":"53e7b7fa06a8f3d4","repo":"BerriAI/litellm","slug":"user-custom-key-generate-must-be-a-coroutine","errorCode":null,"errorMessage":"user_custom_key_generate must be a coroutine","messagePattern":"user_custom_key_generate must be a coroutine","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/proxy/management_endpoints/key_management_endpoints.py","lineNumber":1707,"sourceCode":"        if data.max_budget is not None and (not math.isfinite(data.max_budget) or data.max_budget < 0):\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": f\"max_budget must be a non-negative finite number. Received: {data.max_budget}\"},\n            )\n        if data.soft_budget is not None and (not math.isfinite(data.soft_budget) or data.soft_budget < 0):\n            raise HTTPException(\n                status_code=400,\n                detail={\"error\": f\"soft_budget must be a non-negative finite number. Received: {data.soft_budget}\"},\n            )\n\n        custom_key_generate_hook: Final[Callable[..., Awaitable[Mapping[str, object]]] | None] = (\n            user_custom_key_generate\n        )\n        if custom_key_generate_hook is not None:\n            if inspect.iscoroutinefunction(custom_key_generate_hook):\n                result: Final = await custom_key_generate_hook(data)\n            else:\n                raise ValueError(\"user_custom_key_generate 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        _check_allowed_routes_caller_permission(\n            allowed_routes=data.allowed_routes,\n            user_api_key_dict=user_api_key_dict,\n            allowed_routes_was_provided=\"allowed_routes\" in data.model_fields_set,\n        )\n        _check_passthrough_routes_caller_permission(\n            data=data,\n            user_api_key_dict=user_api_key_dict,\n        )\n\n        # For non-admin internal users: auto-assign caller's user_id if not provided\n        # This prevents creating unbound keys with no user association (LIT-1884)\n        _is_proxy_admin: Final = (","sourceCodeStart":1689,"sourceCodeEnd":1725,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/management_endpoints/key_management_endpoints.py#L1689-L1725","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"# custom_auth.py (before)\ndef custom_key_generate(data):\n    return {\"decision\": True}\n\n# after\nasync def custom_key_generate(data):\n    # async logic, e.g. await some_check(data)\n    return {\"decision\": True}","handlingStrategy":"type-guard","validationCode":"# in custom_auth.py, fail fast at import time\nimport inspect\n\nasync def custom_key_generate(data):\n    return {\"decision\": True}\n\nassert inspect.iscoroutinefunction(custom_key_generate), \"custom_key_generate must be async\"","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/generate\", json=payload)\nexcept httpx.HTTPStatusError as e:\n    if e.response.status_code == 500 and \"must be a coroutine\" in e.response.text:\n        raise RuntimeError(\"proxy custom_auth hook is sync -- fix module and restart proxy\") from e\n    raise","preventionTips":["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"],"tags":["litellm","custom-auth","hooks","async","configuration"],"backgroundTag":"invalid-callback-signature","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}