JuliusBrussee/caveman · error · ValueError

Rebind CavemanLLM from the original provider configuration…

Error message

Rebind CavemanLLM from the original provider configuration and trusted runtime; active middleware is not serializable

What it means

to_config_dict serializes the LLM configuration (e.g. for CrewAI's config export/caching). An active CavemanLLM wraps a live runtime, bindings, and contextvars that cannot be serialized safely, so the method unconditionally raises ValueError directing you to re-create the wrapper from the original provider configuration instead.

Solutions

  1. Serialize the ORIGINAL delegate LLM's config (llm.delegate.to_config_dict()) instead of the wrapper.
  2. Keep the original provider configuration (model, api key env, params) and reconstruct CavemanLLM(delegate, runtime, scope) after deserialization.
  3. Avoid exporting config from wrapped LLMs; wrap at load time, not at save time.

Example fix

// before
cfg = caveman_llm.to_config_dict()
// after
cfg = caveman_llm.delegate.to_config_dict()
# later: caveman_llm = CavemanLLM(LLM(**cfg), runtime=runtime, scope=scope)
Defensive patterns

Strategy: try-catch

Validate before calling

if hasattr(llm, "delegate"):
    cfg = llm.delegate.to_config_dict()
else:
    cfg = llm.to_config_dict()

Type guard

def serializable_llm(llm) -> bool:
    return not hasattr(llm, "delegate")

Try / catch

try:
    cfg = llm.to_config_dict()
except ValueError:
    cfg = llm.delegate.to_config_dict()  # serialize the native LLM instead

Prevention

When it happens

Trigger: Calling to_config_dict() on a CavemanLLM instance — e.g. CrewAI (or user code) exporting agent/LLM config to a dict, saving crews, or round-tripping configuration through a cache.

Common situations: Persisting a configured crew to disk/JSON; CrewAI's internal config export paths encountering the wrapped LLM; attempting to pickle/serialize a crew containing the middleware-wrapped LLM.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


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

Appendix: source

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

        return self.delegate.supports_stop_words()

    def get_context_window_size(self):
        return self.delegate.get_context_window_size()

    def supports_multimodal(self):
        return self.delegate.supports_multimodal()

    def format_text_content(self, text):
        return self.delegate.format_text_content(text)

    def get_file_uploader(self):
        return self.delegate.get_file_uploader()

    def get_token_usage_summary(self):
        return self.delegate.get_token_usage_summary()

    def to_config_dict(self):
        raise ValueError("Rebind CavemanLLM from the original provider configuration and trusted runtime; active middleware is not serializable")

    def _options(self, messages, tools, available_functions, from_task, from_agent, response_model):
        reference = self._context.get()
        self._context.set(None)
        executor = reference() if reference is not None else None
        if owner.get() is not None:
            return None
        reason = ("closed" if self.closed else "off" if self.runtime.mode == "off" else
                  "unsupported_version" if not matches_framework(("crewai", "1.15", "2")) else
                  "unsupported_shape" if type(messages) is not list else None)
        if reason:
            return Attempt(self.runtime, self.scope, str(uuid.uuid4()), str(uuid.uuid4()),
                           passive=True, reason=reason, adapter="crewai"), {}, None
        context = manifest(messages)
        if context is None:
            return Attempt(self.runtime, self.scope, str(uuid.uuid4()), str(uuid.uuid4()),
                           passive=True, reason="unsupported_shape", adapter="crewai"), {}, None
        bound = (executor is not None and executor.llm is self and executor.messages is messages

View on GitHub (pinned to 3ee70a1026)