Skip to content

[https://nvbugs/6456085][fix] Harmony: stop discarding malformed tool-call messages silently - #17464

Open
JunyiXu-nv wants to merge 2 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-fix-harmony-silent-toolcall-drop
Open

[https://nvbugs/6456085][fix] Harmony: stop discarding malformed tool-call messages silently#17464
JunyiXu-nv wants to merge 2 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-fix-harmony-silent-toolcall-drop

Conversation

@JunyiXu-nv

@JunyiXu-nv JunyiXu-nv commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Description

Reported in GitHub #16377 / NVBug 6456085: with gpt-oss-120b + Eagle3 on trtllm-serve, a tool call is returned as an empty tool_calls array with empty content — HTTP 200, finish_reason: "stop", and nothing in the logs. A caller cannot tell this apart from the model declining to call the tool.

This PR fixes the silence, not the upstream corruption (see Caveat below).

Mechanism

HarmonyAdapter._strip_incomplete_messages removes everything from the last <|start|> onwards when no <|message|> token follows it:

if not has_message_token:
    # Remove everything from the last <|start|> onwards
    clean_tokens = clean_tokens[:last_start_idx]

If a tool-call message arrives with a malformed header, its entire span is deleted. The remaining analysis message then parses cleanly, so _harmony_parsing_failed is never set and the raw-text fallback never runs. The result is a well-formed response that is simply missing the tool call — while completion_tokens still bills the discarded tokens.

Stripping a truncated trailing message is correct when generation was cut short (e.g. max_tokens). It is not benign when the discarded span carries a stop token or a to=functions. recipient: that message was complete, only malformed. This PR warns in that case and logs the benign case at debug level, so the warning stays meaningful.

Test Coverage

tests/unittest/llmapi/apps/test_harmony_parsing.py::TestStripIncompleteMessagesReporting — 3 cases built from canonical harmony tokens (rendered by openai_harmony, not hand-assembled):

Test Asserts
test_wellformed_tool_call_is_parsed_without_warning baseline: tool call parsed, no warning
test_malformed_tool_call_header_is_reported dropped tool call now warns, naming the function
test_truncated_tail_does_not_warn benign truncation stays quiet

Verified as a genuine regression test: test_malformed_tool_call_header_is_reported fails on the unmodified adapter and passes with this change. Full file: 3 passed / 49 deselected.

Caveat — this does not fix the root cause

Why the harmony header is malformed in the first place is not addressed here, and I was unable to reproduce that half. Attempted repro with the reporter's exact container (nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc13), gpt-oss-120b + gpt-oss-120b-Eagle3, running the reporter's A/B config unchanged:

Variant Hardware Result
A — Eagle3 enabled GB110, sm_103 tool call returned 5/5
B — Eagle3 disabled GB110, sm_103 tool call returned 5/5

The repair bot likewise could not reproduce on B200. The reporter is on GB10 / sm_121 (DGX Spark), which neither attempt covers, and uses a different draft checkpoint (nvidia/gpt-oss-120b-Eagle3-long-context). Both remain untested variables.

So the failing input shape here is inferred from the sibling reports (#8615, #10612), not observed directly. That is exactly why the logging matters: with this change, a user hitting it gets a warning naming the dropped function instead of silence, which is enough to confirm or refute the hypothesis from a normal server log.

Related, not fixed here

While investigating I found a second silent path: a message whose <|channel|> token is missing but whose recipient is functions.* is routed to final_content, turning a tool call into assistant text — also with no warning. It does not match this bug's signature (content was empty, not populated), so I left it out to keep this PR to one concern. Worth a follow-up.

PR Checklist

  • PR title follows [JIRA/NVBUG/None][type] summary
  • Commit signed off (DCO)
  • New tests added and passing
  • Pre-commit hooks pass

Dev Engineer Review

  • _strip_incomplete_messages now reports discarded trailing tokens.
  • _log_discarded_tokens warns when discarded tokens contain a stop token or a to=functions.* recipient.
  • Warning logs include safe metadata only. They do not include decoded message content or tool-call arguments.
  • Benign truncation remains at debug level.
  • The change preserves the public API and has no configuration or test-list changes.
  • The implementation addresses the silent-discard regression without changing the upstream malformed-header path.

QA Engineer Review

Added tests:

  • test_wellformed_tool_call_is_parsed_without_warning
  • test_malformed_tool_call_header_is_reported
  • test_truncated_tail_does_not_warn

The tests are not listed in tests/integration/test_lists/ for CI (test-db/) or manual QA (qa/).

Verdict: insufficient. Add the test coverage to the applicable test list, or document why unit-test discovery does not require test-list registration.

…-call messages silently

When a harmony tool-call message reaches the non-streaming adapter with a
malformed header (no <|message|> token), _strip_incomplete_messages drops
everything from the last <|start|> onwards. The remaining analysis message
then parses cleanly, so no fallback is triggered and nothing is logged.

The response is a valid HTTP 200 with finish_reason "stop", a populated
reasoning_content and an empty tool_calls list -- indistinguishable from
the model choosing not to call a tool. Callers cannot detect the loss, and
the generated tokens are still billed in completion_tokens.

Stripping an incomplete trailing message is correct when generation was cut
short (e.g. max_tokens). It is not benign when the discarded span carries a
stop token or a tool recipient: that message was complete, only malformed.
Warn in that case, and log the benign case at debug level so the warning
stays meaningful.

This makes the failure diagnosable; it does not change which tool calls are
recovered.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
@JunyiXu-nv
JunyiXu-nv requested a review from a team as a code owner August 10, 2026 06:03
@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

/bot run

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 79dc6732-2f39-4eb4-ba0d-e631357de47c

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee26e9 and ad759ae.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/harmony_adapter.py
  • tests/unittest/llmapi/apps/test_harmony_parsing.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tensorrt_llm/serve/harmony_adapter.py
  • tests/unittest/llmapi/apps/test_harmony_parsing.py

Walkthrough

The Harmony adapter reports discarded tokens from incomplete sequences. It warns for malformed tool calls and logs ordinary truncated messages at debug level. Tests cover valid, malformed, and truncated tool-call sequences.

Changes

Harmony diagnostics

Layer / File(s) Summary
Discarded-token reporting and validation
tensorrt_llm/serve/harmony_adapter.py, tests/unittest/llmapi/apps/test_harmony_parsing.py
The adapter decodes discarded tokens and logs warnings for malformed messages containing stop tokens or function recipients. Tests validate well-formed tool calls, malformed tool-call headers, and truncated trailing headers.

Estimated code review effort: 2 (Simple) | ~10 minutes

Suggested reviewers: cascade812, mikeiovine

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the required ticket and type format and clearly describes the primary Harmony parsing fix.
Description check ✅ Passed The description explains the issue, solution, limitations, tests, and checklist status with sufficient detail.
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
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
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Actionable comments posted: 3

🤖 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.

Inline comments:
In `@tensorrt_llm/serve/harmony_adapter.py`:
- Around line 1081-1093: Update the discarded-token logging around
_safe_decode_utf8 to remove raw text from both warning and debug messages.
Extract the function recipient from the discarded content, include only the
token count and recipient in the warning, and preserve the debug message with
token count but no discarded text; ensure the warning identifies the dropped
function.

In `@tests/unittest/llmapi/apps/test_harmony_parsing.py`:
- Around line 1341-1346: Update the adapter fixture to stop converting arbitrary
HarmonyAdapter initialization failures into skipped tests. In adapter, either
remove the broad exception handling so construction errors fail normally, or
catch only the specific exception that indicates the test environment is
unavailable while preserving pytest.skip for that case.
- Around line 1409-1430: Update test_truncated_tail_does_not_warn to assert
mock_logger.debug receives a message containing “incomplete trailing harmony
message”, while retaining the no-warning assertion. Narrow the adapter fixture’s
exception handling to only the expected setup exceptions so unexpected
initialization errors fail tests rather than causing skips. Run pytest
tests/unittest/ before merge and verify the added harmony parsing tests remain
covered.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 914b9af4-6954-4f6f-b33e-4762d452bd7b

📥 Commits

Reviewing files that changed from the base of the PR and between 3bffdc3 and 4ee26e9.

📒 Files selected for processing (2)
  • tensorrt_llm/serve/harmony_adapter.py
  • tests/unittest/llmapi/apps/test_harmony_parsing.py

Comment thread tensorrt_llm/serve/harmony_adapter.py Outdated
Comment thread tests/unittest/llmapi/apps/test_harmony_parsing.py Outdated
Comment thread tests/unittest/llmapi/apps/test_harmony_parsing.py
@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64970 [ run ] triggered by Bot. Commit: 4ee26e9 Link to invocation

@tensorrt-cicd

Copy link
Copy Markdown
Collaborator

PR_Github #64970 [ run ] completed with state FAILURE. Commit: 4ee26e9
/LLM/main/L0_MergeRequest_PR pipeline #52803 completed with status: 'FAILURE'

CI Report

⚠️ Action Required:

  • Please check the failed tests and fix your PR
  • If you cannot view the failures, ask the CI triggerer to share details
  • Once fixed, request an NVIDIA team member to trigger CI again

CI Agent Failure Analysis

Link to invocation

…ssage text

The warning logged the decoded discarded span, which holds the tool call's
arguments -- routinely user data, occasionally secrets. This path fires at
warning level in production, so it would persist that data in logs.

Inspect the decoded text but never log it: extract the functions.* recipient
and report only the token count and the dropped function name, which is what
makes the drop actionable. Drop the text from the debug branch too.

Also narrow the test fixture: the module-level pytestmark already skips when
harmony is unavailable, so catching every exception from HarmonyAdapter() only
hid real regressions as skips. Assert the benign path still logs at debug, and
assert the warning does not leak tool arguments.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

Thanks @coderabbitai — all three addressed in ad759ae28b.

1. Do not log the full discarded message text (Major, Security/Privacy) — agreed, and this was the real bug in the patch. The discarded span is the tool call's arguments, so a warning-level path in production would have persisted user data, and potentially secrets, into logs. The decoded text is now inspected but never logged: the functions.* recipient is extracted with a regex and the warning reports only the token count and the dropped function name, which is the part that makes it actionable. Removed from the debug branch too.

2. Broad except Exception in the fixture — agreed. Rather than narrowing it, I removed the guard: the module-level pytestmark already skips when harmony is unavailable, so anything HarmonyAdapter() raises here is a genuine regression and should fail loudly rather than turn into a skip. (Note the pre-existing fixtures in this file use the same pattern; I left those alone to keep this PR to one concern.)

3. Assert the debug path in test_truncated_tail_does_not_warn — done, asserts the benign case still logs "incomplete trailing harmony message" at debug alongside the existing no-warning assertion. I also added an assertion that the warning does not contain the tool arguments, so finding 1 cannot silently regress.

Re-ran in release:1.3.0rc13: 3 passed / 49 deselected.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@JunyiXu-nv Thank you for the detailed update.

The fixes address all three findings. In particular, excluding decoded tool-call arguments from both warning and debug logs prevents sensitive data exposure. The targeted test result is also consistent with the intended behavior.

🐇 ✅


✏️ Learnings added
Learnt from: JunyiXu-nv
URL: https://github.com/NVIDIA/TensorRT-LLM/pull/17464

Timestamp: 2026-08-10T09:10:23.605Z
Learning: In `tensorrt_llm/serve/harmony_adapter.py`, discarded Harmony message spans can contain tool-call arguments and sensitive user data. Logging for discarded messages must not emit decoded message text or tool-call arguments. Warnings may report safe metadata such as the token count and extracted `functions.*` recipient.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator Author

The failure on 4ee26e9 is unrelated to this PR: DGX_B200-PyTorch-3 hit TestLlama3_1_8BInstruct::test_nvfp4_kv[v2_kv_cache=False-attn_backend=TRTLLM-torch_compile=True] with a PyTorch caching-allocator internal assert (it->second->use_count > 0 ... CUDACachingAllocator.cpp:3045), and SBSA cascaded from it. Both Build-x86_64 and Build-SBSA succeeded. This PR only touches harmony parsing/logging and its unit tests.

Re-running on ad759ae, which additionally carries the CodeRabbit review fixes.

/bot run

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