langchain - ✅(Solved) Fix fix(openrouter): streaming reasoning_details fragmentation causes multi-turn BadRequestResponseError [2 pull requests, 1 comments, 1 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#36400Fetched 2026-04-08 01:57:52
View on GitHub
Comments
1
Participants
1
Timeline
9
Reactions
0
Author
Participants
Assignees
Timeline (top)
labeled ×3cross-referenced ×2assigned ×1commented ×1

During streaming, AIMessageChunk.__add__ list-concatenates reasoning_details in additional_kwargs, fragmenting a single entry into many. When _convert_message_to_dict() serializes conversation history back to the OpenRouter API for the next turn, these fragmented entries are passed through as-is. The API rejects the malformed payload with BadRequestResponseError.

Non-streaming (invoke()) is unaffected — it returns complete entries in a single response.

The fix should merge fragmented reasoning_details entries (same type + same index) back into single entries before serialization. A PR with the fix and tests is ready.

Error Message

BadRequestResponseError: Provider returned error

Root Cause

During streaming, AIMessageChunk.__add__ list-concatenates reasoning_details in additional_kwargs, fragmenting a single entry into many. When _convert_message_to_dict() serializes conversation history back to the OpenRouter API for the next turn, these fragmented entries are passed through as-is. The API rejects the malformed payload with BadRequestResponseError.

Non-streaming (invoke()) is unaffected — it returns complete entries in a single response.

The fix should merge fragmented reasoning_details entries (same type + same index) back into single entries before serialization. A PR with the fix and tests is ready.

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.

PR fix notes

PR #36401: fix(openrouter): merge fragmented reasoning_details in streaming

Description (problem / solution / changelog)

Description

Fixes #36400

During streaming, AIMessageChunk.__add__ list-concatenates reasoning_details in additional_kwargs, fragmenting a single entry into many. When _convert_message_to_dict() serializes conversation history back to the OpenRouter API for the next turn, these fragmented entries cause BadRequestResponseError.

Changes

  • Add _merge_reasoning_details() helper that merges consecutive entries sharing the same type and index (streaming fragments) while preserving distinct entries (legitimate non-streaming data)
  • Metadata from later fragments (e.g. signature) is preserved in the merged result
  • Entries without index are never merged (safe for non-streaming responses)
  • Call _merge_reasoning_details() in _convert_message_to_dict() before serializing reasoning_details

Why merge instead of drop?

Non-streaming users (invoke()) rely on reasoning_details for structured metadata (type, signature, format, index). Dropping it entirely would be a regression. This approach fixes streaming while preserving non-streaming functionality, similar to langchain-openai's _implode_reasoning_blocks().

Test plan

  • Fragmented entries (same type + same index) are merged into one
  • Distinct entries (different index) are preserved separately
  • Entries without index are never merged
  • Metadata from later fragments (e.g. signature) is preserved
  • Single-entry lists pass through unchanged
  • Round-trip (dict → message → dict) works correctly
  • All 210 unit tests pass

Changed files

  • libs/partners/openrouter/langchain_openrouter/chat_models.py (modified, +77/-5)
  • libs/partners/openrouter/tests/unit_tests/test_chat_models.py (modified, +161/-6)

PR #124: feat: enable reasoning for OpenRouter via extra_body to prevent multi…

Description (problem / solution / changelog)

Description

Enable OpenRouter's unified reasoning API for all models routed through OpenRouter. The reasoning config is passed via extra_body (not as a top-level kwarg) to avoid multi-turn 400 errors caused by reasoning blocks contaminating conversation history.

Background: OpenRouter normalizes reasoning across providers (Anthropic, OpenAI, Google, etc.) via a single reasoning parameter. However, both langchain-openrouter (ChatOpenRouter) and ChatOpenAI Responses API paths fail on multi-turn conversations because LangChain's serialization of reasoning blocks doesn't match what the upstream API expects on re-submission. Using extra_body routes through Chat Completions, where the model reasons internally (reasoning_tokens > 0) but reasoning content is not returned in the response, keeping conversation history clean.

What changed:

  • EvoScientist/llm/models.py: Added extra_body.reasoning = {"effort": "high", "summary": "auto"} for OpenRouter provider in the _OPENAI_ROUTED_PROVIDERS handler block

Type of change

  • New feature — link issue: #

Checklist

  • I have read the Contributing Guidelines
  • This targets core functionality used by the majority of users
  • I have added/updated tests where applicable
  • uv run ruff check . passes
  • uv run pytest passes
<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

Summary by CodeRabbit

  • New Features

    • Dedicated support for OpenRouter with user-configurable reasoning effort and automatic default reasoning parameters.
    • Interactive onboarding step to configure reasoning effort when OpenRouter is selected.
  • Bug Fixes / Compatibility

    • Improved provider compatibility and sanitized AI message content by stripping internal thinking/reasoning fragments and flattening list-based message content.
  • Tests

    • Updated tests to validate OpenRouter routing, reasoning defaults, and content-sanitization behavior.
  • Chores

    • Added OpenRouter LangChain integration dependency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Changed files

  • EvoScientist/cli/tui_interactive.py (modified, +10/-2)
  • EvoScientist/cli/widgets/timestamp_mixin.py (modified, +2/-1)
  • EvoScientist/cli/widgets/tool_call_widget.py (modified, +2/-1)
  • EvoScientist/config/onboard.py (modified, +42/-0)
  • EvoScientist/config/settings.py (modified, +3/-0)
  • EvoScientist/llm/models.py (modified, +27/-150)
  • EvoScientist/llm/patches.py (added, +192/-0)
  • EvoScientist/middleware/tool_selector.py (modified, +4/-4)
  • EvoScientist/stream/events.py (modified, +4/-0)
  • pyproject.toml (modified, +1/-0)
  • tests/test_llm.py (modified, +41/-14)

Code Example

from langchain_openrouter import ChatOpenRouter
from langchain_core.messages import AIMessage, HumanMessage

model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4-6",
    api_key="your-key-here",
    reasoning={"effort": "high", "summary": "auto"},
)

# Turn 1 — streaming
msgs = [HumanMessage(content="What is 2+2? Think step by step.")]
chunks = list(model.stream(msgs))
merged = chunks[0]
for c in chunks[1:]:
    merged = merged + c

# Inspect: reasoning_details is fragmented (e.g. 6 entries instead of 1)
details = merged.additional_kwargs.get("reasoning_details", [])
print(f"reasoning_details count: {len(details)}")  # Expected: 1, Actual: 6

# Turn 2 — fails
ai_msg = AIMessage(
    content=merged.content,
    additional_kwargs=merged.additional_kwargs,
    response_metadata=merged.response_metadata,
)
msgs.append(ai_msg)
msgs.append(HumanMessage(content="Now what is 3+3?"))

# This raises BadRequestResponseError
chunks2 = list(model.stream(msgs))

---

BadRequestResponseError: Provider returned error
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

No response

Reproduction Steps / Example Code (Python)

from langchain_openrouter import ChatOpenRouter
from langchain_core.messages import AIMessage, HumanMessage

model = ChatOpenRouter(
    model="anthropic/claude-sonnet-4-6",
    api_key="your-key-here",
    reasoning={"effort": "high", "summary": "auto"},
)

# Turn 1 — streaming
msgs = [HumanMessage(content="What is 2+2? Think step by step.")]
chunks = list(model.stream(msgs))
merged = chunks[0]
for c in chunks[1:]:
    merged = merged + c

# Inspect: reasoning_details is fragmented (e.g. 6 entries instead of 1)
details = merged.additional_kwargs.get("reasoning_details", [])
print(f"reasoning_details count: {len(details)}")  # Expected: 1, Actual: 6

# Turn 2 — fails
ai_msg = AIMessage(
    content=merged.content,
    additional_kwargs=merged.additional_kwargs,
    response_metadata=merged.response_metadata,
)
msgs.append(ai_msg)
msgs.append(HumanMessage(content="Now what is 3+3?"))

# This raises BadRequestResponseError
chunks2 = list(model.stream(msgs))

Error Message and Stack Trace (if applicable)

BadRequestResponseError: Provider returned error

Description

During streaming, AIMessageChunk.__add__ list-concatenates reasoning_details in additional_kwargs, fragmenting a single entry into many. When _convert_message_to_dict() serializes conversation history back to the OpenRouter API for the next turn, these fragmented entries are passed through as-is. The API rejects the malformed payload with BadRequestResponseError.

Non-streaming (invoke()) is unaffected — it returns complete entries in a single response.

The fix should merge fragmented reasoning_details entries (same type + same index) back into single entries before serialization. A PR with the fix and tests is ready.

System Info

System Information

OS: Darwin OS Version: Darwin Kernel Version 25.3.0 Python Version: 3.11.14

Package Information

langchain_core: 1.2.23 langchain: 1.2.13 langchain_openrouter: 0.2.1 langchain_openai: 1.1.11

Other Dependencies

openrouter: 0.8.0 pydantic: 2.12.5

extent analysis

TL;DR

The most likely fix is to merge fragmented reasoning_details entries before serializing conversation history back to the OpenRouter API.

Guidance

  • Identify and merge reasoning_details entries with the same type and index in the additional_kwargs dictionary of AIMessage objects.
  • Modify the _convert_message_to_dict() function to handle the merged reasoning_details entries correctly.
  • Test the fix with both streaming and non-streaming scenarios to ensure it works as expected.
  • Verify that the BadRequestResponseError is no longer raised after applying the fix.

Example

# Merge fragmented reasoning_details entries
def merge_reasoning_details(details):
    merged_details = {}
    for detail in details:
        key = (detail['type'], detail['index'])
        if key in merged_details:
            merged_details[key].update(detail)
        else:
            merged_details[key] = detail
    return list(merged_details.values())

# Update the additional_kwargs dictionary with merged reasoning_details
merged.additional_kwargs['reasoning_details'] = merge_reasoning_details(merged.additional_kwargs.get('reasoning_details', []))

Notes

The provided code snippet assumes that the reasoning_details entries have a type and index field. If the actual structure of the reasoning_details entries is different, the merging logic may need to be adjusted accordingly.

Recommendation

Apply the workaround by merging fragmented reasoning_details entries before serializing conversation history back to the OpenRouter API. This should fix the BadRequestResponseError issue until a more permanent solution is implemented.

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