fix(openai): re-generate when gpt-5.4 leaks internal tool-call format into assistant text#6483
Open
markine wants to merge 1 commit into
Open
fix(openai): re-generate when gpt-5.4 leaks internal tool-call format into assistant text#6483markine wants to merge 1 commit into
markine wants to merge 1 commit into
Conversation
… into text
The gpt-5.4 series intermittently emits its internal parallel tool-call
format -- `to=multi_tool_use.parallel {"tool_uses":[{"recipient_name":...,
"parameters":{...}}, ...]}`, often interleaved with memorized training-data
spam -- as assistant TEXT instead of as structured function calls. The
Responses plugin faithfully forwards that text as ChoiceDelta content, so the
corruption reaches the caller (spoken aloud by a voice agent, shown in the
transcript, and fed back into the next turn's context).
Detect the signature at the very start of the assistant text -- within a
bounded head buffer, before any of it is streamed -- and raise a retryable
APIStatusError so the existing LLMStream retry loop re-issues the request for
a fresh, clean generation. Because we raise before response.completed, no
corrupt state is committed and previous_response_id chaining is untouched.
We deliberately do NOT reconstruct the intended tool calls from the corrupt
payload: acting on a misread of garbage tokens is worse than regenerating.
Scoped to the gpt-5.4 family; every other model keeps the original, verbatim
streaming path (no buffering, no detection, no behavior change). If content
was already streamed this response, the leak is raised non-retryable instead
(a retry would double-emit what the caller received).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Hi team. This is a Claude-generated patch, please let me know if there are conventions in your repo that I've missed or if there's a stylistic improvement you'd like to make. I don't know the intricacies of your websocket API. This is a hard model glitch to replicate live so I'm relying on the crafted unit test. Thank you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The gpt-5.4 series intermittently emits its internal parallel tool-call format as
assistant text instead of as structured function calls — e.g.
often interleaved with memorized training-data spam (non-Latin SEO tokens). The
Responses plugin faithfully forwards this as
ChoiceDelta.content, so the corruptionreaches the caller: for a voice agent it is spoken aloud, it lands in the transcript,
and it is fed back into the next turn's context.
This adds a small, model-scoped guard to the Responses
LLMStream: detect thesignature at the very start of the assistant text — before any of it is streamed — and
raise a retryable error so the existing
LLMStreamretry loop re-issues therequest for a fresh, clean generation.
Corroborating reports / prior art
multi_tool_use.parallel/recipient_name/tool_usesare old ChatGPT plugin-erainternal constructs that should never surface to an API caller. This corruption is
well documented:
shape on gpt-5.4: internal
multi_tool_use.parallelformat emitted as plain text,interleaved with memorized training-data spam. (OpenAI's own CLI hit it.)
long-standing function-call variant (
multi_tool_use.parallelreturned as a toolname).
The "multi_tool_use.parallel" bug and how to fix it,
Model tries to call unknown function multi_tool_use.parallel.
a community patch that reconstructs tool calls from the function-call variant. We
deliberately do not reconstruct here (see below) — the text-content variant is
interleaved with garbage tokens, so re-generating is safer than interpreting it.
Why this shape
calls from the
tool_usespayload — acting on a misread of corrupted tokens is worsethan regenerating, and the barf is stochastic so a re-generation comes back clean.
_SCAFFOLD_HEAD_CHARS, 40 chars)holds the first tokens of each message item so the signature is caught before anything
is streamed. Since we raise before
response.completed, no response state is committedand
previous_response_idchaining is untouched.been forwarded (re-running would double-emit). If assistant text was already streamed
this response, the leak is raised non-retryable (fail the turn) instead.
gpt-5.4family (_model_may_leak_scaffolding). Everyother model keeps the original, verbatim streaming path — no buffering, no detection,
no behavior change.
Testing
tests/test_plugin_openai_responses.py(drives the real_process_eventdispatch withconstructed Responses events; no network):
output_item.done;gpt-4.1) streams the same leak input verbatim (guard off);ruff check/ruff formatclean.Note
On this base,
tests/test_plugin_openai_responses.py::test_reasoning_object_serialized_without_null_fieldsalready fails on a clean checkout (
_ResponsesWebsocket.__init__() missing 1 required positional argument: 'model'— a pre-existing test/source signature mismatch unrelatedto this change). Left untouched.
🤖 Generated with Claude Code