Skip to content

fix(security): prevent streaming tool calls from finalizing on parsable partial JSON#13137

Merged
gr2m merged 3 commits intomainfrom
fix/streaming-tool-call-early-finalization
Mar 5, 2026
Merged

fix(security): prevent streaming tool calls from finalizing on parsable partial JSON#13137
gr2m merged 3 commits intomainfrom
fix/streaming-tool-call-early-finalization

Conversation

@gr2m
Copy link
Copy Markdown
Collaborator

@gr2m gr2m commented Mar 5, 2026

Background

In 5 OpenAI-compatible providers (openai, openai-compatible, groq, deepseek, alibaba), streaming tool call arguments were finalized using isParsableJson() as a heuristic for completion. If partial accumulated JSON happened to be valid JSON before all chunks arrived, the tool call would be executed with incomplete arguments, potentially dropping safety-relevant parameters.

For example, a model streaming {"query": "test", "limit": 10} might produce {"query": "test"} as an intermediate value — which is valid JSON. The tool call would be finalized early with only {"query": "test"}, and the "limit": 10 parameter would be silently lost.

Providers not affected: anthropic (uses explicit content_block_stop events), OpenAI Responses API (different protocol).

Summary

  • Removed all inline isParsableJson() checks from the streaming tool call delta handlers across 5 providers
  • Moved tool call finalization to the flush() handler, which only runs after the stream is fully consumed, ensuring tool calls always receive complete arguments
  • Added a regression test demonstrating the vulnerability (first commit fails against unfixed code, second commit fixes it)
  • Updated existing test snapshots to reflect the new ordering (tool calls finalize in flush() after text-end, rather than inline during streaming)

Manual Verification

The first commit (573a744) adds the regression test only — it fails against the unfixed code, proving the vulnerability exists:

- Expected: input: '{"query": "test"}, "limit": 10}'
+ Received: input: '{"query": "test"}'

The second commit (7ae73dd) applies the fix, making all tests pass.

Checklist

  • Tests have been added / updated (for bug fixes / features)
  • Documentation has been added / updated (for bug fixes / features)
  • A patch changeset for relevant packages has been added (for bug fixes / features - run pnpm changeset in the project root)
  • I have reviewed this pull request (self-review)

Related Issues

VULN-6861

gr2m and others added 2 commits March 5, 2026 13:25
…ization

Demonstrates the vulnerability: when streamed tool call arguments form
valid JSON before all chunks have arrived (e.g., {"query": "test"} when
the full args are {"query": "test", "limit": 10}), the tool call is
finalized prematurely with incomplete arguments.

This test is expected to FAIL until the fix is applied.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…le partial JSON

Streaming tool call arguments were finalized using isParsableJson() as a
heuristic for completion. If partial accumulated JSON happened to be valid
JSON before all chunks arrived, the tool call would be executed with
incomplete arguments (e.g., missing safety-relevant parameters).

Move tool call finalization from inline isParsableJson checks to the
flush() handler, which only runs after the stream is fully consumed.
This ensures tool calls always receive complete arguments.

Affected providers: openai, openai-compatible, groq, deepseek, alibaba.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@tigent tigent bot added ai/core core functions like generateText, streamText, etc. Provider utils, and provider spec. ai/provider related to a provider package. Must be assigned together with at least one `provider/*` label bug Something isn't working as documented provider/deepseek Issues related to the @ai-sdk/deepseek provider provider/groq Issues related to the @ai-sdk/groq provider provider/openai Issues related to the @ai-sdk/openai provider provider/openai-compatible Issues related to the @ai-sdk/openai-compatible provider labels Mar 5, 2026
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gr2m gr2m marked this pull request as ready for review March 5, 2026 22:18
@gr2m
Copy link
Copy Markdown
Collaborator Author

gr2m commented Mar 5, 2026

This should be good to go now.

I also wondered if we can get rid of the code duplication, I created a draft PR of how that could look like:
https://github.com/vercel/ai/pull/13141/changes

@gr2m gr2m added the backport Admins only: add this label to a pull request in order to backport it to the prior version label Mar 5, 2026
@gr2m gr2m enabled auto-merge (squash) March 5, 2026 22:32
@gr2m gr2m merged commit 45b3d76 into main Mar 5, 2026
22 of 23 checks passed
@gr2m gr2m deleted the fix/streaming-tool-call-early-finalization branch March 5, 2026 22:45
vercel-ai-sdk bot pushed a commit that referenced this pull request Mar 5, 2026
…le partial JSON (#13137)

## Background

In 5 OpenAI-compatible providers (`openai`, `openai-compatible`, `groq`,
`deepseek`, `alibaba`), streaming tool call arguments were finalized
using `isParsableJson()` as a heuristic for completion. If partial
accumulated JSON happened to be valid JSON before all chunks arrived,
the tool call would be executed with incomplete arguments, potentially
dropping safety-relevant parameters.

For example, a model streaming `{"query": "test", "limit": 10}` might
produce `{"query": "test"}` as an intermediate value — which is valid
JSON. The tool call would be finalized early with only `{"query":
"test"}`, and the `"limit": 10` parameter would be silently lost.

Providers not affected: `anthropic` (uses explicit `content_block_stop`
events), OpenAI Responses API (different protocol).

## Summary

- Removed all inline `isParsableJson()` checks from the streaming tool
call delta handlers across 5 providers
- Moved tool call finalization to the `flush()` handler, which only runs
after the stream is fully consumed, ensuring tool calls always receive
complete arguments
- Added a regression test demonstrating the vulnerability (first commit
fails against unfixed code, second commit fixes it)
- Updated existing test snapshots to reflect the new ordering (tool
calls finalize in `flush()` after text-end, rather than inline during
streaming)

## Manual Verification

The first commit (`573a744`) adds the regression test only — it fails
against the unfixed code, proving the vulnerability exists:
```
- Expected: input: '{"query": "test"}, "limit": 10}'
+ Received: input: '{"query": "test"}'
```

The second commit (`7ae73dd`) applies the fix, making all tests pass.

## Checklist

- [x] Tests have been added / updated (for bug fixes / features)
- [ ] Documentation has been added / updated (for bug fixes / features)
- [x] A _patch_ changeset for relevant packages has been added (for bug
fixes / features - run `pnpm changeset` in the project root)
- [x] I have reviewed this pull request (self-review)

## Related Issues

VULN-6861

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@vercel-ai-sdk vercel-ai-sdk bot removed the backport Admins only: add this label to a pull request in order to backport it to the prior version label Mar 5, 2026
@vercel-ai-sdk
Copy link
Copy Markdown
Contributor

vercel-ai-sdk bot commented Mar 5, 2026

✅ Backport PR created: #13145

// Regression test: if streamed tool call arguments form valid JSON before
// all chunks have arrived, the tool call must NOT be finalized early.
// For example, {"query": "test"} is valid JSON but the full args are
// {"query": "test", "limit": 10}. Finalizing early would lose "limit".
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this argument is not correct

@vercel-ai-sdk
Copy link
Copy Markdown
Contributor

vercel-ai-sdk bot commented Mar 6, 2026

🚀 Published in:

Package Version
ai 7.0.0-beta.5
@ai-sdk/alibaba 2.0.0-beta.2
@ai-sdk/angular 3.0.0-beta.5
@ai-sdk/azure 4.0.0-beta.3
@ai-sdk/baseten 2.0.0-beta.2
@ai-sdk/cerebras 3.0.0-beta.2
@ai-sdk/deepinfra 3.0.0-beta.2
@ai-sdk/deepseek 3.0.0-beta.2
@ai-sdk/fireworks 3.0.0-beta.2
@ai-sdk/groq 4.0.0-beta.2
@ai-sdk/huggingface 2.0.0-beta.2
@ai-sdk/langchain 3.0.0-beta.5
@ai-sdk/llamaindex 3.0.0-beta.5
@ai-sdk/moonshotai 3.0.0-beta.2
@ai-sdk/openai 4.0.0-beta.3
@ai-sdk/openai-compatible 3.0.0-beta.2
@ai-sdk/react 4.0.0-beta.5
@ai-sdk/rsc 3.0.0-beta.5
@ai-sdk/svelte 5.0.0-beta.5
@ai-sdk/togetherai 3.0.0-beta.2
@ai-sdk/vercel 3.0.0-beta.2
@ai-sdk/vue 4.0.0-beta.5
@ai-sdk/xai 4.0.0-beta.2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai/core core functions like generateText, streamText, etc. Provider utils, and provider spec. ai/provider related to a provider package. Must be assigned together with at least one `provider/*` label bug Something isn't working as documented provider/deepseek Issues related to the @ai-sdk/deepseek provider provider/groq Issues related to the @ai-sdk/groq provider provider/openai Issues related to the @ai-sdk/openai provider provider/openai-compatible Issues related to the @ai-sdk/openai-compatible provider

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants