From 4498d3dc84a85688fa4d15476403a900bc7f9114 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Tue, 5 May 2026 11:10:55 -0400 Subject: [PATCH] fix(fireworks): strip non-wire keys from `ToolMessage` text content blocks (#37187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fireworks's chat completions endpoint rejects unknown fields on tool message content blocks — specifically the `id` key that LangChain auto-generates on `TextContentBlock`. Add `_sanitize_chat_completions_content` to strip those extra keys before the payload hits the wire, preventing `Extra inputs are not permitted` errors on tool message round-trips. --- .../langchain_fireworks/chat_models.py | 23 +++++++++++++++- .../tests/unit_tests/test_chat_models.py | 27 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/libs/partners/fireworks/langchain_fireworks/chat_models.py b/libs/partners/fireworks/langchain_fireworks/chat_models.py index b05c83f3cdd..671d03d24c4 100644 --- a/libs/partners/fireworks/langchain_fireworks/chat_models.py +++ b/libs/partners/fireworks/langchain_fireworks/chat_models.py @@ -170,6 +170,25 @@ def _convert_dict_to_message(_dict: Mapping[str, Any]) -> BaseMessage: return ChatMessage(content=_dict.get("content", ""), role=role or "") +def _sanitize_chat_completions_content(content: Any) -> Any: + """Strip non-wire keys from text content blocks. + + Fireworks's chat completions endpoint rejects unknown fields on tool + message content blocks (e.g. the `id` that LangChain auto-generates on + `TextContentBlock`). For list content, keep only `type` and `text` on + text blocks; pass other blocks and non-list content through unchanged. + """ + if not isinstance(content, list): + return content + sanitized: list[Any] = [] + for block in content: + if isinstance(block, dict) and block.get("type") == "text" and "text" in block: + sanitized.append({"type": "text", "text": block["text"]}) + else: + sanitized.append(block) + return sanitized + + def _format_message_content(content: Any) -> Any: """Format message content for the Fireworks chat completions wire format. @@ -296,7 +315,9 @@ def _convert_message_to_dict(message: BaseMessage) -> dict: elif isinstance(message, ToolMessage): message_dict = { "role": "tool", - "content": _format_message_content(message.content), + "content": _sanitize_chat_completions_content( + _format_message_content(message.content) + ), "tool_call_id": message.tool_call_id, } else: diff --git a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py index 785928214fe..3259cde5726 100644 --- a/libs/partners/fireworks/tests/unit_tests/test_chat_models.py +++ b/libs/partners/fireworks/tests/unit_tests/test_chat_models.py @@ -31,6 +31,7 @@ from langchain_fireworks.chat_models import ( _convert_dict_to_message, _convert_message_to_dict, _format_message_content, + _sanitize_chat_completions_content, _usage_to_metadata, ) @@ -106,6 +107,32 @@ def test_format_message_content_passthrough_string() -> None: assert _format_message_content("hello") == "hello" +def test_sanitize_chat_completions_text_blocks_strips_id() -> None: + """LangChain auto-generated `id` on text blocks must not reach the wire. + + Fireworks's chat completions schema rejects unknown keys on tool message + content blocks (`Extra inputs are not permitted, ... [0].id`). + """ + message = ToolMessage( + content=[{"type": "text", "text": "foo", "id": "lc_abc123"}], + tool_call_id="def456", + ) + assert _convert_message_to_dict(message) == { + "role": "tool", + "content": [{"type": "text", "text": "foo"}], + "tool_call_id": "def456", + } + + +def test_sanitize_chat_completions_content_passthrough_string() -> None: + assert _sanitize_chat_completions_content("hello") == "hello" + + +def test_sanitize_chat_completions_content_passthrough_non_text_block() -> None: + blocks = [{"type": "image_url", "image_url": {"url": "https://x/y.png"}}] + assert _sanitize_chat_completions_content(blocks) == blocks + + def test_format_message_content_translates_v1_image_block() -> None: """Canonical v1 image block is translated to OpenAI image_url + data URI.""" blocks = [{"type": "image", "base64": "abc", "mime_type": "image/png"}]