Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion src/agents/models/chatcmpl_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -581,7 +581,26 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
pending_thinking_blocks = None # Clear after using

tool_calls = list(asst.get("tool_calls", []))
arguments = func_call["arguments"] if func_call["arguments"] else "{}"
raw_args = func_call["arguments"]
# Validate JSON arguments to prevent downstream parsing failures
# Invalid JSON can break sessions with Anthropic models (issue #2061)
if raw_args:
try:
json.loads(raw_args) # Validate JSON is parseable
arguments = raw_args
except json.JSONDecodeError:
# Invalid JSON - use empty object to prevent session corruption
from ..logger import logger
logger.warning(
f"Invalid JSON in tool call arguments for '{func_call['name']}', "
f"replacing with empty object. Original: {raw_args[:100]}..."
if len(raw_args) > 100 else
f"Invalid JSON in tool call arguments for '{func_call['name']}', "
f"replacing with empty object. Original: {raw_args}"
)
arguments = "{}"
else:
arguments = "{}"
new_tool_call = ChatCompletionMessageFunctionToolCallParam(
id=func_call["call_id"],
type="function",
Expand Down
48 changes: 48 additions & 0 deletions tests/test_openai_chatcompletions_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,3 +467,51 @@ def test_assistant_messages_in_history():
assert messages[1]["content"] == "Hello?"
assert messages[2]["role"] == "user"
assert messages[2]["content"] == "What was my Name?"


def test_items_to_messages_handles_invalid_json_arguments():
"""
Invalid JSON in tool call arguments should be replaced with empty object
to prevent session corruption with Anthropic models (issue #2061).
"""
# Construct a function call with invalid JSON (missing closing brace)
function_call: ResponseFunctionToolCallParam = {
"id": "fc_invalid",
"call_id": "call_invalid_json",
"name": "get_server_time",
"arguments": '{"format":"%Y-%m-%d"', # Invalid JSON - missing closing brace
"type": "function_call",
}

messages = Converter.items_to_messages([function_call])
assert len(messages) == 1
tool_msg = messages[0]
assert tool_msg["role"] == "assistant"

tool_calls = list(tool_msg.get("tool_calls", []))
assert len(tool_calls) == 1

tool_call = tool_calls[0]
# Invalid JSON should be replaced with empty object
assert tool_call["function"]["arguments"] == "{}"


def test_items_to_messages_preserves_valid_json_arguments():
"""
Valid JSON in tool call arguments should be preserved unchanged.
"""
valid_json = '{"format": "%Y-%m-%d %H:%M:%S", "timezone": "UTC"}'
function_call: ResponseFunctionToolCallParam = {
"id": "fc_valid",
"call_id": "call_valid_json",
"name": "get_server_time",
"arguments": valid_json,
"type": "function_call",
}

messages = Converter.items_to_messages([function_call])
tool_calls = list(messages[0].get("tool_calls", []))

# Valid JSON should be preserved
assert tool_calls[0]["function"]["arguments"] == valid_json