JuliusBrussee/caveman · error · Error

Native model does not support bindTools

Error message

Native model does not support bindTools

What it means

CavemanLangChainModel.bindTools delegates to the wrapped native model's bindTools. If the inner BaseChatModel does not implement bindTools (it is undefined), the library throws rather than silently dropping tool-calling. The middleware cannot project a tool-bound runnable without native support.

Solutions

  1. Swap the wrapped model for one that supports tool calling (e.g. ChatOpenAI, ChatAnthropic).
  2. Upgrade @langchain/core and the model package so the inner model implements bindTools.
  3. If tools are unnecessary for that model, call invoke directly without bindTools.
  4. Guard with a capability check before binding.

Example fix

// before
const model = new CavemanLangChainModel(new SomeLegacyModel());
model.bindTools([myTool]); // throws
// after
if (new SomeLegacyModel().bindTools) {
  new CavemanLangChainModel(new SomeLegacyModel()).bindTools([myTool]);
}
Defensive patterns

Strategy: type-guard

Type guard

function supportsBindTools(m: BaseChatModel): m is BaseChatModel & { bindTools: NonNullable<BaseChatModel['bindTools']> } {
  return typeof m.bindTools === 'function';
}

Try / catch

try {
  return cavemanModel.bindTools(tools, kwargs);
} catch (e) {
  if (e instanceof Error && e.message === 'Native model does not support bindTools') {
    // fall back to a model with native tool support, or run unbound
    return plainToolCapableModel.bindTools(tools, kwargs);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling cavemanModel.bindTools(tools) where this.inner.bindTools is falsy — i.e. the wrapped chat model class has no bindTools implementation.

Common situations: Wrapping a model that predates LangChain.js tool-calling support, using a community/custom model that never implemented bindTools, or a LangChain version mismatch where the inner model lacks the API.

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/e7276cffa145d393. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/typescript/src/langchain-model.ts:30

  if(Array.isArray(input))return input.map(coerceMessageLikeToMessage);
  return input.toChatMessages();
}

/** Public BaseChatModel delegate; provider configuration lives on inner only. */
export class CavemanChatModel<Options extends BaseChatModelCallOptions=BaseChatModelCallOptions> extends BaseChatModel<Options>{
  override withStructuredOutput:BaseChatModel<Options>['withStructuredOutput'];
  constructor(readonly inner:BaseChatModel<Options>,readonly caveman:LangChainOptions){
    super({});
    this.withStructuredOutput=((...args:Parameters<BaseChatModel<Options>['withStructuredOutput']>)=>this.project().pipe(inner.withStructuredOutput(...args))) as BaseChatModel<Options>['withStructuredOutput'];
  }
  _llmType():string{return this.inner?this.inner._llmType():'caveman';}
  private project(){return RunnableLambda.from<BaseLanguageModelInput,BaseMessage[],Options>(async(input,config)=>{
    const prepared=await prepareLangChain(messages(input),this.caveman,config);
    if(prepared.attempt){const attempt=prepared.attempt;attempt.runtime.report(attempt.optimization,{reason:attempt.reason??'no_candidate',adapter:'langchain',logicalCallId:attempt.logicalCallId,attemptId:attempt.attemptId});}
    return prepared.messages;
  });}
  override bindTools(tools:BindToolsInput[],kwargs?:Partial<Options>){
    if(!this.inner.bindTools)throw new Error('Native model does not support bindTools');
    return this.project().pipe(this.inner.bindTools(tools,kwargs)) as ReturnType<NonNullable<BaseChatModel<Options>['bindTools']>>;
  }
  override async invoke(input:BaseLanguageModelInput,config?:Partial<Options>):Promise<AIMessageChunk>{
    const prepared=await prepareLangChain(messages(input),this.caveman,config as RunnableConfig|undefined);
    if(!prepared.attempt)return this.inner.invoke(input,config);
    const attempt=prepared.attempt;observe(attempt,'dispatch_intent');
    try{const response=await withOwner(attempt,()=>this.inner.invoke(prepared.messages,config));observe(attempt,'completed',isAIMessage(response)?langChainUsage(response.usage_metadata):null);return response;}
    catch(error){observe(attempt,config?.signal?.aborted?'cancelled':'failed');throw error;}
  }
  override async stream(input:BaseLanguageModelInput,config?:Partial<Options>):Promise<IterableReadableStream<AIMessageChunk>>{
    const prepared=await prepareLangChain(messages(input),this.caveman,config as RunnableConfig|undefined);
    if(!prepared.attempt)return this.inner.stream(input,config);
    const attempt=prepared.attempt;observe(attempt,'dispatch_intent');
    const stream=await withOwner(attempt,()=>this.inner.stream(prepared.messages,config));
    async function* values(){
      const iterator=stream[Symbol.asyncIterator]();let last,finished=false;
      try{for(;;){const next=await withOwner(attempt,()=>iterator.next());if(next.done){finished=true;observe(attempt,'completed',langChainUsage(last));return;}if(isAIMessageChunk(next.value)&&next.value.usage_metadata)last=next.value.usage_metadata;yield next.value;}}
      catch(error){observe(attempt,config?.signal?.aborted?'cancelled':'failed');finished=true;throw error;}

View on GitHub (pinned to 3ee70a1026)