Skip to content
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
82d7df2
[None][fix] GLM tool parsers: stream exactly what the whole-text pars…
JunyiXu-nv Oct 7, 2026
c4c08d0
[None][feat] GLM reasoning parser
JunyiXu-nv Oct 7, 2026
f9f698a
[None][fix] Responses API: honour web_search when the client disables…
JunyiXu-nv Aug 28, 2026
a676def
[TRTLLM-15894][feat] Responses API: support disaggregated serving
JunyiXu-nv Aug 28, 2026
83bea9d
[None][fix] Say what happens to `store` on disagg, and accept dict-sh…
JunyiXu-nv Sep 4, 2026
a0987ab
[None][feat] Serve /v1/models on the disaggregated server
JunyiXu-nv Sep 4, 2026
c45513b
[None][fix] Accept the input shapes a Responses client replays
JunyiXu-nv Sep 5, 2026
f0f56b4
[None][fix] Honour the tools Codex declares as an input item
JunyiXu-nv Sep 6, 2026
0aba576
[None][fix] Translate a tool result that arrives as content parts
JunyiXu-nv Sep 6, 2026
c6842ec
[None][fix] Render `developer` messages instead of dropping them
JunyiXu-nv Sep 7, 2026
9422984
[None][fix] Forward a request under the field names it arrived with
JunyiXu-nv Sep 7, 2026
e18cec3
[None][fix] Write responses under their wire field names too
JunyiXu-nv Sep 7, 2026
d2bc0d2
[None][fix] Report the cached token count the context phase measured
JunyiXu-nv Sep 9, 2026
1edd91d
[None][fix] Emit the reasoning that shares a chunk with the answer
JunyiXu-nv Sep 12, 2026
efc3755
[None][fix] Give the streaming test fake the accumulated text it reads
JunyiXu-nv Sep 12, 2026
36276b4
[None][fix] Order the response snapshot reasoning-first, as the strea…
JunyiXu-nv Sep 12, 2026
2ba85de
[None][fix] Say so when a Responses stream stops before completing
JunyiXu-nv Sep 12, 2026
e114013
[None][fix] responses: tool calls and usage a client can actually use
JunyiXu-nv Sep 22, 2026
e5dec9b
[None][fix] responses: degrade images in tool outputs too, cap error …
JunyiXu-nv Sep 30, 2026
271a1ed
[None][fix] responses: the final response must describe the stream th…
JunyiXu-nv Sep 30, 2026
76ae88e
[None][fix] responses: honor what the request asked and what the clie…
JunyiXu-nv Sep 30, 2026
b73e569
[None][fix] responses: stored context that survives replay, terminals…
JunyiXu-nv Sep 30, 2026
1593a0d
[None][fix] Make Responses final snapshot agree with the streamed view
JunyiXu-nv Oct 6, 2026
c6f66ca
[None][test] Run the new Responses test files in the CPU stage
JunyiXu-nv Oct 6, 2026
7c9cdcb
[None][fix] Cover the routes the disaggregated server gained
JunyiXu-nv Sep 4, 2026
6829f2e
[None][fix] Stop a test from flipping prometheus into multiprocess mode
JunyiXu-nv Sep 4, 2026
67e6990
[None][fix] Responses: review fixes, and less code and tests to review
JunyiXu-nv Oct 7, 2026
c13a8f0
[None][feat] Route Responses requests on the disaggregated server
JunyiXu-nv Oct 7, 2026
a4c91dc
[None][fix] Responses: keep a processor's numbering when no frame cou…
JunyiXu-nv Oct 7, 2026
524b242
[None][chore] Merge main (ff94288d99) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 9, 2026
95e275a
[None][test] Responses: fix the two tests CI run 76914 failed
JunyiXu-nv Oct 9, 2026
f487fa6
[None][fix] Responses: context-only prompt ids with postprocessing wo…
JunyiXu-nv Oct 10, 2026
363e4be
[None][test] Responses: a list-mode context-only reply carries no bas…
JunyiXu-nv Oct 10, 2026
4399d53
[None][fix] Responses: address review of the input, stream and disagg…
JunyiXu-nv Oct 10, 2026
0c38b38
[None][doc] Responses API on the disaggregated server; release notes
JunyiXu-nv Oct 10, 2026
6208a8e
[None][chore] Merge main (7179c5b01b) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 10, 2026
1ed6fb2
[None][fix] Responses: folding a tool call does not change the reques…
JunyiXu-nv Oct 10, 2026
42c02b4
[None][chore] Merge main (16df7eaf25) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 11, 2026
571a1e3
[None][fix] Responses: reject a malformed prompt_token_ids_b64 at val…
JunyiXu-nv Oct 11, 2026
f5408be
[None][chore] Merge main (efeda44574) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 12, 2026
9fb0942
[None][doc] Responses: file the release notes under Release 1.4
JunyiXu-nv Oct 12, 2026
81999f9
[None][chore] Merge main (a8d7911bed) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 12, 2026
2283d92
[None][fix] Responses: keep the disaggregated handoff off client requ…
JunyiXu-nv Oct 13, 2026
05e644f
[None][fix] Responses disagg: drop client prompt ids; complete a stre…
JunyiXu-nv Oct 13, 2026
3de4ca9
[None][chore] Merge main (c018848da6) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 20, 2026
8851a43
[None][fix] Responses: leave image input to main's handling
JunyiXu-nv Oct 20, 2026
4ee109c
[None][fix] Responses disagg: time the first token from the first out…
JunyiXu-nv Oct 21, 2026
778dfc4
[None][chore] Merge main (afc1c168fd) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 22, 2026
9fdcdba
[None][chore] Merge main (9bb7931d6e) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 22, 2026
7b2d9c3
[None][chore] Merge main (9cc37929e8) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 22, 2026
39f7307
[None][chore] Merge main (3024eb446e) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 23, 2026
0179f70
[None][chore] Merge main (9619d5b2ee) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 23, 2026
54fea18
[None][chore] Merge main (687359e228) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 26, 2026
d77d894
[None][chore] Merge main (dd75334918) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 26, 2026
9e67daf
[None][chore] Merge main (9ffa6f4395) into dev-junyix-fix-responses-api
JunyiXu-nv Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/source/features/disagg-serving.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,18 @@ curl http://localhost:8000/v1/completions \
}' -w "\n"
```

#### Responses API

The disaggregated server also serves `/v1/responses`, with both `context_first` and `generation_first` scheduling, and `/v1/models`, which it answers from a context worker. A streamed response carries the generation worker's events unchanged. Unlike a single `trtllm-serve` instance, the disaggregated server keeps no response history:

- A request with `previous_response_id` is rejected with a 400, because a stored response lives in one worker's memory and a later request may reach another worker. Send the earlier turns in `input` instead.
- `store` is ignored: nothing is persisted, and the server logs a warning once when a client asks for it.

Two limitations apply to `/v1/responses` on every server, disaggregated or not:

- `parallel_tool_calls: false`, and a `tool_choice` other than `auto` or `none` on non-Harmony models, are accepted but not enforced. The server logs a warning once.
- An image in the input, in a message or in a tool output, is replaced by a text placeholder that names what was dropped, since the endpoint accepts text only.

#### Launching disaggregated servers on SLURM clusters

Please refer to [Disaggregated Inference Benchmark Scripts](source:examples/disaggregated/slurm).
Expand Down
6 changes: 6 additions & 0 deletions docs/source/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ All published functionality in the Release Notes has been fully tested and verif

## TensorRT-LLM Release 1.3

### Key Features and Enhancements

- Disaggregated serving supports the Responses API: `trtllm-serve disaggregated` serves `/v1/responses` with context-first and generation-first scheduling, and `/v1/models`. `previous_response_id` is rejected and `store` is ignored there, since no response history is kept. See [Disaggregated Serving](features/disagg-serving.md#responses-api).

### API Changes

- `/v1/responses`: a response cut short by `max_output_tokens` now reports `incomplete_details.reason` `max_output_tokens`, and when streamed it ends with `response.incomplete` instead of `response.completed`, as in the OpenAI API.

- NVFP4 cold-page compression remains opt-in and disabled by default. When enabled for DeepSeek-V4, NoPE uses NVFP4 and RoPE now defaults to 2FP4 (`nvfp4_residual_dim=64`). Set `skip_rope_quantization=True` to keep RoPE at its original precision, as before, or `nvfp4_residual_dim=0` to use single NVFP4. The active GPU KV-cache dtype is unchanged. See the [NVFP4 cold-page compression example](source:examples/kv_cache_compression/nvfp4_cold_page.md#deepseek-v4).

- **[DEPRECATION]** The TRITON MoE backend (`TritonFusedMoE`, `moe_config.backend="TRITON"`) is deprecated as of TensorRT-LLM 1.3 (2026-09) and will be removed after the 3-month migration period. Its only remaining role is a modest performance edge for GPT-OSS on Hopper with `W4A16_MXFP4` — the single configuration `AUTO` resolves to TRITON, and the format an MXFP4 GPT-OSS checkpoint takes on SM90. As the model set and the supported platforms keep growing, a single-scenario MoE path is no longer worth its maintenance cost. `moe_config.backend="CUTLASS"` replaces it functionally on Hopper: it serves `W4A16_MXFP4` on SM90 along with the unquantized BF16 and FP8 per-tensor paths, and MoE backend resolution already degrades to it automatically when TRITON declines a layer. During the migration period TRITON keeps working and logs a one-time warning. See the [deprecation policy](https://github.com/NVIDIA/TensorRT-LLM#deprecation-policy).
Expand Down
55 changes: 52 additions & 3 deletions tensorrt_llm/serve/openai_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
ChatCompletionResponse,
CompletionRequest,
CompletionResponse,
ResponsesRequest,
ResponsesResponse,
UCompletionRequest,
UCompletionResponse,
)
Expand Down Expand Up @@ -82,7 +84,20 @@
# returned object to the type the caller asked for, so a mismatch is a type
# error rather than an Any that silently propagates.
class _SerializableRequest(Protocol):
def model_dump(self, *, mode: str = ..., exclude_unset: bool = ...) -> dict: ...
def model_dump(
self, *, mode: str = ..., exclude_unset: bool = ..., by_alias: bool = ...
) -> dict: ...


def _encode_request_body(request: _SerializableRequest) -> bytes:
"""Encode a request forwarded to a worker.

Fields go out under their wire names, e.g. ``schema``, which the model
holds as ``schema_``.
"""
return _msgpack_encoder.encode(
request.model_dump(mode="json", exclude_unset=True, by_alias=True)
)


_ResponseT = TypeVar("_ResponseT", bound=BaseModel)
Expand Down Expand Up @@ -113,6 +128,10 @@ async def send_request(
hooks,
req_id,
)
elif isinstance(request, ResponsesRequest):
return await self._send_request(
"v1/responses", request, ResponsesResponse, server, hooks, req_id
)
else:
raise ValueError(f"Invalid request type: {type(request)}")

Expand Down Expand Up @@ -153,6 +172,16 @@ async def post_json(
"""
...

@abstractmethod
async def get_json(
self,
endpoint: str,
response_type: Type[_ResponseT],
server: str,
) -> _ResponseT:
"""GET a worker endpoint, outside routing state, like post_json."""
...

async def shutdown(self) -> None: ...

@abstractmethod
Expand Down Expand Up @@ -235,7 +264,7 @@ async def post_json(
# same way -- hence the same msgpack transport as _send_request. The
# worker decodes it because _MsgspecRoute is the app's route_class, so
# it covers every route rather than just the completion ones.
body = _msgpack_encoder.encode(request.model_dump(mode="json", exclude_unset=True))
body = _encode_request_body(request)
headers = dict(MSGPACK_HEADERS)
async with self._session.post(url, data=body, headers=headers) as response:
if response.status >= 400:
Expand All @@ -249,6 +278,26 @@ async def post_json(
)
return response_type(**await response.json())

async def get_json(
self,
endpoint: str,
response_type: Type[_ResponseT],
server: str,
) -> _ResponseT:
server_url = server if server.startswith("http") else f"http://{server}"
url = f"{server_url.rstrip('/')}/{endpoint}"
async with self._session.get(url) as response:
if response.status >= 400:
error_body = await response.text()
raise aiohttp.ClientResponseError(
response.request_info,
response.history,
status=response.status,
message=f"{response.reason}: {error_body[:2048]}",
headers=response.headers,
)
return response_type(**await response.json())

async def _send_request(
self,
endpoint: str,
Expand Down Expand Up @@ -390,7 +439,7 @@ async def _post_with_retry(
# Serialize once on the orchestrator's single event-loop thread.
# Content-Type stays application/json so FastAPI still routes the
# body through Request.json(); the header picks the decoder.
body = _msgpack_encoder.encode(request.model_dump(mode="json", exclude_unset=True))
body = _encode_request_body(request)
headers = dict(MSGPACK_HEADERS)
if self._request_perf_metrics:
headers[RETURN_METRICS_HEADER] = "1"
Expand Down
42 changes: 37 additions & 5 deletions tensorrt_llm/serve/openai_disagg_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@
OpenAIDisaggregatedService, ResponseHooks)
from tensorrt_llm.serve.openai_protocol import (
ChatCompletionRequest, ChatCompletionResponse, CompletionRequest,
ConversationParams, ErrorResponse, UCompletionRequest, UCompletionResponse,
ensure_request_chat_template_allowed)
ConversationParams, ErrorResponse, ResponsesRequest, UCompletionRequest,
UCompletionResponse, ensure_request_chat_template_allowed)
from tensorrt_llm.serve.perf_metrics import (DisaggPerfMetricsCollector,
PerfMetricsJsonlWriter,
PerfMetricsMiddleware,
combine_disagg_metrics)
from tensorrt_llm.serve.responses_utils import (ServerArrivalTimeMiddleware,
get_steady_clock_now_in_seconds)
get_steady_clock_now_in_seconds,
guard_responses_stream,
stream_error_event)
from tensorrt_llm.serve.router import Router
from tensorrt_llm.usage import TerminalOutcome, record_termination_observation
from tensorrt_llm.version import __version__ as VERSION
Expand Down Expand Up @@ -345,6 +347,8 @@ def register_routes(self):
self.app.add_api_route("/v1/chat/completions", self._wrap_entry_point(self._service.openai_chat_completion, ChatCompletionRequest), methods=["POST"])
self.app.add_api_route("/v1/messages", self.anthropic_messages, methods=["POST"])
self.app.add_api_route("/v1/messages/count_tokens", self.anthropic_count_tokens, methods=["POST"])
self.app.add_api_route("/v1/responses", self._wrap_entry_point(self._service.openai_responses, ResponsesRequest), methods=["POST"])
self.app.add_api_route("/v1/models", self.get_model, methods=["GET"])
self.app.add_api_route("/health", self.health, methods=["GET"])
self.app.add_api_route("/cluster_info", self.cluster_info, methods=["GET"])
self.app.add_api_route("/version", self.version, methods=["GET"])
Expand Down Expand Up @@ -437,10 +441,18 @@ async def wrapper(req: request_type, raw_req: Request) -> Response:
response_or_generator = await entry_point(req, hooks)
self._perf_metrics_collector.total_responses.inc()
if req.stream:
stream = response_or_generator
if isinstance(req, ResponsesRequest):
# A relayed Responses stream that fails early ends with
# an `error` event; other protocols end on a sentinel.
stream = guard_responses_stream(stream,
stream_error_event)
Comment thread
JunyiXu-nv marked this conversation as resolved.
Resolved
return StreamingResponse(
content=response_or_generator,
content=stream,
media_type="text/event-stream")
return JSONResponse(content=response_or_generator.model_dump())
# by_alias: e.g. text.format.schema goes out under its wire name.
return JSONResponse(
content=response_or_generator.model_dump(by_alias=True))
except Exception as e:
# Usually raises; returns a Response for worker errors that
# carry a machine-readable code (context_length_exceeded).
Expand Down Expand Up @@ -497,6 +509,26 @@ async def anthropic_messages(self, request: AnthropicMessagesRequest,
return JSONResponse(content=anthropic_response.model_dump(
exclude_none=True))

async def get_model(self) -> Response:
"""List the served model, as a context worker reports it."""
try:
return JSONResponse(
content=(await self._service.get_model()).model_dump())
except aiohttp.ClientResponseError as error:
status, message = error.status or 500, _upstream_error_message(
error)
except (RuntimeError, ValueError) as error:
# No context worker yet; the client may retry.
status, message = 503, str(error)
return JSONResponse(
status_code=status,
content=ErrorResponse(
message=message,
type=("invalid_request_error"
if 400 <= status < 500 else "api_error"),
code=status,
).model_dump())

async def anthropic_count_tokens(
self, request: AnthropicCountTokensRequest) -> Response:
"""Count Anthropic input tokens on a context worker."""
Expand Down
Loading