BerriAI/litellm · error · ValueError

litellm_settings.callbacks entry '{error.entry}' resolved to

Error message

litellm_settings.callbacks entry '{error.entry}' resolved to the class {error.loaded.__module__}.{error.loaded.__qualname__}, which is neither a CustomLogger instance nor a callable, so the proxy would never run it. Point it at an instance instead, e.g. add `proxy_handler_instance = {error.loaded.__name__}()` to {module_path} and set `callbacks: ["{module_path}.proxy_handler_instance"]`.

What it means

Startup ValueError from callback resolution: a litellm_settings.callbacks entry resolved to a Python class object (isinstance(loaded, type)) rather than a CustomLogger instance or a callable. _classify_loaded_callback deliberately rejects classes (calling the class would create an instance at an unpredictable time), and _raise_callback_load_error tells you to point the entry at a module-level instance instead. The message names the exact entry, the class, and the module to fix.

Source

Thrown at litellm/proxy/common_utils/callback_utils.py:95

    Decide whether what a ``litellm_settings.callbacks`` dotted path resolved to can be dispatched.

    A dotted path only ever runs as a ``CustomLogger`` instance or as a callback function. Anything
    else (most commonly a class instead of an instance) used to load without complaint and then be
    skipped on every request, with no log line and no error.
    """
    if isinstance(loaded, CustomLogger) or (callable(loaded) and not isinstance(loaded, type)):
        return loaded
    if isinstance(loaded, type):
        return _CallbackResolvedToClass(entry=entry, loaded=loaded)
    return _CallbackNotDispatchable(entry=entry, loaded=loaded)


def _raise_callback_load_error(error: _CallbackLoadError) -> NoReturn:
    """The one edge that raises: map a load error onto config load's failure contract."""
    match error:
        case _CallbackResolvedToClass():
            module_path: Final = error.entry.rsplit(".", 1)[0] if "." in error.entry else error.entry
            raise ValueError(
                f"litellm_settings.callbacks entry '{error.entry}' resolved to the class "
                f"{error.loaded.__module__}.{error.loaded.__qualname__}, which is neither a "
                "CustomLogger instance nor a callable, so the proxy would never run it."
                f" Point it at an instance instead, e.g. add `proxy_handler_instance = {error.loaded.__name__}()` to "
                f'{module_path} and set `callbacks: ["{module_path}.proxy_handler_instance"]`.'
            )
        case _CallbackNotDispatchable():
            raise ValueError(
                f"litellm_settings.callbacks entry '{error.entry}' resolved to "
                f"{type(error.loaded).__name__} {error.loaded!r}, which is neither a "
                "CustomLogger instance nor a callable, so the proxy would never run it."
            )
    assert_never(error)


def _loaded_callback_or_raise(entry: str, loaded: object) -> CustomLogger | Callable[..., object]:
    resolved: Final = _classify_loaded_callback(entry=entry, loaded=loaded)
    if isinstance(resolved, _CallbackResolvedToClass | _CallbackNotDispatchable):

View on GitHub (pinned to 77b7c6c40c)

Solutions

  1. Instantiate at module level in my_callbacks.py: proxy_handler_instance = MyCustomLogger().
  2. Reference the instance in config: callbacks: ['my_callbacks.proxy_handler_instance'].
  3. Alternatively expose any plain function (functions are callable and accepted): def my_hook(kwargs): ... ; callbacks: ['my_callbacks.my_hook'].
  4. Restart the proxy after fixing; this fails fast at config load.

Example fix

// before (my_callbacks.py)
class MyCustomLogger(CustomLogger):
    ...
# config.yaml: litellm_settings: callbacks: ["my_callbacks.MyCustomLogger"]

// after (my_callbacks.py)
class MyCustomLogger(CustomLogger):
    ...
proxy_handler_instance = MyCustomLogger()
# config.yaml: litellm_settings: callbacks: ["my_callbacks.proxy_handler_instance"]
Defensive patterns

Strategy: validation

Validate before calling

import importlib
from litellm.integrations.custom_logger import CustomLogger

def validate_callback_entries(entries):
    for entry in entries:
        module_name, _, attr = entry.rpartition('.')
        obj = getattr(importlib.import_module(module_name), attr)
        assert not isinstance(obj, type), f'{entry} is a class; point at an instance'
        assert isinstance(obj, CustomLogger) or callable(obj), f'{entry} is not dispatchable'

Type guard

def is_dispatchable_callback(entry: str) -> bool:
    module_name, _, attr = entry.rpartition('.')
    try:
        obj = getattr(importlib.import_module(module_name), attr)
    except Exception:
        return False
    return isinstance(obj, CustomLogger) or (callable(obj) and not isinstance(obj, type))

Prevention

When it happens

Trigger: Configuring callbacks: ['my_callbacks.MyCustomLogger'] where MyCustomLogger is declared with class MyCustomLogger(CustomLogger) and never instantiated. The dotted path is imported successfully, but the attribute found is the class object itself.

Common situations: Writing a first custom logger and following intuition ('point at the class'); refactoring a module so a former instance is now a class; copying example configs that show class names; name collisions where the instance and class share a name and the class wins.

Related errors


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