Repository navigation
perf(functions): skip non-JSON offsets in parseJSONLegacy (8 KB prose stream with tools 5.8 s to 0.25 s) - #13174
Open
deadstrobe5 wants to merge 1 commit into
Open
deadstrobe5 wants to merge 1 commit into
deadstrobe5 wants to merge 1 commit into
Conversation
Prose replies to requests with tools are re-parsed on every streamed token, and parseJSONLegacy created a json.Decoder at every byte offset of the text. Skip the offsets where the decoder fails before reading any value and jump to the offset its SyntaxError would report, so the objects found and the errors returned stay the same. Assisted-by: Claude:claude-opus-5-5 Signed-off-by: Afonso Ribeiro <afonso.diamantino2@gmail.com>
This branch has not been deployed
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.
Description
When a request has tools and the model answers in prose, the streaming endpoints (OpenAI chat, Anthropic messages, Open Responses) re-parse the whole accumulated reply on every token.
ParseJSONIterativeandParseJSONfind no JSON in prose and fall back toparseJSONLegacy, which creates ajson.Decoderat every byte offset and lets it fail. That makes the parse cost of a reply grow with the square of its length: an 8 KB prose reply streamed in 4-byte tokens spends about 5.8 s of CPU in these parsers.This change lets
parseJSONLegacyskip, without a decoder, the offsets where the decoder fails before reading any value: the first non-space byte cannot start a JSON value, or atrue/false/nullis broken off by a wrong byte. It moves to the offset the decoder'sSyntaxErrorwould report, so the objects found and the errors returned stay the same. Objects, arrays, strings, numbers, complete literals and input cut off at the end still go through the decoder.To check equivalence I compared the old and new
parseJSONLegacyon every string up to 5 bytes over a 16-symbol alphabet (literal letters, braces, quotes, digits, spaces, a stray UTF-8 byte), 6,000 random mixes of prose, tool-call JSON, broken literals, tags and raw bytes plus every prefix of a fifth of them, and a shortgo test -fuzzrun: about 1.28 million inputs, identical results and errors under Go 1.26.0 and Go 1.27 (bothencoding/jsonimplementations). Thepkg/functionsandcore/http/endpoints/openaitests pass, and I added a case for prose with broken literals between objects.CleanupLLMResultandParseXMLIterative)Medians of alternating old/new rounds in one process (old code copied in), Ryzen 5 5600X. Short inputs do not regress: an input that is only a tool call takes the same time, and
Higoes from 2.2 us to 0.27 us.Notes for Reviewers
The skip only applies where the decoder would return a
SyntaxErrorbefore reading a value, and it returns the offset the decoder reports there, so later objects are found from the same positions as before. Quirks of the legacy loop stay as they are (for examplenul{...}still swallows that object).Signed commits