Skip to content

[TRTLLM-17267][refactor] Move the operational endpoints into serving classes and routers - #21725

Draft
JunyiXu-nv wants to merge 85 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-ops-routers
Draft

JunyiXu-nv wants to merge 85 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-ops-routers

Conversation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator

@coderabbitai summary

Description

Stacked on #21676 (chat serving class), #21556 (shared app assembly) and #21552 (VisualGen handler move): the first commits below belong to those PRs and disappear from this diff when they merge; this PR's own change is the last 4 commits. The integration base is two merge commits (b9aae2f42b: #21556 into the head of #21676; 463f37af4a: #21552 into that). Merge 2 carries two one-line test fixes that #21552 does not have yet and will need once it rebases onto #21556/#21447 and #21393/#21445: tests/unittest/_torch/visual_gen/test_trtllm_serve_endpoints.py imports ServerArrivalTimeMiddleware from tensorrt_llm.serve.middleware.arrival_time (its old home in responses_utils is gone), and the _save_bytes entry of tests/unittest/serve/private_access_baseline.yaml is re-keyed to tensorrt_llm/serve/api/visual_gen/serving_images.py / ImageServing.openai_image_generation. Nothing else in the merges is a change of its own.

This PR moves the remaining operational endpoints off OpenAIServer into one serving class per endpoint group under tensorrt_llm/serve/api/ops/, each with a router created by make_router() and attached through the router table (ROUTERS in tensorrt_llm/serve/_base/routing.py): /health_generate, /v1/data_transceiver_state, /energy_metrics, /steady_clock_offset (GET and POST), /kv_cache_events and /server_info (ops/worker/); the JSON /metrics with the iteration-stats collector (ops/metrics/) and the Prometheus mount (ops/metrics/prometheus.py); the resource-governor routes (ops/control/, where the ResourceGovernor keeps registering its own routes); /start_profile and /stop_profile (ops/profile/); the runtime-control endpoints (ops/runtime_control/); and the deprecated RL control endpoints (ops/rl/). The render routes attach through the same table. Five new ServerCapabilities fields (governor_queue, rl_control, runtime_control, perf_metrics, render_endpoints) carry the conditions the server used to evaluate inline; the composition root computes them in OpenAIServer._server_capabilities(), after the serving objects exist. Every serving class takes its dependencies as keyword-only constructor arguments without defaults, and the composition root hands it its own objects: the adjusted steady clock, the shared clock offset, the chat serving object, the render resources built once, the metrics collector, the wake-up event, the replay cache.

Part of the trtllm-serve refactor (epic TRTLLM-17056, task TRTLLM-17267: move every endpoint implementation off OpenAIServer into serving classes and routers). Builds on "Add the trtllm-serve router skeleton and serve the system endpoints through it" (#21448), "Build the aggregated and disaggregated trtllm-serve apps with build_http_app and serve_uvicorn" (#21556), "Move the VisualGen handlers and modules into serve/api/visual_gen" (#21552), the embeddings, multimodal-encoder and tokenize serving classes (#21575), the completions serving class (#21640), the chat serving class (#21676) and the private-access lint (#21393, #21445).

Behavior

No behavior change. Routes, route classes, conflict-relevant registration order and middleware order are pinned by the route and middleware-order snapshots, which are unchanged for every configuration. Known issues carried as they are: the enable_iter_perf_stats getattr default in /metrics; /start_profile, /stop_profile, /kv_cache_events, /steady_clock_offset and the governor routes stay unauthenticated; the RL control endpoints stay deprecated and mutually exclusive with runtime control.

Moves

Pure-move check
## Move spec

```yaml
moves:
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/ops/worker/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: WorkerServing
    names:
    - data_transceiver_state
    - health_generate
    - get_energy_metrics
    - set_steady_clock_offset
    - get_steady_clock_offset
    - get_kv_cache_events
    - _live_tokens_per_block
    - get_server_info
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/ops/metrics/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: MetricsServing
    names:
    - _iteration_stats_buffer_maxlen
    - get_iteration_stats
    - _iteration_stats_collector_loop
    - _record_iteration_stat
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/ops/profile/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: ProfileServing
    names:
    - start_profile
    - stop_profile
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/ops/runtime_control/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: RuntimeControlServing
    names:
    - _require_runtime_control_auth
    - _runtime_control_error_response
    - _call_runtime_memory_method
    - runtime_release_memory
    - runtime_resume_memory
    - runtime_memory_status
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/ops/rl/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: RLServing
    names:
    - _require_rl_control_auth
    - release_memory
    - resume_memory
    - update_weights
method_to_function:
- from: tensorrt_llm/serve/openai_server.py::OpenAIServer.mount_metrics
  to: tensorrt_llm/serve/api/ops/metrics/prometheus.py::mount_metrics
  group: B
  self_attributes:
    app: app
rewrites:
- symbol: tensorrt_llm/serve/api/ops/worker/serving.py::WorkerServing.get_server_info
  old: RenderResources.from_server(self).fingerprint()
  new: self._render_resources.fingerprint()
replaced:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_mm_encoder_routes
  by: tensorrt_llm/serve/api/ops/metrics/api_router.py::get_iteration_stats
  reason: /metrics on the multimodal-encoder role comes from the metrics router
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._register_runtime_control_routes
  by: tensorrt_llm/serve/api/ops/runtime_control/api_router.py::attach_router
  reason: the runtime-control routes come from their router
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._register_rl_control_routes
  by: tensorrt_llm/serve/api/ops/rl/api_router.py::attach_router
  reason: the RL control routes come from their router
added:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._server_capabilities
  reason: builds the ServerCapabilities that decide which routers attach, the five
    new fields included
- symbol: tensorrt_llm/serve/api/ops/worker/serving.py::WorkerServing
  reason: serving class of the worker endpoints
- symbol: tensorrt_llm/serve/api/ops/worker/serving.py::WorkerServing.__init__
  reason: explicit dependencies instead of server attributes, so missing wiring fails
    at construction
- symbol: tensorrt_llm/serve/api/ops/worker/serving.py::_ChatBackend
  reason: local Protocol for the injected chat serving (no import of the chat package)
- symbol: tensorrt_llm/serve/api/ops/worker/serving.py::_ChatBackend.openai_chat
  reason: the Protocol's one method
- symbol: tensorrt_llm/serve/api/ops/metrics/serving.py::MetricsServing
  reason: serving class of /metrics, owns the iteration-stats machinery
- symbol: tensorrt_llm/serve/api/ops/metrics/serving.py::MetricsServing.__init__
  reason: explicit dependencies and the metrics state moved out of the composition
    root's constructor
- symbol: tensorrt_llm/serve/api/ops/profile/serving.py::ProfileServing
  reason: serving class of the profiling endpoints
- symbol: tensorrt_llm/serve/api/ops/profile/serving.py::ProfileServing.__init__
  reason: explicit dependencies
- symbol: tensorrt_llm/serve/api/ops/runtime_control/serving.py::RuntimeControlServing
  reason: serving class of the runtime-control endpoints
- symbol: tensorrt_llm/serve/api/ops/runtime_control/serving.py::RuntimeControlServing.__init__
  reason: explicit dependencies, shared replay cache
- symbol: tensorrt_llm/serve/api/ops/rl/serving.py::RLServing
  reason: serving class of the RL control endpoints
- symbol: tensorrt_llm/serve/api/ops/rl/serving.py::RLServing.__init__
  reason: explicit dependencies
- symbol: tensorrt_llm/serve/api/ops/metrics/prometheus.py::attach_router
  reason: router-table entry point of the Prometheus mount
- symbol: tensorrt_llm/serve/render/api_router.py::attach_router
  reason: router-table entry point of the render routes
- symbol: tensorrt_llm/serve/_base/routing.py::_when_governor_queue
  reason: condition of the resource-governor router
- symbol: tensorrt_llm/serve/_base/routing.py::_when_rl_control
  reason: condition of the RL control router
- symbol: tensorrt_llm/serve/_base/routing.py::_when_runtime_control
  reason: condition of the runtime-control router
- symbol: tensorrt_llm/serve/_base/routing.py::_when_render_endpoints
  reason: condition of the render routes
- symbol: tensorrt_llm/serve/_base/routing.py::_when_perf_metrics
  reason: condition of the Prometheus mount
- symbol: tensorrt_llm/serve/api/ops/worker/api_router.py::*
  reason: new router module
- symbol: tensorrt_llm/serve/api/ops/metrics/api_router.py::*
  reason: new router module
- symbol: tensorrt_llm/serve/api/ops/control/api_router.py::*
  reason: new router module
- symbol: tensorrt_llm/serve/api/ops/profile/api_router.py::*
  reason: new router module
- symbol: tensorrt_llm/serve/api/ops/runtime_control/api_router.py::*
  reason: new router module
- symbol: tensorrt_llm/serve/api/ops/rl/api_router.py::*
  reason: new router module
modified:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.__init__
  reason: metrics state moved to MetricsServing; energy-monitor hand-over and the
    stats machinery on the metrics serving object in the lifespan
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._attach_role_routes
  reason: 'the method that holds the role switch: serving objects and the governor
    built in the role branches and put on app.state; the register_mm_encoder_routes
    call deleted (replaced by the router table); attach_routers with the full capabilities'
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_routes
  reason: the operational routes, the governor, the render routes and the Prometheus
    mount come from routers; the Responses routes stay until their router exists
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_visual_gen_routes
  reason: its GET /metrics registration is deleted (the metrics router serves /metrics
    on VISUAL_GEN), and its docstring no longer says /metrics is registered there
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._register_launcher_forwarding_routes
  reason: its docstring names the router table instead of the deleted registration
    method; the body is unchanged
- symbol: tensorrt_llm/serve/render/api_router.py::mount_render_endpoints
  reason: takes the RenderResources the composition root built instead of the server
- symbol: tensorrt_llm/serve/_base/routing.py::ServerCapabilities
  reason: 'five new fields: governor_queue, rl_control, runtime_control, perf_metrics,
    render_endpoints'
- symbol: tensorrt_llm/serve/_base/routing.py::ROUTERS
  reason: the RouterSpecs of the routers this PR adds
rewrite_internal_imports: true

Pure-move check: base 463f37a, head 1ad84f4, tools dbe72ce65eef

Symbol From To Result
ROUTERS tensorrt_llm/serve/_base/routing.py tensorrt_llm/serve/_base/routing.py modified (declared)
ServerCapabilities tensorrt_llm/serve/_base/routing.py tensorrt_llm/serve/_base/routing.py modified (declared)
OpenAIServer.register_mm_encoder_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/metrics/api_router.py replaced
OpenAIServer.mount_metrics tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/metrics/prometheus.py verbatim
OpenAIServer._iteration_stats_buffer_maxlen tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/metrics/serving.py verbatim
OpenAIServer._iteration_stats_collector_loop tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/metrics/serving.py verbatim
OpenAIServer._record_iteration_stat tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/metrics/serving.py verbatim
OpenAIServer.get_iteration_stats tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/metrics/serving.py verbatim
OpenAIServer.start_profile tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/profile/serving.py verbatim
OpenAIServer.stop_profile tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/profile/serving.py verbatim
OpenAIServer._register_rl_control_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/rl/api_router.py replaced
OpenAIServer._require_rl_control_auth tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/rl/serving.py verbatim
OpenAIServer.release_memory tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/rl/serving.py verbatim
OpenAIServer.resume_memory tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/rl/serving.py verbatim
OpenAIServer.update_weights tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/rl/serving.py verbatim
OpenAIServer._register_runtime_control_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/runtime_control/api_router.py replaced
OpenAIServer._call_runtime_memory_method tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/runtime_control/serving.py verbatim
OpenAIServer._require_runtime_control_auth tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/runtime_control/serving.py verbatim
OpenAIServer._runtime_control_error_response tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/runtime_control/serving.py verbatim
OpenAIServer.runtime_memory_status tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/runtime_control/serving.py verbatim
OpenAIServer.runtime_release_memory tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/runtime_control/serving.py verbatim
OpenAIServer.runtime_resume_memory tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/runtime_control/serving.py verbatim
OpenAIServer._live_tokens_per_block tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py verbatim
OpenAIServer.data_transceiver_state tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py verbatim
OpenAIServer.get_energy_metrics tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py verbatim
OpenAIServer.get_kv_cache_events tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py verbatim
OpenAIServer.get_server_info tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py rewritten
OpenAIServer.get_steady_clock_offset tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py verbatim
OpenAIServer.health_generate tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py verbatim
OpenAIServer.set_steady_clock_offset tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/ops/worker/serving.py verbatim
OpenAIServer.__init__ tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer._attach_role_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer._register_launcher_forwarding_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer.register_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer.register_visual_gen_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
mount_render_endpoints tensorrt_llm/serve/render/api_router.py tensorrt_llm/serve/render/api_router.py modified (declared)
_when_governor_queue — tensorrt_llm/serve/_base/routing.py added
_when_perf_metrics — tensorrt_llm/serve/_base/routing.py added
_when_render_endpoints — tensorrt_llm/serve/_base/routing.py added
_when_rl_control — tensorrt_llm/serve/_base/routing.py added
_when_runtime_control — tensorrt_llm/serve/_base/routing.py added
tensorrt_llm/serve/api/ops/control/api_router.py::* — tensorrt_llm/serve/api/ops/control/api_router.py added
tensorrt_llm/serve/api/ops/metrics/api_router.py::* — tensorrt_llm/serve/api/ops/metrics/api_router.py added
attach_router — tensorrt_llm/serve/api/ops/metrics/prometheus.py added
MetricsServing.__init__ — tensorrt_llm/serve/api/ops/metrics/serving.py added
MetricsServing — tensorrt_llm/serve/api/ops/metrics/serving.py added
tensorrt_llm/serve/api/ops/profile/api_router.py::* — tensorrt_llm/serve/api/ops/profile/api_router.py added
ProfileServing.__init__ — tensorrt_llm/serve/api/ops/profile/serving.py added
ProfileServing — tensorrt_llm/serve/api/ops/profile/serving.py added
tensorrt_llm/serve/api/ops/rl/api_router.py::* — tensorrt_llm/serve/api/ops/rl/api_router.py added
RLServing.__init__ — tensorrt_llm/serve/api/ops/rl/serving.py added
RLServing — tensorrt_llm/serve/api/ops/rl/serving.py added
tensorrt_llm/serve/api/ops/runtime_control/api_router.py::* — tensorrt_llm/serve/api/ops/runtime_control/api_router.py added
RuntimeControlServing.__init__ — tensorrt_llm/serve/api/ops/runtime_control/serving.py added
RuntimeControlServing — tensorrt_llm/serve/api/ops/runtime_control/serving.py added
tensorrt_llm/serve/api/ops/worker/api_router.py::* — tensorrt_llm/serve/api/ops/worker/api_router.py added
WorkerServing.__init__ — tensorrt_llm/serve/api/ops/worker/serving.py added
WorkerServing — tensorrt_llm/serve/api/ops/worker/serving.py added
_ChatBackend.openai_chat — tensorrt_llm/serve/api/ops/worker/serving.py added
_ChatBackend — tensorrt_llm/serve/api/ops/worker/serving.py added
OpenAIServer._server_capabilities — tensorrt_llm/serve/openai_server.py added
attach_router — tensorrt_llm/serve/render/api_router.py added

Replaced

  • OpenAIServer.register_mm_encoder_routes -> tensorrt_llm/serve/api/ops/metrics/api_router.py::get_iteration_stats: /metrics on the multimodal-encoder role comes from the metrics router
  • OpenAIServer._register_runtime_control_routes -> tensorrt_llm/serve/api/ops/runtime_control/api_router.py::attach_router: the runtime-control routes come from their router
  • OpenAIServer._register_rl_control_routes -> tensorrt_llm/serve/api/ops/rl/api_router.py::attach_router: the RL control routes come from their router

Added

  • OpenAIServer._server_capabilities: builds the ServerCapabilities that decide which routers attach, the five new fields included
  • WorkerServing: serving class of the worker endpoints
  • WorkerServing.__init__: explicit dependencies instead of server attributes, so missing wiring fails at construction
  • _ChatBackend: local Protocol for the injected chat serving (no import of the chat package)
  • _ChatBackend.openai_chat: the Protocol's one method
  • MetricsServing: serving class of /metrics, owns the iteration-stats machinery
  • MetricsServing.__init__: explicit dependencies and the metrics state moved out of the composition root's constructor
  • ProfileServing: serving class of the profiling endpoints
  • ProfileServing.__init__: explicit dependencies
  • RuntimeControlServing: serving class of the runtime-control endpoints
  • RuntimeControlServing.__init__: explicit dependencies, shared replay cache
  • RLServing: serving class of the RL control endpoints
  • RLServing.__init__: explicit dependencies
  • attach_router: router-table entry point of the Prometheus mount
  • attach_router: router-table entry point of the render routes
  • _when_governor_queue: condition of the resource-governor router
  • _when_rl_control: condition of the RL control router
  • _when_runtime_control: condition of the runtime-control router
  • _when_render_endpoints: condition of the render routes
  • _when_perf_metrics: condition of the Prometheus mount
  • tensorrt_llm/serve/api/ops/worker/api_router.py::*: new router module
  • tensorrt_llm/serve/api/ops/metrics/api_router.py::*: new router module
  • tensorrt_llm/serve/api/ops/control/api_router.py::*: new router module
  • tensorrt_llm/serve/api/ops/profile/api_router.py::*: new router module
  • tensorrt_llm/serve/api/ops/runtime_control/api_router.py::*: new router module
  • tensorrt_llm/serve/api/ops/rl/api_router.py::*: new router module

Rewritten

  • OpenAIServer.get_server_info (tensorrt_llm/serve/api/ops/worker/serving.py): RenderResources.from_server(self).fingerprint() -> self._render_resources.fingerprint()

Modified (declared)

  • OpenAIServer.__init__: metrics state moved to MetricsServing; energy-monitor hand-over and the stats machinery on the metrics serving object in the lifespan
    --- base
    +++ head
    @@ -54,10 +54,6 @@
    self._media_load_executor = ThreadPoolExecutor(max_workers=media_load_workers, thread_name_prefix='trtllm_media_load')
    BaseMediaIO.set_executor(self._media_load_executor)
    log_allowed_local_media_path()

    • self._metrics_stats_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix='trtllm_metrics_stats')
    • self._metrics_stats_read: Optional[asyncio.Future] = None
    • self._metrics_stats_read_waiting = False
    • self._metrics_stats_read_consumed = True
      model_dir = Path(model)
      if model_dir.exists() and model_dir.is_dir():
      self.model = model_dir.name
      @@ -85,7 +81,6 @@
      self._iteration_stats_poll_interval: Optional[float] = 1.0 if multi_frontend_serving is not None and multi_frontend_serving.is_launcher else None
      self._iteration_stats_collector_task = None
      self._iteration_stats_wakeup_event = asyncio.Event()
    • self._iteration_stats_buffer: Optional[deque] = None
      self._adjusted_steady_clock = AdjustedSteadyClock()
      self.energy_monitor = None
      self.disagg_cluster_storage = None
      @@ -110,14 +105,16 @@
      except Exception as e:
      logger.warning(f'Failed to initialize GPU energy monitoring: {e}')
      self.energy_monitor = None
    •        if self._worker_serving is not None:
      
    •            self._worker_serving.energy_monitor = self.energy_monitor
             if self._launcher_forwarder is not None:
                 self._launcher_watchdog_task = asyncio.create_task(AttachedFrontendWatchdog(self._launcher_forwarder, self._multi_frontend.launcher_pid, self._on_launcher_lost).run())
                 self._clock_offset_task = asyncio.create_task(follow_clock_offset(self._shared_clock_offset, self._apply_shared_clock_offset))
                 logger.info(f'Attached frontend: forwarding launcher-owned routes to {self._multi_frontend.launcher_uds}')
             elif self.metrics_collector and getattr(self.generator.args, 'enable_iter_perf_stats', True):
      
    •            max_buf = self._iteration_stats_buffer_maxlen(getattr(self.generator.args, 'iter_stats_max_iterations', 1000))
      
    •            self._iteration_stats_buffer = deque(maxlen=max_buf)
      
    •            self._iteration_stats_collector_task = asyncio.create_task(self._iteration_stats_collector_loop())
      
    •            max_buf = self._metrics_serving._iteration_stats_buffer_maxlen(getattr(self.generator.args, 'iter_stats_max_iterations', 1000))
      
    •            self._metrics_serving._iteration_stats_buffer = deque(maxlen=max_buf)
      
    •            self._iteration_stats_collector_task = asyncio.create_task(self._metrics_serving._iteration_stats_collector_loop())
                 self._iteration_stats_wakeup_event.set()
                 logger.info('Started background iteration stats collector task')
         if self.embedding_batcher is not None:
      

    @@ -148,7 +145,7 @@
    if self.resource_governor is not None:
    self.resource_governor.close()
    self.generator.shutdown()

    •    self._metrics_stats_executor.shutdown(wait=False, cancel_futures=True)
      
    •    self._metrics_serving._metrics_stats_executor.shutdown(wait=False, cancel_futures=True)
      

      async def validation_exception_handler(request, exc):
      if request.url.path.startswith('/v1/messages'):

  • OpenAIServer._attach_role_routes: the method that holds the role switch: serving objects and the governor built in the role branches and put on app.state; the register_mm_encoder_routes call deleted (replaced by the router table); attach_routers with the full capabilities
    --- base
    +++ head
    @@ -1,6 +1,11 @@
    def _attach_role_routes(self, app: FastAPI) -> None:
    self.app = app
    app.state.ops_serving_system = SystemServing(model=self.model, health_probe=self._health_probe)

    • from tensorrt_llm.serve.api.ops.metrics import api_router as metrics_router
    • from tensorrt_llm.serve.api.ops.metrics.serving import MetricsServing
    • self._metrics_serving = MetricsServing(model=self.model, generator=self.generator, is_visual_gen=self._is_visual_gen, metrics_collector=self.metrics_collector, iteration_stats_wakeup_event=self._iteration_stats_wakeup_event, iteration_stats_poll_interval=self._iteration_stats_poll_interval)
    • setattr(app.state, metrics_router.APP_STATE_KEY, self._metrics_serving)
    • self._worker_serving = None
      if role_group_of(self.server_role) is RoleGroup.LLM:
      self._register_launcher_forwarding_routes()
      from tensorrt_llm.serve.api.openai.chat import api_router as chat_router
      @@ -19,7 +24,6 @@
      from tensorrt_llm.serve.api.openai.mm_encoder import api_router as mm_encoder_router
      from tensorrt_llm.serve.api.openai.mm_encoder.serving import MMEncoderServing
      setattr(app.state, mm_encoder_router.APP_STATE_KEY, MMEncoderServing(render_resources=self._render_resources, multimodal_server_config=self.multimodal_server_config, allow_request_chat_template=self.allow_request_chat_template, **self._generate_serving_kwargs()))
    •    self.register_mm_encoder_routes()
      
      elif self.server_role is ServerRole.EMBEDDING:
      assert getattr(self.generator.args, 'encode_only', False), 'generator must be an encode_only=True LLM for the embedding server'
      from tensorrt_llm.serve.api.openai.embeddings import api_router as embeddings_router
      @@ -34,5 +38,30 @@
      from tensorrt_llm.serve.api.openai.completions import api_router as completions_router
      from tensorrt_llm.serve.api.openai.completions.serving import CompletionServing
      setattr(app.state, completions_router.APP_STATE_KEY, CompletionServing(**self._generate_serving_kwargs()))
    •    from tensorrt_llm.serve.api.ops.worker import api_router as worker_router
      
    •    from tensorrt_llm.serve.api.ops.worker.serving import WorkerServing
      
    •    self._worker_serving = WorkerServing(model=self.model, generator=self.generator, adjusted_steady_clock=self._adjusted_steady_clock, shared_clock_offset=self._shared_clock_offset, chat_serving=self._chat_serving, render_resources=self._render_resources)
      
    •    setattr(app.state, worker_router.APP_STATE_KEY, self._worker_serving)
      
    •    from tensorrt_llm.serve.api.ops.profile import api_router as profile_router
      
    •    from tensorrt_llm.serve.api.ops.profile.serving import ProfileServing
      
    •    setattr(app.state, profile_router.APP_STATE_KEY, ProfileServing(model=self.model, generator=self.generator))
      
    •    resource_governor_queue = self.generator.resource_governor_queue
      
    •    if resource_governor_queue is not None:
      
    •        from .resource_governor import ResourceGovernor
      
    •        self.resource_governor = ResourceGovernor(resource_governor_queue=resource_governor_queue, tokenizer=self.tokenizer, model_config=self.model_config, processor=self.processor, allow_request_chat_template=self.allow_request_chat_template, harmony_adapter_factory=get_harmony_adapter if self.use_harmony else None, chat_template=self.chat_template)
      
    •        from tensorrt_llm.serve.api.ops.control import api_router as control_router
      
    •        setattr(app.state, control_router.APP_STATE_KEY, self.resource_governor)
      
    •    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
      
    •    if self._enable_runtime_control_endpoints:
      
    •        from tensorrt_llm.serve.api.ops.runtime_control import api_router as runtime_control_router
      
    •        from tensorrt_llm.serve.api.ops.runtime_control.serving import RuntimeControlServing
      
    •        setattr(app.state, runtime_control_router.APP_STATE_KEY, RuntimeControlServing(model=self.model, generator=self.generator, runtime_control_api_key=self._runtime_control_api_key, runtime_control_replay_cache=self._runtime_control_replay_cache))
      
    •    if self._enable_rl_control_endpoints:
      
    •        from tensorrt_llm.serve.api.ops.rl import api_router as rl_router
      
    •        from tensorrt_llm.serve.api.ops.rl.serving import RLServing
      
    •        setattr(app.state, rl_router.APP_STATE_KEY, RLServing(model=self.model, generator=self.generator, rl_control_api_key=self._rl_control_api_key))
      
    •    app.state.render_resources = self._render_resources
         self.register_routes()
      
    • attach_routers(app, dataclasses.replace(ServerCapabilities.for_role(self.server_role), harmony_model=not self._is_visual_gen and self.use_harmony))
    • attach_routers(app, self._server_capabilities())
  • OpenAIServer.register_routes: the operational routes, the governor, the render routes and the Prometheus mount come from routers; the Responses routes stay until their router exists
    --- base
    +++ head
    @@ -1,27 +1,4 @@
    def register_routes(self):

    • self.app.add_api_route('/health_generate', self.health_generate, methods=['GET'])
    • self.app.add_api_route('/v1/data_transceiver_state', self.data_transceiver_state, methods=['GET'])
    • self.app.add_api_route('/metrics', self.get_iteration_stats, methods=['GET'])
    • self.app.add_api_route('/energy_metrics', self.get_energy_metrics, methods=['GET'])
    • self.app.add_api_route('/steady_clock_offset', self.get_steady_clock_offset, methods=['GET'])
    • self.app.add_api_route('/steady_clock_offset', self.set_steady_clock_offset, methods=['POST'])
    • self.app.add_api_route('/kv_cache_events', self.get_kv_cache_events, methods=['POST'])
    • resource_governor_queue = self.generator.resource_governor_queue
    • if resource_governor_queue is not None:
    •    from .resource_governor import ResourceGovernor
      
    •    self.resource_governor = ResourceGovernor(resource_governor_queue=resource_governor_queue, tokenizer=self.tokenizer, model_config=self.model_config, processor=self.processor, allow_request_chat_template=self.allow_request_chat_template, harmony_adapter_factory=get_harmony_adapter if self.use_harmony else None, chat_template=self.chat_template)
      
    •    self.resource_governor.register_routes(self.app)
      
    • 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/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'])
    • self.app.add_api_route('/start_profile', self.start_profile, methods=['POST'])
    • self.app.add_api_route('/stop_profile', self.stop_profile, methods=['POST'])
    • self._register_runtime_control_routes()
    • self._register_rl_control_routes()
    • self.app.add_api_route('/server_info', self.get_server_info, methods=['GET'])
    • mount_render_endpoints(self.app, self)
    • if self.generator.args.return_perf_metrics:
    •    self.mount_metrics()
      
  • OpenAIServer.register_visual_gen_routes: its GET /metrics registration is deleted (the metrics router serves /metrics on VISUAL_GEN), and its docstring no longer says /metrics is registered there
    --- base
    +++ head
    @@ -1,10 +1,8 @@
    def register_visual_gen_routes(self):
    """Wire VisualGen serving for the VISUAL_GEN role.

    •    The VisualGen routes come from their RouterSpec; /metrics is
      
    •    registered here, not by a router.
      
    •    The VisualGen routes come from their RouterSpec.
         """
      
      self._visual_gen_api = importlib.import_module('tensorrt_llm.serve.api.visual_gen.api_router')
      self.media_storage_path = self._visual_gen_api.init_serving_state(self.app.state, generator=self.generator, model=self.model, adjusted_clock=self._adjusted_steady_clock)
      logger.info(f'VisualGen media storage path: {self.media_storage_path}')
    • self.app.add_api_route('/metrics', self.get_iteration_stats, methods=['GET'])
  • OpenAIServer._register_launcher_forwarding_routes: its docstring names the router table instead of the deleted registration method; the body is unchanged
    --- base
    +++ head
    @@ -1,5 +1,5 @@
    def _register_launcher_forwarding_routes(self) -> None:

    • """Attached frontends: forward launcher-owned routes (before register_routes())."""
    • """Attached frontends: forward launcher-owned routes (before the router table)."""
      if self._launcher_forwarder is None:
      return
      for method, path in FORWARDED_ROUTES:
  • mount_render_endpoints: takes the RenderResources the composition root built instead of the server
    --- base
    +++ head
    @@ -1,4 +1,4 @@
    -def mount_render_endpoints(app: FastAPI, server: Any, *, enabled: Optional[bool]=None) -> bool:
    +def mount_render_endpoints(app: FastAPI, resources: RenderResources, *, enabled: Optional[bool]=None) -> bool:
    """Mount the render routes on a serving worker's app.

     They are a separate exposed surface, so a worker mounts them only when
    

    @@ -9,5 +9,5 @@
    """
    if not (render_endpoints_enabled() if enabled is None else enabled):
    return False

    • attach_render_routes(app, ServingRender(lambda: RenderResources.from_server(server)))
    • attach_render_routes(app, ServingRender(lambda: resources))
      return True
  • ServerCapabilities: five new fields: governor_queue, rl_control, runtime_control, perf_metrics, render_endpoints
    --- base
    +++ head
    @@ -4,3 +4,8 @@
    role: Optional[ServerRole]
    role_group: RoleGroup
    harmony_model: bool = False

    • governor_queue: bool = False
    • rl_control: bool = False
    • runtime_control: bool = False
    • perf_metrics: bool = False
    • render_endpoints: bool = False
  • ROUTERS: the RouterSpecs of the routers this PR adds
    --- 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.openai.chat.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.anthropic.api_router', roles=frozenset({RoleGroup.LLM}), when=_anthropic_routes_enabled), RouterSpec('tensorrt_llm.serve.api.visual_gen.api_router', roles=frozenset({RoleGroup.VISUAL_GEN})))
    +ROUTERS: tuple[RouterSpec, ...] = (RouterSpec('tensorrt_llm.serve.api.ops.system.api_router', roles=ALL_ROLE_GROUPS), RouterSpec('tensorrt_llm.serve.api.ops.worker.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.ops.metrics.api_router', roles=ALL_ROLE_GROUPS - {RoleGroup.EMBEDDING}), RouterSpec('tensorrt_llm.serve.api.ops.control.api_router', roles=frozenset({RoleGroup.LLM}), when=_when_governor_queue), 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), RouterSpec('tensorrt_llm.serve.api.ops.profile.api_router', roles=frozenset({RoleGroup.LLM})), RouterSpec('tensorrt_llm.serve.api.ops.runtime_control.api_router', roles=frozenset({RoleGroup.LLM}), when=_when_runtime_control), RouterSpec('tensorrt_llm.serve.api.ops.rl.api_router', roles=frozenset({RoleGroup.LLM}), when=_when_rl_control), RouterSpec('tensorrt_llm.serve.api.visual_gen.api_router', roles=frozenset({RoleGroup.VISUAL_GEN})), RouterSpec('tensorrt_llm.serve.render.api_router', roles=frozenset({RoleGroup.LLM}), when=_when_render_endpoints), RouterSpec('tensorrt_llm.serve.api.ops.metrics.prometheus', roles=frozenset({RoleGroup.LLM}), when=_when_perf_metrics))

24 definitions moved verbatim; 0 differences; 1 rewritten (declared); 3 replaced; 26 added; 8 modified (declared); shim exports 0 names


</details>

Kept shims: none; the moved definitions were methods, never module attributes, so no old import path changes and the release-notes list is empty. History: `git blame -C -C tensorrt_llm/serve/api/ops/<group>/serving.py` attributes the moved lines to their original commits. One declared rewrite: `get_server_info` computes the rendering fingerprint from the render resources the composition root built once (`self._render_resources.fingerprint()`) instead of rebuilding them from the server per call; they are the same resources, because every attribute `RenderResources.from_server` read is fixed after `_init_llm` and the root's `harmony_adapter` stays `None`. One method that merged after this change was planned joined the move: `_record_iteration_stat` (#19394), which only the collector loop calls.

### Removed from OpenAIServer

- `WorkerServing` (`tensorrt_llm/serve/api/ops/worker/serving.py`): `data_transceiver_state`, `health_generate`, `get_energy_metrics`, `set_steady_clock_offset`, `get_steady_clock_offset`, `get_kv_cache_events`, `_live_tokens_per_block`, `get_server_info`.
- `MetricsServing` (`tensorrt_llm/serve/api/ops/metrics/serving.py`): `_iteration_stats_buffer_maxlen`, `get_iteration_stats`, `_iteration_stats_collector_loop`, `_record_iteration_stat`.
- `mount_metrics(app)` in `tensorrt_llm/serve/api/ops/metrics/prometheus.py`: `mount_metrics`, with `self.app` written `app`.
- `ProfileServing` (`tensorrt_llm/serve/api/ops/profile/serving.py`): `start_profile`, `stop_profile`.
- `RuntimeControlServing` (`tensorrt_llm/serve/api/ops/runtime_control/serving.py`): `_require_runtime_control_auth`, `_runtime_control_error_response`, `_call_runtime_memory_method`, `runtime_release_memory`, `runtime_resume_memory`, `runtime_memory_status`.
- `RLServing` (`tensorrt_llm/serve/api/ops/rl/serving.py`): `_require_rl_control_auth`, `release_memory`, `resume_memory`, `update_weights`.
- Deleted registration methods, their routes coming from the routers: `register_mm_encoder_routes`, `_register_runtime_control_routes`, `_register_rl_control_routes`. `register_routes` keeps only the three Responses routes until the Responses serving class exists.

RL frameworks that construct `OpenAIServer` directly: the constructor is unchanged (`enable_rl_control_endpoints`, `rl_control_api_key`, `enable_runtime_control_endpoints`, `runtime_control_api_key` keep their names and meaning); the instance methods listed above are no longer on the class.

### Composition-root edits (`tensorrt_llm/serve/openai_server.py`)

- `__init__`: the metrics state (`_metrics_stats_executor` with its comment, `_metrics_stats_read`, `_metrics_stats_read_waiting`, `_metrics_stats_read_consumed`, `_iteration_stats_buffer` with its comment) moves into `MetricsServing.__init__`; in the lifespan, the energy monitor is handed to the worker serving object right after it is created, and the stats machinery runs on the metrics serving object (`_iteration_stats_buffer_maxlen`, the buffer, the collector loop, the read-thread shutdown). The collector task handle stays on the root: the generate serving classes read it through the getter the completions PR introduced, and the Responses handlers still on the root read it through their borrowed `_extract_metrics`.
- `_attach_role_routes`: builds `MetricsServing` for every role (its collector loop ran for every role, EMBEDDING included, which gets no `/metrics` route) and sets `_worker_serving = None`; in the LLM branch builds `WorkerServing`, `ProfileServing`, the resource governor (moved out of `register_routes` with its warning, now put on `app.state` for the control router, which calls its `register_routes(app)`), `RuntimeControlServing` and `RLServing` under their switches, and puts the render resources on `app.state`; the `register_mm_encoder_routes()` call is gone; `attach_routers(app, self._server_capabilities())`.
- `register_routes`: only the Responses routes remain.
- `register_visual_gen_routes`: its `GET /metrics` registration is deleted (the metrics router serves `/metrics` on VISUAL_GEN); the docstring no longer claims it.
- New `_server_capabilities()`: `harmony_model` as before, plus the five fields for the LLM role group: `governor_queue` (the governor was built), `rl_control` and `runtime_control` (the constructor switches), `perf_metrics` (`generator.args.return_perf_metrics` after the normalization for `perf_metrics_output_dir`), `render_endpoints` (`TRTLLM_ENABLE_RENDER_ENDPOINTS=1`, read once).
- The launcher-forwarding routes of an attached multi-frontend process (#19394) stay on the root, registered before the routers as before. Their one table spans the metrics, worker, profile and Anthropic packages and must register ahead of every local handler; splitting it per package would touch another package and break the table that `tests/unittest/llmapi/test_serve_multi_frontend_route_gate.py` classifies routes against. The composition-root PR can turn it into one forwarding router-table entry, first in the table, under an attached-frontend capability.

### Render API change (for the author of #19964)

`mount_render_endpoints(app, resources, *, enabled=None)` takes the `RenderResources` and builds `ServingRender(lambda: resources)`; `tensorrt_llm/serve/render/api_router.py` gains `attach_router(app)`, the router-table entry point, which reads `app.state.render_resources` and forces the decision (the table already checked the switch). `RenderResources.from_server` stays until the composition root builds the resources field by field.

### OpenAPI document

Not a contract: the order of `paths` follows the new registration order, and the descriptions that the docstrings of `health_generate`, `data_transceiver_state`, `start_profile` and `stop_profile` gave their routes are gone (the route functions carry none); request and response schemas are unchanged (`set_steady_clock_offset` keeps the embedded `offset` body, `start_profile` the optional `StartProfileRequest`).

### CI bookkeeping

- Multi-GPU table: none (`tensorrt_llm/serve/api/ops/` and `tensorrt_llm/serve/openai_server.py` are listed already).
- CODEOWNERS: none (`/tensorrt_llm/serve` covers the new directories).
- legacy-files.txt: `tensorrt_llm/serve/api/ops/{worker,metrics,profile,runtime_control,rl}/serving.py` and `tensorrt_llm/serve/api/ops/metrics/prometheus.py` (Group B: code moved out of a Group B file); derived configs regenerated with `scripts/legacy_utils.py gen-configs`; `ruff-legacy-baseline.json`: no change (the moved methods carry no recorded violation; never a full `lint-update-violations` in a move PR).
- mypy `files`: the six new `api_router.py` modules.
- Test lists: no change. The new files under `tests/unittest/serve/api/ops/` are collected by the `unittest/serve` entry of `l0_cpu.yml` (each carries `pytest.mark.cpu_only`); `tests/unittest/disaggregated/test_openai_server_info.py` gains the marker, so the `unittest/disaggregated` entry of `l0_cpu.yml` now collects it too (its `l0_b200.yml` entry stays).
- The ratchet of `object.__new__` test sites loses the rows of the eight rewritten files and the clock-offset case of the multi-frontend helpers.
- `docs/source/features/sleep-wakeup.md` names `tensorrt_llm/serve/api/ops/runtime_control/`.
- Docstrings that named `OpenAIServer.start_profile` / `stop_profile` (`tensorrt_llm/_torch/pyexecutor/profiling.py`, `tests/integration/defs/test_e2e.py`, `tests/unittest/_torch/pyexecutor/test_profile_endpoints.py`) now name `ProfileServing` in `tensorrt_llm/serve/api/ops/profile/serving.py`; text only.
## Test Coverage

Host: a development container without a GPU; every CPU check below ran there against this branch's code (`tensorrt_llm.__file__` = the worktree), with the compiled artifacts of a source build linked in. GPU-only checks are left to CI (listed last).

New tests (collected by the `unittest/serve` entry of `l0_cpu.yml`; each file carries `pytest.mark.cpu_only`):
- `tests/unittest/serve/api/ops/test_worker.py`: constructor contract; `attach_router` refuses an app without the serving object; the root's clock, shared clock offset, chat serving object and render resources are the worker serving object's (identity); the six worker paths come from the worker router on the LLM roles (default, CONTEXT, GENERATION) and 404 on MM_ENCODER and EMBEDDING; `POST /steady_clock_offset` moves the root's clock; `GET /steady_clock_offset` samples the global clock where the handler now lives; `/server_info` reports the fingerprint of the injected resources; `/v1/data_transceiver_state` 404 without state; `/energy_metrics` 503 without a monitor and 200 once the lifespan handed the monitor over.
- `tests/unittest/serve/api/ops/test_metrics.py`: constructor contract; the root's wake-up event and metrics collector are the metrics serving object's; `/metrics` comes from the metrics router exactly once on the LLM roles and MM_ENCODER and is absent on EMBEDDING (whose collector machinery still exists); with perf metrics and `ENABLE_METRICS` on, `/metrics` stays the JSON route and `/prometheus/metrics` answers; the lifespan's collector loop feeds the metrics serving object's buffer, which `/metrics` serves and clears. A perf-metrics configuration of MM_ENCODER (with `ENABLE_METRICS`) or EMBEDDING gets no Prometheus mount today: no `/prometheus/metrics`, the `/metrics` count as today (1 / 0), no instrumentator middleware and `perf_metrics` False in the capabilities; the mount's router-table entry is LLM-only and last. Both gates are pinned, so widening either one fails a test (planting either mutation alone or both together turns the tests red).
- `tests/unittest/serve/api/ops/test_control_endpoints.py`: constructor contracts of the profile, runtime-control and RL serving classes; `attach_router` of the control, profile, runtime-control and RL routers refuses an app without the serving object; the control routes mount only under their switches, the replay cache is the root's, unsigned requests are 401, both switches together fail at construction; a runtime-control signature over another route's path is rejected (the dependency reads the declared path of the matched route); a signed RL request reaches `collective_rpc`; the governor routes exist only with a queue, with the governor on `app.state`; the profile routes drive the generator with the request's values and with the defaults for an empty body.

Rewritten tests (the handler moved; each builds the serving object or attaches its router instead of `object.__new__(OpenAIServer)`): `tests/unittest/disaggregated/test_openai_server_info.py` (now `cpu_only`), `tests/unittest/disaggregated/test_disagg_openai_client.py` (clock patches retargeted to `tensorrt_llm.serve.api.ops.worker.serving`), `tests/unittest/llmapi/apps/_test_trtllm_serve_profile.py`, `tests/unittest/llmapi/apps/test_openai_server_iteration_stats.py`, `tests/unittest/llmapi/apps/test_render_callers.py`, `tests/unittest/llmapi/apps/test_render_http.py`, `tests/unittest/llmapi/test_llm.py::test_startup_metrics_by_server_info`, `tests/unittest/llmapi/test_rl_control_auth.py`, `tests/unittest/llmapi/test_runtime_control.py`, `tests/unittest/llmapi/test_serve_sleep_mode.py`, `tests/unittest/llmapi/test_serve_multi_frontend_helpers.py`, `tests/unittest/llmapi/test_serve_multi_frontend_route_gate.py`, `tests/unittest/serve/test_openai_server_chat_core.py`, `tests/unittest/serve/test_serve_runtime_api_calls.py`, `tests/unittest/grpc/openengine/test_metrics.py`.

Commands and results:

- Pure-move check (local tool, output above): `24 definitions moved verbatim; 0 differences; 1 rewritten (declared); 3 replaced; 26 added; 8 modified (declared); shim exports 0 names` (the eighth declared modification is the docstring of `_register_launcher_forwarding_routes`, which now names the router table; its body is unchanged); an independent AST comparison of the 25 moved definitions against the base (the declared rewrite and `self.app` → `app` applied to the base text) agrees: 25 equal, nothing left on `OpenAIServer`.
- `PYTHONPATH=tests python3 -m test_common.serve.routes` → exit 0: the route snapshot (`tests/unittest/api_stability/references/trtllm_serve_routes.yaml`) is unchanged for every configuration, VISUAL_GEN and the attached multi-frontend process included.
- `pytest tests/unittest/serve/test_middleware_order.py` → 23 passed (the Prometheus instrumentation still runs before the built-in middleware is registered).
- `pytest -m cpu_only tests/unittest/serve tests/unittest/llmapi tests/unittest/disaggregated tests/unittest/api_stability tests/unittest/_torch/visual_gen/test_trtllm_serve_endpoints.py -q -p no:randomly`, by test id against the integration base `463f37af4a` (same command, the VisualGen endpoints file in scope on both sides): this branch 126 failed / 7816 passed / 3 skipped; the integration base 127 failed / 7774 passed / 3 skipped; 0 failing ids new on this branch, 1 gone (`tests/unittest/disaggregated/test_remoteDictionary.py::TestEtcdDictionary::test_put_and_get`, a flaky etcd-fixture test that passes in isolation on the base and in every other run); the remaining 126 failing ids are identical on both sides, all pre-existing on the stack and environmental on a host without GPUs or models (62 `test_config_database.py`, 26 `test_context_first_profile.py`, 16 `test_lifecycle_activation.py`, 2 `test_peer.py`, 3 `test_mpi_session.py`, 1 `test_llm_utils.py`, 1 `test_executor.py`; 7 `test_serve_routes.py` disaggregated configurations failing with "Duplicated timeseries in CollectorRegistry" when several disaggregated servers are built in one pytest process — the snapshot module alone exits 0; 6 `test_serve_health.py` on `OpenAIServer.health`; the 2 private-access lint tests). The extra passes are the new ops tests and `test_trtllm_serve_endpoints.py` (0 failures).
- `PYTHONPATH=tests python3 -m test_common.serve.cpu_tests` (every serve CPU test, one process per test-db entry): 80 entries; every entry passes except the known pre-existing failures also present on the integration base (6 `tests/unittest/serve/test_serve_health.py` cases on `OpenAIServer.health`; the 2 private-access lint tests with the base's messages) and `unittest/grpc/openengine/test_frontend_lifecycle.py::test_frontend_group_cleanup` (5 cases, "frontend lifecycle operation timed out" while other suites loaded the host; re-run in isolation on this branch and on the integration base: 14 passed on this branch and 14 passed on the integration base, in isolation (the five cases had timed out under host load inside the seam run)). No test skipped as "not importable" and no "trtllm-render is not installed" skip. The route snapshot run by the same queue: exit 0.
- `pytest tests/unittest/api_stability -q` (the serve API, CLI and route gates, in their own process): 108 passed — the route snapshot test passes for every configuration, the disaggregated ones included.
- `pytest -rs tests/unittest/llmapi/apps/test_harmony_parsing.py tests/unittest/llmapi/apps/test_harmony_channel_validation.py`: 79 passed, nothing skipped (no "not importable" line).
- `pytest -m cpu_only tests/unittest/_torch/visual_gen/test_trtllm_serve_endpoints.py -q`: 173 passed.
- `pytest tests/unittest/llmapi/test_llm.py::test_startup_metrics_by_server_info tests/unittest/llmapi/apps/_test_trtllm_serve_profile.py -q` (GPU-stage tests this PR rewrites, run locally): 18 passed.
- Both FastAPI versions of the pinned range (0.136.3 and 0.139.2, each as a `pip --target` tree first on `PYTHONPATH`): the route snapshot test, the ops tests, the VisualGen route test, the routing and middleware-order tests, `test_runtime_control.py` (the signature covers `scope["route"].path`), `test_rl_control_auth.py`, the multi-frontend tests and `test_render_http.py`: 211 passed on each.
- `tests/unittest/serve/test_private_access_lint.py` + its VisualGen variant: the same 2 failed / 11 passed as the integration base, with identical failure messages (the base's pre-existing stale `_postprocess_result` rows and `_PreprocessingWork._release`); the two same-package private calls the new routers make on their own serving objects are not reported.
- `pre-commit run --files <all 47 changed files>` twice: both runs pass with no file modified; `python scripts/legacy_utils.py check-configs` → up to date; ruff 0.9.4 on the six new Group B files and `openai_server.py` through `scripts/legacy_utils.py lint-precommit` → no violation outside the baseline; `bash scripts/run_mypy.sh` → no issues in 39 files (the six new router modules included).
- Name-level import check of every `from tensorrt_llm.serve... import <name>` under `tensorrt_llm/`, `tests/`, `examples/`, `scripts/` at the branch head: 1,590 imported names checked, all resolved (the amended integration base: 1,521, all resolved).
- `git grep -n "from_server" -- tensorrt_llm/serve/api/` → only the comment in the chat serving module; `git grep -n "def health_generate\|def get_server_info\|def get_iteration_stats\|def mount_metrics\|def start_profile\|def update_weights\|def runtime_memory_status" -- tensorrt_llm/serve/openai_server.py` → nothing.

Left to CI (GPU): `test_e2e.py::test_openai_misc_example[pytorch]` (`/health_generate`), `::test_openai_prometheus`, `::test_openai_perf_metrics`, `::test_trtllm_serve_profile_example`, the single-GPU disaggregated block of `l0_a10.yml` (`/steady_clock_offset`, `/server_info`, `/health` through msgpack) and the multi-GPU disaggregated stages.

## 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>
…lope

Pin the exact 422 body at the route-level entry point (FastAPI
RequestValidationError on /v1/images/generations, which declares its
request model) and at the handler-level entry points (pydantic
ValidationError raised while building the request model on
/v1/videos/sync, /v1/videos and /v1/images/edits, JSON and multipart).
All entry points must render byte-identical bodies; these tests pin the
full body so a later refactor of the rendering code has exact coverage.

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

OpenAIServer._create_visual_gen_validation_error_response (for FastAPI's
RequestValidationError) and _VideoRoutesMixin._render_pydantic_validation_error
(for pydantic's ValidationError) were statement-for-statement identical.
Replace both with one module-level function,
render_visual_gen_validation_error in tensorrt_llm/serve/_visual_gen_errors.py,
that accepts either exception and the server's error-response factory, and
point all six call sites at it. The factory argument keeps the error envelope
and the context-length code rule defined in one place and avoids an import
cycle with openai_server.

This also removes the only call OpenAIServer makes to a method defined by the
video-routes mixin (openai_image_edit called the mixin's renderer).

No behavior change: both entry points return the same status and bytes as
before; the characterization tests added in the previous commit pass unchanged
on both sides of this refactor. The new module joins the mypy file list.

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>
… and disaggregated servers

Create tensorrt_llm/serve/middleware/ and move ServerArrivalTimeMiddleware
(from serve/responses_utils.py), PerfMetricsMiddleware (from
serve/perf_metrics.py) and _apply_fastapi_middlewares (from
commands/serve.py) into it, verbatim. Both servers now install their
built-in middleware through serve/middleware/register.py::register_middlewares,
which reproduces each app's stack exactly: the aggregated server passes
adjusted_clock to both middlewares, the disaggregated server to neither.
User --middleware entries are still applied after construction and stay
outermost.

The old import paths are removed with no compatibility shim; no code
outside the repository imports them. commands/serve.py keeps an import of
_apply_fastapi_middlewares for its own two launcher calls.

CI bookkeeping in the same change: the multi-GPU trigger table gains
tensorrt_llm/serve/middleware/; legacy-files.txt registers
middleware/arrival_time.py and middleware/user_middleware.py with the
configs gen-configs regenerates; ruff-legacy-baseline.json carries the
moved docstring's one D212; mypy checks the new register.py.

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

Replace the assignment scope["state"] = {} with scope.setdefault("state", {})
in ServerArrivalTimeMiddleware.__call__. State already present when a request
reaches this middleware - written by a user --middleware entry, or copied in by
the ASGI server from a lifespan that yields a dict - is now kept, and
server_arrival_time is added to it. Before, that state was replaced by an empty
dict. Neither trtllm-serve app yields lifespan state today and the built-in
middleware order is unchanged, so no built-in path changes behavior.

Two tests pin the new behavior (state from an outer middleware and from a
lifespan survive); both fail on the previous commit.

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>
…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>
…yix-feat-serve-ops-routers (integration base, not part of the reviewable change)

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

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ster them with the legacy formatter

The worker, metrics, profile, runtime-control and RL control serving classes
with their constructors only; the moved handlers follow. The new Group B
files, the Prometheus module among them, join legacy-files.txt before any
formatter sees them, and the derived configs are regenerated.

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

Generated move of the worker, metrics, profiling, runtime-control and RL
control handlers from OpenAIServer into WorkerServing, MetricsServing,
ProfileServing, RuntimeControlServing and RLServing, and of mount_metrics
into serve/api/ops/metrics/prometheus.py as a function of the app. The
three registration methods the routers replace are deleted; the server's
own route table and wiring follow in the next commit. One declared edit:
get_server_info reads the render resources the composition root built
instead of rebuilding them per call.

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

The worker, metrics, control, profile, runtime-control and RL control
routers join the router table, each created with make_router() and reading
its serving object from app.state; the render routes and the Prometheus
mount attach through the same table under five new ServerCapabilities
fields (governor_queue, rl_control, runtime_control, perf_metrics,
render_endpoints) that the composition root computes where it decided the
same registrations before. The Prometheus mount stays last, so the JSON
/metrics keeps winning and the instrumentator's middleware stays innermost.

In OpenAIServer: the metrics state moves into MetricsServing (built for
every role, as its collector loop ran for every role); the LLM branch of
the role switch builds the worker, profile, runtime-control and RL serving
objects, the resource governor (moved out of register_routes with its
warning) and puts the render resources on app.state; the lifespan hands the
energy monitor to the worker serving object and drives the stats machinery
on the metrics one; register_routes keeps only the Responses routes;
register_visual_gen_routes no longer registers /metrics, which the metrics
router serves on VISUAL_GEN. mount_render_endpoints takes the
RenderResources the composition root built once instead of the server.
The six new router modules join the type-checked file set.

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

New tests under tests/unittest/serve/api/ops/ pin the constructor contract
of the five serving classes, the objects they share with the composition
root (the adjusted clock, the chat serving object, the render resources
built once, the replay cache, the wake-up event, the metrics collector),
the energy-monitor hand-over in the lifespan, the collector loop feeding
the metrics serving object's buffer, the JSON /metrics winning over the
Prometheus one, the control routers attaching only under their conditions,
the runtime-control signature covering the declared route path, and which
roles serve which routes.

The tests that built OpenAIServer with object.__new__ to call a moved
handler now build the serving object or attach its router: server info,
profiling, runtime control, RL control, iteration stats, the clock
calibration, the fingerprint cache and the startup metrics. The
multi-frontend re-enactments gain the new serving objects; the
clock-offset case moves onto WorkerServing. The sleep-mode test observes
attach_routers instead of the deleted registration method, the governor
tests build the server through the factory, and the render tests pass the
RenderResources. The ratchet baseline loses the rewritten sites; the
factory shuts down the metrics serving object's thread and its fake LLM
answers what /server_info reads. The sleep/wakeup guide names the
runtime-control package.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…nd refresh the moved handlers' cross-references

A perf-metrics configuration of the multimodal-encoder or embedding role
gets no Prometheus mount today: no /prometheus/metrics, no second GET
/metrics, no instrumentator middleware. Two tests pin both gates of that
contract, the router-table entry (LLM only, last) and the perf_metrics
capability the composition root computes, so widening either is a
deliberate change. Docstrings and comments that named
OpenAIServer.start_profile and stop_profile point at ProfileServing, the
launcher-forwarding docstring names the router table, and the new ops
test files say why the thread-leak check is off.

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