langchain - ✅(Solved) Fix bug(openai): Streaming drops `namespace` from deferred `function_call` content blocks, causing 400 "Missing namespace" on round-trip [2 pull requests, 4 comments, 3 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#36096Fetched 2026-04-08 01:02:01
View on GitHub
Comments
4
Participants
3
Timeline
13
Reactions
0
Assignees
Timeline (top)
labeled ×4commented ×3cross-referenced ×2assigned ×1

When using streaming with tool_search + defer_loading, the namespace field on deferred function_call content blocks is silently dropped, causing OpenAI to reject the next request with a 400 error.

Root cause — in _convert_responses_chunk_to_generation_chunk() at base.py L4723-4732:

# response.output_item.added handler for function_call
content.append(
    {
        "type": "function_call",
        "name": chunk.item.name,
        "arguments": chunk.item.arguments,
        "call_id": chunk.item.call_id,
        "id": chunk.item.id,
        "index": current_index,
    }
)

The namespace field from chunk.item is not copied into the content block. Compare with the non-streaming path at L4488 which uses output.model_dump(exclude_none=True) and therefore preserves all fields including namespace.

The round-trip path in _construct_responses_api_input() at L4316-4331 correctly passes function_call blocks through unchanged — so if namespace were present in the content block, it would be sent back to the API correctly.

The fix is 1 line — add namespace to the dict if present on chunk.item:

content_block = {
    "type": "function_call",
    "name": chunk.item.name,
    "arguments": chunk.item.arguments,
    "call_id": chunk.item.call_id,
    "id": chunk.item.id,
    "index": current_index,
}
if hasattr(chunk.item, "namespace") and chunk.item.namespace is not None:
    content_block["namespace"] = chunk.item.namespace
content.append(content_block)

Alternatively, use chunk.item.model_dump(exclude_none=True) (matching the non-streaming path) to capture namespace and any future fields.

Workaround: Remove defer_loading from affected tools so they stay in the default namespace. This eliminates the namespace requirement but sacrifices the token-saving benefits of tool_search.

Error Message

""" Minimal reproduction: streaming drops namespace from deferred function_call content blocks, causing OpenAI 400 "Missing namespace for function_call" on the next turn's round-trip.

Requirements: pip install langchain-openai>=1.1.11 openai>=2.26.0 export OPENAI_API_KEY=sk-...

Model: gpt-5.4-mini (or any gpt-5.4+ model that supports tool_search) """

import asyncio import json from langchain_openai import ChatOpenAI from langchain_core.tools import tool from langchain_core.messages import HumanMessage, ToolMessage

── A tool marked for deferred loading ──────────────────────────────

@tool(extras={"defer_loading": True}) def get_weather(location: str) -> str: """Get current weather for a location.""" return json.dumps({"location": location, "temp": "72°F", "condition": "sunny"})

async def main(): llm = ChatOpenAI( model="gpt-5.4-mini", use_responses_api=True, streaming=True, ).bind_tools([get_weather, {"type": "tool_search"}])

# ── Turn 1: model discovers & calls the deferred tool ───────────
messages = [HumanMessage("What's the weather in Paris?")]

# Stream the response and accumulate
full = None
async for chunk in llm.astream(messages):
    full = chunk if full is None else full + chunk

print("=== Turn 1 AIMessage.content blocks ===")
if isinstance(full.content, list):
    for i, block in enumerate(full.content):
        if isinstance(block, dict) and block.get("type") == "function_call":
            has_ns = "namespace" in block
            print(f"  [{i}] function_call  name={block.get('name')!r}  "
                  f"namespace={'PRESENT: ' + repr(block.get('namespace')) if has_ns else 'MISSING <<<'}")

# ── Execute tool & build Turn 2 ─────────────────────────────────
messages.append(full)
for tc in full.tool_calls:
    result = get_weather.invoke(tc["args"])
    messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))

messages.append(HumanMessage("Now what about London?"))

# ── Turn 2: round-trips the previous function_call → 400 ───────
try:
    full2 = None
    async for chunk in llm.astream(messages):
        full2 = chunk if full2 is None else full2 + chunk
    print("\n=== Turn 2 succeeded (unexpected) ===")
    print(full2.content[:200] if isinstance(full2.content, str) else full2.content[:3])
except Exception as e:
    print(f"\n=== Turn 2 FAILED (expected) ===")
    print(f"  {type(e).__name__}: {e}")

asyncio.run(main())

Root Cause

Root cause — in _convert_responses_chunk_to_generation_chunk() at base.py L4723-4732:

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: Remove defer_loading from affected tools so they stay in the default namespace. This eliminates the namespace requirement but sacrifices the token-saving benefits of tool_search.

PR fix notes

PR #36108: fix(openai): preserve namespace field in streaming function_call chunks

Description (problem / solution / changelog)

Summary

  • Fixes a bug where the namespace field on function_call output items was dropped during streaming, causing a 400 error on subsequent API calls when using tool_search with defer_loading.
  • The non-streaming path already preserves all fields via model_dump(exclude_none=True), but the streaming path manually constructed the dict and omitted namespace.
  • Adds a unit test verifying the namespace is preserved in streamed function_call content blocks.

Test plan

  • Added test_responses_stream_function_call_preserves_namespace unit test
  • Test creates a mock stream with a function_call item containing namespace="my_namespace" and asserts the namespace appears in the final aggregated content
  • All existing streaming tests continue to pass

Fixes #36096

This contribution was developed with the assistance of an AI coding agent.

Changed files

  • libs/partners/openai/langchain_openai/chat_models/base.py (modified, +11/-10)
  • libs/partners/openai/tests/unit_tests/chat_models/test_responses_stream.py (modified, +195/-0)

PR #36121: fix(openai): preserve namespace field in streamed function_call content blocks

Description (problem / solution / changelog)

Description

When the OpenAI Responses API streams a function_call output item that contains a namespace field (e.g. tools created with defer_loading=True via tool_search), the streaming handler builds the content block manually and omits the namespace field. As a result, the field is silently dropped from the accumulated AIMessage.content.

On the next conversation turn, when the AIMessage is serialized and sent back to OpenAI, the function_call block is missing namespace, causing a 400 Bad Request: Missing namespace for function_call.

Root Cause

In _convert_responses_chunk_to_generation_chunk, the response.output_item.added handler for function_call items builds a dict with explicit keys but does not include namespace:

# Before fix
content.append(
    {
        "type": "function_call",
        "name": chunk.item.name,
        "arguments": chunk.item.arguments,
        "call_id": chunk.item.call_id,
        "id": chunk.item.id,
        "index": current_index,
        # ← "namespace" never copied!
    }
)

Fix

# After fix
fc_block = {
    "type": "function_call",
    "name": chunk.item.name,
    "arguments": chunk.item.arguments,
    "call_id": chunk.item.call_id,
    "id": chunk.item.id,
    "index": current_index,
}
if namespace := getattr(chunk.item, "namespace", None):
    fc_block["namespace"] = namespace
content.append(fc_block)

Changes

  • libs/partners/openai/langchain_openai/chat_models/base.py: Copy namespace from the streaming chunk item into the function_call content block when present

Closes #36096

Changed files

  • libs/partners/openai/langchain_openai/chat_models/base.py (modified, +13/-10)

Code Example

"""
Minimal reproduction: streaming drops `namespace` from deferred function_call
content blocks, causing OpenAI 400 "Missing namespace for function_call" on the
next turn's round-trip.

Requirements:
    pip install langchain-openai>=1.1.11 openai>=2.26.0
    export OPENAI_API_KEY=sk-...

Model: gpt-5.4-mini (or any gpt-5.4+ model that supports tool_search)
"""

import asyncio
import json
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage


# ── A tool marked for deferred loading ──────────────────────────────
@tool(extras={"defer_loading": True})
def get_weather(location: str) -> str:
    """Get current weather for a location."""
    return json.dumps({"location": location, "temp": "72°F", "condition": "sunny"})


async def main():
    llm = ChatOpenAI(
        model="gpt-5.4-mini",
        use_responses_api=True,
        streaming=True,
    ).bind_tools([get_weather, {"type": "tool_search"}])

    # ── Turn 1: model discovers & calls the deferred tool ───────────
    messages = [HumanMessage("What's the weather in Paris?")]

    # Stream the response and accumulate
    full = None
    async for chunk in llm.astream(messages):
        full = chunk if full is None else full + chunk

    print("=== Turn 1 AIMessage.content blocks ===")
    if isinstance(full.content, list):
        for i, block in enumerate(full.content):
            if isinstance(block, dict) and block.get("type") == "function_call":
                has_ns = "namespace" in block
                print(f"  [{i}] function_call  name={block.get('name')!r}  "
                      f"namespace={'PRESENT: ' + repr(block.get('namespace')) if has_ns else 'MISSING <<<'}")

    # ── Execute tool & build Turn 2 ─────────────────────────────────
    messages.append(full)
    for tc in full.tool_calls:
        result = get_weather.invoke(tc["args"])
        messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))

    messages.append(HumanMessage("Now what about London?"))

    # ── Turn 2: round-trips the previous function_call → 400 ───────
    try:
        full2 = None
        async for chunk in llm.astream(messages):
            full2 = chunk if full2 is None else full2 + chunk
        print("\n=== Turn 2 succeeded (unexpected) ===")
        print(full2.content[:200] if isinstance(full2.content, str) else full2.content[:3])
    except Exception as e:
        print(f"\n=== Turn 2 FAILED (expected) ===")
        print(f"  {type(e).__name__}: {e}")


asyncio.run(main())

---

openai.BadRequestError: Error code: 400 -
{
  "error": {
    "message": "Missing namespace for function_call 'get_weather'.
               It does not exist in the default namespace.
               Round-trip the model's function_call item with its namespace field included.",
    "type": "invalid_request_error",
    "param": null,
    "code": null
  }
}

---

# response.output_item.added handler for function_call
content.append(
    {
        "type": "function_call",
        "name": chunk.item.name,
        "arguments": chunk.item.arguments,
        "call_id": chunk.item.call_id,
        "id": chunk.item.id,
        "index": current_index,
    }
)

---

content_block = {
    "type": "function_call",
    "name": chunk.item.name,
    "arguments": chunk.item.arguments,
    "call_id": chunk.item.call_id,
    "id": chunk.item.id,
    "index": current_index,
}
if hasattr(chunk.item, "namespace") and chunk.item.namespace is not None:
    content_block["namespace"] = chunk.item.namespace
content.append(content_block)
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

  • #35582 — feat(openai): support tool search (introduced defer_loading support, merged 2026-03-08)
  • #34660 — response.completed chunk missing tool_calls in streaming (related streaming data-loss pattern)

Reproduction Steps / Example Code (Python)

"""
Minimal reproduction: streaming drops `namespace` from deferred function_call
content blocks, causing OpenAI 400 "Missing namespace for function_call" on the
next turn's round-trip.

Requirements:
    pip install langchain-openai>=1.1.11 openai>=2.26.0
    export OPENAI_API_KEY=sk-...

Model: gpt-5.4-mini (or any gpt-5.4+ model that supports tool_search)
"""

import asyncio
import json
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langchain_core.messages import HumanMessage, ToolMessage


# ── A tool marked for deferred loading ──────────────────────────────
@tool(extras={"defer_loading": True})
def get_weather(location: str) -> str:
    """Get current weather for a location."""
    return json.dumps({"location": location, "temp": "72°F", "condition": "sunny"})


async def main():
    llm = ChatOpenAI(
        model="gpt-5.4-mini",
        use_responses_api=True,
        streaming=True,
    ).bind_tools([get_weather, {"type": "tool_search"}])

    # ── Turn 1: model discovers & calls the deferred tool ───────────
    messages = [HumanMessage("What's the weather in Paris?")]

    # Stream the response and accumulate
    full = None
    async for chunk in llm.astream(messages):
        full = chunk if full is None else full + chunk

    print("=== Turn 1 AIMessage.content blocks ===")
    if isinstance(full.content, list):
        for i, block in enumerate(full.content):
            if isinstance(block, dict) and block.get("type") == "function_call":
                has_ns = "namespace" in block
                print(f"  [{i}] function_call  name={block.get('name')!r}  "
                      f"namespace={'PRESENT: ' + repr(block.get('namespace')) if has_ns else 'MISSING <<<'}")

    # ── Execute tool & build Turn 2 ─────────────────────────────────
    messages.append(full)
    for tc in full.tool_calls:
        result = get_weather.invoke(tc["args"])
        messages.append(ToolMessage(content=result, tool_call_id=tc["id"]))

    messages.append(HumanMessage("Now what about London?"))

    # ── Turn 2: round-trips the previous function_call → 400 ───────
    try:
        full2 = None
        async for chunk in llm.astream(messages):
            full2 = chunk if full2 is None else full2 + chunk
        print("\n=== Turn 2 succeeded (unexpected) ===")
        print(full2.content[:200] if isinstance(full2.content, str) else full2.content[:3])
    except Exception as e:
        print(f"\n=== Turn 2 FAILED (expected) ===")
        print(f"  {type(e).__name__}: {e}")


asyncio.run(main())

Error Message and Stack Trace (if applicable)

openai.BadRequestError: Error code: 400 -
{
  "error": {
    "message": "Missing namespace for function_call 'get_weather'.
               It does not exist in the default namespace.
               Round-trip the model's function_call item with its namespace field included.",
    "type": "invalid_request_error",
    "param": null,
    "code": null
  }
}

Description

When using streaming with tool_search + defer_loading, the namespace field on deferred function_call content blocks is silently dropped, causing OpenAI to reject the next request with a 400 error.

Root cause — in _convert_responses_chunk_to_generation_chunk() at base.py L4723-4732:

# response.output_item.added handler for function_call
content.append(
    {
        "type": "function_call",
        "name": chunk.item.name,
        "arguments": chunk.item.arguments,
        "call_id": chunk.item.call_id,
        "id": chunk.item.id,
        "index": current_index,
    }
)

The namespace field from chunk.item is not copied into the content block. Compare with the non-streaming path at L4488 which uses output.model_dump(exclude_none=True) and therefore preserves all fields including namespace.

The round-trip path in _construct_responses_api_input() at L4316-4331 correctly passes function_call blocks through unchanged — so if namespace were present in the content block, it would be sent back to the API correctly.

The fix is 1 line — add namespace to the dict if present on chunk.item:

content_block = {
    "type": "function_call",
    "name": chunk.item.name,
    "arguments": chunk.item.arguments,
    "call_id": chunk.item.call_id,
    "id": chunk.item.id,
    "index": current_index,
}
if hasattr(chunk.item, "namespace") and chunk.item.namespace is not None:
    content_block["namespace"] = chunk.item.namespace
content.append(content_block)

Alternatively, use chunk.item.model_dump(exclude_none=True) (matching the non-streaming path) to capture namespace and any future fields.

Workaround: Remove defer_loading from affected tools so they stay in the default namespace. This eliminates the namespace requirement but sacrifices the token-saving benefits of tool_search.

System Info

OS: Linux (WSL2 6.6.87.2-microsoft-standard-WSL2) Python Version: 3.14.2

Package Information: langchain_core: 1.2.20 langchain: 1.2.12 langchain_openai: 1.1.11 langgraph: 1.1.2 openai: 2.26.0

extent analysis

Fix Plan

To fix the issue, you need to modify the _convert_responses_chunk_to_generation_chunk() function in base.py to include the namespace field in the content block for function_call items.

Here are the steps:

  • Open the base.py file and locate the _convert_responses_chunk_to_generation_chunk() function.
  • Modify the function_call content block creation to include the namespace field:
content_block = {
    "type": "function_call",
    "name": chunk.item.name,
    "arguments": chunk.item.arguments,
    "call_id": chunk.item.call_id,
    "id": chunk.item.id,
    "index": current_index,
}
if hasattr(chunk.item, "namespace") and chunk.item.namespace is not None:
    content_block["namespace"] = chunk.item.namespace
content.append(content_block)

Alternatively, you can use chunk.item.model_dump(exclude_none=True) to capture the namespace field and any future fields:

content.append(chunk.item.model_dump(exclude_none=True))

Verification

To verify that the fix worked, run the provided example code again. The code should no longer throw a `

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