openai-base: streamed `apply_patch` calls reach the app with an empty `diff`

未关闭
#1,947 0 条评论 0 个 reaction 已指派 1 人 在 GitHub 查看

维护者通常 1 天内回复

@tombeckenham 已经在做这个了。

开始于 2026年10月30日。

  • #1948 来自 @peter-daptic —— 未关闭

评估

这个 Issue 还没有评估数据。

描述

has-pr waiting-on: maintainer
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

  1. Call chat() with an OpenAI Responses adapter and tools: [{ ...applyPatchTool(), execute }], where execute logs operation.diff and returns { status: "completed", output: "ok" }.
  2. Ask for a one-line edit to a file whose contents are in the prompt.
  3. execute logs "" every time. The model keeps calling apply_patch until maxIterations stops 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.3k
派生
378
平均合并
1 天 1 分钟
30 天内合并 PR
208

环境准备

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

TanStack/ai 的其他 Issue

查看 TanStack/ai 的全部 Issue

相似的 Issue

更多 TypeScript Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。