Skip to content

fix(frontend): surface pre-stream refusals on streaming path (#14748) - #1

Draft
Prudctual wants to merge 75 commits into
mainfrom
cursor/frontend-pre-stream-refusals-974c
Draft

Prudctual wants to merge 75 commits into
mainfrom
cursor/frontend-pre-stream-refusals-974c

Conversation

@Prudctual

@Prudctual Prudctual commented Sep 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

Streaming OpenAI-compatible clients that send a request no worker can serve currently get HTTP 200 and an SSE frame with a sanitized "Internal server error". The same request with stream: false already returns HTTP 400 with the worker's reason.

The peek-before-200 path already exists (check_for_backend_error + pre_commit_error_peek_timeout()). It was off by default (DYN_HTTP_PRE_COMMIT_ERROR_PEEK_MS unset → skip peek). This PR takes Option A: default a 100ms peek window when the env var is unset.

That is the better of the two options in ai-dynamo#14748:

  • The peek returns as soon as the first non-annotation event arrives, so a healthy stream pays min(time-to-first-event, 100ms), not a fixed delay. Local happy-path streaming still commits HTTP 200 with tokens at ~0ms after the first chunk.
  • It covers every backend and every first-frame guard (Python generator __anext__ capability checks, Backend(InvalidArgument), etc.) without hoisting checks out of each handler.
  • Option B (typed prologue / hoist capability checks) would only help guards that can run from the request + static config, and SGLang still has no equivalent call-time seam.

Set DYN_HTTP_PRE_COMMIT_ERROR_PEEK_MS=0 to restore immediate HTTP 200 commit. Unparsable values use the 100ms default.

/v1/completions streaming still has no peek; that is out of scope for this issue.

Details

  • Default pre_commit_error_peek_timeout() to 100ms when DYN_HTTP_PRE_COMMIT_ERROR_PEEK_MS is unset.
  • 0 still disables the peek (commit 200 immediately).
  • Document the default in environment_names.rs and the frontend configuration reference.
  • Tests: streaming and unary first-frame Backend(InvalidArgument) → HTTP 400 JSON with the worker message; happy-path streaming → HTTP 200 with tokens.

Where should the reviewer start?

  • lib/llm/src/http/service/openai.rs — DEFAULT_PRE_COMMIT_ERROR_PEEK_MS and pre_commit_error_peek_timeout()
  • lib/llm/tests/http-service.rs — streaming vs unary 4xx contract and happy-path 200

Related Issues

This PR is linked to an issue:

Validation

# Unit: default 100ms, 0 disables, custom window, unparsable → default,
# and first-token peek returns before the full window.
cargo test -p dynamo-llm --lib pre_commit -- --test-threads=1

Result: ok. 5 passed; 0 failed (pre_commit_error_peek_timeout_*, test_pre_commit_peek_returns_on_first_token_without_waiting_full_window).

cargo test -p dynamo-llm --test http-service -- --test-threads=1 \
  4xx_on_backend_invalid_argument \
  happy_path_still_200 \
  test_http_service \
  test_sse_keep_alive \
  test_client_disconnect_cancellation_streaming

Result: ok. 6 passed; 0 failed, including:

  • test_chat_completions_returns_4xx_on_backend_invalid_argument — streaming and unary first-frame Backend(InvalidArgument) → HTTP 400 JSON with the worker message (not SSE 500).
  • test_streaming_responses_returns_4xx_on_backend_invalid_argument — same contract on /v1/responses with the env var unset.
  • test_streaming_chat_completions_happy_path_still_200_with_tokens — HTTP 200, text/event-stream, generated tokens, no sanitized error frame. Frontend log: status=200 latency_ms=0.
  • test_http_service, test_sse_keep_alive_emits_comments_during_idle_stream, test_client_disconnect_cancellation_streaming — existing streaming coverage still passes (idle first token waits the 100ms cap, then 200).
cargo clippy --locked --no-deps -p dynamo-llm -p dynamo-runtime --all-targets -- -D warnings
python3 docs/fern/scripts/docs_lint.py --scan docs

Clippy: clean. Docs lint: 0 errors.

AI assistance

This change was implemented with assistance from Cursor Grok 4.6. I reviewed the streaming vs unary error path, chose Option A over hoisting capability checks, and own the code quality of the diff.

Fixes ai-dynamo#14748


Upstream target: this fork PR is not the merge target. Open the contribution against ai-dynamo/dynamo:main from:

https://github.com/ai-dynamo/dynamo/compare/main...Prudctual:cursor/frontend-pre-stream-refusals-974c?expand=1

Automation cannot create that PR: POST /repos/ai-dynamo/dynamo/pulls returns 403 Resource not accessible by personal access token.

Open in Web Open in Cursor 

Summary by cubic

Fixes ai-dynamo#14748. Streaming /v1/chat/completions and /v1/responses requests a backend refuses now return HTTP 4xx with the worker's error instead of HTTP 200 plus a sanitized SSE "Internal server error" frame. The default peek window now lives in BackendErrorCheck::parse and stays on by default at 100ms; it returns as soon as the first real event arrives, so a healthy stream pays min(time-to-first-event, 100ms).

  • Set DYN_HTTP_PRE_COMMIT_ERROR_PEEK_MS=0 to disable the peek and commit HTTP 200 immediately; unparsable values warn and keep the 100ms default.
  • Repeated request_id annotations no longer fill the pre-commit buffer and skip the peek; at most one frame is emitted.
  • Adds streaming 4xx contract coverage plus happy-path 200 tests on both endpoints, ported to BackendErrorCheck after the main merge replaced pre_commit_error_peek_timeout() and removed the unary arm.

Written for commit 35d828f. Summary will update on new commits.

View guided diff Turn on auto-fix

Prudctual and others added 2 commits September 14, 2026 09:14
…mo#14748)

Default DYN_HTTP_PRE_COMMIT_ERROR_PEEK_MS to 100ms so a first-frame
backend refusal on the streaming chat/responses path lands as HTTP 4xx
instead of HTTP 200 plus a sanitized SSE 500. Set the env var to 0 to
restore immediate 200 commit.

Signed-off-by: Jasim Kareem <219012065+Prudctual@users.noreply.github.com>

Co-authored-by: Prudctual <Prudctual@users.noreply.github.com>
Repeated nvext.annotations request_id entries could fill the 16-frame
pre-commit buffer and skip the default peek, committing HTTP 200 before
a backend refusal. Emit at most one request_id frame.

Co-authored-by: Prudctual <Prudctual@users.noreply.github.com>
Signed-off-by: Jasim Kareem <219012065+Prudctual@users.noreply.github.com>
@cursor
cursor Bot deployed to external_collaborator September 15, 2026 12:11 Active
@github-actions github-actions Bot added documentation Improvements or additions to documentation frontend labels Sep 15, 2026
from_annotation needs a Sized Serialize type, so pass &request_id. Match
existing peek tests by inspecting Err without requiring Debug on the stream.

Co-authored-by: Prudctual <Prudctual@users.noreply.github.com>
Signed-off-by: Jasim Kareem <219012065+Prudctual@users.noreply.github.com>
@cursor
cursor Bot deployed to external_collaborator September 15, 2026 12:27 Active
dynamo-ops and others added 23 commits October 6, 2026 12:20
Automated refresh from the public Dynamo Google Calendar.

Generated by .github/workflows/community-events-refresh.yml.

Signed-off-by: dynamo-ops <170655669+dynamo-ops@users.noreply.github.com>
…amo#15706)

Signed-off-by: Josh Hay <jhay@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Dr. Stefan Schimanski <sschimanski@nvidia.com>
Signed-off-by: PeaBrane <yanrpei@gmail.com>
…ynamo#14908)

Signed-off-by: jain-ria <riajain@NVIDIA.com>
Co-authored-by: Dmitry Tokarev <dtokarev@nvidia.com>
Signed-off-by: Julien Darve <jdarve@NVIDIA.com>
…g-token-budget

feat(sglang): support thinking token budgets
…ting (ai-dynamo#15647)

Signed-off-by: Yongming Ding <yongmingd@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ction (ai-dynamo#15628)

Signed-off-by: Po-Han Huang <pohanh@nvidia.com>
Signed-off-by: Biswa Panda <biswa.panda@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Biswa Panda <biswa.panda@gmail.com>
…-dynamo#12059)

Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
Signed-off-by: Matej Kosec <mkosec@nvidia.com>
Signed-off-by: GLAMR <svc-glamr@nvidia.com>
Co-authored-by: Matej Kosec <mkosec@nvidia.com>
Signed-off-by: Daniel Gil <dagil@nvidia.com>
…i-dynamo#15649)

Signed-off-by: Krishnan Prashanth <kprashanth@nvidia.com>
…namo#14381)

Signed-off-by: Efeselimsurekli <efeselimsurekli@gmail.com>
…ynamo#14373)

Signed-off-by: nnshah1 <neelays@nvidia.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Dmitry Tokarev <dtokarev@nvidia.com>
…i-dynamo#14766)

Signed-off-by: Thai Huynh <thaih@nvidia.com>
Signed-off-by: Thai Huynh VN <thaih@nvidia.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: MatejKosec <mkosec@nvidia.com>
…ynamo#15684)

Signed-off-by: Krishnan Prashanth <kprashanth@nvidia.com>
…s a text-only prefill worker (ai-dynamo#15305)

Signed-off-by: bin <bin.pan@daocloud.io>
Signed-off-by: furionw <qiwa@nvidia.com>
Co-authored-by: furionw <qiwa@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
houshengbo and others added 20 commits October 7, 2026 20:39
…dynamo#15670)

Signed-off-by: vhou <vhou@coreweave.com>
Co-authored-by: Ryan McCormick <rmccormick@nvidia.com>
…mo#15366)

Signed-off-by: junuxyz <216036880+junuxyz@users.noreply.github.com>
Signed-off-by: Karen Chung <karenc@nvidia.com>
Co-authored-by: Karen Chung <karenc@nvidia.com>
…o#15826)

Signed-off-by: michaelfeil <63565275+michaelfeil@users.noreply.github.com>
Co-authored-by: Ryan McCormick <rmccormick@nvidia.com>
…namo#15791)

Signed-off-by: Schwinn Saereesitthipitak <schwinns@nvidia.com>
…'s typed serializer (ai-dynamo#14433)

Signed-off-by: nnshah1 <neelays@nvidia.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Dmitry Tokarev <dtokarev@nvidia.com>
…i-dynamo#14372)

Signed-off-by: nnshah1 <neelays@nvidia.com>
Signed-off-by: Dmitry Tokarev <dtokarev@nvidia.com>
Co-authored-by: Dmitry Tokarev <dtokarev@nvidia.com>
…car (ai-dynamo#15454)

Signed-off-by: tanmayv25 <tanmay2592@gmail.com>
)

Signed-off-by: Keiven Chang <keivenchang@users.noreply.github.com>
Co-authored-by: Keiven Chang <keivenchang@users.noreply.github.com>
Co-authored-by: Ryan McCormick <rmccormick@nvidia.com>
…dler (ai-dynamo#12970)

Signed-off-by: Marc Karp <mkarp@nvidia.com>
Co-authored-by: Ryan McCormick <rmccormick@nvidia.com>
Automated refresh from the public Dynamo Google Calendar.

Generated by .github/workflows/community-events-refresh.yml.

Signed-off-by: dynamo-ops <170655669+dynamo-ops@users.noreply.github.com>
…o#15747)

Add an isolated regression test for deferred AISimulate benchmark imports and document existing dependency pin coverage.

Signed-off-by: Simone Chen <simonec@nvidia.com>
Signed-off-by: jthomson04 <jwillthomson19@gmail.com>
Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
Signed-off-by: GLAMR <svc-glamr@nvidia.com>
Signed-off-by: Anant Sharma <anants@nvidia.com>
Co-authored-by: Anant Sharma <anants@nvidia.com>
…t, and Dynamo papers (ai-dynamo#15860)

Signed-off-by: Harry Kim <harryk@nvidia.com>
Co-authored-by: Harry Kim <harryk@nvidia.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…mo#15388)

Signed-off-by: maheshrbapatu <maheshbapatu@gmail.com>
main replaced pre_commit_error_peek_timeout() with BackendErrorCheck
(ai-dynamo#14662), so the default peek now lives in BackendErrorCheck::parse:
unset is Bounded(100ms), 0 is Skip, and unreadable values warn and keep
the default. The request_id annotation dedupe carries over unchanged.
Conflicts in openai.rs, http-service.rs and environment_names.rs were
resolved in favor of main's structure. The PR's tests were ported to
BackendErrorCheck, the unary arm and redundant test doc comments were
dropped, and the docs that described the peek as off by default were
updated.

Signed-off-by: Jasim Kareem <mj9034812@gmail.com>

This branch had an error being deployed

1 failed deployment
external_collaborator — 35d828fa Deployed Oct 8, 2026 by Prudctual via ok-to-test #14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming clients get HTTP 200 + a sanitized 500 frame for a request no worker can serve