Repository navigation
perf(citations): skip plain text between citation markers while streaming - #2451
Merged
jaluma merged 1 commit intoNov 11, 2026
Merged
Conversation
…ming ExtractCitationInterceptor reparses the whole answer for every streamed delta, and IncrementalTextProcessor probed every rule at every character, so a streamed answer cost O(length^2) Python-level probes. A 6,000 character answer spent about 15 s of CPU in citation parsing. Rules can now declare `triggers`, the characters at which they can match. When every rule declares them, the processor copies the text up to the next trigger in one step. DelimitedReferenceRule triggers on its start token and BacktickUnwrapRule on a backtick, so the output is unchanged. Rules without triggers are still probed at every position.
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
With citations on (the bundled UI turns them on whenever documents are used),
ExtractCitationInterceptorcallsextract_citations_by_original_texton the whole accumulated answer for every streamed delta. That goes throughIncrementalTextProcessor.process, which probes every rule at every character in Python and builds aProbeResult.no_match()for each miss. So the work per answer grows with the square of its length, and long answers (or long thinking blocks, which go through the same path) spend seconds of CPU on citation parsing while streaming. A 6,000 character answer spent about 19.5 s of CPU on it in total.This lets a rule declare
triggers, the characters at which itsprobecan match. When every rule declares them, the processor copies the text up to the next trigger in one step instead of probing each character.DelimitedReferenceRuletriggers on the first character of its start token andBacktickUnwrapRuleon a backtick. Both already return NO_MATCH everywhere else, so the output doesn't change. Rules withouttriggers(like the ones in the existing engine tests) are still probed at every position.The interceptor still reparses the full text per delta; that's left as is, this only removes the per-character cost.
Type of Change
How Has This Been Tested?
tests/components/text_processing/test_engine.py: a rule withtriggersis only probed at its trigger characters, and a rule without them is still probed everywhere. The first one fails without the change.tests/engines/test_citation_*.pyandtests/components/text_processing/test_engine.py: 151 passed.【】, streamed in random chunk sizes and then finalized: all 24,779 results (text, citations, indices) were identical.ruff format --check,ruff checkandty checkon the touched modules pass.Benchmark: replays the interceptor loop (
extract_citations_by_original_texton the accumulated text per delta, then a final pass) for an answer with a citation every ~300 characters, streamed in 4-character deltas. Base and patched were run alternately in separate processes, 3 rounds each:The streamed output was byte-identical before and after in every run.
Test Configuration:
Checklist:
make check; make testto ensure type checks and tests pass