fix(fireworks): preserve malformed tool arguments as diagnostic JSON (#40818)

`ChatFireworks` can replay historical tool calls with malformed or
non-object JSON arguments without forwarding invalid argument strings to
Fireworks.

---

Fireworks rejects conversation history containing malformed or
non-object tool-call arguments, preventing an agent from recovering on
its next turn.

Wrap these arguments in a JSON object under
`__invalid_tool_call_arguments` when serializing history, preserving the
original payload, call IDs, and tool-result pairing. Apply this to
parsed and raw tool-call history without mutating messages or executing
repaired arguments; valid object strings stay unchanged.

Made by [Open SWE](https://github.com/langchain-ai/open-swe) · [view
thread](https://openswe.vercel.app/agents/0da06f8d-3ea1-5f01-aa55-953acb9a71fa)
· openai:gpt-6-astra (medium)

Co-authored-by: Mason Daugherty <mdrxy@users.noreply.github.com>
Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2026-09-24 15:40:49 -04:00
1 parent c5ab14d42a
commit fbd70b73d4
2 files changed
+89 -1

No files matched your search

@@ -343,6 +343,21 @@ def _format_message_content(content: Any) -> Any:
return formatted
def _format_tool_call_arguments(arguments: str | dict | None) -> str | dict:
"""Preserve invalid historical arguments inside a JSON object for replay."""
if isinstance(arguments, dict):
return arguments
try:
parsed = json.loads(arguments) if arguments is not None else None
if isinstance(parsed, dict) and arguments is not None:
json.dumps(parsed, allow_nan=False)
return arguments
except ValueError:
logger.debug("Invalid JSON in historical Fireworks tool call arguments")
logger.warning("Wrapping invalid historical Fireworks tool call arguments")
return json.dumps({"__invalid_tool_call_arguments": arguments}, ensure_ascii=False)
def _convert_message_to_dict(message: BaseMessage) -> dict:
"""Convert a LangChain message to a dictionary.
@@ -392,6 +407,19 @@ def _convert_message_to_dict(message: BaseMessage) -> dict:
]
elif "tool_calls" in message.additional_kwargs:
message_dict["tool_calls"] = message.additional_kwargs["tool_calls"]
if "tool_calls" in message_dict:
message_dict["tool_calls"] = [
{
**tool_call,
"function": {
**tool_call["function"],
"arguments": _format_tool_call_arguments(
tool_call["function"]["arguments"]
),
},
}
for tool_call in message_dict["tool_calls"]
]
# If tool calls only, content is None not empty string
if "tool_calls" in message_dict and message_dict["content"] == "":
message_dict["content"] = None
@@ -2,6 +2,7 @@
from __future__ import annotations
import json
import logging
import os
from typing import Any
@@ -205,12 +206,71 @@ def test_convert_v1_message_filters_invalid_tool_call_content() -> None:
{
"type": "function",
"id": "call_invalid",
"function": {"name": "get_weather", "arguments": '{"city":'},
"function": {
"name": "get_weather",
"arguments": json.dumps(
{"__invalid_tool_call_arguments": '{"city":'}
),
},
}
],
}
@pytest.mark.parametrize("raw_history", [False, True])
@pytest.mark.parametrize(
("arguments", "wrapped"),
[
('{"city":', True),
("[]", True),
("null", True),
(None, True),
('{"x": NaN}', True),
('{"city": "Paris"}', False),
],
)
def test_replay_invalid_tool_call_arguments(
arguments: str | None, *, wrapped: bool, raw_history: bool
) -> None:
message = AIMessage(
content="",
tool_calls=[{"name": "get_weather", "args": {"city": "Paris"}, "id": "valid"}],
invalid_tool_calls=[
{"name": "get_weather", "args": arguments, "id": "invalid", "error": "bad"}
],
)
if raw_history:
message.additional_kwargs["tool_calls"] = [
{
"type": "function",
"id": "invalid",
"function": {"name": "get_weather", "arguments": arguments},
}
]
message.tool_calls = []
message.invalid_tool_calls = []
original = message.model_dump()
result = _convert_message_to_dict(message)
assert message.model_dump() == original
invalid = result["tool_calls"][-1]
assert invalid["id"] == "invalid"
assert invalid["function"]["name"] == "get_weather"
if wrapped:
assert json.loads(invalid["function"]["arguments"]) == {
"__invalid_tool_call_arguments": arguments
}
else:
assert invalid["function"]["arguments"] == arguments
if not raw_history:
assert json.loads(result["tool_calls"][0]["function"]["arguments"]) == {
"city": "Paris"
}
tool_result = ToolMessage(content="Invalid JSON", tool_call_id="invalid")
assert _convert_message_to_dict(tool_result)["tool_call_id"] == invalid["id"]
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