LangChain bind_tools schema mismatch — OpenAI vs Anthropic vs Google formats (2026) — Fix Guide (2026) | AI Error Hub
Home Providers LangChain bind_tools mismatch
LangChain Tool Calling · bind_tools Severity: Medium HTTP n/a

LangChain .bind_tools() — provider schema translation failures

<code>.bind_tools()</code> is LangChain's abstraction for attaching tools to any chat model. In principle you write once, run anywhere; in practice, provider-specific schema quirks leak through in ways that only surface at request time.

By Sana K. · Cloud AI Reliability Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: .bind_tools() translates LangChain tools into each provider's native format: OpenAI functions, Anthropic tool_use, Google function_declarations. Failure modes: (a) OpenAI strict mode rejects Optional; (b) Anthropic needs non-empty descriptions; (c) Google rejects certain nested types; (d) tool_choice formats differ. Fix by (a) writing conservative schemas, (b) validating per-provider in CI, and (c) using tool_choice with the LangChain-normalized form.

Real error messages you'll see

These are the exact strings returned by the LangChain framework and its integrations when this error occurs. Copy-paste-searching any of them should land on this page.

OpenAI strict mode rejects Optional
# model = ChatOpenAI(model="gpt-4o").bind_tools(tools, strict=True)
openai.BadRequestError: In strict mode, all fields must be required. Consider adding 'query' to the required list.
Anthropic tool without description
anthropic.BadRequestError: 400 - tools.0.description: Field required
Google Gemini rejects allOf
google.api_core.exceptions.InvalidArgument: 400 Function declarations do not support allOf composition.

Reference

Provider schema quirks that leak through bind_tools

ProviderQuirkWorkaround
OpenAI (strict mode)Every field must be required, no OptionalUse defaults instead of Optional
OpenAI (non-strict)Accepts most schemasDefault behaviour
AnthropicEvery tool needs non-empty descriptionAlways docstring your tools
Google GeminiNo allOf, limited anyOfFlatten nested types
Bedrock ClaudeSame as Anthropic directSame fix
Bedrock non-ClaudeProvider-specificTest each

<code>tool_choice</code> values across providers (LangChain-normalized)

ValueEffect
"auto" (default)Model decides whether to call a tool
"any" / "required"Model MUST call a tool
"none"Model must NOT call a tool
"tool_name"Model must call this specific tool

Root causes, ranked by frequency

Based on developer reports across LangChain forums, GitHub issues, and Discord community during 2025–2026.

  • 25%
    OpenAI strict mode + Optional fields. Strict rejects Optional[str]; either drop strict or refactor.
  • 18%
    Tool missing docstring. Anthropic requires description; docstring-less tools fail.
  • 14%
    Nested Pydantic model with unions. Providers vary in union support.
  • 10%
    tool_choice format wrong per provider. Some providers accept "tool_name"; others need {"type": "tool", "name": "tool_name"}.
  • 8%
    Provider-specific extras leaking. OpenAI functions can have additionalProperties; Anthropic tools cannot.
  • 7%
    bind_tools called twice on same model. Second call replaces the first silently; tools missing.
  • 8%
    Async / sync tool binding mismatch. Binding sync tools to ainvoke path works but degrades performance.
  • 10%
    Deprecated bind_functions still in code. Old bind_functions is OpenAI-specific; migrate to bind_tools.

Fixes — copy-paste solutions

Fix #1

Write conservative schemas that work across all providers

Simple, required, well-described — the portable subset.

Follow these rules for maximum portability: (1) every field required with a default; (2) no Optional, use empty-list or sentinel defaults; (3) enum via Literal; (4) every tool has a docstring.

portable_bind_tools.py
from typing import List, Literal
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI

@tool
def search_docs(
    query: str,
    top_k: int = 5,
    doc_types: List[str] = [],                    # empty list vs Optional
    sort: Literal["relevance", "date"] = "relevance",  # Literal enum
) -> str:
    """Search the internal document index.

    Args:
        query: Search query in plain English.
        top_k: Number of results to return (1-20).
        doc_types: Restrict to these types, or [] for all.
        sort: How to order results.
    """
    return f"[{sort}] {top_k} results for {query!r}"

# Same tool binds cleanly to every provider
openai_llm = ChatOpenAI(model="gpt-4o-mini").bind_tools([search_docs])
anthropic_llm = ChatAnthropic(model="claude-sonnet-5").bind_tools([search_docs])
google_llm = ChatGoogleGenerativeAI(model="gemini-2.5-pro").bind_tools([search_docs])

# All three respond with tool_use blocks LangChain understands uniformly
for name, llm in [("openai", openai_llm), ("anthropic", anthropic_llm), ("google", google_llm)]:
    response = llm.invoke("Find recent policy documents")
    tool_calls = response.tool_calls if hasattr(response, "tool_calls") else []
    print(f"{name}: {len(tool_calls)} tool calls")
For OpenAI strict mode specifically, consider passing strict=True to bind_tools when you want compile-time enforcement. It raises immediately on schemas that would fail at request time.
Fix #2

Validate tool schemas against each target provider in CI

Catch provider incompatibilities before they hit production.

Run a smoke test that binds each tool to each provider and checks the schema serializes cleanly. Do not wait for a user-triggered request to fail.

ci_tool_validation.py
"""Validate tools bind cleanly to every supported provider."""
import pytest
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic
from langchain_google_genai import ChatGoogleGenerativeAI

from my_app.tools import ALL_TOOLS  # your production tool set

PROVIDERS = [
    ("openai_strict", lambda: ChatOpenAI(model="gpt-4o-mini")),
    ("openai_strict_true", lambda: ChatOpenAI(model="gpt-4o-mini")),
    ("anthropic", lambda: ChatAnthropic(model="claude-sonnet-5")),
    ("google", lambda: ChatGoogleGenerativeAI(model="gemini-2.5-pro")),
]

@pytest.mark.parametrize("provider_name,model_factory", PROVIDERS)
@pytest.mark.parametrize("tool", ALL_TOOLS, ids=lambda t: t.name)
def test_tool_binds_to_provider(provider_name, model_factory, tool):
    """Every tool must bind to every supported provider without error."""
    llm = model_factory()
    kwargs = {}
    if provider_name == "openai_strict_true":
        kwargs["strict"] = True
    try:
        bound = llm.bind_tools([tool], **kwargs)
        # For OpenAI, we can also inspect the serialized schema
        # For all providers, this at least validates the LangChain-side schema
    except Exception as e:
        pytest.fail(f"{tool.name} failed to bind to {provider_name}: {e}")

# Optional: check the schema round-trip against provider validators
def test_openai_schema_shape(tool=search_docs):
    from langchain_core.utils.function_calling import convert_to_openai_tool
    openai_schema = convert_to_openai_tool(tool)
    assert "function" in openai_schema
    assert openai_schema["function"]["description"], "Missing description"
Add this as a pre-commit / CI job. It catches provider-format issues weeks before they reach production users.
Fix #3

Handle tool_choice differences with LangChain's normalized form

LangChain normalizes tool_choice — use its form, not provider-specific ones.

Pass tool_choice to bind_tools using LangChain's normalized values ("auto", "any", tool name string). LangChain translates to each provider's native format.

tool_choice_portable.py
from langchain_openai import ChatOpenAI
from langchain_anthropic import ChatAnthropic

# LangChain-normalized tool_choice — same syntax across providers

# 1) Model must call SOME tool (any of the bound tools)
openai_forced = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools, tool_choice="any")
anthropic_forced = ChatAnthropic(model="claude-sonnet-5").bind_tools(tools, tool_choice="any")

# 2) Model must call a SPECIFIC tool
openai_specific = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools, tool_choice="search_docs")
anthropic_specific = ChatAnthropic(model="claude-sonnet-5").bind_tools(tools, tool_choice="search_docs")

# 3) Model must NOT call any tool (get a text response instead)
openai_no_tools = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools, tool_choice="none")

# 4) Auto — model decides (default; usually best)
openai_auto = ChatOpenAI(model="gpt-4o-mini").bind_tools(tools, tool_choice="auto")

# ❌ AVOID — provider-specific dict forms; not portable
# ChatOpenAI(...).bind_tools(tools, tool_choice={"type": "function", "function": {"name": "search_docs"}})
# ChatAnthropic(...).bind_tools(tools, tool_choice={"type": "tool", "name": "search_docs"})
Some LangChain versions accept the provider-native dict form for tool_choice. It works but ties your code to one provider. Prefer the string form.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Design tool schemas for the strictest provider you target (usually OpenAI strict mode).
  • Every tool has a real docstring — Anthropic requires it, others benefit.
  • Test tool binding in CI against every provider you support.
  • Use LangChain-normalized tool_choice values, not provider-specific dicts.
  • Migrate any remaining bind_functions calls to bind_tools.
  • Do not double-bind — llm.bind_tools([a]).bind_tools([b]) loses tool a.
  • For OpenAI strict mode, decide upfront: opt in globally or not at all — mixed is confusing.

Frequently asked questions

bind_functions is legacy OpenAI-specific. bind_tools is the current cross-provider primitive. Use bind_tools; the older method still exists for compatibility but should not be used in new code.
Yes — you can pass OpenAI-format function dicts and LangChain will use them as-is (works only on OpenAI). Cleaner to use @tool or Pydantic models for portability.
Set parallel_tool_calls=True on bind_tools where the provider supports it (OpenAI, Anthropic). The model may return multiple tool_calls in one response. Handle them concurrently in your agent loop.
Yes — every tool schema counts as input tokens on every request. For agents with 20+ tools, this can be 5-10K tokens of overhead. Use prompt caching where available to amortize.
Third-party integrations (Groq, Together, DeepInfra, etc.) usually implement bind_tools but with varying fidelity. Test each. For providers without bind_tools, fall back to plain prompting with structured output.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.