fix(chatcompletions): repair truncated tool-call args before emission

A streaming tool call cut off after the opening brace would leave
state.function_calls[i].arguments = "{". CAI persisted that into
conversation history verbatim, and on the next turn the upstream
litellm proxy strict-parsed it as JSON and rejected the whole
request with HTTP 400 ("unexpected end of data: line 1 column 2"),
wedging the session.

- openai_chatcompletions.py: at end-of-stream, validate every
  accumulated function_call.arguments string. If empty or not
  valid JSON, normalize to "{}" before emitting events.
- message_builder.py: same guard when replaying assistant tool_calls
  out of memory, so a poisoned history loaded from disk also recovers.
This commit is contained in:
Yahya 2026-06-08 09:58:27 +02:00 committed by Rufino Cabrera
parent 23739d0631
commit 967762950c
2 changed files with 21 additions and 0 deletions

View File

@ -424,6 +424,14 @@ class Converter:
arguments = "{}"
elif isinstance(arguments, dict):
arguments = json.dumps(arguments)
else:
# Truncated/streamed-then-cut function call args (e.g. "{")
# are not valid JSON and the upstream proxy rejects the whole
# request with HTTP 400, wedging the conversation.
try:
json.loads(arguments)
except (TypeError, ValueError):
arguments = "{}"
tool_calls_param.append(
ChatCompletionMessageToolCallParam(
id=tc.get("id", "")[:40],

View File

@ -2676,6 +2676,19 @@ class OpenAIChatCompletionsModel(Model):
type="response.content_part.done",
)
# Repair partially-streamed function calls before emission so a
# truncated args string like "{" cannot poison conversation history
# and trigger HTTP 400 on the next request.
for _fc in state.function_calls.values():
_args = _fc.arguments or ""
if not _args.strip():
_fc.arguments = "{}"
else:
try:
json.loads(_args)
except (TypeError, ValueError):
_fc.arguments = "{}"
# Actually send events for the function calls
for function_call in state.function_calls.values():
# First, a ResponseOutputItemAdded for the function call