Repository navigation
fix(request-trace): stop audit records claiming completeness when the response was dropped - #14143
Conversation
`RequestPayloadHandle::emit` hardcoded `payload_complete: true` and `payload_drop_reason: None`, so an audit record produced after a client cancel, an empty response stream, or a failed aggregation claimed to be a faithful copy of what the client received. The payload future collapsed every one of those outcomes to `None` before reaching the emit site, so the reason was gone by the time the record was built. Widen that future's output into a `PayloadOutcome` carrying both the response and an optional drop reason, populate a reason at each of the three collapse sites using the colon-delimited grammar `otel_sink.rs` already publishes, and derive `payload_complete` from the two together. `emit` now takes the reason as a required argument so a new call site cannot silently re-introduce an over-claiming record. When a stream errors mid-flight, the chunks buffered before the error-bearing chunk are re-aggregated so the content the client actually received is preserved alongside the reason. `DeltaAggregator::apply` is untouched: its short-circuit on the first error chunk is load-bearing for the client-facing non-streaming path. Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
|
๐ Hi glamr-agent! Thank you for contributing to ai-dynamo/dynamo. Just a reminder: The ๐ |
Automated evidence record โ validation completeValidation status: complete Evidence summary: [2/2 validated] AI review assessment (advisory): sound. An AI agent judged the change logically sound from the code and the reported results. This is not an approval; repository CI and human reviewers decide whether to merge. Validation result: pass โ every command listed below ran in this checkout and exited zero, and the new behaviour is proven by paired positive and negative tests rather than by a green suite alone. Evidence audit: complete [2/2 validated] โ the command report comes from recorded runs. Commands and results [2/2 validated]Generated from the commands recorded during this run. Check 1Checks the changed files with the repository's fast lint and formatting commands. Result: Passed ( Command: pre-commit run --files lib/llm/src/preprocessor.rs lib/llm/src/request_trace/payload.rs lib/llm/src/request_trace/payload_stream.rs lib/llm/tests/request_trace_nats_integration.rs --hook-stage manualCheck 2Checks the changed Rust crates with Result: Passed ( Command:
|
plan.mdPlan โ Audit payload record claims completeness when the response is missingNo existing review request applies. Discovery found no open or merged pull request that User intentIssue #14134 reports that the request-trace The issue additionally asks to consider preserving whatever partial content the aggregator had The intent of this work item is to make the record honest: when the response is absent, the record Non-goals
DiscoveryAll findings below come from reading the checked-out tree at The defect, as it exists on
|
change.mdChange: record why a captured request payload is incompleteIssue: ai-dynamo/dynamo#14134 The defect
payload_complete: true,
payload_drop_reason: None,So a record emitted after a client cancel, an empty response stream, or a failed The information needed to say otherwise was destroyed one layer up. The payload future
By the time the single production call site in I reproduced this before changing anything, by adding a temporary assertion to the What changedFour files, no wire-schema change. 1. The payload future carries a reason (
|
| Situation | Reason |
|---|---|
| Stream ended with no chunks | empty_response_stream |
| Aggregation returned an error | aggregation_failed:<error> |
| Sender dropped before reporting | client_cancelled |
The aggregation_failed detail is the underlying error's Display output, which for a
DynamoError is itself "{error_type}: {message}". The reason therefore parses as an
identifier plus a free-text remainder โ split on the first colon, not on every colon.
2. Partial content is preserved
New helper aggregate_with_partial_recovery finds the first chunk matching
Annotated::is_error(), truncates the already-buffered Vec at that point, and
re-aggregates the error-free prefix. What the client actually saw before the failure is
kept, alongside the reason explaining why the record stops there.
This is sound because Annotated::into_data returns Err for exactly the chunks
is_error() matches, and DeltaAggregator::apply drives the buffer through a try_fold
that short-circuits on the first of them and discards the accumulator. A buffer
containing an error-tagged chunk can therefore only aggregate to Err; aggregating the
prefix is the only way to recover anything.
DeltaAggregator::apply itself is deliberately not modified. Its short-circuit is
load-bearing on the client-facing non-streaming path, where a typed backend error must
surface as an error rather than as a truncated success, and it is guarded by
preserves_typed_error_after_partial_output in
lib/llm/src/protocols/openai/chat_completions/aggregator.rs. The recovery is local to
the capture path.
The implementation avoids cloning the whole buffer โ which would double peak memory on
every capture-enabled request. It locates the error index first, clones only that single
chunk to extract its error, then truncates in place.
fold_aggregate_with_future's control flow is left as it was. The only edits there are
the channel's type parameter and replacing a silent drop(tx) with a tx.send(...) that
states the reason. This keeps the conflict surface against the in-flight rewrite of that
function to an async_stream::stream! generator as small as it can be.
3. emit requires the reason (lib/llm/src/request_trace/payload.rs)
pub fn emit(
self,
response: Option<Arc<NvCreateChatCompletionResponse>>,
drop_reason: Option<String>,
)payload_complete is now derived: response.is_some() && drop_reason.is_none(). Making
the reason a required second parameter rather than a defaulted one is the point โ a new
call site cannot quietly re-introduce an over-claiming record, because it has to state
what it knows.
A defensive branch covers (None, None): a record with neither a response nor a reason
asserts nothing about why it is empty, which is exactly the state this signature exists
to prevent. It is marked incomplete with the reason unspecified rather than being
allowed to look complete.
4. Call sites
lib/llm/src/preprocessor.rsโ the single production call site. It now logs the reason
and whether a partial response survived, then forwards both toemit.lib/llm/tests/request_trace_nats_integration.rsโ three mechanical updates in
#[ignore]-marked tests (they require a local NATS server and do not run here).
Tests
Three new tests, plus assertions added to existing ones as the negative control:
error_chunk_mid_stream_keeps_partial_content_and_names_the_errorโ the regression
itself. A stream delivers"Hello ", then an error chunk, then more content. Asserts
the record keeps"Hello ", that the error chunk still reaches the client unchanged,
and that the reason starts withaggregation_failed:and names the underlying error.error_as_first_chunk_reports_reason_without_a_responseโ nothing was delivered before
the error, so there is no partial content to keep, but the record must still say why it
is empty.emit_without_response_or_reason_still_marks_the_record_incompleteโ the(None, None)
defensive invariant, exercised through a real bus round-trip.
Extended:
test_empty_stream_handlingandtest_concurrent_futuresnow assert the specific
reason, so an empty stream stays distinguishable from a client cancel.emit_publishes_one_combined_record_with_and_without_responseassertspayload_complete
andpayload_drop_reasonin both directions. The success case is the negative
control: a faithful record must still report itself complete with no reason, so the flag
does not become vacuously false.- The three success-path tests in
payload_stream.rsassertdrop_reason.is_none().
Deviation from the plan, stated plainly
The plan expected the seven existing payload_stream.rs tests to remain green without
modification. That is not achievable: widening the future's output from
Option<NvCreateChatCompletionResponse> to PayloadOutcome makes future.await.is_none()
and future.await.expect(...) fail to compile at the call site. I adapted them
mechanically โ .response.is_none(), .response.expect(...) โ so every existing
assertion's meaning is preserved, and added the new reason assertions on top. No
existing assertion was weakened or removed. Recording this because a reviewer comparing
against the plan will otherwise see modified test bodies where none were predicted.
Validation
Everything below was run in the work-item checkout. All exit codes were zero.
Two toolchain notes, stated because they affect how much these results prove:
- The repository pins toolchain
1.93.1, which in this sandbox has neitherrustfmtnor
clippyinstalled. Both are available on the1.96.1toolchain that is also present, so
formatting and lint ran there, viarustup run. CI runs them on the pinned toolchain;
a pinned-toolchain-only lint would not be caught here. cargo fmt -- <file>does not pass the crate edition through torustfmt, so it
rejects the edition-2024 let-chains already inlib/llm/src/preprocessor.rs. I formatted
each changed file with an explicit--edition 2024and then confirmed the workspace with
cargo fmt --all --check.
Formatting, per changed file:
rustup run 1.96.1-x86_64-unknown-linux-gnu rustfmt --edition 2024 lib/llm/src/request_trace/payload_stream.rs
rustup run 1.96.1-x86_64-unknown-linux-gnu rustfmt --edition 2024 lib/llm/src/request_trace/payload.rs
rustup run 1.96.1-x86_64-unknown-linux-gnu rustfmt --edition 2024 lib/llm/src/preprocessor.rs
rustup run 1.96.1-x86_64-unknown-linux-gnu rustfmt --edition 2024 lib/llm/tests/request_trace_nats_integration.rsWhitespace and formatting:
git diff --check
rustup run 1.96.1-x86_64-unknown-linux-gnu cargo fmt --all --checkBoth clean, no output.
Folder-local checks required by lib/llm/src/request_trace/AGENTS.md:
cargo check -p dynamo-llm --lib --no-default-features
cargo test -p dynamo-llm --lib --no-default-features request_traceFinished dev profile ... in 1m 37s, then
test result: ok. 65 passed; 0 failed; 0 ignored; 0 measured; 2251 filtered out; finished in 0.82s.
All three new tests are in that set.
Recipe the validation command:
cargo check --workspace --all-targets
cargo check --manifest-path lib/bindings/python/Cargo.toml --all-targets
rustup run 1.96.1-x86_64-unknown-linux-gnu cargo clippy --workspace --all-targets --no-deps -- -D warnings
cargo test -p dynamo-llm --libFinished dev profile ... in 1m 57s; Finished dev profile ... in 1m 48s;
Finished dev profile ... in 2m 40s with no diagnostics emitted; and
test result: ok. 2516 passed; 0 failed; 5 ignored; 0 measured; 0 filtered out; finished in 7.95s.
Recipe the validation command, section 2:
pre-commit run --files lib/llm/src/request_trace/payload_stream.rs lib/llm/src/request_trace/payload.rs lib/llm/src/preprocessor.rs lib/llm/tests/request_trace_nats_integration.rs --hook-stage manualEvery applicable hook passed (codespell, check for case conflicts,
check for merge conflicts, mixed line ending, trim trailing whitespace,
check that scripts with shebangs are executable, Report pytest markers,
Validate AGENTS.md and CLAUDE.md pairs). Python- and docs-specific hooks reported
(no files to check) Skipped, correctly โ this change touches no Python or docs.
Not validated here
- The three
#[ignore]-marked tests inlib/llm/tests/request_trace_nats_integration.rs
need a NATS server on localhost and were updated but not executed. Their changes are
mechanical signature updates. - No end-to-end run against a live inference backend. The reasons are asserted at the unit
boundary where they are produced, not observed on a real cancelled request.
change-validation.mdValidation: record why a captured request payload is incompleteBranch TargetEverything below ran locally on the validation host: a single-GPU Linux container I re-ran every command myself. Nothing below is carried over from the change Checks the changed files with the repository's fast lint and formatting commandsNo Python file is touched by this change. I confirmed that rather than assuming it: git diff --name-only HEAD~1..HEADreturns exactly The repository's hook set ran against the four changed files: pre-commit run --files lib/llm/src/preprocessor.rs lib/llm/src/request_trace/payload.rs lib/llm/src/request_trace/payload_stream.rs lib/llm/tests/request_trace_nats_integration.rs --hook-stage manualExit Checks the changed Rust crates with
|
review.md
Review: record why a captured request payload is incompleteBranch I read the post-change source in the clone rather than reviewing from the diff Does the fix address the cause or only the symptomIt addresses the cause. The defect had two halves, and both are fixed at the The first half is that let payload_complete = response.is_some() && drop_reason.is_none();This is a genuine derivation from the record's own data, not a re-hardcoding The second half is that the reason did not survive the trip. The payload future The
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. ๐ โน๏ธ Recent review infoโ๏ธ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Enterprise Run ID: ๐ Files selected for processing (4)
Included review availability: Your plan provides up to 12 included reviews per hour; 11 remain after this review. WalkthroughStreaming payload aggregation now returns explicit outcomes for successful, empty, cancelled, and failed streams. Partial responses are preserved when possible. Request-trace records now report completeness and drop reasons. ChangesRequest payload outcome tracking
Priority: โ Normal Estimated code review effort: 4 (Complex) | ~45 minutes Severity of issue fixed: Medium Merge Risk: โช Minimal ยท up to The payload audit fixes are covered by targeted tests, preserve client behavior, and have no identified merge-blocking risk. ๐ฅ Pre-merge checks | โ 4 | โ 1โ Failed checks (1 warning)
โ Passed checks (4 passed)
Comment |
|
CI passed on head |
โฆing constraint The doc comment restated how `Annotated::into_data` and `DeltaAggregator::apply` behave, which the implementation below already shows. Keep only the part that is not visible from the code: why the aggregator is worked around here rather than taught to recover. Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
|
Repair round 1 โ head Three review findings addressed, one by changing the code and two by answering in-thread with the evidence:
Checks run on the new head, all passing: The last reported |
|
/devin review @coderabbitai full review |
๐งฉ Analysis chain๐ Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Revision and changed files =='
git rev-parse HEAD
git diff --stat HEAD^ HEAD
git diff --name-only HEAD^ HEAD
printf '%s\n' '== Repository instructions =='
fd -HI '^(AGENTS|CLAUDE)\.md$' . -E .git | sort
for f in $(fd -HI '^(AGENTS|CLAUDE)\.md$' . -E .git | sort); do
echo "--- $f"
cat "$f"
done
printf '%s\n' '== Changed-file outlines =='
for f in \
lib/llm/src/preprocessor.rs \
lib/llm/src/request_trace/payload.rs \
lib/llm/src/request_trace/payload_stream.rs \
lib/llm/tests/request_trace_nats_integration.rs
do
if test -f "$f"; then
echo "--- $f"
ast-grep outline "$f" --items all || true
fi
done
printf '%s\n' '== PR diff =='
git diff --find-renames --find-copies --unified=80 HEAD^ HEAD -- \
lib/llm/src/preprocessor.rs \
lib/llm/src/request_trace/payload.rs \
lib/llm/src/request_trace/payload_stream.rs \
lib/llm/tests/request_trace_nats_integration.rs
printf '%s\n' '== Relevant contract locations =='
rg -n -C 4 --type rust \
'\b(struct|type|fn)\s+(PayloadOutcome|PayloadFuture|RequestPayloadHandle|DeltaAggregator)|\b(emit|scan_aggregate_with_future|fold_aggregate_with_future|aggregate_with_partial_recovery|is_error|into_data)\s*\(' \
lib/llm/src lib/runtime/srcLength of output: 158406 ๐ Script executed: #!/bin/bash
set -euo pipefail
base_ref=""
for candidate in origin/main main origin/master master; do
if git rev-parse --verify -q "$candidate" >/dev/null; then
base_ref="$candidate"
break
fi
done
test -n "$base_ref"
merge_base=$(git merge-base "$base_ref" HEAD)
printf 'base_ref=%s\nmerge_base=%s\nhead=%s\n' "$base_ref" "$merge_base" "$(git rev-parse HEAD)"
printf '%s\n' '== Complete PR file list and statistics =='
git diff --stat "$merge_base" HEAD
git diff --name-status "$merge_base" HEAD
printf '%s\n' '== Complete PR diff =='
git diff --find-renames --find-copies --unified=45 "$merge_base" HEAD -- \
lib/llm/src/preprocessor.rs \
lib/llm/src/request_trace/payload.rs \
lib/llm/src/request_trace/payload_stream.rs \
lib/llm/tests/request_trace_nats_integration.rs
printf '%s\n' '== Targeted post-change source =='
sed -n '1,235p' lib/llm/src/request_trace/payload_stream.rs
sed -n '50,155p' lib/llm/src/request_trace/payload.rs
sed -n '6440,6510p' lib/llm/src/preprocessor.rs
sed -n '120,190p' lib/llm/src/request_trace/otel_sink.rs
sed -n '130,230p' lib/llm/src/protocols/openai/chat_completions/aggregator.rs
sed -n '1,165p' lib/runtime/src/protocols/annotated.rs
printf '%s\n' '== Full changed API call-site and test coverage search =='
rg -n -C 3 --type rust \
'RequestPayloadHandle::|\.emit\(|scan_aggregate_with_future\(|fold_aggregate_with_future\(|PayloadOutcome|aggregate_with_partial_recovery|DROP_(EMPTY_RESPONSE_STREAM|CLIENT_CANCELLED|UNSPECIFIED)' \
lib/llm lib/runtime
printf '%s\n' '== Changed test source =='
sed -n '300,440p' lib/llm/src/request_trace/payload.rs
sed -n '500,910p' lib/llm/src/request_trace/payload_stream.rs
sed -n '1,260p' lib/llm/tests/request_trace_nats_integration.rsLength of output: 145950
I found no blocking issue in the current PR head. The change correctly carries the aggregation outcome to The revised tests cover successful capture, empty streams, cancellation, first-chunk errors, mid-stream errors, partial response recovery, and the โ Action performedFull review finished. |
The SSE monitor stops polling and drops the pass-through stream on the first error it receives, so end-of-stream never arrives on the streaming path. Send the aggregation outcome when the error chunk is forwarded, so the recovered prefix and the backend error reach the record instead of a bare client_cancelled. Signed-off-by: svc-glamr@nvidia.com <svc-glamr@nvidia.com>
Signed-off-by: GLAMR <svc-glamr@nvidia.com>
|
nursery: @dynamo-ops please run full CI for 9285f5a |
|
/ok to test 9285f5a |
dmitry-tokarev-nv
left a comment
There was a problem hiding this comment.
Review: verified by execution
Approved. I reproduced the old behavior on today's main, confirmed this head fixes it, and mutation-tested the new tests. Two non-blocking items are below. Nothing blocks the merge.
Reproduction on today's main, and the same input on this head
One content chunk the client already received, then a mid-stream engine error, driven through scan_aggregate_with_future and the real record bus.
On c0eb3fcfaf (merge base, today's main):
{"event_type":"request_payload","payload":{"request_id":"probe-req","endpoint":"openai.chat_completion","model":"m","request":{...},"payload_complete":true}}No response key, no reason, and payload_complete is true. That is the lie.
On 9285f5ae (this head), same input:
{"event_type":"request_payload","payload":{"request_id":"probe-req",...,"response":{"choices":[{"message":{"content":"Hello "}}]},"payload_complete":false,"payload_drop_reason":"aggregation_failed:Internal: engine died mid-stream"}}The record is now honest, and it keeps the prefix the client saw.
Every way the response can be dropped, and what the record says for each
| Drop path | Record written? | Does it tell the truth? |
|---|---|---|
| Engine error mid-stream, streaming | Yes | Yes. payload_complete:false, aggregation_failed:<error>, plus the pre-error prefix. |
| Engine error before any content | Yes | Yes. aggregation_failed:<error>, no response. |
| Engine error, non-streaming fold | Yes | Yes. Reason plus prefix in the record. The client still gets the empty fallback, same as before. |
| Client disconnects mid-stream | Yes | Yes about completeness: response_stream_dropped. The delivered prefix is not kept, because the buffered chunks go away with the stream. |
| Client disconnects before the first chunk | Yes | Yes. Same response_stream_dropped. |
| Request cancelled or gateway timeout | Yes | Yes. The engine context dies, the stream is dropped, so response_stream_dropped. The comment at payload_stream.rs:47 says these two causes cannot be told apart from here, which matches what I read in the code. |
| Empty response stream | Yes | Yes. empty_response_stream. |
| Aggregation fails with no error chunk | Yes | Yes. aggregation_failed:<error>. |
| A call site passes no response and no reason | Yes | Yes. unspecified, and a test pins it. |
Rejected before the stream exists (worker 503, or any of the 6 ? returns between preprocessor.rs:7088 and :7219) |
No record at all | Not applicable. The handle is dropped without emit. This is a different defect from a lying record, it is present on main as well, and this PR does not change it. Issue #14134 lists it. Happy to file it separately. |
| Process shutdown while the request is live | Not guaranteed | The emit runs in a spawned task, so a shutdown can lose it. Unchanged by this PR. |
Unverified: I did not drive a real client disconnect through the HTTP frontend. The three rows about disconnect and cancel are read from the code plus the test_concurrent_futures case, which drops the pass-through stream before the first poll and gets response_stream_dropped.
Who sets the field, and whether two writers can disagree
emit consumes the handle, so one handle can produce at most one record. Production has one call site, preprocessor.rs:7240, inside one spawned task. done_tx is a oneshot behind Option::take, so the error-chunk path and the end-of-stream path cannot both fire. There is no race on payload_complete inside the record.
There is a second writer further down. otel_sink.rs rewrites both fields, and it wins for the exported attribute. See the P2 item.
For a consumer: payload_complete is a plain bool with no skip_serializing_if and no serde(default), so the key is always present on the wire and was always present before. An older reader sees the same key and now sees false where it used to see true. payload_drop_reason is skip_serializing_if = "Option::is_none", so it appears only on a dropped record. No schema change, and no field that defaults to complete.
[P2] The OTEL export still says payload_complete: true for a record that says false. lib/llm/src/request_trace/otel_sink.rs:159 returns the literal true for every record under the size limit, so the exported attribute contradicts the record body this PR just corrected. Please derive it from record.payload.
Measured on this head, with the probe output
I set payload_complete = false and a drop reason on a payload record, then called OtelRequestTraceSink::payload_for_limit with a limit the record fits inside.
PROBE otel attribute payload_complete=true payload_drop_reason=None
PROBE body payload_complete=false body drop_reason="aggregation_failed:engine died mid-stream"
otel_sink.rs:159:
if payload.len() <= max_payload_bytes {
return Some((payload, true, None));
}Those values become the payload_complete and payload_drop_reason attributes at otel_sink.rs:329. A consumer that filters on the indexed attribute, rather than parsing the embedded body, still reads a dropped response as complete.
This is not a regression. Before this PR the body also said true, so the two agreed and were both wrong. The fix simply does not reach the attribute, and the record now contradicts itself. The file is outside this diff, so this is a review-body note. A follow-up PR is fine if you prefer.
New tests: mutation results, with the control
All 19 tests in request_trace::payload and request_trace::payload_stream pass on this head with cargo test -p dynamo-llm --no-default-features. Three mutations, each caught by the test that claims to cover it, with the other 17 tests unchanged as the control:
| Mutation | Result |
|---|---|
emit hardcodes payload_complete: true and payload_drop_reason: None again |
2 failed, 17 passed. Both new assertions fire: "a record with no response must not claim to be complete". |
aggregate_with_partial_recovery drops the pre-error prefix |
2 failed, 17 passed. "content delivered before the error must survive the early drop". |
The error-chunk early settle in poll_next is disabled |
1 failed, 18 passed. Reason falls back to response_stream_dropped, which the test rejects. |
The tests read the outcome the code produced, and the two payload.rs tests read the record off the real record bus, not a record they built themselves.
Overlap with #14008
#14008 rewrites the same function, fold_aggregate_with_future in payload_stream.rs, and renames final_response_to_one_chunk_stream. git merge-tree between the two heads produces conflict markers in that file. The two do not depend on each other, but they cannot both merge unchanged.
The order matters for behavior, not only for text. #14008 keeps the current oneshot::channel::<NvCreateChatCompletionResponse>() and still drops the sender on an aggregation error, which is exactly the bare Option this PR replaces. If #14008 lands second and is rebased by taking its own version of the function, the non-streaming path goes back to a record with no reason. Whoever rebases second must keep PayloadOutcome.
State, staleness, and what I did not verify
Head 9285f5ae. The merge base is c0eb3fcfaf, which is from today, so the branch is 3 commits behind main and not stale. The defect still reproduces at that merge base, so the PR is still needed.
The base moved since @nnshah1 approved a343096c, so I compared the author's own change at the two merge bases instead of old-head...new-head. The only difference is comment text, shortened in response to @alec-flowers. No executable line changed after the approval.
I did not run the HTTP frontend or a real engine. Verification is cargo test -p dynamo-llm --no-default-features on macOS ARM64 plus the probes above. 5 unrelated tests in that crate fail locally on this machine on both trees, in networking code this PR does not touch.
โฆl folds Signed-off-by: Matej Kosec <mkosec@nvidia.com>
|
/ok to test 6e81ea1 |
dmitry-tokarev-nv
left a comment
There was a problem hiding this comment.
Follow-up review: re-approved at 6e81ea1291
The new commit closes the P2 and the P3 from my last round. I verified both by execution, not by reading. One new P3 is below and it does not block the merge.
What moved since my approval of `9285f5ae`, and how I established it
The push is an append, not a rewrite.
compare/9285f5ae...6e81ea12returnsstatus: ahead,ahead_by: 1,behind_by: 0, and the merge base of the two is9285f5aeitself. The commit I approved is still an ancestor of the live head.- The merge base with
mainisc0eb3fcfafbefore the push andc0eb3fcfafafter it. The base did not move, so9285f5ae..6e81ea12is exactly the author's own change and no diff of diffs is needed. - The new commit touches two files:
otel_sink.rs(the export fix plus one test) andpayload_stream.rs(one test only). No production code changed inpayload_stream.rs.
I re-read the live head immediately before posting this review. It was still 6e81ea1291.
The drop paths, re-walked against the new code
The new commit changes one production function, payload_for_limit. Its contract is unchanged: same signature, same return type, and emit at otel_sink.rs:297 is still the only caller.
Every drop path still writes a record and still names a reason. The export now repeats that reason instead of dropping it. The one exception is a record that is also over the size limit, which is the new P3 below.
A record with no captured payload still exports payload_complete = true, as it did before. That is not a claim about a response, because there is no payload on such a record.
The reason string reaches an OTEL attribute for the first time. It is not caller-supplied text, and it is a field of the record that the size cap already bounds, so the attribute is bounded too.
Tests: what I ran and what each result proves
Worktree at 6e81ea1291, macOS, --no-default-features, sandbox off so port binding works.
| Run | Result |
|---|---|
--lib request_trace:: on the clean head |
76 passed, 0 failed |
| Same, with the fold success arm mutated | 75 passed, 1 failed: the new fold test |
--lib request_trace::otel_sink with the export reverted to the old literal |
3 passed, 1 failed: the new sink test |
cargo fmt -p dynamo-llm --check |
clean |
Both new tests fail on the pre-fix code and pass on this head, so each one pins its own arm.
The new sink test builds its own record. I added a separate probe that drives a real errored stream through scan_aggregate_with_future and the record bus, then feeds the record the system produced into payload_for_limit. Body and attribute agree on that path as well. The numbers are on the inline thread.
#14143 and #14008 still conflict, and the order still matters
#14008 is open, head 2b8a915437 from September 1, and its payload_stream.rs change is unchanged since my last round. It rewrites the same fold_aggregate_with_future, still on oneshot::channel::<NvCreateChatCompletionResponse>(), and still sends no drop reason.
The two cannot both merge unchanged. If #14008 lands second and the conflict is resolved by keeping its own function body, the non-streaming record loses its reason again and this PR is silently undone on that path. The safe order is this PR first, then a rebase of #14008 that keeps PayloadOutcome.
I am not reporting this as a finding on this PR. It is a coordination point for whoever merges second.
[P3] An oversized record loses the capture reason it now carries. See the inline comment on otel_sink.rs:169. Non-blocking, and new in the sense that the case did not exist before this PR.
Outstanding: one P3. No P0, no P1. I did not drive a real client disconnect through the HTTP frontend, which was also true last round.
Signed-off-by: GLAMR <svc-glamr@nvidia.com>
|
nursery: @coderabbitai full review |
|
nursery: @dynamo-ops please run full CI for 3612868 |
|
/ok to test 3612868 |
|
Tip For best results, initiate chat on the files or code changes. ๐งฉ Analysis chain๐ Script executed: #!/bin/bash
set -euo pipefail
base_ref=""
for candidate in origin/main main origin/master master; do
if git rev-parse --verify -q "$candidate" >/dev/null; then
base_ref="$candidate"
break
fi
done
test -n "$base_ref"
merge_base="$(git merge-base "$base_ref" HEAD)"
printf 'head=%s\nbase=%s\nmerge_base=%s\n' "$(git rev-parse HEAD)" "$base_ref" "$merge_base"
printf '%s\n' '== Applicable instructions =='
for file in AGENTS.md lib/AGENTS.md lib/llm/AGENTS.md lib/llm/src/AGENTS.md lib/llm/src/request_trace/AGENTS.md; do
if test -f "$file"; then
printf '\n--- %s ---\n' "$file"
cat "$file"
fi
done
printf '%s\n' '== Pull request commits and change set =='
git log --oneline --decorate "$merge_base"..HEAD
git diff --stat "$merge_base" HEAD
git diff --name-status "$merge_base" HEAD
git diff --find-renames --find-copies --unified=55 "$merge_base" HEAD
printf '%s\n' '== Changed Rust file outlines =='
git diff --name-only "$merge_base" HEAD | while IFS= read -r file; do
case "$file" in
*.rs)
printf '\n--- %s ---\n' "$file"
ast-grep outline "$file" --items all || true
;;
esac
done
printf '%s\n' '== Current request-trace implementation =='
sed -n '1,360p' lib/llm/src/request_trace/payload_stream.rs
sed -n '1,250p' lib/llm/src/request_trace/payload.rs
sed -n '6950,7080p' lib/llm/src/preprocessor.rs
printf '%s\n' '== Relevant contracts =='
fd -HI '^annotated\.rs$' lib -x sh -c '
printf "\n--- %s ---\n" "$1"
rg -n -C 12 "fn (is_error|cloned_error|into_data)" "$1" || true
' sh {}
fd -HI '^aggregator\.rs$' lib -x sh -c '
printf "\n--- %s ---\n" "$1"
rg -n -C 22 "fn apply|try_fold|preserves_typed_error_after_partial_output" "$1" || true
' sh {}
fd -HI '^disconnect\.rs$' lib -x sh -c '
printf "\n--- %s ---\n" "$1"
rg -n -C 16 "monitor_for_disconnects_with_timeout_error_and_keep_alive|Some\(Err|break" "$1" || true
' sh {}
printf '%s\n' '== Outcome and emission call sites =='
rg -n -C 6 --type rust \
'PayloadOutcome|PayloadFuture|aggregate_with_partial_recovery|scan_aggregate_with_future\(|fold_aggregate_with_future\(|RequestPayloadHandle|\.emit\(|response_stream_dropped|aggregation_failed|empty_response_stream|unspecified' \
lib/llm/src lib/llm/tests lib/runtime/src
printf '%s\n' '== Request trace schema and downstream drop-reason processing =='
rg -n -C 10 --type rust \
'struct RequestTracePayload|payload_complete|payload_drop_reason|fn marker_payload|marker_payload' \
lib/llm/src/request_traceLength of output: 158414 ๐ Script executed: #!/bin/bash
set -euo pipefail
log=/tmp/coderabbit-shell-logs/shell-output-EFNcZS
printf '%s\n' '== Inspection metadata from first pass =='
sed -n '1,180p' "$log"
base_ref=""
for candidate in origin/main main origin/master master; do
if git rev-parse --verify -q "$candidate" >/dev/null; then
base_ref="$candidate"
break
fi
done
merge_base="$(git merge-base "$base_ref" HEAD)"
printf '%s\n' '== PR-only diff, intended files =='
git diff --find-renames --find-copies --unified=30 "$merge_base" HEAD -- \
lib/llm/src/preprocessor.rs \
lib/llm/src/request_trace/payload.rs \
lib/llm/src/request_trace/payload_stream.rs \
lib/llm/tests/request_trace_nats_integration.rs
printf '%s\n' '== Final fold and preprocessor paths =='
sed -n '210,275p' lib/llm/src/request_trace/payload_stream.rs
sed -n '7200,7250p' lib/llm/src/preprocessor.rs
printf '%s\n' '== Test coverage around error and folding paths =='
sed -n '800,985p' lib/llm/src/request_trace/payload_stream.rs
printf '%s\n' '== All RequestPayloadHandle::emit invocations =='
rg -n -C 4 --type rust '\.emit\s*\(' lib/llm/src lib/llm/tests | rg -C 8 'emit\(|RequestPayloadHandle|payload::create_handle|for_test' || true
printf '%s\n' '== Current tree status =='
git status --shortLength of output: 92135
I found no blocking issue.
The streaming path settles the audit outcome when it receives an error annotation. It does this before the downstream disconnect monitor can drop the pass-through stream. The audit record retains the pre-error prefix and records The fold path uses the same partial-recovery logic. It preserves partial content for the audit record. It still sends the empty fallback response to the client when aggregation is incomplete.
The updated tests cover successful aggregation, empty streams, early stream drops, an error as the first chunk, partial recovery after an error, fold aggregation, and the I did not run build or test commands in this read-only review environment. You are interacting with an AI system. |
Summary
The request-trace audit record could claim to be a faithful copy of a request and its response even when the response had been thrown away.
RequestPayloadHandle::emitwrote the literalspayload_complete: trueandpayload_drop_reason: Noneinto every record it published, including records published with no response at all. A consumer of the audit stream could not tell a request that genuinely produced no content from one whose response we dropped and did not mention. This change makes the record say what it actually knows.Details:
emitnow takes the drop reason as a required second argument and derivespayload_completefrom the data โresponse.is_some() && drop_reason.is_none()โ instead of hardcoding it. A call site that has no response and states no reason gets the reasonunspecifiedrather than a record that looks good.The reason had to be carried down from where it was lost. The payload future in
lib/llm/src/request_trace/payload_stream.rsresolved to a bareOption, collapsing three outcomes into oneNone. It now resolves to aPayloadOutcomeholding both the response and an optional reason, and each site names itself:empty_response_stream,aggregation_failed:<error>, orresponse_stream_dropped. Theidentifier:detailform matches the grammarotel_sink.rsalready publishes.Where a stream delivered content before failing,
aggregate_with_partial_recoveryre-aggregates the error-free prefix, so the record keeps what the client saw and says why it stops there.DeltaAggregator::applyis unchanged; its short-circuit is load-bearing on the client-facing path.OTEL records within the size limit now preserve the captured completeness flag and drop reason in the exported attributes, keeping them consistent with the embedded record body. Oversized OTEL markers also retain the original capture reason alongside the size-limit reason. Regression coverage includes successful folds, incomplete OTEL payloads, and capture reasons on both oversized-marker paths.
No wire-schema change: both fields already exist on
RequestTracePayloadand are already serialized.Where should the reviewer start?
lib/llm/src/request_trace/payload.rsfor the derivation, thenpayload_stream.rsfor the reason plumbing and partial recovery.Validation
361286827a8b237a631ed12dfe41feac6ad68bbe.Related Issues
๐ This PR is linked to an issue:
Summary by CodeRabbit