Skip to content

fix(adk): reject malformed tool call arguments from OpenAI chat completions - #3069

Open
thc1006 wants to merge 1 commit into
kagent-dev:mainfrom
thc1006:fix/openai-invalid-tool-arguments
Open

thc1006 wants to merge 1 commit into
kagent-dev:mainfrom
thc1006:fix/openai-invalid-tool-arguments

Conversation

@thc1006

@thc1006 thc1006 commented Oct 6, 2026

Copy link
Copy Markdown

The OpenAI chat completions adapter ignores the error from json.Unmarshal when it decodes tool call arguments (L462 and L521). When the model hits the token limit in the middle of a tool call, the arguments are cut off, the tool runs with no arguments, and the turn carries on as if nothing went wrong.

This fails the turn instead, with ErrorCode set to MALFORMED_FUNCTION_CALL, along the lines of this review comment on #2230. None of the tool calls in that response run, not even complete ones next to the broken one, so a batch of parallel calls never runs halfway. Text before the calls is kept. Blank arguments still mean a call without arguments, except when the finish reason is length, because then the call may have been cut off before its arguments. That check relies on the server reporting length; arguments cut off mid-JSON fail either way.

I used ErrorCode rather than a Go error. This file already reports STREAM_ERROR and API_ERROR that way, adka2a marks the task failed on it, and the turn's text and event still reach the session. With a Go error, the session keeps only the user's message. adk-go's own OpenAI model returns a Go error (ErrFunctionCallArgs) for arguments it can't decode, so this is a deliberate difference.

Some responses whose tool calls used to run silently now fail the turn: arguments that are valid JSON but not an object (null still means no arguments), streamed parallel calls without an index (main merges them into one call and runs it with no arguments), and chunks that repeat the full arguments. Azure OpenAI, Mistral and Foundry (except Claude deployments) use this adapter, so they behave the same way.

This is separate from #2896, which fails the task when the last response stops at max_tokens. A cut-off tool call runs before that point.

In Go, the Responses API path, SAP AI Core, Anthropic streaming and Bedrock (which passes _raw) still don't fail the turn. In Python, the OpenAI and SAP AI Core adapters fall back to {}, and Bedrock streaming fails the turn with API_ERROR. I'd rather do those in follow-up PRs, as suggested on #2230.

Testing

TestOpenAIModelGenerateContentRejectsMalformedToolArguments runs both stream modes against an httptest server, with valid arguments, blank and null arguments, arguments cut off mid-JSON, a call cut off before its arguments, one of two parallel calls cut off, and arguments that aren't an object. The four error cases fail on main and pass here. The other two pass on both.

go test -race ./adk/... passes, and make -C go lint is clean.

…etions

The OpenAI chat completions adapter discarded json.Unmarshal errors for
tool call arguments, so a call whose arguments were cut off at the token
limit still ran, with no arguments.

Fail the turn with MALFORMED_FUNCTION_CALL instead, and run none of the
tool calls in that response. Blank arguments still mean a call without
arguments, unless the response was cut off at the token limit.

Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
@thc1006
thc1006 requested a review from supreme-gg-gg as a code owner October 6, 2026 01:00
@github-actions github-actions Bot added the bug Something isn't working label Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant