openai-base: streamed `apply_patch` calls reach the app with an empty `diff`
メンテナーはふだん 1 日以内に返信
評価
この issue はまだ評価されていません。
説明
TanStack AI version
@tanstack/ai 0.68.0, @tanstack/ai-openai 0.29.1, @tanstack/openai-base 0.13.1. The same code is on main (@tanstack/openai-base 1.0.0, caaa97b).
Framework/Library version
Node 26.10, Next.js 16.3 (server-side chat()), openai 5.18
Describe the bug and the steps to reproduce it
When the Responses API streams an apply_patch_call, chat() runs the app's apply_patch tool with operation.diff === "". The real diff never reaches the app.
The streamed events for one patch look like this. They were captured from real responses from two models (gpt-5.6 and gpt-6.1), served by Azure OpenAI through a LiteLLM proxy. Both models produced the same sequence:
response.output_item.added item: { type: "apply_patch_call", status: "in_progress",
operation: { type: "update_file", path: "document.json", diff: "" } }
response.apply_patch_call_operation_diff.delta "@@"
response.apply_patch_call_operation_diff.delta "\n"
…
response.apply_patch_call_operation_diff.done
response.output_item.done item: { type: "apply_patch_call", status: "completed",
operation: { type: "update_file", path: "document.json",
diff: "@@\n- \"title\": \"Draft memo\",\n+ \"title\": \"Final memo\",\n" } }
response.completed
processStreamChunks calls userToolChunks(item, …) on response.output_item.added (responses-text.ts#L2029). At that point the item's diff is still "". userToolChunks emits TOOL_CALL_START and TOOL_CALL_END with that input and marks the call ended (#L1598–L1614). When response.output_item.done arrives with the complete diff, the ?.ended check makes userToolChunks return nothing (#L2243). So does the response.completed fallback (#L2426).
In an agent loop this fails silently. The model keeps sending patches that never arrive, until the iteration limit stops the loop.
There's no way around streamed Responses for this tool: Chat Completions rejects it (Invalid value: 'apply_patch'. Supported values are: 'function' and 'custom'.), so apply_patch only works through the Responses API.
The other user-run tools are protected by accident. readUserExecutedCall returns null for a local_shell_call or shell_call until its command list is non-empty, so an incomplete item at output_item.added is skipped and picked up later at output_item.done. apply_patch_call only checks that operation.diff is a string, and "" passes.
The existing test, user-executed shell and apply_patch calls in packages/openai-base/tests/responses-text.test.ts, doesn't catch this. It only sends response.created and then response.completed, with the finished item, so the output_item.added path never runs.
Steps to reproduce
- Call
chat()with an OpenAI Responses adapter andtools: [{ ...applyPatchTool(), execute }], whereexecutelogsoperation.diffand returns{ status: "completed", output: "ok" }. - Ask for a one-line edit to a file whose contents are in the prompt.
executelogs""every time. The model keeps callingapply_patchuntilmaxIterationsstops it.
Suggested fix
Handle user-run calls the way function_call is already handled. For a function call, output_item.added emits only TOOL_CALL_START, and TOOL_CALL_END (with the input) waits until the arguments are complete. User-run calls get both START and END from the first item seen.
The check is the item's own status. The apply_patch_call item arrives at output_item.added with status: "in_progress" and is "completed" at output_item.done and in response.completed. So userToolChunks can emit TOOL_CALL_START while the item is in_progress, and emit TOOL_CALL_END with the input once it isn't. This covers every user-run type (apply_patch_call, local_shell_call, shell_call, computer_call) and doesn't depend on each type's content looking incomplete.
The narrowest fix is to remove the userToolChunks call at L2029, since output_item.done and the response.completed fallback already emit the complete call.
Open question: should an item that ends "incomplete" (for example, the response hit max_output_tokens) be handed to the app at all? A truncated diff probably shouldn't be applied.
Workaround for anyone hitting this: wrap the OpenAI client's responses.create and drop response.output_item.added events whose item.type is apply_patch_call. The call is then emitted from output_item.done, with the full diff.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
An E2E wire route that streams the sequence above to the adapter, with a spec that fails on main: api.openai-apply-patch-wire.ts and openai-apply-patch-wire.spec.ts. The PR adds the same sequence as a unit test in packages/openai-base/tests/responses-text.test.ts.
Do you intend to try to help solve this bug with your own PR?
Yes, I am also opening a PR that solves the problem along side this issue
Terms & Code of Conduct
- I agree to follow this project's Code of Conduct
- I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
- 主要言語
- TypeScript
- スター
- 3.2k
- フォーク
- 372
- 平均マージ
- 1日 5時間
- マージ済み PR(30日)
- 233
環境構築
- Dockerfile・Docker Compose ファイルなし
- プルリクエストのテンプレートあり
- コントリビューションガイドを読む
はじめの一歩
- issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
- 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
- リポジトリをフォークし、ブランチを切って変更します。
- issue 番号を参照したプルリクエストを送ります。
TanStack/ai のほかの issue
-
openai-base: a Responses max_output_tokens cutoff ends as RUN_ERROR without usage instead of finishReason 'length'対応中かも @tombeckenham が 2 日前に担当しました。 オープンhas-pr waiting-on: maintainer
難易度 2/5 1〜3時間 初心者へのやさしさ 74/100
TanStack/ai#1916 · コメント 1 件 · 担当者 1 名 ·
メンテナーはふだん 1 日以内に返信
-
Chat UI: a fallback widget for tool calls whose names aren't known when the kit is created対応中かも @AlemTuzlak が 1 日前に担当しました。 オープンwaiting-on: maintainer
難易度 4/5 1〜2日 初心者へのやさしさ 58/100
メンテナーはふだん 1 日以内に返信
-
`ChatEngine.handleTextMessageContentEvent` is O(n²) in answer length: `startsWith(before)` copies the whole accumulated text on every chunk対応中かも @tombeckenham が 1 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
needsApproval client tools can't be approved without a server (custom connection adapters)対応中かも @jherr が 1 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
-
feat: live speech-to-text sessions for voice and telephony agents対応中かも @AlemTuzlak が 2 日前に担当しました。 オープンwaiting-on: maintainer
メンテナーはふだん 1 日以内に返信
似ている issue
-
area:cli bug P4
難易度 2/5 1〜3時間 初心者へのやさしさ 82/100
メンテナーはふだん 1 日以内に返信
-
bug from-studio
難易度 2/5 1〜3時間 初心者へのやさしさ 65/100
esengine/DeepSeek-Reasonix#14868 ·
メンテナーはふだん 1 日以内に返信
-
難易度 2/5 1〜3時間 初心者へのやさしさ 68/100
メンテナーはふだん 1 日以内に返信
-
bug
難易度 2/5 1〜3時間 初心者へのやさしさ 72/100
loopbackio/loopback-next#11878 ·
メンテナーはふだん 2 日以内に返信
-
bug 🐛
難易度 2/5 1〜3時間 初心者へのやさしさ 78/100
comunica/comunica#1925 · コメント 1 件 ·
メンテナーはふだん 1 日以内に返信