Skip to content

fix(frontend): keep completion errors across full aggregation - #12786

Closed
KrishnanPrash wants to merge 1 commit into
mainfrom
kprashanth/completions-typed-error-followup
Closed

fix(frontend): keep completion errors across full aggregation#12786
KrishnanPrash wants to merge 1 commit into
mainfrom
kprashanth/completions-typed-error-followup

Conversation

@KrishnanPrash

@KrishnanPrash KrishnanPrash commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Overview:

PR #12706 made /v1/completions return the backend's own HTTP status for a typed error such as Backend(InvalidArgument). It did this with a preflight that reads only the first stream event, so a backend that emits a normal chunk and then fails still returned HTTP 500 with the message replaced by Failed to fold completions stream.

This PR removes the preflight and captures the typed error during aggregation instead, so an error at any position keeps its status and its message. The same change removes the batch barrier and the per-prompt boxing that #12706 introduced.

Repro

cargo test -p dynamo-llm --test http-service \
  test_completions_surface_backend_invalid_argument_after_first_chunk

The endpoint shape that first showed the bug:

curl -s localhost:8000/v1/completions -H 'Content-Type: application/json' \
  -d '{"model":"<model>","prompt":"hello","logprobs":2,"max_tokens":16}'

Before

status=500
{"message":"Failed to fold completions stream for c306331a-fe5e-4099-9889-3505ffd59afa","type":"Internal Server Error","code":500}

After

status=400
{"message":"Dynamo's SGLang backend does not currently support logprobs >= 1","type":"Bad Request","code":400}

Details:

lib/llm/src/http/service/openai.rs

  • Adds aggregate_completion_response. It taps the raw events on their way into the aggregator, keeps the first typed backend error in a OnceLock, and returns that error with its own status. The generic 500 now covers only a fold failure with no typed error. Both completions_single and completions_batch call it, so the two handlers share one aggregation path.
  • Removes check_completion_batch_streams. Its try_join_all held every prompt stream until the slowest produced its first frame, so a fast prompt could not report metrics or release http_queue_guard until the slow prompt answered. completions_batch merges with a plain stream::select_all again.
  • Removes BoxedCompletionResponseStream. It only gave the streaming and non-streaming arms the same type, at the cost of one allocation per prompt plus dynamic dispatch on every poll of the streaming path. Streaming is now identical to pre-fix(frontend): preserve completion backend error status #12706.
  • An expected client 400 logs at warn with a structured error_response field instead of at error with the response formatted into the message.

lib/llm/tests/http-service.rs

  • Adds two tests that drive a real POST /v1/completions against a live HttpService. One engine emits the typed error as its first frame, the other emits a chunk first. Each test asserts the single-prompt and the array-prompt shape, so both handlers are covered.
  • Drops the two unit tests from fix(frontend): preserve completion backend error status #12706. They called the helpers directly and stayed green when the production call was removed.

Trade-off: without a preflight, a failing batch drains every prompt stream before it answers, as it did before #12706. In exchange, no sibling generation is abandoned mid-flight.

Related Issues

🚫 This PR is NOT linked to an issue:

  • Confirmed — no related issue

Open in Devin Review

Summary by CodeRabbit

  • Bug Fixes
    • Improved completion error handling so backend errors are detected throughout the entire response, including after output begins.
    • Invalid completion arguments now return clear HTTP 400 responses for single, batch, and streaming requests.
    • Preserved typed status responses while aggregating non-streaming completions.
    • Batch completion responses continue to combine usage information correctly.

Signed-off-by: Krishnan Prashanth <kprashanth@nvidia.com>
@KrishnanPrash
KrishnanPrash requested review from a team as code owners August 6, 2026 23:05

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +965 to +993
async fn aggregate_completion_response(
stream: impl futures::Stream<Item = Annotated<NvCreateCompletionResponse>>,
parsing_options: ParsingOptions,
request_id: &str,
) -> Result<NvCreateCompletionResponse, ErrorResponse> {
let backend_error = Arc::new(std::sync::OnceLock::new());
let first_error = backend_error.clone();
let stream = stream.inspect(move |response| {
if let Some(error) = extract_backend_error_if_present(response) {
// Keep the first error; it is the one that ended generation.
let _ = first_error.set(error);
}
});

let aggregated =
NvCreateCompletionResponse::from_annotated_stream(stream, parsing_options).await;

if let Some((message, status)) = backend_error.get() {
let error_response = backend_error_response(message.clone(), *status);
tracing::warn!(request_id, ?error_response, "Backend error detected");
return Err(error_response);
}

aggregated.map_err(|e| {
tracing::error!(request_id, "Failed to fold completions stream: {e:?}");
ErrorMessage::internal_server_error(&format!(
"Failed to fold completions stream for {request_id}"
))
})

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.

🔍 Error frames no longer short-circuit; a backend that errors but keeps the stream open will hang the request

The removed preflight (check_for_backend_error) returned as soon as the first non-annotation event was an error frame, so the HTTP response was produced without waiting for the stream to terminate. aggregate_completion_response (lib/llm/src/http/service/openai.rs:965-993) only inspects the captured error after from_annotated_stream has folded the entire stream to completion. If a backend emits a typed error frame and then fails to close the stream (or is slow to do so), the client now blocks until the stream ends instead of getting an immediate 4xx. This is fine for backends where an error frame is terminal (which is what the new tests model), but it is worth confirming that all worker paths close the stream right after emitting event: "error".

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Completion aggregation now inspects all events and preserves typed backend HTTP statuses. Single and batch completions use the shared helper. HTTP tests cover invalid-argument errors before and after output for scalar and array prompts.

Changes

Completion error handling

Layer / File(s) Summary
Shared completion aggregation
lib/llm/src/http/service/openai.rs
Non-streaming single and batch completions use aggregate_completion_response. The helper inspects all events, preserves typed backend errors and statuses, and maps folding failures to internal-server responses.
HTTP invalid-argument coverage
lib/llm/tests/http-service.rs
Added completion test engines and helpers for typed errors before or after output. Tests verify HTTP 400 responses for scalar and array prompts.

Estimated code review effort: 4 (Complex) | ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: preserving completion errors during full aggregation.
Description check ✅ Passed The description explains the problem, implementation, tests, trade-offs, and confirms that no related issue exists.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch kprashanth/completions-typed-error-followup

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

🧹 Nitpick comments (1)
lib/llm/tests/http-service.rs (1)

1628-1651: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a success-path test for the shared aggregation helper.

Both tests assert the error path. aggregate_completion_response now inspects every event, so a frame that is misclassified as an error would fail a normal request. A test that serves an engine yielding only valid chunks and asserts HTTP 200 for scalar and array prompts would guard that behavior.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/llm/tests/http-service.rs` around lines 1628 - 1651, Add a success-path
test alongside test_completions_surface_backend_invalid_argument and
test_completions_surface_backend_invalid_argument_after_first_chunk that serves
an engine producing only valid completion chunks, invokes both scalar and array
prompts, and asserts HTTP 200 responses. Exercise the shared
aggregate_completion_response path and verify valid frames are not classified as
errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/llm/tests/http-service.rs`:
- Around line 1628-1651: Add a success-path test alongside
test_completions_surface_backend_invalid_argument and
test_completions_surface_backend_invalid_argument_after_first_chunk that serves
an engine producing only valid completion chunks, invokes both scalar and array
prompts, and asserts HTTP 200 responses. Exercise the shared
aggregate_completion_response path and verify valid frames are not classified as
errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 607351cb-2f6b-4349-b4a8-be6246c3537d

📥 Commits

Reviewing files that changed from the base of the PR and between 8d4a014 and f83614a.

📒 Files selected for processing (2)
  • lib/llm/src/http/service/openai.rs
  • lib/llm/tests/http-service.rs

});

let aggregated =
NvCreateCompletionResponse::from_annotated_stream(stream, parsing_options).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

aggregate_completion_response records backend errors but waits for NvCreateCompletionResponse::from_annotated_stream to drain the entire merged stream before returning them, so a batch request with one immediate backend error keeps the other prompt streams generating and delays the 4xx response. Fix: short-circuit on the first backend error and drop the remaining stream instead of checking backend_error only after aggregation completes.

🤖 AI Fix

In lib/llm/src/http/service/openai.rs, rewrite aggregate_completion_response to poll the completion stream directly, return Err(backend_error_response(...)) immediately when a backend error frame is seen, and otherwise aggregate non-error Annotated<NvCreateCompletionResponse> items to the same final response.

let backend_error = Arc::new(std::sync::OnceLock::new());
let first_error = backend_error.clone();
let stream = stream.inspect(move |response| {
if let Some(error) = extract_backend_error_if_present(response) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Every non-streaming completion chunk now passes through extract_backend_error_if_present, whose normal-data path serializes NvCreateCompletionResponse to JSON even though that type cannot be a top-level {message, code} error payload, adding avoidable per-token CPU and allocations. Fix: use a completion-specific error extractor that skips generic data-payload JSON conversion for normal completion frames.

🤖 AI Fix

In lib/llm/src/http/service/openai.rs, add extract_completion_backend_error_if_present(event: &Annotated<NvCreateCompletionResponse>) that preserves typed event == "error" handling but does not run serde_json::to_value on event.data, and call it from aggregate_completion_response.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant