Skip to content

[TRTLLM-17264][refactor] Move the VisualGen handlers into serve/api/visual_gen - #21552

Draft
JunyiXu-nv wants to merge 24 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-visualgen-handler-move
Draft

JunyiXu-nv wants to merge 24 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-visualgen-handler-move

Conversation

@JunyiXu-nv

@JunyiXu-nv JunyiXu-nv commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #21509 (Move the VisualGen protocol models into serve/api/visual_gen) and #21408 (Merge the two VisualGen validation-error renderers); the first commits below belong to those PRs and disappear from this diff when they merge; this PR's own change is the last 5 commits.

@coderabbitai summary

Description

Moves every VisualGen handler and helper out of OpenAIServer and the video-routes mixin into tensorrt_llm/serve/api/visual_gen/: ImageServing and VideoServing on a shared VisualGenBaseServing with explicit constructors, one router created with make_router(), and the VisualGen utils, metrics and validation-error renderer modules moved whole into the package. OpenAIServer stops inheriting the mixin and loads the package only for the VISUAL_GEN role, so no other role imports VisualGen serving code. The shared error-response factory becomes BaseServing.create_error_response in tensorrt_llm/serve/_base/serving.py; OpenAIServer.create_error_response stays as the same function. The three image/video generation points submit through one _generate() helper. The four old modules are deleted without a compatibility shim: no code outside the repository imports them (GitHub code search on 2026-11-03 finds only forks of this repository).

Part of the trtllm-serve refactor: VisualGen isolation (epic TRTLLM-17056, task TRTLLM-17264). Builds on #21448 (Add the trtllm-serve router skeleton and move /health, /version and /v1/models into ops.system), #21509 and #21408. The PR that lets the CI test selector claim tensorrt_llm/serve/api/visual_gen/ follows immediately; until it merges, VisualGen changes still run the full pre-merge suite.

Behavior

No behavior change. Image edits still generate on the serving event loop (VisualGen.generate()), because #21403 (Generate image edits off the serving event loop) has not merged; the image edit therefore keeps its synchronous call and does not go through _generate(). The three other generation points (image generation, synchronous and asynchronous video) submit through _generate(), which does what they did before: asyncio.to_thread(generate_async, ...).

Differences in /openapi.json, which is not a contract: the VISUAL_GEN listing has /metrics before /health, /version and /v1/models (routers are now attached after the role switch), and the ten VisualGen operations no longer carry the descriptions their method docstrings gave them. Path set, methods, route classes and the order of routes that can match the same request are unchanged (route snapshot unchanged).

Moves

Pure-move check
## Move spec

```yaml
moves:
- from: tensorrt_llm/serve/openai_video_routes.py
  to: tensorrt_llm/serve/api/visual_gen/serving_videos.py
  group: A
  symbols:
  - _video_content_type
  - _KNOWN_VIDEO_OUTPUT_SUFFIXES
  - _REFERENCE_FIELDS
  - _resolve_tensor_only_format
  - _preflight_encoder_format
  - _DEFAULT_TENSOR_FORMAT
  - _path_json_video_response
  methods:
    from_class: _VideoRoutesMixin
    to_class: VideoServing
    names:
    - openai_video_generation_sync
    - _parse_video_generation_request
    - openai_video_generation_async
    - _generate_video_background
    - list_videos
    - get_video_metadata
    - get_video_content
    - delete_video
    - _on_video_task_done
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/visual_gen/serving_images.py
  group: B
  symbols:
  - _normalize_image_output
  - _image_output_size
  methods:
    from_class: OpenAIServer
    to_class: ImageServing
    names:
    - _supports_image_edit
    - _create_not_supported_error
    - openai_image_generation
    - get_image_content
    - _build_image_content_url
    - _image_object
    - _parse_image_edit_request
    - openai_image_edit
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/visual_gen/serving_base.py
  group: B
  methods:
    from_class: OpenAIServer
    to_class: VisualGenBaseServing
    names:
    - _reject_disabled_path
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/visual_gen/media.py
  group: B
  symbols:
  - _new_media_dir
  - _resolve_media_dir
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/_base/serving.py
  group: B
  methods:
    from_class: OpenAIServer
    to_class: BaseServing
    names:
    - create_error_response
file_moves:
- from: tensorrt_llm/serve/visual_gen_utils.py
  to: tensorrt_llm/serve/api/visual_gen/utils.py
  group: A
  shim: false
- from: tensorrt_llm/serve/visual_gen_metrics.py
  to: tensorrt_llm/serve/api/visual_gen/metrics.py
  group: A
  shim: false
- from: tensorrt_llm/serve/_visual_gen_errors.py
  to: tensorrt_llm/serve/api/visual_gen/errors.py
  group: A
  shim: false
deleted_sources:
- tensorrt_llm/serve/openai_video_routes.py
added:
- symbol: tensorrt_llm/serve/_base/serving.py::BaseServing
  reason: serving base class; holds create_error_response
- symbol: tensorrt_llm/serve/api/visual_gen/serving_base.py::VisualGenBaseServing
  reason: base class of the image and video serving classes
- symbol: tensorrt_llm/serve/api/visual_gen/serving_base.py::VisualGenBaseServing.__init__
  reason: explicit dependencies instead of server attributes, so missing wiring fails
    at construction
- symbol: tensorrt_llm/serve/api/visual_gen/serving_base.py::VisualGenBaseServing._generate
  reason: the one generation point of the image and video handlers
- symbol: tensorrt_llm/serve/api/visual_gen/serving_images.py::ImageServing
  reason: image serving class
- symbol: tensorrt_llm/serve/api/visual_gen/serving_videos.py::VideoServing
  reason: replaces _VideoRoutesMixin
- symbol: tensorrt_llm/serve/api/visual_gen/serving_videos.py::VideoServing.__init__
  reason: explicit dependencies instead of server attributes, so missing wiring fails
    at construction
- symbol: tensorrt_llm/serve/api/visual_gen/api_router.py::*
  reason: new router module (every definition in it is new)
changed_in_move:
- symbol: tensorrt_llm/serve/api/visual_gen/serving_images.py::ImageServing.openai_image_generation
  reason: generation call through self._generate
- symbol: tensorrt_llm/serve/api/visual_gen/serving_videos.py::VideoServing.openai_video_generation_sync
  reason: generation call through self._generate
- symbol: tensorrt_llm/serve/api/visual_gen/serving_videos.py::VideoServing.openai_video_generation_async
  reason: generation call through self._generate
- symbol: tensorrt_llm/serve/api/visual_gen/serving_images.py::ImageServing._supports_image_edit
  reason: drop the always-false _is_visual_gen guard
- symbol: tensorrt_llm/serve/api/visual_gen/errors.py::__module__
  reason: 'module docstring split into a summary line and a description, and it no
    longer calls the module private: the file loses the private-module exemption from
    the docstring rules when it leaves its underscore name'
modified:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer
  reason: base class _VideoRoutesMixin removed
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.__init__
  reason: VisualGen branch of the validation handler through self._visual_gen_api;
    attach_routers after the role switch
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._init_visual_gen
  reason: media directory and video tasks move to the VisualGen serving objects
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_visual_gen_routes
  reason: role-gated importlib load and init_serving_state; only /metrics stays
- symbol: tensorrt_llm/serve/_base/routing.py::ROUTERS
  reason: the VisualGen router joins the routing table for the VISUAL_GEN role group
replaced:
- symbol: tensorrt_llm/serve/openai_video_routes.py::_VideoRoutesMixin
  by: tensorrt_llm/serve/api/visual_gen/serving_videos.py::VideoServing
  reason: the mixin's class statement becomes VideoServing; its methods move under
    moves
borrowed:
- name: create_error_response
  alias: staticmethod(BaseServing.create_error_response)
rewrite_internal_imports: true

Pure-move check: base 51fa901, head ece037b, tools 517fc468330a

Symbol From To Result
ROUTERS tensorrt_llm/serve/_base/routing.py tensorrt_llm/serve/_base/routing.py modified (declared)
<module statements> tensorrt_llm/serve/_visual_gen_errors.py tensorrt_llm/serve/api/visual_gen/errors.py changed in move (declared)
render_visual_gen_validation_error tensorrt_llm/serve/_visual_gen_errors.py tensorrt_llm/serve/api/visual_gen/errors.py verbatim
OpenAIServer.create_error_response tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/serving.py verbatim
_new_media_dir tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/media.py verbatim
_resolve_media_dir tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/media.py verbatim
OpenAIServer._reject_disabled_path tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_base.py verbatim
OpenAIServer._build_image_content_url tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
OpenAIServer._create_not_supported_error tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
OpenAIServer._image_object tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
OpenAIServer._parse_image_edit_request tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
OpenAIServer._supports_image_edit tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py changed in move (declared)
OpenAIServer.get_image_content tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
OpenAIServer.openai_image_edit tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
OpenAIServer.openai_image_generation tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py changed in move (declared)
_image_output_size tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
_normalize_image_output tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/visual_gen/serving_images.py verbatim
OpenAIServer.__init__ tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer._init_visual_gen tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer.create_error_response tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py borrowed
OpenAIServer.register_visual_gen_routes tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
_DEFAULT_TENSOR_FORMAT tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_KNOWN_VIDEO_OUTPUT_SUFFIXES tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_REFERENCE_FIELDS tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin._generate_video_background tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin._on_video_task_done tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin._parse_video_generation_request tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin.delete_video tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin.get_video_content tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin.get_video_metadata tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin.list_videos tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_VideoRoutesMixin.openai_video_generation_async tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py changed in move (declared)
_VideoRoutesMixin.openai_video_generation_sync tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py changed in move (declared)
_VideoRoutesMixin tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py replaced
_path_json_video_response tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_preflight_encoder_format tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_resolve_tensor_only_format tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
_video_content_type tensorrt_llm/serve/openai_video_routes.py tensorrt_llm/serve/api/visual_gen/serving_videos.py verbatim
VISUAL_GEN_DENOISE_TIMING tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
VISUAL_GEN_GENERATION_TIMING tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
VISUAL_GEN_POST_DENOISE_TIMING tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
VISUAL_GEN_PRE_DENOISE_TIMING tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
VISUAL_GEN_TOTAL_TIMING tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
_server_timing_metric tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
build_visual_gen_server_timings tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
build_visual_gen_timing_headers tensorrt_llm/serve/visual_gen_metrics.py tensorrt_llm/serve/api/visual_gen/metrics.py verbatim
AsyncDictStore tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
IMAGE_EDIT_MAX_IMAGES tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
IMAGE_EDIT_MAX_IMAGE_BYTES tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
IMAGE_EDIT_MAX_OUTPUT_IMAGES tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
IMAGE_EDIT_MAX_TOTAL_IMAGE_BYTES tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
VIDEO_STORE tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_IMAGE_EDIT_INPUT_FORMATS tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_INVALID_IMAGE_EDIT_INPUT_MESSAGE tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_NO_SEMANTIC_FIELD_WARNINGS tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_apply_deprecated_input_reference tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_b64 tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_build_image_edit_reference_list tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_build_reference_list tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_decode_base64_media tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_decode_image_edit_string tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_decode_inline_media tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_merge_extra_params tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_read_image_edit_upload tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_reference_transport tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_resolve_image_edit_layer_multiplier tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_validate_image_edit_request_limits tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_validate_png_jpeg_image tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
_warn_if_set_with_no_semantic tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
local_media_path_is_disallowed tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
parse_visual_gen_params tensorrt_llm/serve/visual_gen_utils.py tensorrt_llm/serve/api/visual_gen/utils.py verbatim
BaseServing — tensorrt_llm/serve/_base/serving.py added
tensorrt_llm/serve/api/visual_gen/api_router.py::* — tensorrt_llm/serve/api/visual_gen/api_router.py added
VisualGenBaseServing.__init__ — tensorrt_llm/serve/api/visual_gen/serving_base.py added
VisualGenBaseServing._generate — tensorrt_llm/serve/api/visual_gen/serving_base.py added
VisualGenBaseServing — tensorrt_llm/serve/api/visual_gen/serving_base.py added
ImageServing — tensorrt_llm/serve/api/visual_gen/serving_images.py added
VideoServing.__init__ — tensorrt_llm/serve/api/visual_gen/serving_videos.py added
VideoServing — tensorrt_llm/serve/api/visual_gen/serving_videos.py added

Replaced

  • _VideoRoutesMixin -> tensorrt_llm/serve/api/visual_gen/serving_videos.py::VideoServing: the mixin's class statement becomes VideoServing; its methods move under moves

Added

  • BaseServing: serving base class; holds create_error_response
  • VisualGenBaseServing: base class of the image and video serving classes
  • VisualGenBaseServing.__init__: explicit dependencies instead of server attributes, so missing wiring fails at construction
  • VisualGenBaseServing._generate: the one generation point of the image and video handlers
  • ImageServing: image serving class
  • VideoServing: replaces _VideoRoutesMixin
  • VideoServing.__init__: explicit dependencies instead of server attributes, so missing wiring fails at construction
  • tensorrt_llm/serve/api/visual_gen/api_router.py::*: new router module (every definition in it is new)

Changed in move (declared)

  • tensorrt_llm/serve/api/visual_gen/errors.py::__module__: module docstring split into a summary line and a description, and it no longer calls the module private: the file loses the private-module exemption from the docstring rules when it leaves its underscore name
    --- base
    +++ head
    @@ -1 +1 @@
    -'The VisualGen 422 renderer shared by the route-level (RequestValidationError)\nand handler-level (ValidationError) entry points. Private module.'
    +'The VisualGen 422 renderer.\n\nShared by the route-level (RequestValidationError) and handler-level\n(ValidationError) entry points. Internal to the VisualGen serving package.\n'
  • _VideoRoutesMixin.openai_video_generation_sync: generation call through self._generate
    --- base
    +++ head
    @@ -21,7 +21,7 @@
    resolved_encoder_fmt = _preflight_encoder_format(request_format)
    logger.info(f'Generating video: {video_id} with params: {params} and prompt: {request.prompt}')
    sync_video_start = time.perf_counter()
    •        handle = await asyncio.to_thread(self.generator.generate_async, request.prompt, params)
      
    •        handle = await self._generate(request.prompt, params)
             output = await handle.aresult()
         except ValidationError as exc:
             return render_visual_gen_validation_error(exc, self.create_error_response)
      
  • _VideoRoutesMixin.openai_video_generation_async: generation call through self._generate
    --- base
    +++ head
    @@ -22,7 +22,7 @@
    request_format = _resolve_tensor_only_format(request.format, params.extra_params, self.generator.extra_param_specs)
    _preflight_encoder_format(request_format)
    logger.info(f'Generating video: {video_id} with params: {params} and prompt: {request.prompt}')
    •    handle = await asyncio.to_thread(self.generator.generate_async, request.prompt, params)
      
    •    handle = await self._generate(request.prompt, params)
         video_job = VideoJob(created_at=int(time.time()), id=video_id, model=request.model or self.model, prompt=request.prompt, status='queued', duration=request.seconds, fps=params.frame_rate, size=f'{params.width}x{params.height}', response_format=request.response_format, request_started=request_received)
         await VIDEO_STORE.upsert(video_id, video_job)
         task = asyncio.create_task(self._generate_video_background(video_id=video_id, request=request, params=params, request_format=request_format, handle=handle))
      
  • OpenAIServer._supports_image_edit: drop the always-false _is_visual_gen guard
    --- base
    +++ head
    @@ -1,6 +1,4 @@
    def _supports_image_edit(self) -> bool:
    • if not self._is_visual_gen:
    •    return False
      
      executor = getattr(self.generator, 'executor', None)
      if executor is None:
      return False
  • OpenAIServer.openai_image_generation: generation call through self._generate
    --- base
    +++ head
    @@ -15,7 +15,7 @@
    params = parse_visual_gen_params(request, self.generator)
    logger.info(f'Generating image: {image_id} with params: {params} and prompt: {request.prompt}')
    image_gen_start = time.perf_counter()
    •        handle = await asyncio.to_thread(self.generator.generate_async, request.prompt, params)
      
    •        handle = await self._generate(request.prompt, params)
             output = await handle.aresult()
         except ValueError as exc:
             logger.error(f'Image request error: {exc}')
      

Modified (declared)

  • OpenAIServer.__init__: VisualGen branch of the validation handler through self._visual_gen_api; attach_routers after the role switch
    --- base
    +++ head
    @@ -158,7 +158,7 @@
    self.metrics_collector.log_request_error(http_code=400)
    return anthropic_error_response(str(exc), 'invalid_request_error', 400)
    if self.server_role is ServerRole.VISUAL_GEN:
    •        return render_visual_gen_validation_error(exc, self.create_error_response)
      
    •        return self._visual_gen_api.render_visual_gen_validation_error(exc, self.create_error_response)
         if self.metrics_collector:
             self.metrics_collector.log_request_error(http_code=400)
         detail = str(exc)
      
    @@ -166,7 +166,6 @@
    detail = detail[:4096] + ' ... [truncated %d bytes]' % (len(detail) - 4096)
    return JSONResponse(status_code=400, content={'error': detail})
    self.app.state.ops_serving_system = SystemServing(model=self.model, health_probe=self._health_probe)
    • attach_routers(self.app, ServerCapabilities.for_role(self.server_role))
      if self.server_role is ServerRole.VISUAL_GEN:
      assert self._is_visual_gen, 'generator must be a VisualGen for VISUAL_GEN server'
      self.register_visual_gen_routes()
      @@ -180,6 +179,7 @@
      else:
      self._register_launcher_forwarding_routes()
      self.register_routes()
    • attach_routers(self.app, ServerCapabilities.for_role(self.server_role))
      if self._collect_perf_metrics:
      self.app.add_middleware(PerfMetricsMiddleware, expose_headers=self._expose_perf_metrics, writer=self._perf_metrics_writer, adjusted_clock=self._adjusted_steady_clock)
      self.app.add_middleware(ServerArrivalTimeMiddleware, adjusted_clock=self._adjusted_steady_clock)
  • OpenAIServer._init_visual_gen: media directory and video tasks move to the VisualGen serving objects
    --- base
    +++ head
    @@ -1,6 +1,3 @@
    def _init_visual_gen(self):
    self.processor = None
    self.model_config = None
    • self.media_storage_path = _resolve_media_dir()
    • logger.info(f'VisualGen media storage path: {self.media_storage_path}')
    • self.video_gen_tasks = {}
  • OpenAIServer.register_visual_gen_routes: role-gated importlib load and init_serving_state; only /metrics stays
    --- base
    +++ head
    @@ -1,13 +1,10 @@
    def register_visual_gen_routes(self):
    • """Register routes for diffusion model serving."""
    • """Wire VisualGen serving for the VISUAL_GEN role.
    •    The VisualGen routes come from their RouterSpec; /metrics is
      
    •    registered here, not by a router.
      
    •    """
      
    • 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'])
    • self.app.add_api_route('/v1/images/generations', self.openai_image_generation, methods=['POST'])
    • self.app.add_api_route('/v1/images/edits', self.openai_image_edit, methods=['POST'])
    • self.app.add_api_route('/v1/images/{image_id}/content', self.get_image_content, methods=['GET'])
    • self.app.add_api_route('/v1/videos', self.openai_video_generation_async, methods=['POST'])
    • self.app.add_api_route('/v1/videos/sync', self.openai_video_generation_sync, methods=['POST'])
    • self.app.add_api_route('/v1/videos/generations', self.openai_video_generation_sync, methods=['POST'], deprecated=True)
    • self.app.add_api_route('/v1/videos', self.list_videos, methods=['GET'])
    • self.app.add_api_route('/v1/videos/{video_id}', self.get_video_metadata, methods=['GET'])
    • self.app.add_api_route('/v1/videos/{video_id}/content', self.get_video_content, methods=['GET'])
    • self.app.add_api_route('/v1/videos/{video_id}', self.delete_video, methods=['DELETE'])
  • OpenAIServer: base class _VideoRoutesMixin removed
    --- base
    +++ head
    @@ -1,2 +1,2 @@
    -class OpenAIServer(_VideoRoutesMixin):
    +class OpenAIServer:
    pass
  • ROUTERS: the VisualGen router joins the routing table for the VISUAL_GEN role group
    --- base
    +++ head
    @@ -1 +1 @@
    -ROUTERS: tuple[RouterSpec, ...] = (RouterSpec('tensorrt_llm.serve.api.ops.system.api_router', roles=ALL_ROLE_GROUPS),)
    +ROUTERS: tuple[RouterSpec, ...] = (RouterSpec('tensorrt_llm.serve.api.ops.system.api_router', roles=ALL_ROLE_GROUPS), RouterSpec('tensorrt_llm.serve.api.visual_gen.api_router', roles=frozenset({RoleGroup.VISUAL_GEN})))

60 definitions moved verbatim; 0 differences; 5 changed in move (declared); 3 files moved; 1 borrowed; 1 replaced; 8 added; 5 modified (declared); shim exports 0 names


</details>

Each reported difference maps to one of the changes this PR makes on purpose:

| Reported row | Change |
|---|---|
| `ImageServing.openai_image_generation`, `VideoServing.openai_video_generation_sync`, `VideoServing.openai_video_generation_async` (changed in move) | the generation call goes through `self._generate()` |
| `ImageServing._supports_image_edit` (changed in move) | the `if not self._is_visual_gen: return False` guard is dropped: `ImageServing` exists only for a VisualGen generator, so it was always false |
| `errors.py` module statements (changed in move) | the module docstring gains the blank line between summary and description that the docstring rules require once the module leaves its underscore name (`_visual_gen_errors.py` was exempt as a private module) |
| `OpenAIServer`, `OpenAIServer.__init__`, `OpenAIServer._init_visual_gen`, `OpenAIServer.register_visual_gen_routes` (modified) | composition root: mixin base removed; the validation handler reaches the VisualGen renderer through the loaded router module; `attach_routers` after the role switch; `_init_visual_gen` keeps only `processor` and `model_config`; `register_visual_gen_routes` loads the router module once, builds the serving objects through `init_serving_state` and registers only `/metrics` |
| `ROUTERS` (modified) | one `RouterSpec` for `tensorrt_llm.serve.api.visual_gen.api_router`, VISUAL_GEN role group only |
| `_VideoRoutesMixin` (replaced by `VideoServing`), `create_error_response` (borrowed), the 8 added definitions | new class headers, constructors, `_generate`, and the router module |

No shim is kept and nothing is re-exported, also not from `openai_server.py`. Old path -> new path, for the release notes:

- `tensorrt_llm.serve.openai_video_routes` -> `tensorrt_llm.serve.api.visual_gen.serving_videos` (its only class, `_VideoRoutesMixin`, no longer exists; its methods are on `VideoServing`)
- `tensorrt_llm.serve.visual_gen_utils` -> `tensorrt_llm.serve.api.visual_gen.utils`: `IMAGE_EDIT_MAX_IMAGES`, `IMAGE_EDIT_MAX_IMAGE_BYTES`, `IMAGE_EDIT_MAX_TOTAL_IMAGE_BYTES`, `IMAGE_EDIT_MAX_OUTPUT_IMAGES`, `local_media_path_is_disallowed`, `parse_visual_gen_params`, `AsyncDictStore`, `VIDEO_STORE`
- `tensorrt_llm.serve.visual_gen_metrics` -> `tensorrt_llm.serve.api.visual_gen.metrics`: `VISUAL_GEN_PRE_DENOISE_TIMING`, `VISUAL_GEN_DENOISE_TIMING`, `VISUAL_GEN_POST_DENOISE_TIMING`, `VISUAL_GEN_GENERATION_TIMING`, `VISUAL_GEN_TOTAL_TIMING`, `build_visual_gen_server_timings`, `build_visual_gen_timing_headers` (`SERVER_TIMING_HEADER` is defined in neither module: both import it from `tensorrt_llm.serve._perf_metrics_schema`)
- `tensorrt_llm.serve._visual_gen_errors` (private, never in a release) -> `tensorrt_llm.serve.api.visual_gen.errors`

History: `git log --follow` for `utils.py`, `metrics.py` and `errors.py` (whole-file moves); `git log -C --follow` and `git blame -C -C` for `serving_videos.py` and for the code moved out of `openai_server.py`.

### Removed from OpenAIServer

| Name | Was | Now |
|---|---|---|
| `_supports_image_edit`, `_create_not_supported_error`, `openai_image_generation`, `get_image_content`, `_build_image_content_url`, `_image_object`, `_parse_image_edit_request`, `openai_image_edit` | methods | `ImageServing` (`serve/api/visual_gen/serving_images.py`) |
| `_reject_disabled_path` | method | `VisualGenBaseServing` (`serve/api/visual_gen/serving_base.py`) |
| `openai_video_generation_sync`, `_parse_video_generation_request`, `openai_video_generation_async`, `_generate_video_background`, `list_videos`, `get_video_metadata`, `get_video_content`, `delete_video`, `_on_video_task_done` | inherited from the mixin | `VideoServing` (`serve/api/visual_gen/serving_videos.py`) |
| `video_gen_tasks` | attribute set in `_init_visual_gen` | `VideoServing` instance state |

`create_error_response` stays on `OpenAIServer` as `staticmethod(BaseServing.create_error_response)`: the same function object, so every LLM error path is unchanged. `_VideoRoutesMixin` no longer exists and is not re-exported. The composition root loads `tensorrt_llm.serve.api.visual_gen.api_router` once, in `register_visual_gen_routes`, and reaches `init_serving_state` and the validation renderer through it; the media directory is resolved once there and the same `Path` is injected into both serving objects. Routers are attached after the role switch for every role, because each `attach_router` checks that its serving objects are on `app.state`; `/health`, `/version` and `/v1/models` therefore register after the role-specific routes, which conflicts with no route of any role (the route snapshot compares that order and is unchanged). The per-dialect error renderers are not on main yet, so the VisualGen validation renderer is still wired in the validation handler's VisualGen branch.

### Coordination

- #21515 (Split the chat handler into a typed core and lift the SSE generator into serve/_base) edits `OpenAIServer.create_error_response` in place (its body built by `serve/_base/errors.py`). This PR moves `create_error_response` verbatim to `BaseServing` and adds no errors module; it is based on the branch of #21509, not on #21515's, so nothing is duplicated: the two PRs conflict only textually, in `openai_server.py`, the lint blocks generated from `legacy-files.txt`, `serve/_base/routing.py` and `serve/AGENTS.md`. Whichever lands second follows the other: if this PR lands first, #21515 retargets its edit to `BaseServing.create_error_response` in `serve/_base/serving.py`; if #21515 lands first, the regenerated move here carries its body.
- `/openapi.json`: the ten VisualGen operations lose the descriptions their method docstrings gave them (the router functions carry none); paths, methods, names and operation ids are unchanged. Not a contract.
- Route registration order: routers are attached after each role's own table, so `/health`, `/version` and `/v1/models` now register after it (for VISUAL_GEN the listing reads `/metrics`, then `/health`). All 27 snapshot configurations keep identical route sets; 16 order-only differences remain and no pair of routes that can match the same request is reordered, so the route snapshot check passes as is; the next `PYTHONPATH=tests python3 -m test_common.serve.routes --write` carries the reorder into the reference listing.
- Open PRs on the files this PR moves or deletes (land first, or re-apply to the new files after this PR's regeneration): #21226 (Support combined Omni references in video serving: `openai_video_routes.py`, `visual_gen_utils.py`), draft #17541 (observability: the same two files, `openai_server.py`, `bench/benchmark/visual_gen.py`), #21403 (Generate image edits off the serving event loop: `openai_image_edit`, now `ImageServing.openai_image_edit`), #20865 (readable validation errors: the validation handler and the VisualGen endpoint tests), #12746 (several served model names: `openai_server.py` and the VisualGen tests), #18672 (VisualGen serving benchmark: `serve/scripts/benchmark_visual_gen.py`, whose metrics import is rewritten here).

### CI bookkeeping

- Multi-GPU table: none (`tensorrt_llm/serve/api/visual_gen/` is deliberately not added; `serve/openai_server.py` and `serve/_base/` are already listed)
- CODEOWNERS: none (the VisualGen team already owns `serve/api/visual_gen/` since #21509; the deleted paths keep no shim)
- legacy-files.txt: `tensorrt_llm/serve/_base/serving.py`, `tensorrt_llm/serve/api/visual_gen/media.py`, `tensorrt_llm/serve/api/visual_gen/serving_base.py`, `tensorrt_llm/serve/api/visual_gen/serving_images.py` (yapf-formatted code moved out of `openai_server.py`); `serving_videos.py`, `utils.py`, `metrics.py`, `errors.py` and `api_router.py` are ruff-formatted; derived configs regenerated with `scripts/legacy_utils.py gen-configs`; `ruff-legacy-baseline.json`: none (the moved code has no recorded violation)
- mypy file list (`pyproject.toml`): `tensorrt_llm/serve/api/visual_gen/api_router.py` added; `tensorrt_llm/serve/_visual_gen_errors.py` renamed to `tensorrt_llm/serve/api/visual_gen/errors.py`
- Test lists: none (the new tests go into `tests/unittest/_torch/visual_gen/test_trtllm_serve_endpoints.py` and `tests/unittest/llmapi/apps/test_context_length_error.py`, both registered in `tests/integration/test_lists/test-db/l0_cpu.yml` and marked `cpu_only`)
- `tensorrt_llm/serve/AGENTS.md`: "Where code goes" names the VisualGen serving package

## Test Coverage

Run on CPU from the worktree (no GPU on the host); GPU end-to-end (`unittest/_torch/visual_gen/test_trtllm_serve_e2e.py` on B200) is left to CI.

- `pytest -m cpu_only tests/unittest/_torch/visual_gen/test_trtllm_serve_endpoints.py tests/unittest/_torch/visual_gen/test_visual_gen_utils.py tests/unittest/_torch/visual_gen/test_visual_gen_params.py tests/unittest/llmapi/apps/test_context_length_error.py -q`: 346 passed (338 at the merge base + the 8 new tests), no skips
- New tests (endpoint file): `TestVisualGenMediaDirectory::test_images_and_videos_share_one_media_directory`; `TestVisualGenServingClasses::test_construction_needs_no_server_and_no_filesystem`, `::test_router_serves_requests_without_openai_server`, `::test_attach_router_requires_serving_state`, `::test_openai_server_no_longer_hosts_visual_gen_handlers`; `TestVisualGenGenerateEntryPoint::test_generation_points_go_through_generate`, `::test_generate_submits_off_the_event_loop`; `test_context_length_error.py::test_serving_classes_share_the_server_error_factory`
- Patch sites of moved names: the five retargeted sites pass; as a negative control, pointing the `datetime` patch back at `openai_server` makes its test fail (silent old target) and pointing the `image_to_bytes` patch back raises `AttributeError` (loud)
- `pytest tests/unittest/_torch/visual_gen/test_benchmark_visual_gen.py -q`: 45 passed; `pytest -m cpu_only tests/unittest/llmapi/apps/test_openai_server_iteration_stats.py -q`: 13 passed
- Route, layering and routing: `pytest -m cpu_only tests/unittest/api_stability/test_serve_routes.py tests/unittest/serve/api/visual_gen/test_routes_visual_gen.py tests/unittest/serve/test_layering.py tests/unittest/serve/_base/test_routing.py -q`: 53 passed (route snapshot unchanged; every VisualGen route keeps `_MsgspecRoute`)
- Two FastAPI versions (endpoint tests + route snapshots + routing tests): fastapi 0.136.3: 217 passed; fastapi 0.139.2: 217 passed
- `pytest tests/unittest/api_stability -q`: 107 passed
- Serve-package guards (`test_agents_md.py`, `test_no_object_new.py`, `test_shim_identity.py`, `test_middleware_order.py`, `tests/unittest/serve/api/visual_gen`): 53 passed
- Harmony: `LLM_MODELS_ROOT=... pytest -rs tests/unittest/llmapi/apps/test_harmony_parsing.py tests/unittest/llmapi/apps/test_harmony_channel_validation.py`: 79 passed, no "not importable" skip
- Every serve CPU test entry (`LLM_MODELS_ROOT=... PYTHONPATH=tests python3 -m test_common.serve.cpu_tests`, one pytest process per test-db entry): 67 sessions, 4234 passed, 0 failed
- Isolation preview: importing `tensorrt_llm.serve.openai_server` loads none of `tensorrt_llm.serve.api.visual_gen`, `tensorrt_llm.media.encoding`, `tensorrt_llm.media.tensor_payload`; the only static references to the package outside it are the `RouterSpec` string, the `importlib` string in `register_visual_gen_routes`, the lazy table of `openai_protocol` and `serve/scripts/benchmark_visual_gen.py`
- `git grep` for the four deleted module names over `tensorrt_llm/ tests/ examples/ scripts/ docs/`: nothing
- `bash scripts/run_mypy.sh`: no issues in 21 source files (`api_router.py` and `errors.py` included)
- `python scripts/legacy_utils.py check-configs`: up to date; `pre-commit run --files <changed>` twice: no modification
- Property check of the moved and new logic against the pre-move code (Hypothesis, 200 generated inputs per property): `create_error_response` identical to the pre-move function for generated messages, error types and status codes (body, status, media type; context-length code rule kept); `_supports_image_edit` identical to the pre-move function for every executor/pipeline shape once `_is_visual_gen` is true; `attach_router` raises exactly when a serving object is missing and otherwise attaches the ten routes; `_normalize_image_output`, `_image_output_size` and `_parse_image_edit_request` (JSON and multipart) identical to the pre-move functions. Mutation gate: 8 planted bugs, 8 detected.
- CI test selection dry run (`jenkins/scripts/cbts/cbts/command/dryrun.py` over this PR's own commit range, in a scratch clone): `scope: None`, i.e. the full fallback, as expected while `serve/openai_server.py` and `serve/_base/serving.py` are unclaimed; the VisualGen claim comes with the next PR

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

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

…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>
…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>
…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>
…e VisualGen protocol-package branch (NVIDIA#21509)

Integration base for the VisualGen handler move: both open PRs must be on
main before the handler move can merge, and the move is regenerated on the
latest main at that time.

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

Generated move of the VisualGen serving code out of OpenAIServer and the
video-routes mixin, with the shared error-response factory moved to
serve/_base/serving.py:

- serve/openai_video_routes.py -> serve/api/visual_gen/serving_videos.py
  (the module helpers and the nine mixin methods, now on VideoServing)
- the image handlers and helpers of OpenAIServer
  -> serve/api/visual_gen/serving_images.py (ImageServing)
- _reject_disabled_path -> serve/api/visual_gen/serving_base.py
  (VisualGenBaseServing)
- _new_media_dir and _resolve_media_dir -> serve/api/visual_gen/media.py
- create_error_response -> serve/_base/serving.py (BaseServing);
  OpenAIServer keeps staticmethod(BaseServing.create_error_response)
- serve/visual_gen_utils.py, serve/visual_gen_metrics.py and
  serve/_visual_gen_errors.py move whole to
  serve/api/visual_gen/{utils,metrics,errors}.py

The four old modules are deleted without a compatibility shim, and every
import in the repository points at the new modules. The new files that
carry yapf-formatted code are registered in legacy-files.txt and the lint
configs are regenerated from it; the mypy file list follows the renamed
errors module. The errors module docstring gains the blank line between
summary and description that the docstring rules require once the module
leaves its underscore name. The class headers, constructors, router and
composition-root wiring follow in the next commits.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…r into the VISUAL_GEN role

The generated move left the handlers in bare classes; this commit adds
what a move cannot write:

- BaseServing (serve/_base/serving.py) gets its docstring; it holds the
  shared create_error_response and nothing else.
- VisualGenBaseServing (serve/api/visual_gen/serving_base.py) derives
  from BaseServing and receives generator, model, media_storage_path and
  the server AdjustedSteadyClock as keyword-only constructor arguments,
  so missing wiring fails at construction; its _generate() is the one
  place that submits a generation off the event loop. ImageServing and
  VideoServing derive from it; VideoServing owns video_gen_tasks as
  instance state.
- The image generation, synchronous video and asynchronous video
  handlers submit through self._generate(). The image edit keeps its
  synchronous generate() call: routing it through _generate() would stop
  it from blocking the event loop, a behavior change that belongs to its
  own fix (NVIDIA#21403).
- _supports_image_edit drops its _is_visual_gen guard: ImageServing only
  exists for a VisualGen generator, so the guard was always false.
- serve/api/visual_gen/api_router.py: one router made with make_router()
  for the ten image and video routes, bound late to the serving objects
  on app.state; attach_router refuses an app without them;
  init_serving_state resolves the media directory once and injects it
  into both serving objects. The router joins ROUTERS for the VISUAL_GEN
  role group and the mypy file list.
- OpenAIServer no longer derives from the video-routes mixin. For the
  VISUAL_GEN role it loads the router module once with importlib, calls
  init_serving_state and registers only /metrics itself; the validation
  handler reaches the VisualGen renderer through that module. Routers
  are attached after the role switch, because every attach_router checks
  that its serving objects are on app.state; /health, /version and
  /v1/models therefore register after the role-specific routes, which
  conflicts with no route of any role.
- The whole-file-moved utils module gets the repository SPDX header; the
  bench docstring that named the deleted video-routes module names the
  new modules.

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

- The test sites that patched or imported names through their old modules
  now reach the modules the moved code reads: the media clock
  (api/visual_gen/media.datetime), the image encoder
  (serving_images.image_to_bytes), the image-edit limits and base64 on
  api/visual_gen/utils, and resolve_video_format on serving_videos; the
  module-object imports of the deleted modules follow their new paths.
- New tests: images and videos share the one media directory the
  composition root resolves; the serving classes build without a server
  and without touching the filesystem; the router answers image and video
  requests on a bare app with every route on the msgpack route class;
  attach_router refuses an app whose serving objects are missing and
  names what to call; image generation, synchronous video and
  asynchronous video submit through VisualGenBaseServing._generate while
  the image edit still calls the synchronous generate(); _generate runs
  the submission off the event loop; OpenAIServer hosts none of the
  eighteen moved handlers and no video-task table.
- test_context_length_error.py: OpenAIServer and the serving classes
  render errors through the one BaseServing function.

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

The "Where code goes" list points at tensorrt_llm/serve/api/visual_gen/
(ImageServing and VideoServing on VisualGenBaseServing, the router in
api_router.py, one RouterSpec) and records that only the composition root,
for the VISUAL_GEN role, loads the package.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ord the errors docstring

The generated move dropped the type-only comment inside the video module
import block and doubled the blank line after _KNOWN_VIDEO_OUTPUT_SUFFIXES;
both are restored so the module reads as it did before the move. The
errors module docstring no longer calls the module private: it is a
public-named module internal to the VisualGen serving package.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
JunyiXu-nv added a commit to JunyiXu-nv/TensorRT-LLM that referenced this pull request Nov 12, 2026
Pin main at 1ceb4e6. Keep both source changes and the main-side package license headers. Drop redundant unittest/serve per-file registrations and the duplicate stand-in health tests, already covered by the real-constructor system endpoint tests.

Validation: 122 focused tests passed, including 22 public runtime health and 14 private-access lint tests. The 3 VisualGen setup errors match pinned main exactly (12 shared health tests passed on both). Protected API collection also hits the same missing preallocate_nccl_window_buffer.bytes operator on main. Focused pre-commit passed. Keep the current 9 postprocess accesses and _save_bytes baseline location because NVIDIA#21392 and NVIDIA#21552 have not landed.
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