Skip to content

[TRTLLM-17267][refactor] Move the chat handlers into a serving class - #21676

Draft
JunyiXu-nv wants to merge 65 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-chat-serving
Draft

JunyiXu-nv wants to merge 65 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-chat-serving

Conversation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator

@coderabbitai summary

Stacked on #21640 ([TRTLLM-17267][refactor] Move the completions handler into a serving class) and #21551 ([TRTLLM-17263][refactor] Converge the three Anthropic orchestrations into one serving class): the commits up to the integration merge db58a94717 (#21640's branch at e5bf1275f8 merged with #21551's branch at 6998c2b9e6) belong to those PRs and disappear from this diff when they merge; this PR's own change is the last 6 commits (7c57859b8a to f7e11e74f5). The reviewable range is git diff db58a94717...f7e11e74f5. Draft until both land; it is then regenerated on the latest main and marked ready.

Description

Part of the trtllm-serve refactor (epic TRTLLM-17056, task TRTLLM-17267: move the handlers off OpenAIServer into one serving class per endpoint). This PR moves the chat path into ChatServing (tensorrt_llm/serve/api/openai/chat/serving.py): the typed core create_chat_completion, its HTTP wrapper openai_chat, _create_chat_response and chat_harmony, together with the module-level tool-call helpers only the chat path uses (_warn_unresolvable_thinking_once with its warn-once flag, _validate_required_tool_grammar_request, _build_tool_strict_guided_decoding_params, _parser_extracts_forced_tool_calls, _build_forced_tool_call_decoding). /v1/chat/completions is served through chat/api_router.py and the routing table for the LLM role group (no role, CONTEXT, GENERATION). _get_scheduling_params joins GenerateBaseServing next to the other generate-pipeline helpers, and GenerateBaseServing gains the server_role argument it reads. _configure_parser_special_token_decoding goes to tensorrt_llm/serve/_base/serving.py rather than into the chat package: since "Responses: detokenize for the parsers as chat completions does" (#21072) the Responses handler calls it too, and packages under serve/api/ do not import each other, so a helper of chat and Responses belongs in _base/; openai_server.py imports it from there for openai_responses.

It builds on #21640 (the completions serving class, the generate-pipeline constructor arguments and _generate_serving_kwargs()) and #21551 (the Anthropic Messages serving class and its chat backend protocol). ChatServing is that backend itself: stream_model and error_message move from the transitional adapter class into it (the adapter class goes), and the composition root hands the ChatServing to the Anthropic Messages serving class.

Every in-process caller of the chat path is rewired to the serving object in this PR: /health_generate stays on the server and awaits self._chat_serving.openai_chat(...) (one call rewritten in place; it moves later with the other operations endpoints, and its contract is pinned here and by tests/integration/defs/test_e2e.py::test_openai_misc_example[pytorch]), and the Anthropic Messages serving runs on the ChatServing. The three helper aliases whose last caller was the chat code leave OpenAIServer (_logit_bias_vocab_size, _apply_spec_decode_stats_opt_in, _attach_multimodal_data); _validate_internal_disagg_request and _has_cache_transceiver_config stay as aliases because openai_responses still validates the protected disaggregated fields through them.

ChatServing takes one keyword-only argument per server attribute the moved handlers read (tool_parser, tool_call_id_type, multimodal_server_config, allow_request_chat_template, use_harmony, harmony_adapter, render_resources) on top of the GenerateBaseServing arguments. The render resources are the ones the composition root builds once: the moved core reads self._render_resources where it built RenderResources.from_server(self) per request (declared rewrite), which is equal because every attribute they read except the harmony adapter is fixed after _init_llm, and an unset harmony adapter resolves to the same process-wide adapter. The two getattr(self, ...) probes of the moved _get_scheduling_params (_internal_disagg_auth_key, server_role) are kept verbatim; the constructor makes both attributes exist, so the defaults are never taken, and tests pin that the key and the role reach the serving object.

Behavior

No behavior change. Status codes and bodies of the chat wrapper (400 bodies, HTTP 200 with body null after an engine failure, the 400 internal_error of chat_harmony), the SSE frames, the msgpack route class, logging and the endpoint choice on gpt-oss servers are as before; the route set and the middleware order are unchanged (the route snapshot tests/unittest/api_stability/references/trtllm_serve_routes.yaml did not change). The change that serves gpt-oss chat through the shared chat handler has not merged, so the endpoint is still chosen from use_harmony: the chat router holds two routers with one route each and attaches the one the serving object's use_harmony selects, so the route keeps the endpoint name the server gave it (openai_chat, or chat_harmony on a gpt-oss server), and /health_generate keeps calling openai_chat on every server.

Moves

Pure-move check
## Move spec

```yaml
moves:
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/openai/chat/serving.py
  group: B
  append: true
  symbols:
  - _warned_unresolvable_thinking
  - _warn_unresolvable_thinking_once
  - _validate_required_tool_grammar_request
  - _build_tool_strict_guided_decoding_params
  - _parser_extracts_forced_tool_calls
  - _build_forced_tool_call_decoding
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/_base/serving.py
  group: B
  append: true
  symbols:
  - _configure_parser_special_token_decoding
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/openai/chat/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: ChatServing
    names:
    - create_chat_completion
    - openai_chat
    - _create_chat_response
    - chat_harmony
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/_base/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: GenerateBaseServing
    names:
    - _get_scheduling_params
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/openai/chat/serving.py
  group: B
  append: true
  methods:
    from_class: _AggregatedChatBackend
    to_class: ChatServing
    names:
    - stream_model
    - error_message
rewrites:
- symbol: tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing.create_chat_completion
  old: RenderResources.from_server(self)
  new: self._render_resources
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.health_generate
  old: self.openai_chat(
  new: self._chat_serving.openai_chat(
- symbol: tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing.stream_model
  old: self._server.model
  new: self.model
borrowed_removed:
- _logit_bias_vocab_size
- _apply_spec_decode_stats_opt_in
- _attach_multimodal_data
replaced:
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend
  by: tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing
  reason: ChatServing implements the chat backend protocol itself
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend.__init__
  by: tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing.__init__
  reason: no server reference needed
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend.create_chat_completion
  by: tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing.create_chat_completion
  reason: it only delegated to the typed core, which ChatServing now holds
modified:
- symbol: tensorrt_llm/serve/_base/serving.py::GenerateBaseServing.__init__
  reason: server_role argument for _get_scheduling_params
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._generate_serving_kwargs
  reason: server_role
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.__init__
  reason: builds ChatServing in the LLM branch; the Anthropic backend is the ChatServing
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_routes
  reason: /v1/chat/completions comes from the chat router
- symbol: tensorrt_llm/serve/_base/routing.py::ROUTERS
  reason: the chat router joins the routing table for the LLM role group
added:
- symbol: tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing
  reason: serving class of /v1/chat/completions; no harmony subclass
- symbol: tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing.__init__
  reason: explicit dependencies instead of server attributes, so missing wiring fails
    at construction
- symbol: tensorrt_llm/serve/api/openai/chat/api_router.py::*
  reason: new router module
rewrite_internal_imports: true

Pure-move check: base db58a94, head f7e11e7, tools dbe72ce65eef

Symbol From To Result
ROUTERS tensorrt_llm/serve/_base/routing.py tensorrt_llm/serve/_base/routing.py modified (declared)
GenerateBaseServing.__init__ tensorrt_llm/serve/_base/serving.py tensorrt_llm/serve/_base/serving.py modified (declared)
OpenAIServer._get_scheduling_params tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/serving.py verbatim
_configure_parser_special_token_decoding tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/serving.py verbatim
OpenAIServer._create_chat_response tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
OpenAIServer.chat_harmony tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
OpenAIServer.create_chat_completion tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py rewritten
OpenAIServer.openai_chat tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
_AggregatedChatBackend.__init__ tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py replaced
_AggregatedChatBackend.create_chat_completion tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py replaced
_AggregatedChatBackend.error_message tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
_AggregatedChatBackend.stream_model tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py rewritten
_AggregatedChatBackend tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py replaced
_build_forced_tool_call_decoding tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
_build_tool_strict_guided_decoding_params tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
_parser_extracts_forced_tool_calls tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
_validate_required_tool_grammar_request tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
_warn_unresolvable_thinking_once tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
_warned_unresolvable_thinking tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/openai/chat/serving.py verbatim
OpenAIServer.__init__ tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer._apply_spec_decode_stats_opt_in tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py borrowed removed
OpenAIServer._attach_multimodal_data tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py borrowed removed
OpenAIServer._generate_serving_kwargs tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer._logit_bias_vocab_size tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py borrowed removed
OpenAIServer.health_generate tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py rewritten
OpenAIServer.register_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
tensorrt_llm/serve/api/openai/chat/api_router.py::* — tensorrt_llm/serve/api/openai/chat/api_router.py added
ChatServing.__init__ — tensorrt_llm/serve/api/openai/chat/serving.py added
ChatServing — tensorrt_llm/serve/api/openai/chat/serving.py added

Replaced

  • _AggregatedChatBackend -> tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing: ChatServing implements the chat backend protocol itself
  • _AggregatedChatBackend.__init__ -> tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing.__init__: no server reference needed
  • _AggregatedChatBackend.create_chat_completion -> tensorrt_llm/serve/api/openai/chat/serving.py::ChatServing.create_chat_completion: it only delegated to the typed core, which ChatServing now holds

Added

  • ChatServing: serving class of /v1/chat/completions; no harmony subclass
  • ChatServing.__init__: explicit dependencies instead of server attributes, so missing wiring fails at construction
  • tensorrt_llm/serve/api/openai/chat/api_router.py::*: new router module

Rewritten

  • OpenAIServer.health_generate (tensorrt_llm/serve/openai_server.py): self.openai_chat( -> self._chat_serving.openai_chat(
  • OpenAIServer.create_chat_completion (tensorrt_llm/serve/api/openai/chat/serving.py): RenderResources.from_server(self) -> self._render_resources
  • _AggregatedChatBackend.stream_model (tensorrt_llm/serve/api/openai/chat/serving.py): self._server.model -> self.model

Modified (declared)

  • OpenAIServer.__init__: builds ChatServing in the LLM branch; the Anthropic backend is the ChatServing
    --- base
    +++ head
    @@ -170,7 +170,11 @@
    self.app.state.ops_serving_system = SystemServing(model=self.model, health_probe=self._health_probe)
    if role_group_of(self.server_role) is RoleGroup.LLM:
    self._register_launcher_forwarding_routes()
    •    messages_serving = AnthropicMessagesServing(_AggregatedChatBackend(self))
      
    •    from tensorrt_llm.serve.api.openai.chat import api_router as chat_router
      
    •    from tensorrt_llm.serve.api.openai.chat.serving import ChatServing
      
    •    self._chat_serving = ChatServing(tool_parser=self.tool_parser, tool_call_id_type=self.tool_call_id_type, multimodal_server_config=self.multimodal_server_config, allow_request_chat_template=self.allow_request_chat_template, use_harmony=self.use_harmony, harmony_adapter=self.harmony_adapter, render_resources=self._render_resources, **self._generate_serving_kwargs())
      
    •    setattr(self.app.state, chat_router.APP_STATE_KEY, self._chat_serving)
      
    •    messages_serving = AnthropicMessagesServing(self._chat_serving)
         self.app.state.anthropic_serving_messages = messages_serving
         self.app.state.anthropic_serving_count_tokens = AnthropicCountTokensServing(render_resources=lambda: RenderResources.from_server(self))
         self.app.state.anthropic_serving_batches = AnthropicBatchesServing(runner=messages_serving.run_batched_message)
      
  • OpenAIServer._generate_serving_kwargs: server_role
    --- base
    +++ head
    @@ -1,3 +1,3 @@
    def _generate_serving_kwargs(self) -> Dict[str, Any]:
    """Constructor arguments of every GenerateBaseServing, read once."""
    • return dict(model=self.model, generator=self.generator, tokenizer=self.tokenizer, model_config=self.model_config, collect_perf_metrics=self._collect_perf_metrics, metrics_collector=self.metrics_collector, adjusted_steady_clock=self._adjusted_steady_clock, iteration_stats_wakeup_event=self._iteration_stats_wakeup_event, iteration_stats_collector_task_getter=lambda: self._iteration_stats_collector_task, internal_disagg_auth_key=self._internal_disagg_auth_key, per_request_spec_decode_stats=self._per_request_spec_decode_stats, spec_decode_num_spec_tokens=self._spec_decode_num_spec_tokens, input_proc_executor=self._input_proc_executor, postproc=self._postproc)
    • return dict(model=self.model, generator=self.generator, tokenizer=self.tokenizer, model_config=self.model_config, collect_perf_metrics=self._collect_perf_metrics, metrics_collector=self.metrics_collector, adjusted_steady_clock=self._adjusted_steady_clock, iteration_stats_wakeup_event=self._iteration_stats_wakeup_event, iteration_stats_collector_task_getter=lambda: self._iteration_stats_collector_task, internal_disagg_auth_key=self._internal_disagg_auth_key, server_role=self.server_role, per_request_spec_decode_stats=self._per_request_spec_decode_stats, spec_decode_num_spec_tokens=self._spec_decode_num_spec_tokens, input_proc_executor=self._input_proc_executor, postproc=self._postproc)
  • OpenAIServer.register_routes: /v1/chat/completions comes from the chat router
    --- base
    +++ head
    @@ -14,7 +14,6 @@
    else:
    logger.warning('Resource governor is disabled: the executor backend does not provide a resource_governor_queue (e.g. RPC orchestrator mode or explicit opt-out). Endpoints under /_resource_governor/ will not be available.')
    self.resource_governor = None
    • self.app.add_api_route('/v1/chat/completions', self.openai_chat if not self.use_harmony else self.chat_harmony, methods=['POST'])
      self.app.add_api_route('/v1/responses', self.openai_responses, methods=['POST'])
      self.app.add_api_route('/v1/responses/{response_id}', self.openai_responses_get_response, methods=['GET'])
      self.app.add_api_route('/v1/responses/{response_id}', self.openai_responses_delete_response, methods=['DELETE'])
  • GenerateBaseServing.__init__: server_role argument for _get_scheduling_params
    --- base
    +++ head
    @@ -1,4 +1,4 @@
    -def init(self, *, model: str, generator: Any, tokenizer: Any, model_config: Any, collect_perf_metrics: bool, metrics_collector: Optional[MetricsCollector], adjusted_steady_clock: AdjustedSteadyClock, iteration_stats_wakeup_event: asyncio.Event, iteration_stats_collector_task_getter: Callable[[], Optional[asyncio.Task]], internal_disagg_auth_key: Optional[str], per_request_spec_decode_stats: bool, spec_decode_num_spec_tokens: Optional[int], input_proc_executor: ThreadPoolExecutor, postproc: PostprocStrategy) -> None:
    +def init(self, *, model: str, generator: Any, tokenizer: Any, model_config: Any, collect_perf_metrics: bool, metrics_collector: Optional[MetricsCollector], adjusted_steady_clock: AdjustedSteadyClock, iteration_stats_wakeup_event: asyncio.Event, iteration_stats_collector_task_getter: Callable[[], Optional[asyncio.Task]], internal_disagg_auth_key: Optional[str], server_role: Optional[ServerRole], per_request_spec_decode_stats: bool, spec_decode_num_spec_tokens: Optional[int], input_proc_executor: ThreadPoolExecutor, postproc: PostprocStrategy) -> None:
    self.model = model
    self.generator = generator
    self.tokenizer = tokenizer
    @@ -9,6 +9,7 @@
    self._iteration_stats_wakeup_event = iteration_stats_wakeup_event
    self._iteration_stats_collector_task_getter = iteration_stats_collector_task_getter
    self._internal_disagg_auth_key = internal_disagg_auth_key
    • self.server_role = server_role
      self._per_request_spec_decode_stats = per_request_spec_decode_stats
      self._spec_decode_num_spec_tokens = spec_decode_num_spec_tokens
      self._input_proc_executor = input_proc_executor
  • ROUTERS: the chat router joins the routing table for the LLM role group
    --- base
    +++ head
    @@ -1 +1 @@
    -ROUTERS: tuple[RouterSpec, ...] = (RouterSpec('tensorrt_llm.serve.api.ops.system.api_router', roles=ALL_ROLE_GROUPS), RouterSpec('tensorrt_llm.serve.api.openai.embeddings.api_router', roles=frozenset({RoleGroup.EMBEDDING})), RouterSpec('tensorrt_llm.serve.api.openai.mm_encoder.api_router', roles=frozenset({RoleGroup.MM_ENCODER})), RouterSpec('tensorrt_llm.serve.api.openai.tokenize.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.openai.completions.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.anthropic.api_router', roles=frozenset({RoleGroup.LLM}), when=_anthropic_routes_enabled))
    +ROUTERS: tuple[RouterSpec, ...] = (RouterSpec('tensorrt_llm.serve.api.ops.system.api_router', roles=ALL_ROLE_GROUPS), RouterSpec('tensorrt_llm.serve.api.openai.embeddings.api_router', roles=frozenset({RoleGroup.EMBEDDING})), RouterSpec('tensorrt_llm.serve.api.openai.mm_encoder.api_router', roles=frozenset({RoleGroup.MM_ENCODER})), RouterSpec('tensorrt_llm.serve.api.openai.tokenize.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.openai.completions.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.openai.chat.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.anthropic.api_router', roles=frozenset({RoleGroup.LLM}), when=_anthropic_routes_enabled))

12 definitions moved verbatim; 0 differences; 3 rewritten (declared); 3 borrowed removed; 3 replaced; 3 added; 5 modified (declared); shim exports 0 names


</details>

The three declared rewrites, each behavior-preserving: `create_chat_completion` reads the `RenderResources` the composition root built once instead of building them per request (equal, see above); `health_generate` awaits the same `openai_chat` on the `ChatServing` the root built; `stream_model` returns `self.model`, which is the root's `model` passed through `_generate_serving_kwargs()`, where the adapter returned `self._server.model`.

Kept shims: none. Release notes: the seven module-level helpers are private, so no public import path changes; the methods that left `OpenAIServer` are listed below. History: `git blame -C -C tensorrt_llm/serve/api/openai/chat/serving.py` attributes the moved lines to their original commits.

### Removed from OpenAIServer

- `openai_chat` (the HTTP wrapper), `create_chat_completion` (the typed chat core), `_create_chat_response`, `chat_harmony` → `ChatServing` (`tensorrt_llm/serve/api/openai/chat/serving.py`), reached through `app.state.openai_serving_chat` on an LLM-role server.
- `_get_scheduling_params` → `GenerateBaseServing` (`tensorrt_llm/serve/_base/serving.py`).
- Aliases removed (their last `OpenAIServer` caller moved): `_logit_bias_vocab_size`, `_apply_spec_decode_stats_opt_in`, `_attach_multimodal_data`. Still aliased while `openai_responses` calls them: `_validate_internal_disagg_request`, `_has_cache_transceiver_config`; `tests/unittest/serve/test_openai_server_borrowed_helpers.py` pins the rows that stay and that the moved names are no longer defined on the class.
- The transitional Anthropic chat-backend adapter class is deleted: `ChatServing` implements `create_chat_completion`, `stream_model` and `error_message` itself.
- Module level: the six chat helpers → `tensorrt_llm/serve/api/openai/chat/serving.py`; `_configure_parser_special_token_decoding` → `tensorrt_llm/serve/_base/serving.py` (imported back into `openai_server.py` for `openai_responses`). None is re-exported; the 20 imports in `tests/unittest/llmapi/apps/test_tool_parsers.py` name the new modules.

### CI bookkeeping

- Multi-GPU table (`getMultiGpuFileChanged` in `jenkins/L0_MergeRequest.groovy`): added `tensorrt_llm/serve/api/openai/chat/`, so the chat code that left `tensorrt_llm/serve/openai_server.py` keeps triggering the disaggregated stages (harmony with disaggregation, KV-aware routing).
- CODEOWNERS: none (`/tensorrt_llm/serve` covers the new directory).
- legacy-files.txt: `tensorrt_llm/serve/api/openai/chat/serving.py` (Group B, like `openai_server.py` its code comes from); `api_router.py` and `__init__.py` are new code (Group A, ruff); derived configs regenerated with `scripts/legacy_utils.py gen-configs` (`ruff-legacy.toml`, the managed blocks of `pyproject.toml` and `.pre-commit-config.yaml`); `ruff-legacy-baseline.json`: one new entry, `tensorrt_llm/serve/api/openai/chat/serving.py: F821 1`, the `anext` of `chat_harmony`'s streaming generator that moved with it (one of the three F821 recorded under `openai_server.py`, which stays at 3 as the earlier moves left it: `lint-update-violations` rewrites about 50 unrelated entries and is not run in a move). ruff before 0.14 (the repository pins v0.9.4; the `ruff-legacy` hook itself installs an unpinned ruff) reports `anext` as undefined under the py39 target, so the entry is what keeps `python scripts/legacy_utils.py lint-precommit` green there; newer ruff knows the builtin, treats the file as clean and the hook then only prints its tip about an over-recorded entry. Needs infra-devs.
- mypy: `tensorrt_llm/serve/api/openai/chat/api_router.py` joins `[tool.mypy] files`; `serving.py` stays out like the other modules that hold moved code without annotations.
- Test lists: no change. `tests/unittest/serve/api/openai/chat/test_serving.py` is collected by the `unittest/serve` entry of `tests/integration/test_lists/test-db/l0_cpu.yml` (it carries `pytest.mark.cpu_only`; `python scripts/test_to_stage_mapping.py --tests "unittest/serve"` names the CPU-Generic stages). The rewritten tests keep their registrations.
- `tests/unittest/serve/test_no_object_new.py` (the ratchet against servers built with `object.__new__`): the rows of the eight rewritten sites are deleted (`test_disagg_internal_auth.py`, `test_openai_disagg_service.py`, `test_router.py`, `test_serving_extensions.py` leave the baseline; `test_anthropic_routes.py` loses its `_chat_stream` row); `test_kv_hints.py` keeps its row for its Responses case.
- `tests/unittest/serve/private_access_baseline.yaml`: three rows stay keyed to scopes this PR removes from `openai_server.py` (`OpenAIServer._create_chat_response`, `OpenAIServer.chat_harmony.create_streaming_generator`, `OpenAIServer.openai_chat.chat_stream_generator`, member `_postprocess_result`). The accesses themselves were removed by "Choose the trtllm-serve post-processing path once per server" (#21392), so these rows are part of the known item between #21392 and "Add public LLM health queries and a private-access lint for the serving frontends" (#21393): whichever of the two lands second deletes them, and this PR leaves them (the lint test fails on the stacked base and on this head alike).
- `tests/test_common/serve/serving_stubs.py` (new): `make_chat_serving()` and `generate_serving_kwargs()` build serving objects with placeholder dependencies for tests that call a handler off the app, with render resources built from the same stubs.
- `tests/unittest/llmapi/apps/openai_server_stub.py`: the stub server now gets its model config from the config loader during construction (the constructor builds the serving objects and the render resources from it) and takes `chat_template` and `allow_request_chat_template` as constructor arguments; its chat users stub `server._chat_serving`. The Responses usage fixture mounts the chat route through the chat router with the serving object the server built (its chat mount was `server.openai_chat`, which left the server); its Messages mount is #21551's own (the Anthropic route function over the serving object the server built), kept as it is.
- `tests/unittest/llmapi/apps/test_serving_extensions.py`: the chat integration fixture became a builder that takes the model type and parsers, because the serving object and its render resources are built from them once, as the composition root does; the tests that mutated the stub server afterwards now build a client with their configuration.
- Every direct construction of a generate serving object in tests passes `server_role`; the constructor tests of `GenerateBaseServing`, `MMEncoderServing` and `CompletionServing` require it.
- The OpenEngine Prometheus test's `_init_llm` stub also sets `tool_call_id_type` and `harmony_adapter`, which the constructor now reads for the chat serving object.
- One path in `.claude/skills/perf-regression-cookbook/references/kv-cache-transceiver/disagg-gen-retokenize-and-memcpy.md`: its grep points at the module the greped code moved to. Needs agent-devs.

### What changes in `/openapi.json`

Only the order of `paths` (`/v1/chat/completions` is registered by its router after the local table, after `/_internal/tokenize` and `/v1/completions`) and, on gpt-oss servers, the route description: `chat_harmony`'s docstring ("Chat Completion API with harmony format support.") was the description of `/v1/chat/completions`; the router's route function has none. Operation ids (`openai_chat`, `chat_harmony`), parameters, body schema and response schema are unchanged (the route functions keep the handlers' names and signatures).

### In-flight PRs on the chat handlers (2026-11-04)

74 open PRs touch `tensorrt_llm/serve/openai_server.py` (the refactor's own stack included); the ones whose titles name the chat path: #21624 (GLM tool parsers stop after the first tool call), #21528 (one local media input policy for chat and VisualGen), #21414 (warn when an output format cannot be enforced), #21308 (per-turn prompt handling cost), #21111 (Kimi-K3 chat prompt ids), #21062 (fail a request whose generation degenerates into a token run), #21059 (parallel_tool_calls=false for DSML), #20739 (input-processing timing metrics, edits `openai_chat` and `chat_harmony`), #19461 (media prompt-token counts in `openai_chat`), #12746 (several served model names), #16252 (KV cache disk tier), drafts #19800 and #19605. Merged and absorbed: #20626 (`_attach_multimodal_data`, whose alias this PR removes), #20269 (KV hints), #20867 (the chat SSE generator, through #21551), #20815 (its merged form adds no chat helper). The PR is regenerated on the latest `main` before it merges, after the move announcement.

## Test Coverage

All commands ran from the worktree against this branch's code, on CPU (no GPU on the host), with `base` = the integration merge `db58a94717`.

- `import tensorrt_llm; tensorrt_llm.__file__` printed the worktree path.
- Pure-move check against `db58a94717` on the head commit (output above): 12 definitions moved verbatim, 0 differences, 3 rewritten (declared), 3 borrowed removed, 3 replaced, 3 added, 5 modified (declared).
- Independent comparison of every moved definition, outside the check above: the generated copies are byte-identical to the dedented base spans before the formatters ran (14/14, the two rewrites applied to the base text); after formatting, syntax-tree comparison with imports dropped: 15/15 identical (the six chat helpers, `_configure_parser_special_token_decoding`, `openai_chat`, `_create_chat_response`, `chat_harmony`, `_get_scheduling_params`, `error_message`, and `create_chat_completion`, `stream_model`, `health_generate` with their rewrites applied to the base text); the staying `OpenAIServer` definitions are unchanged except the three declared modifications (`__init__`, `_generate_serving_kwargs`, `register_routes`) and the `health_generate` rewrite.
- New tests: `pytest -m cpu_only tests/unittest/serve/api/openai/chat -q` → 22 passed (every constructor argument required and keyword-only; each chat argument stored where the moved handlers read it; the serving object is `server._chat_serving` and uses the root's render resources (`is`, and a chat through the route reaches the engine rendered with the server chat template, which `RenderResources.from_server(serving)` would drop), clock, stats event, input-processor pool, metrics collector, post-processing strategy and server role; one chat on the three LLM roles; a stream ending in one `[DONE]`; msgpack bodies; the endpoint name `openai_chat` on a text server and `chat_harmony` on a gpt-oss server built in the same process, each request reaching the method of its own serving object; `/health_generate` 200 through one engine call, 500 on a rejected chat, and the beam-search arguments of its request with `max_beam_width=4`; the Anthropic Messages backend is the `ChatServing`, `stream_model` returns the served name (distinct from the request's), `error_message` returns the body message else the JSON body; the wired internal auth key rejects protected fields with 400 "Invalid internal" and the control without a key lets them reach the engine; the embedding role 404s and the encoder role serves the path with its own handler; `attach_router` without the serving object; the endpoint picked from `use_harmony`).
- Rewritten tests and guards, together with the new tests: `pytest tests/unittest/serve/api/openai/chat tests/unittest/serve/test_openai_server_borrowed_helpers.py tests/unittest/serve/test_no_object_new.py tests/unittest/serve/_base/test_serving.py tests/unittest/serve/api/openai/completions/test_serving.py tests/unittest/serve/api/openai/mm_encoder/test_serving.py tests/unittest/serve/test_openai_server_chat_core.py tests/unittest/serve/api/anthropic/test_messages_behavior.py tests/unittest/llmapi/apps/test_anthropic_routes.py tests/unittest/llmapi/apps/test_serving_extensions.py tests/unittest/llmapi/apps/test_openai_chat_disagg_multimodal.py tests/unittest/llmapi/apps/test_openai_chat_multimodal_prompt.py tests/unittest/llmapi/apps/test_responses_usage.py tests/unittest/disaggregated/test_disagg_internal_auth.py tests/unittest/disaggregated/test_openai_disagg_service.py tests/unittest/llmapi/test_serve_multi_frontend_helpers.py tests/unittest/llmapi/test_serve_multi_frontend_route_gate.py tests/unittest/serve/test_layering.py tests/unittest/llmapi/test_kv_hints.py tests/unittest/disaggregated/test_openai_server_stream_errors.py tests/unittest/executor/test_spec_decode_stats_payload.py "tests/unittest/llmapi/test_llm.py::test_openai_completion_list_prompt_stream_reuses_stream_metadata" "tests/unittest/grpc/openengine/test_metrics.py::test_prometheus_contract[http_enablement]" -q` → 775 passed (the route-snapshot file `tests/unittest/api_stability/test_serve_routes.py` is covered by the `tests/unittest/api_stability` runs below, 108 passed on both sides).
- Two FastAPI versions (`fastapi==0.136.3` and `0.139.2`): `pytest tests/unittest/api_stability/test_serve_routes.py tests/unittest/serve/api/openai/chat/test_serving.py tests/unittest/serve/api/openai/completions/test_serving.py tests/unittest/serve/test_middleware_order.py -q` → 92 passed on each.
- Patch sites: `git grep` over `tests/` for `openai_server.<name>`, the quoted names, `setattr(<module>, "<name>"` and `patch(` followed by the module, for the 14 moved names → none left (the `parse_chat_messages_coroutines` patch of `test_openai_disagg_service.py` and the `_record_generator_termination` patches of the typed-core and Anthropic behavior tests target `tensorrt_llm.serve.api.openai.chat.serving`, where the moved code looks the names up; the health probe's patch stays on `openai_server`, where `_health_probe` lives).
- `git grep -n "from_server(self)" -- tensorrt_llm/serve/api/` → nothing; `git grep -n "\.openai_chat(\|\.create_chat_completion(\|\._create_chat_response(\|\.chat_harmony(" -- tensorrt_llm/` → only `serve/api/openai/chat/`, the Anthropic serving's backend calls and the root's `self._chat_serving.openai_chat(` in `health_generate`.
- `python scripts/legacy_utils.py check-configs` → up to date; `python scripts/legacy_utils.py lint-precommit tensorrt_llm/serve/api/openai/chat/serving.py tensorrt_llm/serve/_base/serving.py tensorrt_llm/serve/openai_server.py` → passes with ruff 0.9.4 (the repository's pinned version; without the new baseline entry it reports `chat/serving.py:600 F821 anext` as a regression and exits 1) and with ruff 0.15.11 (the hook's current environment; tips only: `openai_server.py` over-recorded by 3, `chat/serving.py` by 1, since that ruff knows `anext`); `bash scripts/run_mypy.sh` → no issues in 29 source files.
- `pre-commit run --files <all 40 changed files>` twice → clean on both runs (`tests/unittest/disaggregated/test_router.py`, whose import block carries yapf markers, imports the stub helper inside the two test functions).
- Left to CI (GPU): `tests/integration/defs/test_e2e.py::test_openai_misc_example[pytorch]` (`/health_generate` 200 with and without beam search), `::test_openai_chat_example[pytorch]`, `::test_openai_chat_multimodal_example`, `::test_openai_reasoning[pytorch]`, `::test_openai_chat_harmony`, `::test_openai_chat_harmony_perf_metrics`, `::test_anthropic_messages`, `disaggregated/test_disaggregated.py::test_disaggregated_chat_completion_tool_calls`, and the multi-GPU harmony-with-disaggregation and KV-aware routing cases once the approval label is applied, since the trigger table changed.

Full suites, A/B against the integration base (same commands on the base merge `db58a94717` first, then on this head, one heavy suite at a time; a failure counts only if its test id is new):

- `pytest -m cpu_only tests/unittest/serve tests/unittest/llmapi/apps tests/unittest/disaggregated -q`: base 52 failed / 4287 passed; head 52 failed / 4312 passed; the failing test ids are the same set on both sides (no new failure, none fixed). The 52 are not this PR's: 44 environment failures in `tests/unittest/disaggregated` (`test_context_first_profile.py`, `test_lifecycle_activation.py`, `test_peer.py`), 6 in `tests/unittest/serve/test_serve_health.py` (`OpenAIServer` has no attribute `health`) and the two private-access lint tests (stale baseline rows for the `_postprocess_result` accesses the post-processing strategy PR removed, plus `_PreprocessingWork._release` in `tensorrt_llm/grpc/openengine/servicer.py`).
- `pytest tests/unittest/api_stability -q`: base 108 passed; head 108 passed (route snapshot, protocol and CLI references unchanged).
- `pytest -m cpu_only tests/unittest/api_stability tests/unittest/serve tests/unittest/llmapi/test_kv_hints.py tests/unittest/llmapi/apps/test_responses_usage.py -q` on the head: 8 failed / 643 passed, the 8 being the base's `test_serve_health.py` and private-access lint failures above; both rewritten files pass.
- `LLM_MODELS_ROOT=... pytest -rs tests/unittest/llmapi/apps/test_harmony_parsing.py tests/unittest/llmapi/apps/test_harmony_channel_validation.py`: base 79 passed; head 79 passed, no test skipped as "not importable" or "not available".
- `pytest tests/unittest/llmapi/apps/test_serving_extensions.py tests/unittest/llmapi/apps/test_tool_parsers.py tests/unittest/llmapi/test_reasoning_parser.py -q`: base 1443 passed, 1 skipped; head 1443 passed, 1 skipped (the 20 rewritten imports of `test_tool_parsers.py` and the rebuilt extension fixtures included).
- `pytest tests/unittest/llmapi/test_kv_hints.py -q`: base 187 passed; head 187 passed (the chat and harmony cases run on a `ChatServing` built from the test's stubs; the minimal request object the handlers receive is #21551's).
- `LLM_MODELS_ROOT=... PYTHONPATH=tests python3 -m test_common.serve.cpu_tests` (every serve CPU test, one pytest process per test-db entry, 76 entries), run on this change before its rebase onto #21551's current head (the rebase changed only the two test files #21551's last commit touches): 8 distinct failing test ids, all of them the base's known failures (the six `tests/unittest/serve/test_serve_health.py` tests and the two private-access lint tests; the lint and health files are collected by several entries, so they fail once per entry); no other failure. The `unittest/disaggregated` entry of that run was wrecked by a port clash with leftovers of an earlier test run on this host (its HTTP cluster-storage fixture binds a fixed port) and was re-run alone: 44 failed / 1601 passed, the 44 being the base's environment failures in `test_context_first_profile.py`, `test_lifecycle_activation.py` and `test_peer.py`.
- `PYTHONPATH=tests python3 -m test_common.serve.routes` → exit 0 on the head (every server configuration, the gpt-oss one included, matches the committed route snapshot).

## PR Checklist

Please review the following before submitting your PR:
- PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.
- PR Follows [TRT-LLM CODING GUIDELINES](https://github.com/NVIDIA/TensorRT-LLM/blob/main/CODING_GUIDELINES.md) to the best of your knowledge.
- Test cases are provided for new code paths (see [test instructions](https://github.com/NVIDIA/TensorRT-LLM/tree/main/tests#1-how-does-the-ci-work))
- If PR introduces API changes, an appropriate PR label is added - either `api-compatible` or `api-breaking`. For `api-breaking`, include `BREAKING` in the PR title.
- Any new dependencies have been scanned for license and vulnerabilities
- [CODEOWNERS](https://github.com/NVIDIA/TensorRT-LLM/blob/main/.github/CODEOWNERS) updated if ownership changes
- Documentation updated as needed
- Update [tava architecture diagram](https://github.com/NVIDIA/TensorRT-LLM/blob/main/.github/tava_architecture_diagram.md) if there is a significant design change in PR.
- The reviewers assigned automatically/manually are appropriate for the PR.


- [x] Please check this after reviewing the above items as appropriate for this PR.

## GitHub Bot Help

To see a list of available CI bot commands, please comment `/bot help`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

…ce per server

OpenAIServer decided nine times, in four handlers, whether a response's
post-processed value comes from a postproc worker (read the value the
worker stored on outputs[0]) or must be computed in the server process
(call the post-processor inline). The flag never changes after
construction, so the server now chooses a strategy object once:
select_postproc_strategy picks WorkerPostprocStrategy or
InlinePostprocStrategy from num_postprocess_workers; __init__ pre-caches
it and a cached property keeps server instances that unit tests build
without __init__ working. The nine decision points become one strategy
call each, and the conditional post-processor unpacks they fed are gone.

CompletionOutput gains the read-only public accessor postprocess_result
so the server no longer reads the executor's private field; the
api-stability reference records the new property. The submission-side
uses of postproc_worker_enabled (what generate_async receives, filling
postproc_args for in-server post-processing, the Responses store
decision) are deliberately unchanged.

No behavior change. With more than one candidate the two modes still
differ in what the frontend records (worker mode: only outputs[0]
carries the result and metrics arrive merged; inline mode records
metrics per candidate); that known difference is documented on the
strategy class and left for a separate change.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ss lint for the serving frontends

The HTTP server, the SMG gRPC frontend and the OpenEngine gRPC frontend
learned engine health by reading private members (_check_health, _executor,
_fatal_error, _engine_dead). Add the public health API on the classes that
own those members and switch all three frontends to it, with no behavior
change:

- BaseLLM.check_health() (public rename of _check_health; the private name
  stays for one release as an alias for out-of-tree callers that look it
  up), BaseLLM.fatal_error, BaseLLM.shutdown_started, BaseLLM.engine_dead.
- GenerationExecutor.fatal_error and engine_dead;
  GenerationExecutorProxy.engine_dead also reads the sticky worker-death
  flag set before the fatal error is recorded.

Add a private-access lint (tests/unittest/serve/test_private_access_lint.py)
that counts cross-package private accesses in tensorrt_llm/serve,
tensorrt_llm/commands and tensorrt_llm/grpc against a checked-in baseline
(tests/unittest/serve/private_access_baseline.yaml) that only shrinks, with
companion test files under tests/unittest/serve/api/visual_gen/ and
tests/unittest/grpc/openengine/ so VisualGen-only and OpenEngine-only pull
requests keep running the check.

Replace the three frontend health mirror tests with tests of the production
code, rename the _check_health test doubles to the public name, and register
the new CPU test files in l0_cpu.yml.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…erving-frontend private accesses

The serving frontends (tensorrt_llm/serve, tensorrt_llm/commands, the
OpenEngine gRPC frontend) read private members of LLM, the executors,
CompletionOutput, SamplingParams, MultimodalDataTracker,
OpenAIDisaggServer and PyExecutor. Give each remaining read a thin
public entry point on the class that owns the member, switch the
frontends to it with no behavior change, and shrink the private-access
baseline from 50 recorded accesses to 19. The reads of the two private
LlmArgs attributes stay (exposing them is an LLM-args change for its
own PR); the remaining baseline entries belong to other in-flight work.

New public members (status prototype where api-stability tracks them):
LLM.record_fatal_error / multi_frontend_attach_info /
multi_frontend_ipc_dir / engine_in_worker_process / hf_model_dir /
hf_model_config / resource_governor_queue / encode_limits /
get_kv_cache_load / get_openengine_discovery;
GenerationExecutor.record_fatal_error and multi_frontend_ipc_dir;
CompletionOutput.spec_dec_counters and text_diff_offset (and the
SpecDecCounters rename); SamplingParams.set_request_provided_fields and
stop_word_ids; MultimodalDataTracker.multimodal_server_config;
OpenAIDisaggServer.address; PyExecutor.set_openengine_discovery and
set_openengine_node_server; registered_model_type() in inputs/registry.

Also adds tensorrt_llm/llmapi/engine_client.py: the EngineClient
protocol naming the engine operations every frontend shares (generate,
health, load), checked by tests rather than at runtime.

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

The shut-down-LLM case of the launcher-lost watchdog test now calls the
production method unbound on a stand-in object whose generator double
defines the public members the method reads, the form the file's other
stand-in tests use. New tests must not build servers with object.__new__.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…apshots and a CPU test seam for trtllm-serve

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… baselines at the merge base

The route reference is generated from the live apps; the object.__new__
baseline and the gRPC import allowance record what exists on main today
and only shrink.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…n the snapshot matrix

An attached frontend of a multi-frontend server registers ten launcher
forwarding routes before the local table; seven share a method and path
with local handlers and three use a path parameter that matches every
batch id, so registration order decides which handler serves them. The
new configuration records all of it, a live invariant pins the
forwarding-before-local order independently of the reference, and the
route-class failure message now says why the msgpack route class matters.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
- AGENTS.md: the CPU test command sets LLM_MODELS_ROOT, which the runner
  requires, and the stage-mapping note says the mapper matches list-entry
  text, so a file listed through its directory maps by the directory.
- check_contracts matches each from-import as <module>.<name>, so
  'from pkg import sub' and 'from .. import sub' count as imports of the
  submodule; new test for both forms and for the allowance entry.
- The VisualGen /v1/models test asserts the 200 status.
- The AGENTS.md command check also recognizes commands that start with
  LLM_MODELS_ROOT=.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…on the current main

The disaggregated frontend now registers its routes with KvHintsRoute
(NVIDIA#20269), which strips kv_hints from request bodies unless the server
trusts its ingress. Regenerate the route reference: the six
disaggregated configurations record KvHintsRoute where they recorded
APIRoute; their route sets and order are unchanged.

The route-class invariant, which a regenerated reference cannot hide,
now requires KvHintsRoute on every API route of the disaggregated
frontend and APIRoute on the coordinator app. Dropping the KV-hint
policy from the frontend fails it even after a regeneration, and both
apps still reject the aggregated server's msgpack route class.

The object.__new__ baseline records the sites on main at the merge base:
test_kv_hints.py builds one server that way (NVIDIA#20269), and
test_harmony_parsing.py no longer builds any (NVIDIA#21176).

The harmony parsing tests are now registered as a whole file (NVIDIA#21411),
so the node-id entry that the CPU-selection test used as its example is
gone. The node-id check now runs select_entries on a one-entry list of
its own, so it no longer depends on which node-id entries l0_cpu.yml
holds.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ail on silent regressions

The route snapshot now requires every configuration other than the three
invalid ones to build, so a configuration that starts failing at construction
cannot pass through a regenerated reference. The object.__new__ ratchet
resolves 'from ... import <server class> as <name>' aliases and records each
baseline site as its enclosing function or class plus the server class, so
swapping one site for another in the same file no longer passes. The baseline
keeps the same 52 sites in the same 26 files.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…s outside-subclass contract

Every OpenAI request on the disaggregated frontend, and every Anthropic
/v1/messages request, goes through OpenAIDisaggServer._wrap_entry_point.
Before reorganizing that closure, pin what it does today:

- tests/unittest/disaggregated/test_disagg_entry_point.py pins the
  per-request counters, the chat-template 400, the conversation identity
  from the headers, the serialization of streaming and non-streaming
  responses (including the guarded streamed Responses relay and
  model_dump(by_alias=True)), the error mapping through
  _handle_exception, and the single disaggregated_request trace span.
- tests/unittest/disaggregated/test_disagg_server_contract.py pins the
  surface NeMo-RL's OpenAIDisaggServerAdaptor subclass relies on: the
  constructor (config, req_timeout_secs=..., coordinator_url=None),
  self.app with every route registered when __init__ returns,
  __call__(host, port, sockets=None), _wrap_entry_point,
  _handle_exception and _perf_metrics_collector.http_exceptions.
- The OpenAIDisaggServer class docstring and tensorrt_llm/serve/AGENTS.md
  name that surface, so a change to it is agreed with the NeMo-RL owners
  first.

Both files are CPU-only and collected by the unittest/disaggregated
entry of tests/integration/test_lists/test-db/l0_cpu.yml.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…into prepare, run, finish and serialize steps

No behavior change. The body of the closure OpenAIDisaggServer builds in
_wrap_entry_point becomes a composition of five methods, so that later
changes (disconnect watching, in-stream errors, request trace) land in
the step they belong to instead of growing one closure:

- _prepare_request: the request counters, the chat-template permission
  (HTTPException 400), the conversation identity, the per-request hooks
- _run_entry_point: the call into the disaggregated service
- _finish_request: success accounting (total_responses)
- _finish_failed_request: every failure, through _handle_exception while
  the exception is being handled (its traceback logging needs that)
- _serialize_response: StreamingResponse with the Responses stream guard,
  or JSONResponse(content=...model_dump(by_alias=True)), as before

The trace span stays on the wrapper that FastAPI calls; the steps open no
spans of their own. The statements keep their order inside the same try
block: the counters run before the template check, total_responses before
serialization, and a serialization failure still reaches
_handle_exception. Some step parameters (raw_req of _run_entry_point, req
and hooks of the finish steps) are not read yet; they are the inputs the
known upcoming changes to each step need, so those changes will not have
to alter the signatures again.

The characterization tests and the subclass contract tests of the
previous commit pass unchanged on both sides of the split.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…lm/serve/_base/msgspec_route.py

Generated move: _msgpack_decoder, _MsgspecRequest and _MsgspecRoute leave
tensorrt_llm/serve/openai_server.py byte for byte; the module imports
_MsgspecRoute back for its app router and keeps no re-export (the three
names are private). The two disaggregated test importers now import the
new module. OpenAIServer.health, .version and .get_model are removed
here and replaced by the system router added in the following commits.

tensorrt_llm/serve/_base/msgspec_route.py joins legacy-files.txt (its
code comes from a legacy-formatted file), and the generated lint configs
are regenerated with scripts/legacy_utils.py gen-configs.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ve the system endpoints through it

New structure: tensorrt_llm/serve/_base/ (shared serving infrastructure)
and tensorrt_llm/serve/api/ (endpoint packages, one router plus one
serving class each; the package __init__ imports nothing so routers load
lazily, by role). serve/_base/routing.py provides make_router(), which
always uses the msgpack-decoding route class, and attach_routers(),
which imports and attaches only the routers whose RouterSpec matches the
server role.

The first endpoint package, serve/api/ops/system/, serves /health,
/version and /v1/models for every role. OpenAIServer builds a
SystemServing with the injected _health_probe (today's probe body,
SIGINT side effect included) and attaches the routers before the role
dispatch; the 12 add_api_route lines for the three paths leave the four
role tables. Same routes, methods, route classes, bodies and
conflict-relevant order: the recorded route reference is unchanged.

The layering tests gain the new packages' contracts, the route-snapshot
failure message points new endpoints at the router pattern, and the
serve agent docs describe where a new endpoint goes.

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

tensorrt_llm/serve/_base/ and tensorrt_llm/serve/api/ops/ join the
multi-GPU trigger list: the msgpack route class decodes the
disaggregated frontend's worker requests, and /health is the
disaggregated readiness probe, so changes under these directories keep
running the multi-GPU stages that exercise that path.

The three new modules join the strict mypy file set, and the type-check
pre-commit hook now also triggers on changes under tensorrt_llm/serve/
and tensorrt_llm/commands/serve_cli/, so edits to typed serving modules
get local type checking; the hook still checks only the declared file
set.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… tests that build routes by hand

Two multi-frontend tests build an OpenAIServer without its constructor
and call register_routes() themselves. Since /health, /version and
/v1/models are attached through the system router by the constructor,
those tests no longer saw the three routes: one asserted that /version
stays local on an attached frontend (404), the other reported the three
per-frontend-safe entries as removed. Both helpers now set the serving
object and attach the routers before the hand registration, in the
constructor's order, and the route enumeration walks included routers,
which FastAPI 0.139 keeps as objects without a path in app.routes.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…n the OpenEngine metrics test

The HTTP-enablement case builds a real OpenAIServer through its
constructor and looked for /prometheus/metrics by reading .path off
every entry of app.routes. Since the constructor attaches the system
router, FastAPI 0.139 keeps an included-router object there that has no
.path, and the child process failed with AttributeError. The test now
matches a GET /prometheus/metrics request against the routes with the
public route-matching API, which works on both ends of the pinned
FastAPI range.

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

The check that endpoint packages under tensorrt_llm/serve/api do not
import each other read the package off the from-module alone, so
`from tensorrt_llm.serve import api` inside an endpoint package passed
and `from tensorrt_llm.serve.api.ops import system` inside ops/system
failed. It now reads the package off <module>.<name> per imported name,
as the layering contracts do, and a synthetic-edge test pins both forms.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…e/api/visual_gen

Move the eight VisualGen request and response models (ImageGenerationRequest,
ImageEditRequest, ImageObject, ImageGenerationResponse, MediaReferenceItem,
VideoGenerationRequest, VideoJob, VideoJobList) out of
tensorrt_llm/serve/openai_protocol.py into the new package
tensorrt_llm/serve/api/visual_gen/ (protocol.py), unchanged. openai_protocol
keeps the eight names importable through a lazy module __getattr__: importing
it no longer loads the VisualGen models or tensorrt_llm.visual_gen, and the
re-export is part of its own interface, so it never warns and has no removal
release. The imports only the moved block used (UploadFile, AliasChoices,
MediaRole) leave openai_protocol with it.

Every importer in the repository now imports the new module; the benchmarking
guide links the new file. protocol.py joins legacy-files.txt (its code comes
from a legacy-formatted file) and the derived lint configs are regenerated.

No behavior change. OpenAPI component names may change with the move; they are
not a contract.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…isualGen team in CODEOWNERS

VisualGen serving code now lives under tensorrt_llm/serve/api/visual_gen/. The
new rule, placed after the /tensorrt_llm/serve rule so that it wins, makes
@NVIDIA/trt-llm-torch-visual-gen-devs the owner of that directory: changes there
need the VisualGen team's approval and no longer need runtime-devs.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…y re-export under a visual_gen path

Add tests/unittest/api_stability/visual_gen/test_visual_gen_serve_api.py: it
runs the checks of test_serve_api.py on the models of
references/trtllm_serve_api.yaml that are defined in
tensorrt_llm.serve.api.visual_gen.protocol, and checks that
tensorrt_llm.serve.openai_protocol re-exports the eight VisualGen names lazily
(importing it does not load the VisualGen module; unknown names still raise
AttributeError). The file lives under a visual_gen/ path and has its own
l0_cpu.yml entry so that CI selection for VisualGen-only pull requests keeps
running it; the reference YAML is unchanged.

Add the eight identity rows of the VisualGen names to the VisualGen shim
identity table: identity only, no warning and no ban on the old path, because
the re-export is openai_protocol's permanent interface.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…its no-warning contract

Resolve every pinned model with the full gate's own loader, so a VisualGen
model pinned in the reference YAML that is later renamed or removed fails this
gate too instead of being dropped silently; the four known models stay a
secondary check. Make the lazy-load probe's diagnostic best-effort and
identity-based, so a failure prints which modules hold the VisualGen classes
instead of crashing on module-level __getattr__ hooks while walking
sys.modules. In the same fresh-interpreter probe, assert that reading each of
the eight names from openai_protocol emits no FutureWarning or
DeprecationWarning.

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

Three tests run OpenAIServer.openai_chat through a server built with the
real constructor and the fake generator, and pin what it answers today:
a request the handler rejects keeps its 400 body byte for byte, an engine
failure keeps HTTP 200 with body null (and /health_generate keeps turning
that into a 500), a response that cannot be JSON-encoded keeps its 400,
and a stream keeps its frames and media type. They pass on the unsplit
handler and must still pass once it is split into a typed core and an
HTTP wrapper.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…serve/_base/sse.py

Generated move: the chat_stream_generator closure of OpenAIServer.openai_chat
is lifted to module level in tensorrt_llm/serve/_base/sse.py unchanged, apart
from its two free variables, which become the keyword-only parameters
`server` (was `self`) and `raw_request`. openai_chat imports it back and
passes server=self, raw_request=raw_request at its one call. The generator
keeps its `except Exception:` clause and its two `_postprocess_result` reads.

tensorrt_llm/serve/_base/sse.py joins legacy-files.txt (its code comes from a
legacy-formatted file) and the generated lint configs are regenerated with
scripts/legacy_utils.py gen-configs; ruff-legacy-baseline.json gains the
module's one recorded F821 (the `anext` builtin ruff does not know), which
openai_server.py keeps counting until its baseline is regenerated.

The Anthropic stream tests patched the logger of openai_server to check what
the generator logs; they now patch the logger of serve/_base/sse.py, where
the generator lives.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ompletion and an HTTP wrapper

OpenAIServer.create_chat_completion is the former body of openai_chat with
a typed contract: it returns the ChatCompletionResponse for a non-streaming
request and the SSE stream as strings for a streaming one, and it raises
ServingError (the HTTP status apart from the OpenAI error body) where it
used to return an error response, or EngineFatalError after the three side
effects of an engine failure. Nothing else in the body changes: the six
early rejections raise where they returned, the ValueError and the generic
handler raise what they rendered, and the ServingError handler comes first
so an early rejection is neither logged nor re-wrapped.

openai_chat keeps its name, signature and route and becomes the wrapper
that renders the typed result: JSONResponse for the response, a streaming
response for the stream, the error body with its status for ServingError,
and HTTP 200 with body null after an engine failure, as before. Its own
ValueError branch keeps the 400 for a response that cannot be JSON-encoded,
which used to fail inside the core. Every caller (the route, /health_generate,
the Anthropic messages and batch paths) is unchanged.

tensorrt_llm/serve/_base/errors.py holds error_body, ServingError,
serving_error and EngineFatalError; create_error_response builds its body
with error_body, so a ServingError body and a rendered error can never
differ (the over-length-prompt code lives in one place).

Tests: the typed core and its errors through a server built with the real
constructor; the error builders against create_error_response; the lifted
SSE generator with fakes. The three answer-pinning tests added before the
split pass unchanged.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
The new error module is written from scratch, so it joins the mypy file
list in pyproject.toml; scripts/run_mypy.sh passes with it (the strict
global settings apply). tensorrt_llm/serve/_base/sse.py stays out: it is
moved legacy code and joins type checking with its package later.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…se in the multimodal prompt test

The chat handler now serializes a ChatCompletionResponse and streams any
other result of its core, so a stand-in for _create_chat_response that only
has model_dump() is no longer rendered as JSON. The test stubs a real
ChatCompletionResponse instead; what it checks (the rendered prompt, the
decoded image and chat_template_applied reaching the engine) is unchanged.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…d 400 without a traceback

The HTTP wrapper answers a response that cannot be JSON-encoded from its
own ValueError branch, which logs nothing; the generic handler would render
the same 400 but log a traceback. The answer-pinning test now also asserts
that no ERROR record is emitted, so dropping that branch fails the test.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… one submodule per API

tensorrt_llm/serve/openai_protocol.py held every OpenAI-dialect request and
response model plus the helpers that turn them into engine parameters. It
becomes the package tensorrt_llm/serve/openai_protocol/ with one submodule per
API: common, sampling, tools, chat, completions, embeddings, responses,
tokenize, models, kv_cache, control and disagg. Every definition moves
unchanged (93 definitions; generated by an AST tool and checked definition by
definition against the former module).

The package __init__.py is a lazy facade: a name-to-module table and a module
__getattr__ import a submodule only when one of its names is first read, so
every import from tensorrt_llm.serve.openai_protocol keeps working for every
name the former module had, the underscore helpers that tests import included,
and the VisualGen models re-exported from
tensorrt_llm.serve.api.visual_gen.protocol. The facade is the package's own
interface, not a compatibility shim: no warning, no removal release, and no
importer in the repository changes. Its TYPE_CHECKING block imports the public
names from their submodules so type checkers resolve them; the VisualGen names
are left out of it on purpose and appear only as strings in the table, so that
importing the facade never loads the VisualGen package. DisaggScheduleStyle,
which the former module imported and two importers took from it, stays
reachable through the facade.

Submodules import each other directly, never through the package, along an
acyclic graph: common imports no sibling; sampling, tools, models, tokenize,
embeddings and control import common; completions and responses import common
and sampling; chat imports common, sampling and tools; kv_cache imports
common, tools and chat; disagg imports common, completions and chat. torch is
imported only by sampling and disagg, openai_harmony only by chat, and the
serving-extension registry only by sampling, so code that needs only the tool
types no longer loads the chat or Responses models.

The 12 submodules are registered in legacy-files.txt (their code comes from a
legacy-formatted file) and the deleted module's line is removed; the derived
lint configs are regenerated with scripts/legacy_utils.py gen-configs. The
facade is new code and stays under the default formatter.

No behavior change. OpenAPI component names may change for classes that share
a short name with an openai type, because pydantic qualifies them with the
module path; they are not a contract.

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

tests/unittest/serve/openai_protocol/test_openai_protocol_facade.py keeps the
package tensorrt_llm/serve/openai_protocol/ honest: every name the former
single-file module defined (plus DisaggScheduleStyle) is reachable through the
facade and is the object its submodule defines; the facade table is frozen at
those names; __all__, dir() and the TYPE_CHECKING block agree with the table;
unknown names raise AttributeError; the submodule import graph stays inside
the allowed acyclic graph and no submodule imports the package itself;
importing the package loads no submodule, importing tools loads neither chat
nor responses nor completions, and reading ChatCompletionRequest does not load
the serving extensions; the private helpers that other tests import through
the package stay importable.

tests/unittest/serve/api/visual_gen/test_openai_protocol_facade_visual_gen.py
holds the VisualGen assertions under a visual_gen/ path so that CI selection
runs them for VisualGen-only changes: the eight VisualGen models map to
tensorrt_llm.serve.api.visual_gen.protocol, nothing but a lookup of one of
them loads that module, and the facade names it only as strings in its table,
never in an import statement.

tests/unittest/serve/test_layering.py gains the contract that the protocol
package imports no handler module (servers, handlers and routers import the
models, never the reverse), and tensorrt_llm/serve/AGENTS.md tells new code to
import the submodule that defines a model rather than the package.

Both test files are collected by the existing unittest/serve and
unittest/serve/api/visual_gen/ entries of l0_cpu.yml and carry the cpu_only
marker.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…s as strings and bind the test's culprit list unconditionally

The table comment in tensorrt_llm/serve/openai_protocol/__init__.py said that a
TYPE_CHECKING import of the VisualGen models would load their package at run
time, which a TYPE_CHECKING block never does. The real reason is the static
import-isolation check that lets CI run only the VisualGen tests for
VisualGen-only changes: it forbids an import edge from this package to the
VisualGen package even under TYPE_CHECKING, so the facade names that module
only as a string.

In tests/unittest/serve/openai_protocol/test_openai_protocol_facade.py the
list of submodules that import the serving extensions was bound only on the
failing path and read in the assertion message; it is now bound before the
branch, so an unconditional message can never raise UnboundLocalError.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…junyix-feat-serve-embeddings-mm-encoder

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

# Conflicts:
#	pyproject.toml
#	tensorrt_llm/grpc/openengine/streaming.py
#	tensorrt_llm/serve/openai_protocol.py
#	tensorrt_llm/serve/openai_server.py
#	tests/integration/test_lists/test-db/l0_cpu.yml
#	tests/unittest/serve/__init__.py
#	tests/unittest/serve/api/__init__.py
#	tests/unittest/serve/api/visual_gen/__init__.py
…gs, multimodal encoder and tokenize

Create tensorrt_llm/serve/_base/serving.py with BaseServing and
GenerateBaseServing, and the EmbeddingServing, MMEncoderServing and
TokenizeServing skeletons under tensorrt_llm/serve/api/openai/. Every
dependency is a keyword-only constructor argument. The new Group B files
are registered in legacy-files.txt and the generated lint configs are
regenerated.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…tokenize handlers into their serving classes

Generated move. OpenAIServer loses openai_embedding,
_normalize_embedding_input, _init_embedding_batcher (to EmbeddingServing),
openai_mm_encoder (to MMEncoderServing) and tokenize (to TokenizeServing);
register_embedding_routes is deleted, its route is served by the
embeddings router. create_error_response, _attach_multimodal_data and
await_disconnected move to the serving bases in
tensorrt_llm/serve/_base/serving.py and stay reachable on OpenAIServer
as aliases of the moved objects while chat and completions still call
them. _record_generator_termination moves to the same module.
tensorrt_llm/serve/encode_batcher.py becomes
tensorrt_llm/serve/api/openai/embeddings/batcher.py; the old module is
deleted and every import of it is rewritten.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…tokenize routers and serving objects

Each new package gets an api_router.py built with make_router() and a
RouterSpec in ROUTERS for exactly the roles the route had. The
composition root builds RenderResources once after _init_llm and
injects it, constructs the serving objects inside the role branches,
puts them on app.state, and attaches the routers after the role switch,
where every attach_router can check its app.state key. The two route
registrations the routers now serve leave register_routes and
register_mm_encoder_routes.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…enize serving classes on CPU

Construction tests (every dependency is a required keyword-only argument),
route tests through the real composition root and routers (msgpack bodies
included), the batcher reference the health probe reads, the injected
RenderResources carrying the server chat template, the serving bases and
the borrowed-helper identity on OpenAIServer. The encode-only fake now
encodes fixed embeddings and scripted outputs can carry disaggregated
params; the app factory passes the chat-template knobs. The runtime-API
tests build EmbeddingServing and patch the termination telemetry where
the moved helper looks it up; the lazy-import guard also checks that
importing tensorrt_llm.serve loads no endpoint package.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… and the serve guide

The multi-GPU trigger table gains the three package prefixes, so code
moved out of tensorrt_llm/serve/openai_server.py keeps its trigger; the
three router modules join the mypy file list; the serve guide states the
constructor rule of serving classes.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… stub provide the tokenizer

The test runs the real OpenAIServer constructor with _init_llm replaced
by a stub. The constructor now builds the tokenize serving object for
the LLM role from self.tokenizer, so the stub sets it too; a missing
dependency fails at construction on purpose instead of at request time.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ition root with the tokenize serving object

Both tests build a partial OpenAIServer and register its routes the way
the constructor does. The constructor now puts the tokenize serving object
on app.state and attaches the role's routers after the local table, so the
re-enactments do the same; the tokenize router refuses to attach without
its serving object.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…unyix-feat-serve-completions-serving (integration base, not part of the reviewable change)

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…nd the generate-pipeline constructor arguments

Create tensorrt_llm/serve/api/openai/completions/ with the CompletionServing
skeleton, and give GenerateBaseServing the keyword-only constructor arguments
that the completions handler and the helpers it shares with chat and Responses
read: the perf-metrics switch and collector, the adjusted steady clock, the
iteration-stats wake-up event and a getter for the collector task that the
lifespan creates later, the internal disaggregated auth key, the per-request
speculative-decoding opt-in and its draft-token bound, the input-processor
thread pool and the post-processing strategy. The new Group B files are
registered in legacy-files.txt and the generated lint configs are regenerated.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…Serving and lift its closures

Generated move: openai_completion leaves OpenAIServer for
CompletionServing in tensorrt_llm/serve/api/openai/completions/serving.py;
its five nested functions become module-level functions of
tensorrt_llm/serve/api/openai/completions/_merge.py whose free variables
(the serving object, the raw request, the disaggregated parameters) are
keyword-only parameters, so they can be unit-tested; the six helpers it
shares with the chat and Responses handlers (postproc_worker_enabled,
_extract_metrics, _logit_bias_vocab_size, _validate_internal_disagg_request,
_has_cache_transceiver_config, _apply_spec_decode_stats_opt_in) move into
GenerateBaseServing and stay reachable on OpenAIServer as aliases of the
moved objects while chat and Responses call them there. The one recorded
F821 (anext) of the lifted generator_wrapper follows it into the lint
baseline under its new path.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…s router and serving object

The LLM role group builds CompletionServing in the composition root, next
to TokenizeServing, with the generate-pipeline arguments read once by
_generate_serving_kwargs(), and puts it on app.state; the new
tensorrt_llm/serve/api/openai/completions/api_router.py maps POST
/v1/completions to it through make_router(), so the route keeps the
msgpack route class the disaggregated frontend relies on, and joins the
routing table for the LLM role group. register_routes no longer registers
the route itself.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…, and build the completions tests on serving objects

New CPU tests: tests/unittest/serve/api/openai/completions/test_merge.py
exercises each lifted function alone (usage merging, generator merging,
the SSE error frame, the first-token stamp, context-only prompt ids, the
error path); test_serving.py covers the constructor contract, the shared
composition-root objects, the route for one and several prompts, msgpack
bodies, the wired internal disaggregated auth key, the stats-collector
wake-up and the role gate. The tests that built an OpenAIServer with
object.__new__ to reach the completions code (the streaming error test,
the list-prompt streaming test, the cache-transceiver auth test, the
spec-decode opt-in test and the KV-hints handler test) now build a
CompletionServing with explicit arguments, and the object.__new__ ratchet
drops their rows. The direct MMEncoderServing constructions pass the new
base arguments; the borrowed-helper table gains the six helpers; the
fixture factory can set the internal disaggregated auth key and a cache
transceiver backend.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…-GPU trigger table and type checking

The multi-GPU trigger table gains the tensorrt_llm/serve/api/openai/completions/
prefix, so the completions code that left tensorrt_llm/serve/openai_server.py
keeps triggering the disaggregated stages; the new router module joins the
mypy file list. The moved serving and lifted-function modules stay out of it,
like the other modules that hold moved legacy code.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…se-class test the new generate-pipeline arguments

The multi-frontend tests re-enact the composition root by hand: they now
put a CompletionServing with placeholder dependencies on app.state before
attaching the role's routers, as the constructor does, because the
completions router refuses to attach without it. The OpenEngine Prometheus
test's constructor stub sets model_config too, since the LLM role now reads
it when it builds the completions serving object. The base-class test
requires every GenerateBaseServing argument, pins the attribute each one is
stored as, and checks that the collector-task property follows the root's
current binding.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…r what it is and cover the merged several-prompt response

GenerateBaseServing's constructor argument that carries the getter of the
composition root's iteration-stats collector task is now called
iteration_stats_collector_task_getter at every site: the base class, the
root's _generate_serving_kwargs(), the direct constructions in tests and the
hand-built route tables. The stored attribute and the read-only property
keep their names. The completions route test suite gains the non-streaming
several-prompt case: one merged response with one choice per prompt and the
summed usage, which fails when the merge call loses its serving argument or
the merge drops a prompt.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…outer and pass the kv-hints handlers a request

tests/unittest/llmapi/apps/test_responses_usage.py mounted
OpenAIServer.anthropic_messages, which no longer exists: the Anthropic
routes live in tensorrt_llm/serve/api/anthropic/api_router.py and read
their serving object from app.state. The test's bare app now mounts the
router's route function and carries the AnthropicMessagesServing the
server's constructor built, so the Messages route still runs over the
same chat core as the chat route.

tests/unittest/llmapi/test_kv_hints.py called the handlers with
raw_request=None. The chat wrapper now wraps the request in a
RequestContext, which needs an object to delegate to, so the handlers
get a minimal request (empty headers, an empty state), as the chat core
tests do.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…-junyix-feat-serve-chat-serving (integration base, not part of the reviewable change)

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…server-role constructor argument

ChatServing is the serving class of POST /v1/chat/completions; this commit adds
its constructor (one keyword-only argument per server attribute the chat
handlers read) and registers the module with the legacy formatting group of
the code it will receive. GenerateBaseServing gains server_role, which the
scheduling-parameter helper reads, and the composition root passes it with the
other generate-pipeline arguments.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
openai_chat (the HTTP wrapper), create_chat_completion (the typed core),
_create_chat_response and chat_harmony leave OpenAIServer for ChatServing;
_get_scheduling_params joins GenerateBaseServing next to the other
generate-pipeline helpers. The module-level tool-call helpers move with the
chat code; _configure_parser_special_token_decoding goes to
tensorrt_llm/serve/_base/serving.py because the Responses handler calls it
too and packages under serve/api/ do not import each other. The aggregated
chat backend of the Anthropic Messages serving class is ChatServing itself:
its stream_model and error_message move there and the adapter class goes.
The five helper aliases whose last caller was the chat code leave
OpenAIServer. Every repository import of the moved helpers is rewritten;
none is re-exported from openai_server.py. The moved anext of chat_harmony's
generator is recorded for the new module in the legacy lint baseline.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…router and serving object

The composition root builds ChatServing in the LLM role group, puts it on
app.state, and hands it to the Anthropic Messages serving class as its chat
backend. The chat router joins the routing table for the LLM role group; it
holds two routers with one route each and attaches the one the serving
object's use_harmony selects, so the route keeps the endpoint name the server
gave it (openai_chat, or chat_harmony on a gpt-oss server). /health_generate
stays on the root and calls the serving object's openai_chat. The new package
joins the multi-GPU trigger table and the typed modules.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…on aliases the Responses handler still calls

OpenAIServer.openai_responses validates the protected disaggregated fields
through _validate_internal_disagg_request, which reads
_has_cache_transceiver_config on the same object, so both stay reachable on
the class as the moved helpers until the Responses handler moves. The three
aliases whose last caller was the chat code are gone.

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

New: tests/unittest/serve/api/openai/chat/test_serving.py pins the constructor
(every dependency keyword-only and required, each stored where the moved
handlers read it), that the serving object is the composition root's and uses
its render resources, clock, stats event, input-processor pool, metrics
collector, post-processing strategy and server role, the chat route on the
three LLM roles (one chat, a stream ending in [DONE], msgpack bodies), the
endpoint name on harmony and non-harmony servers built in one process,
/health_generate through the serving object (200, 500 on a rejected chat, the
beam-search arguments), the Anthropic Messages backend identity with
stream_model and error_message, the wired internal auth key with its control,
and the embedding and encoder roles. tests/test_common/serve/serving_stubs.py
builds serving objects with placeholder dependencies for tests that call a
handler off the app.

Rewritten onto serving objects: the tests that built OpenAIServer with
object.__new__ to call a chat handler (internal auth, disaggregated service,
router, kv hints, serving extensions, Anthropic routes), the stubs of the
typed-core and Anthropic behavior tests (now on server._chat_serving, with the
engine-failure recorder patched where the moved core looks it up), the two
multimodal fixtures on the stub server (which now gets its model config from
the loader, so the constructor builds the serving objects from it), the
Responses usage fixture (the chat route from its router; the Messages route
keeps the mount NVIDIA#21551 gave it), the multi-frontend re-enactments (a chat
serving object before attaching) and the OpenEngine Prometheus stub. Every
direct construction of a generate serving object passes server_role. The
ratchet and borrowed-helper tables follow. The legacy lint baseline keeps its
unrelated entries where they were (the generated move had reordered two of
them) and, for now, no entry for the moved code; the next commit records it.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ne and pin the server chat template through the chat route

The legacy lint baseline gets the one violation recorded for the moved chat
code, the anext of chat_harmony's streaming generator, under its new module:
ruff releases that assume Python 3.9 report it as an undefined name, and the
baseline is what keeps the legacy lint green with them. The render-resources
test now also posts a chat through the route and checks that the prompt the
handler renders and the ids that reach the engine come from the server chat
template, which a RenderResources built from the serving object would drop.

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

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant