Skip to content

Commit 7dbff48

Browse files
authored
fix(mistralai): strip non-wire keys from ToolMessage (#37188)
Same as #37187
1 parent 913816c commit 7dbff48

2 files changed

Lines changed: 40 additions & 1 deletion

File tree

libs/partners/mistralai/langchain_mistralai/chat_models.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,25 @@ def _clean_block(block: dict) -> dict:
373373
return new_block
374374

375375

376+
def _sanitize_chat_completions_content(content: Any) -> Any:
377+
"""Strip non-wire keys from text content blocks.
378+
379+
Mistral's chat completions endpoint rejects unknown fields on tool
380+
message content blocks (e.g. the `id` that LangChain auto-generates on
381+
`TextContentBlock`). For list content, keep only `type` and `text` on
382+
text blocks; pass other blocks and non-list content through unchanged.
383+
"""
384+
if not isinstance(content, list):
385+
return content
386+
sanitized: list[Any] = []
387+
for block in content:
388+
if isinstance(block, dict) and block.get("type") == "text" and "text" in block:
389+
sanitized.append({"type": "text", "text": block["text"]})
390+
else:
391+
sanitized.append(block)
392+
return sanitized
393+
394+
376395
def _format_message_content(content: Any) -> Any:
377396
"""Format message content for the Mistral chat completions wire format.
378397
@@ -484,7 +503,7 @@ def _convert_message_to_mistral_chat_message(
484503
if isinstance(message, ToolMessage):
485504
return {
486505
"role": "tool",
487-
"content": message.content,
506+
"content": _sanitize_chat_completions_content(message.content),
488507
"name": message.name,
489508
"tool_call_id": _convert_tool_call_id_to_mistral_compatible(
490509
message.tool_call_id

libs/partners/mistralai/tests/unit_tests/test_chat_models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
InvalidToolCall,
1717
SystemMessage,
1818
ToolCall,
19+
ToolMessage,
1920
)
2021
from pydantic import SecretStr
2122

@@ -26,11 +27,30 @@
2627
_convert_tool_call_id_to_mistral_compatible,
2728
_format_message_content,
2829
_is_valid_mistral_tool_call_id,
30+
_sanitize_chat_completions_content,
2931
)
3032

3133
os.environ["MISTRAL_API_KEY"] = "foo"
3234

3335

36+
def test_sanitize_chat_completions_text_blocks_strips_id() -> None:
37+
"""LangChain auto-generated `id` on text blocks must not reach the wire.
38+
39+
Mistral's chat completions endpoint returns 422 with `extra_forbidden`
40+
on `messages[*].tool.content.list[...].text.id` if not stripped.
41+
"""
42+
message = ToolMessage(
43+
content=[{"type": "text", "text": "foo", "id": "lc_abc123"}],
44+
tool_call_id="abc12345",
45+
)
46+
result = _convert_message_to_mistral_chat_message(message)
47+
assert result["content"] == [{"type": "text", "text": "foo"}]
48+
49+
50+
def test_sanitize_chat_completions_content_passthrough_string() -> None:
51+
assert _sanitize_chat_completions_content("hello") == "hello"
52+
53+
3454
def test_mistralai_model_param() -> None:
3555
llm = ChatMistralAI(model="foo") # type: ignore[call-arg]
3656
assert llm.model == "foo"

0 commit comments

Comments
 (0)