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

Abierto
#1,947 0 comentarios 0 reacciones 1 asignado Ver en GitHub

Los mantenedores suelen responder en 1 día

@tombeckenham ya está trabajando en esto.

Desde el 30/10/2026.

  • #1948 de @peter-daptic — abierto

Evaluación

Este issue todavía no se ha evaluado.

Descripción

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.
Lenguaje dominante
TypeScript
Estrellas
3.3k
Forks
378
Merge medio
1 d 1 h
PR fusionados (30 d)
214

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de TanStack/ai

Todos los issues de TanStack/ai

Issues similares

Más issues de TypeScript

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.