langchain - ✅(Solved) Fix [langchain-openai] with_structured_output() silently drops previously bound tools and lacks support for OpenAI native tool bindings [1 pull requests, 12 comments, 5 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
langchain-ai/langchain#35320Fetched 2026-04-08 00:26:40
View on GitHub
Comments
12
Participants
5
Timeline
32
Reactions
0
Timeline (top)
commented ×12mentioned ×7subscribed ×7labeled ×3

Part 1: Bug — Silent dropping of all tool bindings

Calling .bind(tools=[...]) followed by .with_structured_output(schema) on a ChatOpenAI instance causes the tool bindings to be silently discarded.

Root cause: with_structured_output() internally creates a new RunnableSequence that configures its own model bindings to enforce the output schema (via response_format or tool_choice). These new bindings do not merge with the previously bound kwargs from .bind(). The tools kwarg is effectively overwritten.

This silent drop affects all tool types — both OpenAI native tools ({"type": "web_search"}) and custom LangChain tools (callables passed via bind_tools()). Any kwargs set via .bind() before calling .with_structured_output() are lost.

I verified this by injecting httpx event hooks to log the raw HTTP request body. The logs confirmed:

Scenariotools in payloadresponse_format in payload
.bind(tools=...) then .with_structured_output(...)MissingPresent
.bind(tools=..., response_format=...) (workaround)PresentPresent

Part 2: Feature gap — No Pydantic-friendly path for native tools + structured output

The OpenAI API fully supports sending both tools and response_format in the same request — these are independent, composable features. This is especially important for OpenAI's native tools (web_search, code_interpreter, file_search), which are designed to work alongside structured output: the model uses the tool internally and returns results in the structured format.

However, there is currently no way to combine a Pydantic class (via with_structured_output()) with native tools. The only workaround is to:

  1. Manually convert the Pydantic class to an OpenAI-compatible JSON schema dict (replicating LangChain's internal _convert_to_openai_response_format logic)
  2. Pass both tools and response_format in a single .bind() call

This defeats the purpose of the with_structured_output() abstraction and forces users to work at a lower level of the API.

Error Message

There is no error. That is the core problem — this fails completely silently.

No exception is raised, no warning is logged, no deprecation notice is emitted. The tools parameter is simply absent from the HTTP request payload sent to OpenAI. This was confirmed by inspecting raw HTTP requests via httpx event hooks.

The model returns a plausible-looking response (hallucinated data), making it very difficult to notice that tools were dropped.

Root Cause

Step 3 — Invoke

result = chain.invoke("What is the weather in San Francisco right now?") print(result)

=> temperature=57.0

condition='Estimated typical current weather: cool and often foggy.

I can't access live weather data — ...'

The model HALLUCINATED the weather because web_search was silently dropped.

No error, no warning — it just disappears.

Fix Action

Fix / Workaround

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

----------------------------------------------------------------

WORKAROUND: bypass with_structured_output(), use a single .bind()

----------------------------------------------------------------

Scenariotools in payloadresponse_format in payload
.bind(tools=...) then .with_structured_output(...)MissingPresent
.bind(tools=..., response_format=...) (workaround)PresentPresent

PR fix notes

PR #35322: feat(core): forward bound kwargs in RunnableBinding.getattr

Description (problem / solution / changelog)

  • RunnableBinding.getattr now forwards bound kwargs to delegated methods that accept **kwargs.
  • Previously, only bound config was passed to the underlying runnable's methods. Now, bound kwargs are also forwarded to methods with **kwargs, with explicit kwargs taking precedence.
runnable.bind(foo="bar").some_method(baz="qux")
# some_method receives: foo="bar", baz="qux"

Use case was identified in #35320

Changed files

  • libs/core/langchain_core/runnables/base.py (modified, +37/-25)
  • libs/core/tests/unit_tests/runnables/test_runnable.py (modified, +36/-0)

Code Example

from langchain_openai import ChatOpenAI
from langchain_core.utils.function_calling import convert_to_openai_function
from pydantic import BaseModel, Field

class WeatherResponse(BaseModel):
    temperature: float = Field(description="The temperature in fahrenheit")
    condition: str = Field(description="The weather condition")

llm = ChatOpenAI(model="gpt-5-mini")

# ----------------------------------------------------------------
# BUG: .with_structured_output() silently drops tools from .bind()
# ----------------------------------------------------------------

# Step 1Bind an OpenAI native tool (e.g. web_search)
llm_with_tools = llm.bind(tools=[{"type": "web_search"}])

# Step 2Apply structured output on top
chain = llm_with_tools.with_structured_output(WeatherResponse)

# Step 3Invoke
result = chain.invoke("What is the weather in San Francisco right now?")
print(result)
# => temperature=57.0
#    condition='Estimated typical current weather: cool and often foggy.
#    I can't access live weather data — ...'
#
# The model HALLUCINATED the weather because web_search was silently dropped.
# No error, no warning — it just disappears.

# ----------------------------------------------------------------
# WORKAROUND: bypass with_structured_output(), use a single .bind()
# ----------------------------------------------------------------

# Users must manually replicate LangChain's internal schema conversion
# to build the response_format dict — no Pydantic convenience here.
function = convert_to_openai_function(WeatherResponse, strict=True)
function["schema"] = function.pop("parameters")
response_format = {"type": "json_schema", "json_schema": function}

llm_fixed = llm.bind(
    tools=[{"type": "web_search"}],
    tool_choice="auto",
    response_format=response_format,
)

result = llm_fixed.invoke("What is the weather in San Francisco right now?")
print(result)
# => content=[..., {'type': 'web_search_call', 'status': 'completed', ...},
#    ..., {'type': 'text', 'text': '{"temperature":52,"condition":"Mostly cloudy ..."}'}]
#
# The model correctly performed a web search and returned real data.

---

**There is no error.** That is the core problem — this fails **completely silently**.

No exception is raised, no warning is logged, no deprecation notice is emitted. The `tools` parameter is simply absent from the HTTP request payload sent to OpenAI. This was confirmed by inspecting raw HTTP requests via `httpx` event hooks.

The model returns a plausible-looking response (hallucinated data), making it very difficult to notice that tools were dropped.

---

# Inside with_structured_output(), before creating the new chain:
existing_kwargs = getattr(self, 'kwargs', {})
# Merge tools, tool_choice, etc. from existing_kwargs into the new binding

---

# Allow users to specify tools and structured output together:
llm.with_structured_output(
    WeatherResponse,
    tools=[{"type": "web_search"}],
    tool_choice="auto",
)

---

if self.kwargs.get("tools"):
    raise ValueError(
        "with_structured_output() does not preserve previously bound tools. "
        "Use llm.bind(tools=..., response_format=...) instead to combine "
        "tools with structured output."
    )
RAW_BUFFERClick to expand / collapse

Checked other resources

  • This is a bug, not a usage question.
  • I added a clear and descriptive title that summarizes this issue.
  • I used the GitHub search to find a similar question and didn't find it.
  • I am sure that this is a bug in LangChain rather than my code.
  • The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
  • This is not related to the langchain-community package.
  • I posted a self-contained, minimal, reproducible example. A maintainer can copy it and run it AS IS.

Package (Required)

  • langchain
  • langchain-openai
  • langchain-anthropic
  • langchain-classic
  • langchain-core
  • langchain-model-profiles
  • langchain-tests
  • langchain-text-splitters
  • langchain-chroma
  • langchain-deepseek
  • langchain-exa
  • langchain-fireworks
  • langchain-groq
  • langchain-huggingface
  • langchain-mistralai
  • langchain-nomic
  • langchain-ollama
  • langchain-openrouter
  • langchain-perplexity
  • langchain-qdrant
  • langchain-xai
  • Other / not sure / general

Related Issues / PRs

Related issue: #28848

This is related to #28848 (bind_tools not callable after with_structured_output), which describes the reverse ordering problem. This issue covers a different but connected scenario: when .bind(tools=...) is called before .with_structured_output(), the tools are silently dropped from the API request with no error or warning. Together, these issues show that with_structured_output() and tool bindings are fundamentally incompatible in both directions — and neither direction gives the user a clear signal.

Reproduction Steps / Example Code (Python)

from langchain_openai import ChatOpenAI
from langchain_core.utils.function_calling import convert_to_openai_function
from pydantic import BaseModel, Field

class WeatherResponse(BaseModel):
    temperature: float = Field(description="The temperature in fahrenheit")
    condition: str = Field(description="The weather condition")

llm = ChatOpenAI(model="gpt-5-mini")

# ----------------------------------------------------------------
# BUG: .with_structured_output() silently drops tools from .bind()
# ----------------------------------------------------------------

# Step 1 — Bind an OpenAI native tool (e.g. web_search)
llm_with_tools = llm.bind(tools=[{"type": "web_search"}])

# Step 2 — Apply structured output on top
chain = llm_with_tools.with_structured_output(WeatherResponse)

# Step 3 — Invoke
result = chain.invoke("What is the weather in San Francisco right now?")
print(result)
# => temperature=57.0
#    condition='Estimated typical current weather: cool and often foggy.
#    I can't access live weather data — ...'
#
# The model HALLUCINATED the weather because web_search was silently dropped.
# No error, no warning — it just disappears.

# ----------------------------------------------------------------
# WORKAROUND: bypass with_structured_output(), use a single .bind()
# ----------------------------------------------------------------

# Users must manually replicate LangChain's internal schema conversion
# to build the response_format dict — no Pydantic convenience here.
function = convert_to_openai_function(WeatherResponse, strict=True)
function["schema"] = function.pop("parameters")
response_format = {"type": "json_schema", "json_schema": function}

llm_fixed = llm.bind(
    tools=[{"type": "web_search"}],
    tool_choice="auto",
    response_format=response_format,
)

result = llm_fixed.invoke("What is the weather in San Francisco right now?")
print(result)
# => content=[..., {'type': 'web_search_call', 'status': 'completed', ...},
#    ..., {'type': 'text', 'text': '{"temperature":52,"condition":"Mostly cloudy ..."}'}]
#
# The model correctly performed a web search and returned real data.

Error Message and Stack Trace (if applicable)

**There is no error.** That is the core problem — this fails **completely silently**.

No exception is raised, no warning is logged, no deprecation notice is emitted. The `tools` parameter is simply absent from the HTTP request payload sent to OpenAI. This was confirmed by inspecting raw HTTP requests via `httpx` event hooks.

The model returns a plausible-looking response (hallucinated data), making it very difficult to notice that tools were dropped.

Description

Part 1: Bug — Silent dropping of all tool bindings

Calling .bind(tools=[...]) followed by .with_structured_output(schema) on a ChatOpenAI instance causes the tool bindings to be silently discarded.

Root cause: with_structured_output() internally creates a new RunnableSequence that configures its own model bindings to enforce the output schema (via response_format or tool_choice). These new bindings do not merge with the previously bound kwargs from .bind(). The tools kwarg is effectively overwritten.

This silent drop affects all tool types — both OpenAI native tools ({"type": "web_search"}) and custom LangChain tools (callables passed via bind_tools()). Any kwargs set via .bind() before calling .with_structured_output() are lost.

I verified this by injecting httpx event hooks to log the raw HTTP request body. The logs confirmed:

Scenariotools in payloadresponse_format in payload
.bind(tools=...) then .with_structured_output(...)MissingPresent
.bind(tools=..., response_format=...) (workaround)PresentPresent

Part 2: Feature gap — No Pydantic-friendly path for native tools + structured output

The OpenAI API fully supports sending both tools and response_format in the same request — these are independent, composable features. This is especially important for OpenAI's native tools (web_search, code_interpreter, file_search), which are designed to work alongside structured output: the model uses the tool internally and returns results in the structured format.

However, there is currently no way to combine a Pydantic class (via with_structured_output()) with native tools. The only workaround is to:

  1. Manually convert the Pydantic class to an OpenAI-compatible JSON schema dict (replicating LangChain's internal _convert_to_openai_response_format logic)
  2. Pass both tools and response_format in a single .bind() call

This defeats the purpose of the with_structured_output() abstraction and forces users to work at a lower level of the API.

Why This Is a Problem

  1. Silent failures are dangerous. The code appears to work — it returns structured JSON — but the model is hallucinating instead of using the tool. In a production system this produces incorrect results with no signal that something is wrong. Libraries should fail fast and loud, not silently swallow configuration.

  2. No Pydantic path for a common use case. Combining native tools with structured output is a mainstream OpenAI pattern (e.g., "search the web and return results as this Pydantic schema"). Today, users cannot use with_structured_output(MyPydanticModel) for this — they must manually build the response_format dict, which is error-prone and bypasses LangChain's schema validation and strict-mode handling.

  3. The workaround is non-trivial. Users must replicate LangChain's internal _convert_to_openai_response_format logic (using convert_to_openai_function, renaming parametersschema, wrapping in the json_schema envelope). This is fragile and may break across LangChain versions.

Suggested Fix

Option A — Preserve existing bindings (minimal fix for the bug):

with_structured_output() should read the current self.kwargs before creating new bindings and merge them, so previously bound tools are not lost:

# Inside with_structured_output(), before creating the new chain:
existing_kwargs = getattr(self, 'kwargs', {})
# Merge tools, tool_choice, etc. from existing_kwargs into the new binding

Option B — Accept a tools parameter directly (fixes both bug and feature gap):

Allow with_structured_output() to accept tools alongside the Pydantic schema, so users can combine them in a single call:

# Allow users to specify tools and structured output together:
llm.with_structured_output(
    WeatherResponse,
    tools=[{"type": "web_search"}],
    tool_choice="auto",
)

This would provide the Pydantic convenience that users expect, while correctly sending both tools and response_format to the OpenAI API.

Option C — At minimum, warn or raise (fixes the silent failure):

If supporting both is not feasible, detect when previously bound kwargs would be lost and raise an explicit error:

if self.kwargs.get("tools"):
    raise ValueError(
        "with_structured_output() does not preserve previously bound tools. "
        "Use llm.bind(tools=..., response_format=...) instead to combine "
        "tools with structured output."
    )

System Info

System Information

OS: Darwin OS Version: Darwin Kernel Version 25.2.0: Tue Nov 18 21:09:41 PST 2025; --- Python Version: 3.13.11 (main, Dec 5 2025, 16:06:33)---

Package Information

langchain_core: 1.2.8 langsmith: 0.4.60 langchain_openai: 1.1.7 langgraph_sdk: 0.3.3

Optional packages not installed

langserve

Other Dependencies

httpx: 0.28.1 jsonpatch: 1.33 openai: 2.16.0 opentelemetry-api: 1.39.1 opentelemetry-sdk: 1.39.1 orjson: 3.11.7 packaging: 25.0 pydantic: 2.12.5 pytest: 8.4.2 pyyaml: 6.0.3 requests: 2.32.5 requests-toolbelt: 1.0.0 rich: 14.3.2 tenacity: 9.1.2 tiktoken: 0.12.0 typing-extensions: 4.15.0 uuid-utils: 0.14.0 zstandard: 0.25.0

extent analysis

Problem Summary

The issue is about a silent failure in the langchain library when using with_structured_output() after binding tools. The tools are silently dropped from the API request, causing the model to hallucinate instead of using the tool.

Root Cause Analysis

The root cause is that with_structured_output() internally creates a new RunnableSequence that configures its own model bindings to enforce the output schema, which does not merge with the previously bound kwargs from .bind().

Fix Plan

Option A: Preserve existing bindings (minimal fix for the bug)

  1. In with_structured_output(), read the current self.kwargs before creating new bindings and merge them, so previously bound tools are not lost.
# Inside with_structured_output(), before creating the new chain:
existing_kwargs = getattr(self, 'kwargs', {})
# Merge tools, tool_choice, etc. from existing_kwargs into the new binding

Option B: Accept a tools parameter directly (fixes both bug and feature gap)

  1. Allow with_structured_output() to accept tools alongside the Pydantic schema, so users can combine them in a single call.
# Allow users to specify tools and structured output together:
llm.with_structured_output(
    WeatherResponse,
    tools=[{"type": "web_search"}],
    tool_choice="auto",
)

Option C: At minimum, warn or raise (fixes the silent failure)

  1. If supporting both is not feasible, detect when previously bound kwargs would be lost and raise an explicit error.
if self.kwargs.get("tools"):
    raise ValueError(
        "with_structured_output() does not preserve previously bound tools. "
        "Use llm.bind(tools=..., response_format=...) instead to combine "
        "tools with structured output."
    )

Verification

  1. Test the fix by

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

langchain - ✅(Solved) Fix [langchain-openai] with_structured_output() silently drops previously bound tools and lacks support for OpenAI native tool bindings [1 pull requests, 12 comments, 5 participants]