Hi all,
I reported what appears to be a bug a few days ago but it may have been missed, so I’m resurfacing it here in case a maintainer can take a look.
When aggregating streamed AIMessageChunks, merge_dicts concatenates identical string metadata fields such as model_name and finish_reason, resulting in values like stopstop and duplicated model names. The issue seems to stem from an incomplete deduplication allowlist in merge_dicts.
GitHub issue with a minimal reproducible example, root cause analysis, and proposed fix:
opened 04:52PM - 22 Jun 26 UTC
bug
core
external
### Submission checklist
- [x] This is a bug, not a usage question.
- [x] I add… ed a clear and descriptive title that summarizes this issue.
- [x] I used the GitHub search to find a similar question and didn't find it.
- [x] I am sure that this is a bug in LangChain rather than my code.
- [x] The bug is not resolved by updating to the latest stable version of LangChain (or the specific integration package).
- [x] This is not related to the langchain-community package.
- [x] 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
- [x] 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)
```python
import os
from langchain_openrouter import ChatOpenRouter
from pydantic import SecretStr
llm = ChatOpenRouter(
api_key=SecretStr("sk-or-v1-..."),
model_name="deepseek/deepseek-v4-pro-20260423",
)
# Stream and manually aggregate chunks
result = llm.stream("Hello")
final = None
for chunk in result:
final = chunk if final is None else final + chunk
print(final.response_metadata.get("model_name"))
# Output: "deepseek/deepseek-v4-pro-20260423deepseek/deepseek-v4-pro-20260423"
# Expected: "deepseek/deepseek-v4-pro-20260423"
print(final.response_metadata.get("finish_reason"))
# Output: "stopstop"
# Expected: "stop"
if __name__ == "__main__":
pass
```
### Error Message and Stack Trace (if applicable)
```shell
{'model_provider': 'openrouter'} # × ~19 intermediate content chunks
...
{'finish_reason': 'stop', 'model_name': 'deepseek/deepseek-v4-pro-20260423', 'id': 'gen-...', 'object': 'chat.completion.chunk', 'model_provider': 'openrouter'}
{'finish_reason': 'stop', 'model_name': 'deepseek/deepseek-v4-pro-20260423', 'id': 'gen-...', 'object': 'chat.completion.chunk', 'model_provider': 'openrouter', 'cost': 0.00028218, ...}
{}
---
OpenRouter sends **two terminal chunks** with `finish_reason: "stop"` — one without usage, one with. Both carry identical `model_name`, `finish_reason`, `object`, `id`. Aggregating them via `AIMessageChunk.__add__` calls `merge_dicts`, which concatenates equal string values instead of deduplicating them.
```
### Description
`merge_dicts` in `/libs/core/langchain_core/utils/_merge.py` has an existing guard for this exact failure mode, but its allowlist is incomplete:
```python
# Current code — only skips 3 keys
if (right_k == "index" and merged[right_k].startswith("lc_")) or (
right_k in {"id", "output_version", "model_provider"}
and merged[right_k] == right_v
):
continue
merged[right_k] += right_v # ← identical strings self-concatenate here
```
`model_name`, `finish_reason`, `object`, `system_fingerprint`, and `format` (in `reasoning_details`) all fall through to `+=`.
**Proposed fix:**
```python
if (right_k == "index" and merged[right_k].startswith("lc_")) or (
right_k in {
"id", "output_version", "model_provider",
"model_name", "finish_reason", "native_finish_reason",
"object", "system_fingerprint", "format",
}
and merged[right_k] == right_v
):
continue
```
Same pattern already used for `model_provider` — just extend the set to the other stable metadata fields.
**Note:** `.invoke()` is unaffected (single response object, no chunk merge). Bug only manifests with `.stream()` / `.astream()` when the provider sends more than one terminal chunk.
### System Info
System Information
------------------
> OS: Linux
> OS Version: #1 SMP PREEMPT_DYNAMIC Wed, 10 Jun 2026 08:58:02 +0000
> Python Version: 3.12.13 (main, Jun 11 2026, 01:09:00) [GCC 14.2.0]
Package Information
-------------------
> langchain_core: 1.4.8
> langchain: 1.3.10
> langsmith: 0.8.17
> deepagents: 0.6.11
> langchain_anthropic: 1.4.6
> langchain_google_genai: 4.2.5
> langchain_mcp_adapters: 0.3.0
> langchain_mcp_filters: 0.1.0
> langchain_openai: 1.3.2
> langchain_openrouter: 0.2.3
> langchain_protocol: 0.0.18
> langgraph_sdk: 0.4.2
Optional packages not installed
-------------------------------
> deepagents-cli
Other Dependencies
------------------
> anthropic: 0.111.0
> filetype: 1.2.0
> google-genai: 2.9.0
> httpx: 0.28.1
> jsonpatch: 1.33
> langgraph: 1.2.6
> mcp: 1.28.0
> openai: 2.43.0
> openrouter: 0.10.0
> orjson: 3.11.9
> packaging: 26.2
> pydantic: 2.13.4
> pyyaml: 6.0.3
> requests: 2.34.2
> requests-toolbelt: 1.0.0
> rich: 15.0.0
> tenacity: 9.1.4
> tiktoken: 0.13.0
> typing-extensions: 4.15.0
> uuid-utils: 0.16.2
> wcmatch: 10.1
> websockets: 15.0.1
> xxhash: 3.7.0
> zstandard: 0.25.0
@codeonym Better if you tag maintainers on GitHub as well, specifically @mdrxy
thanks for mentioning that