Visitar URL original
perf: avoid quadratic start check when streaming from OpenAIResponsesChatGenerator by deadstrobe5 · Pull Request #13716 · deepset-ai/haystack · GitHub
Skip to content

perf: avoid quadratic start check when streaming from OpenAIResponsesChatGenerator - #13716

Open
deadstrobe5 wants to merge 1 commit into
deepset-ai:mainfrom
deadstrobe5:perf/responses-streaming-start
Open

deadstrobe5 wants to merge 1 commit into
deepset-ai:mainfrom
deadstrobe5:perf/responses-streaming-start

Conversation

@deadstrobe5

Copy link
Copy Markdown

Related Issues

  • No existing issue.

Proposed Changes:

_convert_response_chunk_to_streaming_chunk sets StreamingChunk.start on each response.output_text.delta by checking whether an earlier chunk with the same output_index already has content. On main it walks previous_chunks from the front for every delta. When a reasoning summary streams before the answer, each text delta first walks past every reasoning chunk, so a stream with R reasoning deltas and T text deltas does about R x T checks.

The check now walks reversed(previous_chunks) with the same predicate, all(c.content == "" ...), and stops at the previous text delta, which is usually the last chunk. The separate all(c.index != chunk.output_index ...) clause goes away because all() over no matching chunks is already True. The refusal check gets the same reversed(); any() gives the same result in either order. No new state. run_async and AzureOpenAIResponsesChatGenerator use the same function.

How did you test it?

  • pytest -m "not integration" on test/components/generators/, test/components/agents/, test_query_expander.py, test_llm_ranker.py and test/dataclasses/test_streaming_chunk.py: 870 passed. ruff check, ruff format --check and mypy on the module are clean.
  • Equivalence: the same ResponseStreamEvent sequences were run through the function on main and on this branch, comparing every StreamingChunk field except the received_at timestamp. 20,000 random sequences (text, refusal, reasoning and function call deltas, items added and done, interleaved output_index values, empty deltas) and full realistic streams gave identical chunks.
  • Benchmark: realistic streams (reasoning summary item, then the message), main and this branch in one process, alternating order. "Conversion" calls the function over the whole stream. run() and run_async() use a streaming callback and an httpx.MockTransport that serves the SSE body, so SDK parsing is included. Median of 21 runs, min-max in brackets, Python 3.12, openai 3.49.0, shared Linux machine.
Stream main this PR
Conversion, 1000 reasoning + 2000 text deltas 159.9 ms (149.1-217.9) 20.9 ms (19.2-30.7) 7.7x
Conversion, 300 reasoning + 1000 text deltas 30.4 ms (28.0-42.4) 8.9 ms (8.4-10.4) 3.4x
Conversion, 2000 text deltas, no reasoning 14.6 ms (13.8-47.8) 13.4 ms (12.1-46.1) 1.1x
run(), 1000 reasoning + 2000 text deltas 381 ms (364-464) 234 ms (224-308) 1.6x
run_async(), 1000 reasoning + 2000 text deltas 473 ms (385-2584) 288 ms (234-850) 1.6x
run(), 2000 text deltas, no reasoning 183 ms (155-343) 175 ms (159-283) 1.0x

Notes for the reviewer

Answers without reasoning barely change, because main already stops early there. The gain grows with the length of the reasoning that precedes the answer. No test on main pins the start flag of refusal deltas; none was added, since that line only gains reversed(). The pre-commit hooks were not run as such; ruff, mypy, codespell and scripts/release_note_backticks.py were run directly on the changed files.

This PR was fully generated with Claude Code, which also ran the tests and benchmarks above.

Checklist

  • I have read the contributors guidelines and the code of conduct.
  • I have updated the related issue with new insights and changes.
  • I have added unit tests and updated the docstrings.
  • I've used one of the conventional commit types for my PR title: fix:, feat:, build:, chore:, ci:, docs:, style:, refactor:, perf:, test: and added ! in case the PR includes breaking changes.
  • I have documented my code.
  • I have added a release note file, following the contributors guidelines.
  • I have run pre-commit hooks and fixed any issue.

@deadstrobe5
deadstrobe5 requested a review from a team as a code owner November 12, 2026 11:27
@deadstrobe5
deadstrobe5 requested review from sjrl and removed request for a team November 12, 2026 11:27
@vercel

vercel Bot commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

@deadstrobe5 is attempting to deploy a commit to the deepset Team on Vercel.

A member of the Team first needs to authorize it.

@sjrl sjrl self-assigned this Nov 12, 2026
Comment on lines +881 to +882
# A text output starts when no earlier chunk with the same output_index has content. Scanning backwards stops
# at the previous text delta instead of rescanning every earlier chunk, such as a long reasoning summary.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we drop the "instead of rescanning every earlier chunk" part? Lets keep the comment to what the code does now.

Suggested change
# A text output starts when no earlier chunk with the same output_index has content. Scanning backwards stops
# at the previous text delta instead of rescanning every earlier chunk, such as a long reasoning summary.
# A text output starts when no earlier chunk with the same output_index has content.
# Scanning backwards stops at the previous text delta.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  haystack/components/generators/chat
  openai_responses.py
Project Total  

This report was generated by python-coverage-comment-action

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants