Repository navigation
Python: FunctionCallContent.combine_arguments drops a streamed "{}" chunk, producing invalid JSON arguments #14512
Description
What happens
When a streamed function call has its arguments split across chunks, FunctionCallContent.combine_arguments() silently drops any chunk that is exactly the two characters {}. The accumulated arguments string is then invalid JSON, and parse_arguments() raises FunctionCallInvalidArgumentsException.
Where
python/semantic_kernel/contents/function_call_content.py, FunctionCallContent.combine_arguments():
EMPTY_VALUES: Final[list[str | None]] = ["", "{}", None]
...
if arg1 in EMPTY_VALUES and arg2 in EMPTY_VALUES:
return "{}"
if arg1 in EMPTY_VALUES:
return arg2 or "{}"
if arg2 in EMPTY_VALUES:
return arg1 or "{}"
return (arg1 or "") + (arg2 or "")The "{}" sentinel is meant for a whole "no arguments" value, for example a first chunk that is just a placeholder. But __add__ is also used to concatenate raw token deltas (chat_completion_client_base.py does reduce(lambda x, y: x + y, all_messages) over the stream, and the OpenAI connector passes tool.function.arguments from each delta straight into FunctionCallContent(arguments=...)). A mid-stream fragment that happens to equal {} is an empty JSON object that belongs in the output, not a placeholder. It gets thrown away.
Repro
Tested with semantic-kernel==1.44.1. I diffed function_call_content.py against current main and the file is identical.
from functools import reduce
from semantic_kernel.contents import StreamingChatMessageContent, FunctionCallContent, AuthorRole
def chunk(i, args):
fc = FunctionCallContent(id="call_1" if i == 0 else None, index=0,
name="search-find" if i == 0 else None, arguments=args)
return StreamingChatMessageContent(role=AuthorRole.ASSISTANT, items=[fc], choice_index=0)
deltas = ['{"query":"x","filters":', '{}', '}']
full = reduce(lambda a, b: a + b, [chunk(i, d) for i, d in enumerate(deltas)])
call = [i for i in full.items if isinstance(i, FunctionCallContent)][0]
print("concatenated deltas:", "".join(deltas))
print("accumulated :", call.arguments)
call.parse_arguments()Output:
concatenated deltas: {"query":"x","filters":{}}
accumulated : {"query":"x","filters":}
...
semantic_kernel.exceptions.content_exceptions.FunctionCallInvalidArgumentsException: Function Call arguments are not valid JSON even after preprocessing.
Other splits fail the same way:
| deltas | accumulated | expected |
|---|---|---|
'{"a":[', '{}', ',', '{}', ']}' |
{"a":[,]} |
{"a":[{},{}]} |
'{"a": {"b":', '{}', '}}' |
{"a": {"b":}} |
{"a": {"b":{}}} |
Expected
The accumulated string equals the concatenation of the deltas, so the arguments parse to {"query": "x", "filters": {}}.
Actual
The {} delta is dropped, the JSON is broken, and the auto function invocation path fails on parse_arguments() for a call the model produced correctly.
Note on likelihood
This needs a tokenizer or provider to emit {} as its own delta. That is a valid and common token for compact JSON, and some providers chunk arbitrarily, but I did not capture it from a live model. The defect is in the merge logic and reproduces deterministically with the deltas above.
Suggested fix
Only treat "{}" as an empty placeholder when the other side is empty or is itself a complete-looking object. One option: keep "" and None as always-empty, and treat "{}" as empty only when it is the accumulated arg1 and arg2 starts a new object, or when both sides are "{}". Otherwise fall through to plain string concatenation.
I searched issues and PRs for combine_arguments, EMPTY_VALUES, FunctionCallContent.__add__ and streaming function-call argument problems. Nothing covers this.
Activity
WitBlue6 commentedon Sep 30, 2026
I can take this on. I confirmed there is no existing PR for
combine_argumentsor #14512. I plan to preserve mid-stream{}deltas while retaining the initial placeholder behavior, and add regression tests for both.VANDRANKI commentedon Oct 1, 2026
Heads up, I opened a fix for this in #14513 a few minutes before your comment (it keeps mid-stream
{}deltas and keeps the placeholder behavior, with tests for both). It already has a follow-up commit for a review finding. If you see something it misses, a review comment there would help more than a second PR.