BerriAI/litellm · error · CustomCodeCompilationError

'apply_guardrail' must be a callable function

Error message

'apply_guardrail' must be a callable function

What it means

CustomCodeCompilationError raised when the sandboxed code defines a top-level apply_guardrail name but it is not callable (e.g. assigned an int, string, or None). The compile pipeline found the symbol yet cannot use it as the guardrail entry function.

Source

Thrown at litellm/proxy/guardrails/guardrail_hooks/custom_code/custom_code_guardrail.py:163

            GuardrailEventHooks.during_mcp_call,
            GuardrailEventHooks.logging_only,
        ]

    def _do_compile(self) -> None:
        """Internal compilation method without lock. Expected to run inside _compile_lock."""
        exec_globals: Final = build_sandbox_globals()
        compiled: Final = compile_sandboxed(self.custom_code)
        exec(compiled, exec_globals)  # noqa: S102

        if "apply_guardrail" not in exec_globals:
            raise CustomCodeCompilationError(
                "Custom code must define an 'apply_guardrail' function. "
                "Expected signature: apply_guardrail(inputs, request_data, input_type)"
            )

        apply_fn: Final = exec_globals["apply_guardrail"]
        if not callable(apply_fn):
            raise CustomCodeCompilationError("'apply_guardrail' must be a callable function")

        self._compiled_function = apply_fn

    def _compile_custom_code(self) -> None:
        """
        Compile the custom code and extract the apply_guardrail function.

        The code runs in a sandboxed environment with only the allowed primitives.
        """
        with self._compile_lock:
            if self._compiled_function is not None:
                return

            try:
                self._do_compile()
                verbose_proxy_logger.debug("Custom code guardrail '%s' compiled successfully", self.guardrail_name)

            except SyntaxError as e:

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Make apply_guardrail a plain def (or other callable) at top level
  2. Remove any variable, constant, or doc string that rebinds the name
  3. Re-run the sandbox preflight (compile + exec + callable check) before redeploying

Example fix

# before: name shadowed by a constant
apply_guardrail = 'block if flagged'

# after
def apply_guardrail(inputs, request_data, input_type):
    return allow()
Defensive patterns

Strategy: validation

Validate before calling

from litellm.proxy.guardrails.guardrail_hooks.custom_code.sandbox import (
    build_sandbox_globals, compile_sandboxed,
)

def preflight(custom_code: str) -> None:
    g = build_sandbox_globals()
    exec(compile_sandboxed(custom_code), g)
    fn = g.get('apply_guardrail')
    assert fn is not None, 'apply_guardrail not defined'
    assert callable(fn), 'apply_guardrail must be a function, not a value'

Type guard

def is_valid_entrypoint(exec_globals: dict) -> bool:
    fn = exec_globals.get('apply_guardrail')
    return fn is not None and callable(fn)

Prevention

When it happens

Trigger: custom_code contains apply_guardrail = None, apply_guardrail = 'see docs', or a variable that shadows a previously defined function of that name; compilation succeeds up to the callable() check and then aborts.

Common situations: Reusing the name for a flag/config constant; a templating step assigns a string value over the function; accidental paste of documentation text at module level.

Related errors


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