JuliusBrussee/caveman · error · TypeError
Expected a native LangChain BaseChatModel
Error message
Expected a native LangChain BaseChatModel
What it means
with_caveman_model wraps a LangChain chat model by copying it (model_copy) and must receive an instance of langchain_core BaseChatModel. Anything else — a wrapped pipeline, a string model name, a non-native provider wrapper — raises this TypeError.
Solutions
- Pass an actual BaseChatModel instance, e.g. ChatOpenAI(model='gpt-4o').
- Wrap only the LLM node of the chain, not the full RunnableSequence.
- For custom providers, subclass BaseChatModel or use a community class that extends it.
Example fix
// before wrapped = with_caveman_model(runtime, scope, 'gpt-4o') // after from langchain_openai import ChatOpenAI wrapped = with_caveman_model(runtime, scope, ChatOpenAI(model='gpt-4o'))
Defensive patterns
Strategy: type-guard
Validate before calling
from langchain_core.language_models import BaseChatModel
if not isinstance(model, BaseChatModel):
raise TypeError('with_caveman_model expects a BaseChatModel instance') Type guard
def is_chat_model(m): return isinstance(m, BaseChatModel)
Try / catch
try:
wrapped = with_caveman_model(runtime, scope, model)
except TypeError as e:
if 'BaseChatModel' in str(e):
raise RuntimeError(f'{model!r} is not a BaseChatModel; wrap the LLM, not the chain') from e
raise Prevention
- Never pass model-name strings; instantiate a provider chat model class first.
- Wrap only the LLM node, never prompt|llm sequences.
- Add a unit test asserting isinstance(model, BaseChatModel) before wrapping.
When it happens
Trigger: Calling with_caveman_model(runtime, scope, model=...) with a non-BaseChatModel: a model name string, a ChatPromptTemplate, a RunnableSequence (prompt | llm), or a third-party chat wrapper not subclassing BaseChatModel.
Common situations: Passing 'gpt-4o' instead of ChatOpenAI('gpt-4o'); wrapping the whole chain instead of just the LLM; using a community model class that subclasses Runnable rather than BaseChatModel.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- scope resolver must return a Caveman Scope
- Synchronous LangChain calls require MiddlewareRuntime
- ASGI context must come from authenticated server state
- Expected an OpenAI or AsyncOpenAI client
- functions must map native tool names to callables
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/fbd18e9b1fa10911.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/langchain.py:272
def with_caveman_agent(options: dict, *, runtime, scope) -> dict:
"""Return native create_agent keyword arguments; do not run another loop."""
middleware = CavemanMiddleware(runtime=runtime, scope=scope)
tools = list(options.get("tools", []))
collision = any((tool.get("name") if plain(tool) else getattr(tool, "name", None)) == "caveman_retrieve" for tool in tools)
if not collision and runtime.mode == "compress" and middleware.recovery_tool is not None:
tools.append(middleware.recovery_tool)
return {**options, "tools": tools, "middleware": [*options.get("middleware", []), middleware]}
def with_caveman_model(model: BaseChatModel, *, runtime, scope):
"""Native model clone preserving bind_tools/structured helpers and callbacks.
The pinned providers' public batch APIs call invoke/ainvoke for each item,
so every item resolves its own RunnableConfig scope. This variant has no
recovery executor; use native agent middleware for recoverable lossiness.
"""
if not isinstance(model, BaseChatModel):
raise TypeError("Expected a native LangChain BaseChatModel")
if not _supported(runtime) and runtime.mode != "off":
runtime.decline("unsupported_version")
native = model.model_copy()
connection = _Connection(runtime, scope)
def messages(input):
if isinstance(input, PromptValue):
return input.to_messages()
if isinstance(input, str):
return convert_to_messages([("human", input)])
return convert_to_messages(input)
def wrap_call(method):
@functools.wraps(method)
def call(input, config=None, **kwargs):
view, attempt = connection.prepare(messages(input), config)
if attempt is None:
return method(input, config, **kwargs)View on GitHub (pinned to 3ee70a1026)