JuliusBrussee/caveman · error · TypeError

Expected an installed CrewAI BaseLLM

Error message

Expected an installed CrewAI BaseLLM

What it means

CavemanLLM (the CrewAI middleware LLM wrapper) is a pydantic-model wrapper around a native CrewAI BaseLLM instance. Its __init__ type-checks the delegate and raises TypeError if the object passed as delegate is not an instance of crewai BaseLLM, because all delegation (model, provider, stream, stop, is_litellm) relies on BaseLLM attributes.

Solutions

  1. Pass an instantiated crewai BaseLLM (e.g. from crewai.llms import LLM; LLM(model=...)) as the delegate.
  2. Check isinstance(delegate, BaseLLM) before constructing the wrapper.
  3. If on an older/newer CrewAI where the base class moved, verify the import 'from crewai.llms.base_llm import BaseLLM' resolves and use that class.

Example fix

// before
wrapped = CavemanLLM("gpt-4o", runtime=runtime, scope=scope)
// after
from crewai.llms import LLM
wrapped = CavemanLLM(LLM(model="gpt-4o"), runtime=runtime, scope=scope)
Defensive patterns

Strategy: type-guard

Validate before calling

from crewai.llms.base_llm import BaseLLM
if not isinstance(delegate, BaseLLM):
    raise TypeError("delegate must be a crewai BaseLLM instance")

Type guard

def is_valid_delegate(obj) -> bool:
    from crewai.llms.base_llm import BaseLLM
    return isinstance(obj, BaseLLM)

Try / catch

try:
    wrapped = CavemanLLM(delegate, runtime=runtime, scope=scope)
except TypeError as e:
    if "BaseLLM" in str(e):
        delegate = LLM(model=delegate) if isinstance(delegate, str) else None
        wrapped = CavemanLLM(delegate, runtime=runtime, scope=scope)
    else:
        raise

Prevention

When it happens

Trigger: Passing a non-BaseLLM object as the first positional argument to CavemanLLM's __init__ — e.g. a raw litellm/openai client, a string model name, a Crew (agent) object, or a None value instead of the constructed LLM.

Common situations: Constructing the wrapper before building the actual CrewAI LLM; confusing the model name string with the LLM instance; passing an already-wrapped CavemanLLM back in (double wrapping); older CrewAI versions with different LLM base classes so isinstance fails.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/c0fac66717606e0a. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/crewai.py:111

    Use ``with_caveman_agent`` to register the matching recovery BaseTool.
    Register before creating the Agent, whose executor snapshots native hooks.
    ``close`` removes only this delegate's registrations. The application keeps
    ownership of the native provider client and the Caveman runtime.
    """
    llm_type: str = "caveman"
    delegate: BaseLLM = Field(exclude=True, repr=False)
    runtime: Any = Field(exclude=True, repr=False)
    scope: Scope
    closed: bool = Field(default=False, exclude=True)
    _context: Any = PrivateAttr()
    _hook: Any = PrivateAttr(default=None)
    _completed: Any = PrivateAttr(default=None)
    _recovery: Any = PrivateAttr(default=None)

    def __init__(self, delegate, *, runtime, scope):
        if not isinstance(delegate, BaseLLM):
            raise TypeError("Expected an installed CrewAI BaseLLM")
        if not isinstance(runtime, MiddlewareRuntime) or not isinstance(scope, Scope):
            raise TypeError("CrewAI requires a MiddlewareRuntime and a stable Scope per agent context")
        super().__init__(delegate=delegate, runtime=runtime, scope=scope, model=delegate.model,
                         provider=delegate.provider, stream=delegate.stream, stop=list(delegate.stop),
                         is_litellm=delegate.is_litellm)
        self._context = contextvars.ContextVar(f"caveman_crewai_context_{id(self)}", default=None)
        supported = matches_framework(("crewai", "1.15", "2"))
        if not supported and runtime.mode != "off":
            runtime.decline("unsupported_version")
        if not supported or runtime.mode == "off":
            return
        reference = weakref.ref(self)

        @on(InterceptionPoint.PRE_MODEL_CALL)
        def before(context):
            model = reference()
            if model is not None and not model.closed and context.llm is model:
                # Store only a weak executor reference. Aborted hooks and stale

View on GitHub (pinned to 3ee70a1026)