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.
Quick fix (TL;DR)
.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.
# 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.BadRequestError: 400 - tools.0.description: Field required
google.api_core.exceptions.InvalidArgument: 400 Function declarations do not support allOf composition.
Reference
Provider schema quirks that leak through bind_tools
| Provider | Quirk | Workaround |
|---|---|---|
| OpenAI (strict mode) | Every field must be required, no Optional | Use defaults instead of Optional |
| OpenAI (non-strict) | Accepts most schemas | Default behaviour |
| Anthropic | Every tool needs non-empty description | Always docstring your tools |
| Google Gemini | No allOf, limited anyOf | Flatten nested types |
| Bedrock Claude | Same as Anthropic direct | Same fix |
| Bedrock non-Claude | Provider-specific | Test each |
<code>tool_choice</code> values across providers (LangChain-normalized)
| Value | Effect |
|---|---|
"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
ainvokepath works but degrades performance. - 10%Deprecated
bind_functionsstill in code. Oldbind_functionsis OpenAI-specific; migrate tobind_tools.
Fixes — copy-paste solutions
Write conservative schemas that work across all providers
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.
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")
strict=True to bind_tools when you want compile-time enforcement. It raises immediately on schemas that would fail at request time.Validate tool schemas against each target provider in CI
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.
"""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"
Handle tool_choice differences with LangChain's normalized form
Pass tool_choice to bind_tools using LangChain's normalized values ("auto", "any", tool name string). LangChain translates to each provider's native format.
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"})
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_choicevalues, not provider-specific dicts. - Migrate any remaining
bind_functionscalls tobind_tools. - Do not double-bind —
llm.bind_tools([a]).bind_tools([b])loses toola. - 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.@tool or Pydantic models for portability.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.bind_tools but with varying fidelity. Test each. For providers without bind_tools, fall back to plain prompting with structured output.Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.