System Info
[Bug] Eagle3 speculative decoding silently drops tool_calls for gpt-oss on trtllm-serve
Summary
With gpt-oss-120b on trtllm-serve, enabling Eagle3 speculative decoding makes the server
return an empty tool_calls array and empty content, even though the model clearly decided
to call the function (its reasoning_content says so, and it emitted the tokens).
Disabling speculative decoding — changing nothing else — makes tool calling work correctly.
The Harmony adapter itself is active in both cases (reasoning_content is populated either way), so
this is not a model-detection or chat-template problem: the tool-call channel is lost specifically
when Eagle3 is on.
This may be the missing link between #8615 and #10612
Two open issues describe what look like the same underlying failure, seen from different angles:
Our A/B adds the missing variable: Eagle3 is the trigger, and the damage lands on the Harmony
control tokens — which are exactly the rare, structural tokens a draft head is worst at predicting
(<|channel|>, <|message|>).
Our failure mode is the most dangerous of the three, because it is completely silent. #8615 loops
loudly; #10612 returns visibly malformed JSON. Here: HTTP 200, finish_reason: "stop", no warning —
the tool call simply is not there. A framework cannot tell this apart from "the model chose not to call
the tool".
(#7163 is closed and used xgrammar with no speculative_config, so it is a different path.)
Environment
|
|
| Container |
nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc13 |
| GPU |
NVIDIA GB10 (DGX Spark), compute capability sm_121 |
| Driver |
580.159.03 |
| Backend |
--backend pytorch, --tp_size 1 |
| Model |
openai/gpt-oss-120b (MXFP4) |
| Draft model |
nvidia/gpt-oss-120b-Eagle3-long-context |
Reproduction
A — WITH Eagle3 → tool_calls is empty (bug)
extra-llm-api-config.yml:
enable_attention_dp: false
disable_overlap_scheduler: false
enable_autotuner: false
cuda_graph_config:
max_batch_size: 4
speculative_config:
decoding_type: Eagle
max_draft_len: 5
speculative_model_dir: /opt/gpt-oss-120b-Eagle3/
kv_cache_config:
free_gpu_memory_fraction: 0.9
enable_block_reuse: false
trtllm-serve openai/gpt-oss-120b \
--tokenizer <local snapshot path> \
--backend pytorch --tp_size 1 --max_batch_size 4 \
--host 0.0.0.0 --port 8005 \
--extra_llm_api_options extra-llm-api-config.yml
Request:
curl -s http://127.0.0.1:8005/v1/chat/completions -H 'Content-Type: application/json' -d '{
"model":"openai/gpt-oss-120b",
"messages":[{"role":"user","content":"What is the weather in Rome? Use the tool."}],
"tools":[{"type":"function","function":{
"name":"get_weather","description":"Get the weather of a city",
"parameters":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}}],
"tool_choice":"auto","max_tokens":300}'
Response (bug):
{
"message": {
"role": "assistant",
"content": "",
"reasoning_content": "User asks weather in Rome. Use function.",
"tool_calls": []
},
"finish_reason": "stop",
"usage": { "completion_tokens": 37 }
}
Note: completion_tokens: 37 while the reasoning text is only ~10 words — the tool-call tokens were
generated and then dropped. reasoning_content being populated proves the Harmony adapter is active
(openai_server.py: use_harmony = (model_config.model_type == "gpt_oss")).
B — WITHOUT Eagle3 → works
Same command, same image, same model, same tokenizer, same request. Only the config differs — the
speculative_config block is removed:
enable_attention_dp: false
disable_overlap_scheduler: false
enable_autotuner: false
cuda_graph_config:
max_batch_size: 4
kv_cache_config:
free_gpu_memory_fraction: 0.9
enable_block_reuse: false
Response (correct):
{
"tool_calls": [
{ "function": { "name": "get_weather", "arguments": "{\"city\": \"Rome\"}" } }
]
}
Why this matters
The failure is silent: HTTP 200, finish_reason: "stop", no warning in the logs. An agent
framework sees an assistant message with empty content and no tool call, and simply believes the model
declined to act. Any multi-agent system built on tool calling will appear to "work" while doing
nothing — which is how we spent hours blaming the model.
Also worth noting: the DGX Spark playbook
(dgx-spark-playbooks/nvidia/speculative-decoding, Option 1) recommends exactly this Eagle3 recipe for
gpt-oss-120b on a single Spark. Anyone following it and then using tools will hit this.
Suggested area to look at
tensorrt_llm/serve/harmony_adapter.py — the token-batch path
(process_token_batch / _get_or_create_tool_call) versus how tokens are delivered when
Eagle3OneModelSampler is in use. The reasoning channel survives; the tool-call channel does not.
A loud error — or even a startup warning that tool calling is unsupported with speculative decoding —
would already be a large improvement over silently returning an empty tool_calls.
Who can help?
No response
Information
Tasks
Reproduction
|
|
| Container |
nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc13 |
| GPU |
NVIDIA GB10 (DGX Spark), compute capability sm_121 |
| Driver |
580.159.03 |
| Backend |
--backend pytorch, --tp_size 1 |
| Model |
openai/gpt-oss-120b (MXFP4) |
| Draft model |
nvidia/gpt-oss-120b-Eagle3-long-context |
A — WITH Eagle3 → tool_calls is empty (bug)
config.yml:
enable_attention_dp: false
disable_overlap_scheduler: false
enable_autotuner: false
cuda_graph_config:
max_batch_size: 4
speculative_config:
decoding_type: Eagle
max_draft_len: 5
speculative_model_dir: /opt/gpt-oss-120b-Eagle3/
kv_cache_config:
free_gpu_memory_fraction: 0.9
enable_block_reuse: false
Server:
trtllm-serve openai/gpt-oss-120b \
--tokenizer <path to the local HF snapshot> \
--backend pytorch --tp_size 1 --max_batch_size 4 \
--host 0.0.0.0 --port 8000 \
--extra_llm_api_options config.yml
Request:
curl -s http://localhost:8000/v1/chat/completions -H 'Content-Type: application/json' -d '{
"model": "openai/gpt-oss-120b",
"messages": [{"role": "user", "content": "What is the weather in Rome? Use the tool."}],
"tools": [{"type": "function", "function": {
"name": "get_weather",
"description": "Get the weather of a city",
"parameters": {"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]}}}],
"tool_choice": "auto",
"max_tokens": 300}'
B — WITHOUT Eagle3 → works
Same image, same model, same tokenizer, same request. Only the speculative_config block is removed
from config.yml.
---
### Expected behavior
```markdown
The response should contain the tool call the model decided to make — which is exactly what happens
when Eagle3 is disabled:
```json
{
"message": {
"role": "assistant",
"tool_calls": [
{"function": {"name": "get_weather", "arguments": "{\"city\": \"Rome\"}"}}
]
}
}
---
### actual behavior
```markdown
With Eagle3 enabled, the tool call is silently lost:
```json
{
"message": {
"role": "assistant",
"content": "",
"reasoning_content": "User asks weather in Rome. Use function.",
"tool_calls": []
},
"finish_reason": "stop",
"usage": { "completion_tokens": 37 }
}
Three things are worth pointing out:
- The model did decide to call the function —
reasoning_content literally says "Use function."
completion_tokens: 37, while the reasoning text is only ~10 words. The tool-call tokens were
generated and then dropped.
- The Harmony adapter is active —
reasoning_content being populated proves it
(openai_server.py: use_harmony = (model_config.model_type == "gpt_oss")). So this is not a
model-detection or chat-template problem.
There is no error, no warning, and finish_reason is "stop". The response is a perfectly valid
HTTP 200. An agent framework cannot distinguish this from "the model chose not to call the tool".
---
### additional notes
```markdown
### This may be the missing link between #8615 and #10612
Two **open** issues look like the same underlying failure seen from different angles:
- **#8615** (*Investigating*) — speculative decoding with `gpt-oss-120b-Eagle3` produces **corrupted
channel tokens** (`<|channel|>!!!!!!!!!!!!…`) and an infinite loop.
- **#10612** (*open*) — the Harmony **channel-establishment sequence**
(`<|channel|>final<|message|>`, token IDs `200005`, `17196`, `200008`) **is not generated**, the
parser hits *"Unexpected EOS while waiting for message header to complete"*, and falls back to
corrupted or empty output.
The A/B above adds the missing variable: **Eagle3 is the trigger**, and the damage lands on the Harmony
**control tokens** — precisely the rare, structural tokens (`<|channel|>`, `<|message|>`) that a draft
head is worst at predicting.
**This failure mode is the most dangerous of the three, because it is completely silent.** #8615 loops
loudly; #10612 returns visibly malformed JSON. Here everything looks fine and the tool call is simply
absent.
(#7163 is closed and used xgrammar with **no** `speculative_config` — a different path.)
### Suggested area to look at
`tensorrt_llm/serve/harmony_adapter.py` — the token-batch path (`process_token_batch` /
`_get_or_create_tool_call`) versus how tokens are delivered when `Eagle3OneModelSampler` is in use.
The reasoning channel survives; the tool-call channel does not.
### Why this matters beyond one user
The official DGX Spark playbook (`dgx-spark-playbooks/nvidia/speculative-decoding`, Option 1)
recommends **exactly this Eagle3 recipe** for gpt-oss-120b on a single Spark. Anyone who follows it and
then uses tools will hit this — and, because it is silent, will likely blame the model.
Even just a startup warning ("tool calling is not supported with speculative decoding") would be a large
improvement over returning an empty `tool_calls` with HTTP 200.
Before submitting a new issue...
System Info
[Bug] Eagle3 speculative decoding silently drops
tool_callsfor gpt-oss on trtllm-serveSummary
With
gpt-oss-120bontrtllm-serve, enabling Eagle3 speculative decoding makes the serverreturn an empty
tool_callsarray and emptycontent, even though the model clearly decidedto call the function (its
reasoning_contentsays so, and it emitted the tokens).Disabling speculative decoding — changing nothing else — makes tool calling work correctly.
The Harmony adapter itself is active in both cases (
reasoning_contentis populated either way), sothis is not a model-detection or chat-template problem: the tool-call channel is lost specifically
when Eagle3 is on.
This may be the missing link between #8615 and #10612
Two open issues describe what look like the same underlying failure, seen from different angles:
gpt-oss-120b-Eagle3produces corruptedchannel tokens:
<|channel|>!!!!!!!!!!!!...and an infinite loop.(
<|channel|>final<|message|>, token IDs200005,17196,200008) is not generated, theparser hits "Unexpected EOS while waiting for message header to complete", and falls back to
corrupted/empty output.
Our A/B adds the missing variable: Eagle3 is the trigger, and the damage lands on the Harmony
control tokens — which are exactly the rare, structural tokens a draft head is worst at predicting
(
<|channel|>,<|message|>).Our failure mode is the most dangerous of the three, because it is completely silent. #8615 loops
loudly; #10612 returns visibly malformed JSON. Here: HTTP 200,
finish_reason: "stop", no warning —the tool call simply is not there. A framework cannot tell this apart from "the model chose not to call
the tool".
(#7163 is closed and used xgrammar with no
speculative_config, so it is a different path.)Environment
nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc13--backend pytorch,--tp_size 1openai/gpt-oss-120b(MXFP4)nvidia/gpt-oss-120b-Eagle3-long-contextReproduction
A — WITH Eagle3 →
tool_callsis empty (bug)extra-llm-api-config.yml:Request:
Response (bug):
{ "message": { "role": "assistant", "content": "", "reasoning_content": "User asks weather in Rome. Use function.", "tool_calls": [] }, "finish_reason": "stop", "usage": { "completion_tokens": 37 } }Note:
completion_tokens: 37while the reasoning text is only ~10 words — the tool-call tokens weregenerated and then dropped.
reasoning_contentbeing populated proves the Harmony adapter is active(
openai_server.py:use_harmony = (model_config.model_type == "gpt_oss")).B — WITHOUT Eagle3 → works
Same command, same image, same model, same tokenizer, same request. Only the config differs — the
speculative_configblock is removed:Response (correct):
{ "tool_calls": [ { "function": { "name": "get_weather", "arguments": "{\"city\": \"Rome\"}" } } ] }Why this matters
The failure is silent: HTTP 200,
finish_reason: "stop", no warning in the logs. An agentframework sees an assistant message with empty content and no tool call, and simply believes the model
declined to act. Any multi-agent system built on tool calling will appear to "work" while doing
nothing — which is how we spent hours blaming the model.
Also worth noting: the DGX Spark playbook
(
dgx-spark-playbooks/nvidia/speculative-decoding, Option 1) recommends exactly this Eagle3 recipe forgpt-oss-120b on a single Spark. Anyone following it and then using tools will hit this.
Suggested area to look at
tensorrt_llm/serve/harmony_adapter.py— the token-batch path(
process_token_batch/_get_or_create_tool_call) versus how tokens are delivered whenEagle3OneModelSampleris in use. The reasoning channel survives; the tool-call channel does not.A loud error — or even a startup warning that tool calling is unsupported with speculative decoding —
would already be a large improvement over silently returning an empty
tool_calls.Who can help?
No response
Information
Tasks
examplesfolder (such as GLUE/SQuAD, ...)Reproduction
nvcr.io/nvidia/tensorrt-llm/release:1.3.0rc13--backend pytorch,--tp_size 1openai/gpt-oss-120b(MXFP4)nvidia/gpt-oss-120b-Eagle3-long-contextA — WITH Eagle3 →
tool_callsis empty (bug)config.yml:Server:
Request:
B — WITHOUT Eagle3 → works
Same image, same model, same tokenizer, same request. Only the
speculative_configblock is removedfrom
config.yml.Three things are worth pointing out:
reasoning_contentliterally says "Use function."completion_tokens: 37, while the reasoning text is only ~10 words. The tool-call tokens weregenerated and then dropped.
reasoning_contentbeing populated proves it(
openai_server.py:use_harmony = (model_config.model_type == "gpt_oss")). So this is not amodel-detection or chat-template problem.
There is no error, no warning, and
finish_reasonis"stop". The response is a perfectly validHTTP 200. An agent framework cannot distinguish this from "the model chose not to call the tool".
Before submitting a new issue...