Skip to content

[TRTLLM-17263][refactor] Converge the three Anthropic orchestrations into one serving class - #21551

Draft
JunyiXu-nv wants to merge 26 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-anthropic-serving-class
Draft

JunyiXu-nv wants to merge 26 commits into
NVIDIA:mainfrom
JunyiXu-nv:dev-junyix-feat-serve-anthropic-serving-class

Conversation

@JunyiXu-nv

Copy link
Copy Markdown
Collaborator

@coderabbitai summary

Description

Stacked on #21518 (Move the Anthropic serving modules into serve/api/anthropic), #21515 (Split the chat handler into a typed core and lift the SSE generator into serve/_base) and #21442 (Split the disaggregated server's entry-point wrapper into prepare, run, finish and serialize steps); the first commits below belong to those PRs, merged into an integration base, and disappear from this diff when they merge; this PR's own change is the last 4 commits.

The Anthropic Messages orchestration existed three times: the aggregated /v1/messages handler, the aggregated Message Batches runner and the disaggregated /v1/messages handler. Each one serialized the chat result to JSON, parsed it back and sniffed the error body for a message, and the batch runner built a fake ASGI request to enter the chat pipeline. This PR replaces the three with one AnthropicMessagesServing over an injected chat backend: the aggregated backend is the typed chat core create_chat_completion, the disaggregated backend is the entry-point wrapper's prepare, run and finish steps without the serialization step. A RequestContext carries what the chat core reads from an HTTP request (headers, state, raw JSON, disconnect polling), so the batch path builds one directly instead of a synthetic request. The eight aggregated Anthropic routes (messages, count_tokens, six Message Batches routes) move from OpenAIServer into a router under tensorrt_llm/serve/api/anthropic/, attached through the router table and gated as before (not on a harmony/gpt-oss model). Each side keeps its observable behavior; what differs between them (the model a stream without chunks announces, how an error message is extracted) comes from its backend.

Part of the trtllm-serve refactor (epic TRTLLM-17056, task TRTLLM-17263: typed return contract and Anthropic convergence). Builds on #21406 (route, middleware-order and shim-identity snapshots and the CPU test seam), #21448 (router skeleton), #21515 (typed chat core), #21518 (Anthropic package) and #21442 (disaggregated entry-point steps).

Behavior

No behavior change. The two servers answered some cases differently before this PR and keep doing so; each row is pinned by tests/unittest/serve/api/anthropic/test_messages_behavior.py, unchanged from #21518:

Aggregated Disaggregated After this PR
Streamed model when no chunk names one the served model name the client's request model kept, per backend (stream_model)
Error message the chat error body's message, else the whole body as JSON HTTPException: its detail; returned envelope: message / detail / error / the whole body kept, per backend (error_message)
Engine failure 500 api_error "Internal server error" after SIGINT the same kept: both backends raise EngineFatalError, rendered once
Anthropic routes on a harmony (gpt-oss) model not registered (404) always registered kept (router gate harmony_model; the disaggregated frontend keeps its own route)
Message Batches six routes none kept
Multi-frontend attached frontend the batch routes forward to the launcher n/a kept: the forwarding routes register before the routers, as they registered before the local table

Two theoretical differences of the removed JSON round trip, both unreachable: a chat response that cannot be encoded as JSON (a non-finite float) failed in the chat route's serialization (aggregated 400, disaggregated 500) and would now reach the Anthropic conversion instead; unreachable because the Anthropic request never asks for logprobs, the one remaining float field is a ratio of token counts, and a worker cannot send a non-finite float. A disaggregated success without a body was a 500 from None.model_dump() and would now read as an engine failure; unreachable because the disaggregated client raises when no body arrives.

Route order on an attached frontend of a multi-frontend server (#19394's forwarding): the launcher-owned routes, the five Message Batches routes among them, are forwarded to the launcher and must match before the local handlers. The refactor plan assumed the Anthropic routes conflict with no other route, which is wrong for that configuration: attaching the Anthropic router at the shared attach_routers call, which ran before _register_launcher_forwarding_routes(), would have let an attached frontend serve batches from its own store. The forwarding routes now register before the routers, inside the LLM-role block (the same condition as the old branch that registered them), so they keep winning; an A/B of every route's resolution against the base shows 0 differences and the route snapshot is unchanged.

RequestContext.from_http(None) raises ValueError (which openai_chat's ValueError branch would answer as a 400). Before this PR openai_chat(request, None) was a supported metrics-skipping call, and the chat core still carries the None guards that served it (None if ctx is None else ctx.headers, the early return of await_disconnected). No caller passes None today, and the wrapper always builds a context, so those guards are dead code now; they go when the chat core moves into its own serving class (follow-up), not here, so that the core's text stays a pure rename for that move.

tensorrt_llm/serve/api/anthropic/protocol.py changes by one comment line: "(see anthropic_batches)" becomes "(see batches)", the stale reference left by #21518, which could not touch it inside its whole-file move; this PR is the first one that edits the file after it. Nothing else in that module changes.

The error contract: both chat backends raise the typed chat core's ServingError (HTTP status apart from the body) or EngineFatalError; a later PR adds named dialect-neutral error types with per-dialect renderers and switches the backends to them.

/openapi.json (not a contract): the eight Anthropic paths now appear where the router attaches them (before the role's own table), and the descriptions of /v1/messages and /v1/messages/count_tokens are the router functions' one-line docstrings instead of the moved methods' docstrings.

Moves

Pure-move check
## Move spec

```yaml
moves:
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/anthropic/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: AnthropicCountTokensServing
    names:
    - anthropic_count_tokens
- from: tensorrt_llm/serve/openai_server.py
  to: tensorrt_llm/serve/api/anthropic/serving.py
  group: B
  append: true
  methods:
    from_class: OpenAIServer
    to_class: AnthropicBatchesServing
    names:
    - _batch_store
    - anthropic_create_batch
    - anthropic_list_batches
    - anthropic_get_batch
    - anthropic_cancel_batch
    - anthropic_delete_batch
    - anthropic_batch_results
    - _no_such_batch
rewrites:
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicCountTokensServing.anthropic_count_tokens
  old: RenderResources.from_server(self)
  new: self._render_resources()
replaced:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.anthropic_messages
  by: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing
  reason: one serving class over a ChatBackend, shared by the aggregated and disaggregated
    servers; route function anthropic_messages in api_router.py
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._run_batched_message
  by: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing
  reason: same orchestration as messages, as run_batched_message; no synthetic request
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer._synthetic_request
  by: tensorrt_llm/serve/_base/request_context.py::RequestContext
  reason: RequestContext.internal replaces the synthetic ASGI scope
added:
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::_error_type
  reason: status to Anthropic error type, today's rule
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing
  reason: new serving class, used by both servers
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing.__init__
  reason: takes the ChatBackend
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing.create_message
  reason: the messages orchestration, once
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing.run_batched_message
  reason: the batch orchestration, once
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicCountTokensServing
  reason: hand-written skeleton class that receives anthropic_count_tokens
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicCountTokensServing.__init__
  reason: explicit dependencies instead of server attributes, so missing wiring fails
    at construction
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicBatchesServing
  reason: hand-written skeleton class that receives the eight batch methods
- symbol: tensorrt_llm/serve/api/anthropic/serving.py::AnthropicBatchesServing.__init__
  reason: explicit dependencies instead of server attributes, so missing wiring fails
    at construction
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend
  reason: ChatBackend over the typed chat core; transitional until the chat handlers
    move into their own serving class
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend.__init__
  reason: same
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend.create_chat_completion
  reason: same
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend.stream_model
  reason: same
- symbol: tensorrt_llm/serve/openai_server.py::_AggregatedChatBackend.error_message
  reason: same
- symbol: tensorrt_llm/serve/openai_disagg_server.py::_envelope_message
  reason: today's message extraction for a returned error envelope
- symbol: tensorrt_llm/serve/openai_disagg_server.py::_disagg_serving_error
  reason: a disaggregated failure as a ServingError
- symbol: tensorrt_llm/serve/openai_disagg_server.py::_DisaggChatBackend
  reason: ChatBackend over prepare, run, finish
- symbol: tensorrt_llm/serve/openai_disagg_server.py::_DisaggChatBackend.__init__
  reason: same
- symbol: tensorrt_llm/serve/openai_disagg_server.py::_DisaggChatBackend.create_chat_completion
  reason: same
- symbol: tensorrt_llm/serve/openai_disagg_server.py::_DisaggChatBackend.stream_model
  reason: same
- symbol: tensorrt_llm/serve/openai_disagg_server.py::_DisaggChatBackend.error_message
  reason: same
- symbol: tensorrt_llm/serve/openai_disagg_server.py::OpenAIDisaggServer._call_entry_point
  reason: prepare, run, finish without serialize, in the disaggregated_request span
- symbol: tensorrt_llm/serve/_base/routing.py::_anthropic_routes_enabled
  reason: 'the gate of the Anthropic router: not on a harmony (gpt-oss) model, as
    the route registration was gated'
modified:
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.__init__
  reason: Anthropic serving objects on app.state; harmony_model capability; the launcher
    forwarding routes register before the routers so that they keep winning over the
    local batch routes
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.register_routes
  reason: the Anthropic routes come from serve/api/anthropic/api_router.py
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.create_chat_completion
  reason: parameter raw_request renamed to ctx (a RequestContext); no other change
- symbol: tensorrt_llm/serve/openai_server.py::OpenAIServer.openai_chat
  reason: passes RequestContext.from_http(raw_request) to the core
- symbol: tensorrt_llm/serve/_base/sse.py::chat_stream_generator
  reason: keyword parameter raw_request renamed to ctx
- symbol: tensorrt_llm/serve/openai_disagg_server.py::OpenAIDisaggServer.__init__
  reason: builds the AnthropicMessagesServing
- symbol: tensorrt_llm/serve/openai_disagg_server.py::OpenAIDisaggServer.anthropic_messages
  reason: delegates to AnthropicMessagesServing
- symbol: tensorrt_llm/serve/_base/routing.py::ServerCapabilities
  reason: harmony_model field, read by the Anthropic router's gate
- symbol: tensorrt_llm/serve/_base/routing.py::ROUTERS
  reason: the Anthropic RouterSpec joins ROUTERS after ops.system
rewrite_internal_imports: false

Pure-move check: base 1c031dc, head 3f0a7e0, tools 2e8bb639de1e

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)
chat_stream_generator tensorrt_llm/serve/_base/sse.py tensorrt_llm/serve/_base/sse.py modified (declared)
OpenAIDisaggServer.__init__ tensorrt_llm/serve/openai_disagg_server.py tensorrt_llm/serve/openai_disagg_server.py modified (declared)
OpenAIDisaggServer.anthropic_messages tensorrt_llm/serve/openai_disagg_server.py tensorrt_llm/serve/openai_disagg_server.py modified (declared)
OpenAIServer._synthetic_request tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/_base/request_context.py replaced
OpenAIServer._batch_store tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer._no_such_batch tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer._run_batched_message tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py replaced
OpenAIServer.anthropic_batch_results tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer.anthropic_cancel_batch tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer.anthropic_count_tokens tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py rewritten
OpenAIServer.anthropic_create_batch tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer.anthropic_delete_batch tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer.anthropic_get_batch tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer.anthropic_list_batches tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py verbatim
OpenAIServer.anthropic_messages tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/api/anthropic/serving.py replaced
OpenAIServer.__init__ tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer.create_chat_completion tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_server.py modified (declared)
OpenAIServer.openai_chat 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)
_anthropic_routes_enabled — tensorrt_llm/serve/_base/routing.py added
AnthropicBatchesServing.__init__ — tensorrt_llm/serve/api/anthropic/serving.py added
AnthropicBatchesServing — tensorrt_llm/serve/api/anthropic/serving.py added
AnthropicCountTokensServing.__init__ — tensorrt_llm/serve/api/anthropic/serving.py added
AnthropicCountTokensServing — tensorrt_llm/serve/api/anthropic/serving.py added
AnthropicMessagesServing.__init__ — tensorrt_llm/serve/api/anthropic/serving.py added
AnthropicMessagesServing.create_message — tensorrt_llm/serve/api/anthropic/serving.py added
AnthropicMessagesServing.run_batched_message — tensorrt_llm/serve/api/anthropic/serving.py added
AnthropicMessagesServing — tensorrt_llm/serve/api/anthropic/serving.py added
_error_type — tensorrt_llm/serve/api/anthropic/serving.py added
OpenAIDisaggServer._call_entry_point — tensorrt_llm/serve/openai_disagg_server.py added
_DisaggChatBackend.__init__ — tensorrt_llm/serve/openai_disagg_server.py added
_DisaggChatBackend.create_chat_completion — tensorrt_llm/serve/openai_disagg_server.py added
_DisaggChatBackend.error_message — tensorrt_llm/serve/openai_disagg_server.py added
_DisaggChatBackend.stream_model — tensorrt_llm/serve/openai_disagg_server.py added
_DisaggChatBackend — tensorrt_llm/serve/openai_disagg_server.py added
_disagg_serving_error — tensorrt_llm/serve/openai_disagg_server.py added
_envelope_message — tensorrt_llm/serve/openai_disagg_server.py added
_AggregatedChatBackend.__init__ — tensorrt_llm/serve/openai_server.py added
_AggregatedChatBackend.create_chat_completion — tensorrt_llm/serve/openai_server.py added
_AggregatedChatBackend.error_message — tensorrt_llm/serve/openai_server.py added
_AggregatedChatBackend.stream_model — tensorrt_llm/serve/openai_server.py added
_AggregatedChatBackend — tensorrt_llm/serve/openai_server.py added

Replaced

  • OpenAIServer.anthropic_messages -> tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing: one serving class over a ChatBackend, shared by the aggregated and disaggregated servers; route function anthropic_messages in api_router.py
  • OpenAIServer._run_batched_message -> tensorrt_llm/serve/api/anthropic/serving.py::AnthropicMessagesServing: same orchestration as messages, as run_batched_message; no synthetic request
  • OpenAIServer._synthetic_request -> tensorrt_llm/serve/_base/request_context.py::RequestContext: RequestContext.internal replaces the synthetic ASGI scope

Added

  • _error_type: status to Anthropic error type, today's rule
  • AnthropicMessagesServing: new serving class, used by both servers
  • AnthropicMessagesServing.__init__: takes the ChatBackend
  • AnthropicMessagesServing.create_message: the messages orchestration, once
  • AnthropicMessagesServing.run_batched_message: the batch orchestration, once
  • AnthropicCountTokensServing: hand-written skeleton class that receives anthropic_count_tokens
  • AnthropicCountTokensServing.__init__: explicit dependencies instead of server attributes, so missing wiring fails at construction
  • AnthropicBatchesServing: hand-written skeleton class that receives the eight batch methods
  • AnthropicBatchesServing.__init__: explicit dependencies instead of server attributes, so missing wiring fails at construction
  • _AggregatedChatBackend: ChatBackend over the typed chat core; transitional until the chat handlers move into their own serving class
  • _AggregatedChatBackend.__init__: same
  • _AggregatedChatBackend.create_chat_completion: same
  • _AggregatedChatBackend.stream_model: same
  • _AggregatedChatBackend.error_message: same
  • _envelope_message: today's message extraction for a returned error envelope
  • _disagg_serving_error: a disaggregated failure as a ServingError
  • _DisaggChatBackend: ChatBackend over prepare, run, finish
  • _DisaggChatBackend.__init__: same
  • _DisaggChatBackend.create_chat_completion: same
  • _DisaggChatBackend.stream_model: same
  • _DisaggChatBackend.error_message: same
  • OpenAIDisaggServer._call_entry_point: prepare, run, finish without serialize, in the disaggregated_request span
  • _anthropic_routes_enabled: the gate of the Anthropic router: not on a harmony (gpt-oss) model, as the route registration was gated

Rewritten

  • OpenAIServer.anthropic_count_tokens (tensorrt_llm/serve/api/anthropic/serving.py): RenderResources.from_server(self) -> self._render_resources()

Modified (declared)

  • OpenAIServer.__init__: Anthropic serving objects on app.state; harmony_model capability; the launcher forwarding routes register before the routers so that they keep winning over the local batch routes
    --- base
    +++ head
    @@ -164,7 +164,13 @@
    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 role_group_of(self.server_role) is RoleGroup.LLM:
    •    self._register_launcher_forwarding_routes()
      
    •    messages_serving = AnthropicMessagesServing(_AggregatedChatBackend(self))
      
    •    self.app.state.anthropic_serving_messages = messages_serving
      
    •    self.app.state.anthropic_serving_count_tokens = AnthropicCountTokensServing(render_resources=lambda: RenderResources.from_server(self))
      
    •    self.app.state.anthropic_serving_batches = AnthropicBatchesServing(runner=messages_serving.run_batched_message)
      
    • attach_routers(self.app, dataclasses.replace(ServerCapabilities.for_role(self.server_role), harmony_model=not self._is_visual_gen and self.use_harmony))
      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()
      @@ -176,7 +182,6 @@
      self._init_embedding_batcher()
      self.register_embedding_routes()
      else:
    •    self._register_launcher_forwarding_routes()
         self.register_routes()
      
      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)
  • OpenAIServer.register_routes: the Anthropic routes come from serve/api/anthropic/api_router.py
    --- base
    +++ head
    @@ -16,15 +16,6 @@
    self.resource_governor = None
    self.app.add_api_route('/v1/completions', self.openai_completion, methods=['POST'])
    self.app.add_api_route('/v1/chat/completions', self.openai_chat if not self.use_harmony else self.chat_harmony, methods=['POST'])

    • if not self.use_harmony:
    •    self.app.add_api_route('/v1/messages', self.anthropic_messages, methods=['POST'])
      
    •    self.app.add_api_route('/v1/messages/count_tokens', self.anthropic_count_tokens, methods=['POST'])
      
    •    self.app.add_api_route('/v1/messages/batches', self.anthropic_create_batch, methods=['POST'])
      
    •    self.app.add_api_route('/v1/messages/batches', self.anthropic_list_batches, methods=['GET'])
      
    •    self.app.add_api_route('/v1/messages/batches/{batch_id}', self.anthropic_get_batch, methods=['GET'])
      
    •    self.app.add_api_route('/v1/messages/batches/{batch_id}', self.anthropic_delete_batch, methods=['DELETE'])
      
    •    self.app.add_api_route('/v1/messages/batches/{batch_id}/cancel', self.anthropic_cancel_batch, methods=['POST'])
      
    •    self.app.add_api_route('/v1/messages/batches/{batch_id}/results', self.anthropic_batch_results, methods=['GET'])
      
      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'])
  • OpenAIServer.openai_chat: passes RequestContext.from_http(raw_request) to the core
    --- base
    +++ head
    @@ -1,6 +1,6 @@
    async def openai_chat(self, request: ChatCompletionRequest, raw_request: Request) -> Response:
    try:

    •    result = await self.create_chat_completion(request, raw_request)
      
    •    result = await self.create_chat_completion(request, RequestContext.from_http(raw_request))
         if isinstance(result, ChatCompletionResponse):
             return JSONResponse(content=result.model_dump())
         return StreamingResponse(content=result, media_type='text/event-stream')
      
  • OpenAIServer.create_chat_completion: parameter raw_request renamed to ctx (a RequestContext); no other change
    --- base
    +++ head
    @@ -1,4 +1,4 @@
    -async def create_chat_completion(self, request: ChatCompletionRequest, raw_request: Request) -> Union[ChatCompletionResponse, AsyncIterator[str]]:
    +async def create_chat_completion(self, request: ChatCompletionRequest, ctx: RequestContext) -> Union[ChatCompletionResponse, AsyncIterator[str]]:
    """Run one chat completion and return a typed result.

         Returns the ChatCompletionResponse for a non-streaming request, and
    

    @@ -74,13 +74,13 @@
    postproc_args.num_prompt_tokens_offset = excluded_tokens
    if dynamic_tool_params:
    postproc_args.tools = (postproc_args.tools or []) + dynamic_tool_params

    •    self._validate_internal_disagg_request(request, raw_request)
      
    •    self._validate_internal_disagg_request(request, ctx)
         disaggregated_params = to_llm_disaggregated_params(request.disaggregated_params)
         resolve_media = not (disaggregated_params is not None and disaggregated_params.request_type == 'generation_only' and (request.prompt_token_ids is not None or request.prompt_token_ids_b64))
         try:
             conversation, mm_coroutines, mm_placeholder_counts, mm_item_order = parse_chat_messages_coroutines(request.messages, self.model_config, self.multimodal_server_config, request_media_io_kwargs=request.media_io_kwargs, resolve_media=resolve_media)
         except ValidationError:
      
    •        raw_body = await raw_request.json()
      
    •        raw_body = await ctx.json()
             raw_messages = raw_body.get('messages', [])
             conversation, mm_coroutines, mm_placeholder_counts, mm_item_order = parse_chat_messages_coroutines(raw_messages, self.model_config, self.multimodal_server_config, request_media_io_kwargs=request.media_io_kwargs, resolve_media=resolve_media)
         if request.prompt_token_ids is None and request.prompt_token_ids_b64:
      

    @@ -134,24 +134,24 @@
    if conversation and conversation[-1].get('content') and (conversation[-1].get('role') == get_role()):
    postproc_args.last_message_content = conversation[-1]['content']
    postproc_params = PostprocParams(post_processor=chat_stream_post_processor if request.stream else chat_response_post_processor, postproc_args=postproc_args)

    •    trace_headers = None if raw_request is None else tracing.extract_trace_headers(raw_request.headers)
      
    •    resolve_request_conversation_id(request, None if raw_request is None else raw_request.headers)
      
    •    trace_headers = None if ctx is None else tracing.extract_trace_headers(ctx.headers)
      
    •    resolve_request_conversation_id(request, None if ctx is None else ctx.headers)
         conversation_params = to_llm_conversation_params(request.conversation_params)
      
    •    scheduling_params = self._get_scheduling_params(request, raw_request)
      
    •    scheduling_params = self._get_scheduling_params(request, ctx)
         generate_inputs = prompt
         preprocess_fn = getattr(self.generator, 'preprocess', None)
         if preprocess_fn is not None:
             loop = asyncio.get_event_loop()
             generate_inputs = await loop.run_in_executor(self._input_proc_executor, functools.partial(preprocess_fn, prompt, sampling_params, disaggregated_params))
         promise = self.generator.generate_async(inputs=generate_inputs, sampling_params=sampling_params, _postproc_params=postproc_params if self.postproc_worker_enabled else None, streaming=request.stream, lora_request=request.lora_request, disaggregated_params=disaggregated_params, conversation_params=conversation_params, cache_salt=request.cache_salt, trace_headers=trace_headers, scheduling_params=scheduling_params, priority=request.priority if request.priority is not None else DEFAULT_REQUEST_PRIORITY)
      
    •    asyncio.create_task(self.await_disconnected(raw_request, promise))
      
    •    asyncio.create_task(self.await_disconnected(ctx, promise))
         if not self.postproc_worker_enabled:
             postproc_args.tokenizer = self.tokenizer
             postproc_args.num_prompt_tokens = len(promise.prompt_token_ids)
         if request.stream:
      
    •        return chat_stream_generator(promise, postproc_params, server=self, raw_request=raw_request)
      
    •        return chat_stream_generator(promise, postproc_params, server=self, ctx=ctx)
         else:
      
    •        response = await self._create_chat_response(promise, postproc_params, raw_request, disaggregated_params)
      
    •        response = await self._create_chat_response(promise, postproc_params, ctx, disaggregated_params)
             if request.disaggregated_params is not None and request.disaggregated_params.return_prompt_token_ids_b64 and (response.prompt_token_ids is not None):
                 import numpy as np
                 response.prompt_token_ids_b64 = base64.b64encode(np.asarray(response.prompt_token_ids, dtype=np.int32).tobytes()).decode('ascii')
      
  • OpenAIDisaggServer.__init__: builds the AnthropicMessagesServing
    --- base
    +++ head
    @@ -21,6 +21,7 @@
    self._ctx_router = self._coordinator.ctx_router
    self._gen_router = self._coordinator.gen_router
    self._service = OpenAIDisaggregatedService(self._config, self._coordinator, self._create_client, req_timeout_secs=self._req_timeout_secs)

    • self._anthropic_messages_serving = AnthropicMessagesServing(_DisaggChatBackend(self))
      try:
      otlp_cfg = config.otlp_config
      if otlp_cfg and otlp_cfg.otlp_traces_endpoint:
  • OpenAIDisaggServer.anthropic_messages: delegates to AnthropicMessagesServing
    --- base
    +++ head
    @@ -1,29 +1,3 @@
    async def anthropic_messages(self, request: AnthropicMessagesRequest, raw_request: Request) -> Response:
    """Serve Anthropic Messages through the disaggregated chat pipeline."""

    • try:
    •    chat_request = convert_anthropic_request(request)
      
    • except (AnthropicRequestError, ValidationError) as e:
    •    return anthropic_error_response(str(e), 'invalid_request_error', 400)
      
    • try:
    •    openai_response = await self._wrap_entry_point(self._service.openai_chat_completion)(chat_request, raw_request)
      
    • except HTTPException as e:
    •    error_type = 'invalid_request_error' if 400 <= e.status_code < 500 else 'api_error'
      
    •    return anthropic_error_response(str(e.detail), error_type, e.status_code)
      
    • if isinstance(openai_response, StreamingResponse):
    •    return StreamingResponse(content=reframe_openai_stream(openai_response.body_iterator, model=request.model), media_type='text/event-stream')
      
    • status = getattr(openai_response, 'status_code', 500)
    • if status != 200:
    •    try:
      
    •        payload = json.loads(openai_response.body)
      
    •        message = payload.get('message') or payload.get('detail') or payload.get('error') or json.dumps(payload)
      
    •    except (json.JSONDecodeError, AttributeError, TypeError):
      
    •        message = 'Internal server error'
      
    •    error_type = 'invalid_request_error' if 400 <= status < 500 else 'api_error'
      
    •    return anthropic_error_response(str(message), error_type, status)
      
    • try:
    •    chat_response = ChatCompletionResponse(**json.loads(openai_response.body))
      
    •    anthropic_response = convert_chat_response(chat_response)
      
    • except (AnthropicResponseError, ValidationError, json.JSONDecodeError):
    •    logger.error(f'Invalid response from OpenAI chat pipeline:\n{traceback.format_exc()}')
      
    •    return anthropic_error_response('Internal server error', 'api_error', 500)
      
    • return JSONResponse(content=anthropic_response.model_dump(exclude_none=True))
    • return await self._anthropic_messages_serving.create_message(request, RequestContext.from_http(raw_request))
  • ServerCapabilities: harmony_model field, read by the Anthropic router's gate
    --- base
    +++ head
    @@ -3,3 +3,4 @@
    """What the composition root decides once at startup; routers are attached from it."""
    role: Optional[ServerRole]
    role_group: RoleGroup

    • harmony_model: bool = False
  • ROUTERS: the Anthropic RouterSpec joins ROUTERS after ops.system
    --- 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.anthropic.api_router', roles=frozenset({RoleGroup.LLM}), when=_anthropic_routes_enabled))

  • chat_stream_generator: keyword parameter raw_request renamed to ctx
    --- base
    +++ head
    @@ -1,9 +1,9 @@
    -async def chat_stream_generator(promise: RequestOutput, postproc_params: PostprocParams, *, server, raw_request) -> AsyncGenerator[str, None]:
    +async def chat_stream_generator(promise: RequestOutput, postproc_params: PostprocParams, *, server, ctx) -> AsyncGenerator[str, None]:
    try:
    if not server.postproc_worker_enabled:
    post_processor, args = (postproc_params.post_processor, postproc_params.postproc_args)
    first_response = await anext(promise)

    •    raw_request.state.server_first_token_time = server._adjusted_steady_clock.now()
      
    •    ctx.state.server_first_token_time = server._adjusted_steady_clock.now()
         pp_results = first_response.outputs[0]._postprocess_result if server.postproc_worker_enabled else post_processor(first_response, args)
         for pp_res in pp_results:
             yield pp_res
      

    @@ -13,7 +13,7 @@
    for pp_res in pp_results:
    yield pp_res
    yield 'data: [DONE]\n\n'

    •    await server._extract_metrics(res, raw_request)
      
    •    await server._extract_metrics(res, ctx)
         nvtx_mark('generation ends')
      
      except Exception:
      logger.error(traceback.format_exc())

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


</details>

No old import path changes: the moved and replaced definitions are methods, so no shim and an empty release-notes list. History: `git blame -C -C tensorrt_llm/serve/api/anthropic/serving.py` attributes the moved methods to their original commits. Commit structure (squashed at merge): the hand-written skeleton of the two receiving classes with the legacy-formatting registration; the generated move of the nine methods; the hand-written serving class, request context, router and server wiring; the tests.

### Removed from OpenAIServer

| Method | Now |
|---|---|
| `anthropic_messages` | `AnthropicMessagesServing.create_message` (`tensorrt_llm/serve/api/anthropic/serving.py`); route function `anthropic_messages` in `tensorrt_llm/serve/api/anthropic/api_router.py` |
| `anthropic_count_tokens` | `AnthropicCountTokensServing.anthropic_count_tokens` (moved; one declared rewrite: `RenderResources.from_server(self)` becomes the injected `self._render_resources()`) |
| `_synthetic_request` | `RequestContext.internal` (`tensorrt_llm/serve/_base/request_context.py`) |
| `_batch_store` | `AnthropicBatchesServing._batch_store` (moved) |
| `_run_batched_message` | `AnthropicMessagesServing.run_batched_message` |
| `anthropic_create_batch`, `anthropic_list_batches`, `anthropic_get_batch`, `anthropic_cancel_batch`, `anthropic_delete_batch`, `anthropic_batch_results` | the same names on `AnthropicBatchesServing` (moved) |
| `_no_such_batch` | `AnthropicBatchesServing._no_such_batch` (moved) |

The `if not self.use_harmony:` block of `register_routes` is deleted; the gate is the router table's `harmony_model` capability. `create_chat_completion` takes a `RequestContext` instead of the raw request (a parameter rename; `openai_chat` wraps the request with `RequestContext.from_http`), and the chat SSE generator's keyword parameter is renamed the same way. The disaggregated server gains `_call_entry_point` (prepare, run, finish, in the `disaggregated_request` span, without serialize) and a chat-backend adapter; `_wrap_entry_point` and `_handle_exception` are unchanged. A transitional `_AggregatedChatBackend` in `openai_server.py` goes away when the chat handlers move into their own serving class, and the per-request `RenderResources.from_server(self)` in the count-tokens wiring goes away when the server assembly builds the resources once.

Decisions followed: a module that receives moved legacy code stays out of the mypy file list while the new modules (`request_context.py`, `chat_backend.py`, `api_router.py`) join it; all eight aggregated Anthropic routes move, none stays on `OpenAIServer`; the backends raise `ServingError` / `EngineFatalError` until the dialect-neutral error types exist.

### Coordination with open PRs

Open PRs that touch the Anthropic handlers or their tests; whichever lands second rebases or retargets onto the other:

- #20865 (serve: answer a request that fails validation with a readable error): edits both servers' validation handlers (unchanged here) and `tests/unittest/llmapi/apps/test_anthropic_routes.py`, which this PR rewrites on the fixture factory; its added test mounts `server.anthropic_messages` on a bare server and, after this PR, builds the app with `isolated_serve_env` and posts to the real route.
- #21107 (Anthropic Messages: answer an over-length prompt before any stream): edits both servers' `anthropic_messages`; after this PR the orchestration is `AnthropicMessagesServing.create_message`, shared by both.
- #20936 (Anthropic Messages: carry the engine request id in the message id): calls `_run_batched_message`, now `AnthropicMessagesServing.run_batched_message`.
- #21388 (Disaggregated Anthropic Messages: start the stream and ping while the context phase runs): rewrites the disaggregated `anthropic_messages`; after this PR the disaggregated chat path is `_DisaggChatBackend.create_chat_completion` over `_call_entry_point`.
- #21414, #21111: tests mounting `server.anthropic_messages` / `server.anthropic_count_tokens` on bare servers; mount the serving objects or build the real app instead.
- #21062, #21059: adapter and chat-core edits in `openai_server.py` without Anthropic handler lines; they rebase without changes to the Anthropic routes.

### CI bookkeeping

- Multi-GPU table: added `tensorrt_llm/serve/api/anthropic/api_router.py` and `tensorrt_llm/serve/api/anthropic/serving.py` (they carry the disaggregated `/v1/messages` path); the three modules moved by #21518 are not added.
- CODEOWNERS: none.
- legacy-files.txt: `tensorrt_llm/serve/api/anthropic/serving.py` added (legacy-formatted: its moved methods come from `openai_server.py`); derived configs regenerated with `scripts/legacy_utils.py gen-configs`; `ruff-legacy-baseline.json`: none (the moved methods carry no recorded violation).
- mypy: `tensorrt_llm/serve/_base/chat_backend.py`, `tensorrt_llm/serve/_base/request_context.py`, `tensorrt_llm/serve/api/anthropic/api_router.py` added to `[tool.mypy] files`.
- Test lists: none new; the `unittest/serve` entry of `l0_cpu.yml` collects the three new test files (`tests/unittest/serve/_base/test_request_context.py`, `tests/unittest/serve/api/anthropic/test_serving.py`, `tests/unittest/serve/api/anthropic/test_api_router.py`, each marked `cpu_only`), and the rewritten `test_anthropic_routes.py` and `test_render_callers.py` are registered already.
- `tensorrt_llm/serve/AGENTS.md`: one "Where code goes" line for the Anthropic package.
- Release notes: no import path changes (the moved and replaced definitions are methods); the multi-GPU trigger table gains `tensorrt_llm/serve/api/anthropic/serving.py` and `tensorrt_llm/serve/api/anthropic/api_router.py`.

## Test Coverage

All CPU checks run from the worktree with its code on `PYTHONPATH` (`tensorrt_llm.__file__` printed the worktree path); GPU checks are left to CI.

- `pytest -m cpu_only tests/unittest/serve tests/unittest/llmapi/apps/test_anthropic_routes.py tests/unittest/llmapi/apps/test_anthropic_adapter.py tests/unittest/llmapi/apps/test_anthropic_batches.py tests/unittest/llmapi/apps/test_render_callers.py tests/unittest/llmapi/apps/test_chat_template_kwargs_guard.py tests/unittest/llmapi/apps/test_context_length_error.py tests/unittest/llmapi/apps/test_openai_chat_multimodal_prompt.py tests/unittest/llmapi/test_serve_multi_frontend_route_gate.py tests/unittest/llmapi/test_serve_multi_frontend_helpers.py tests/unittest/api_stability/test_serve_routes.py tests/unittest/disaggregated/test_disagg_entry_point.py tests/unittest/disaggregated/test_disagg_server_contract.py`: 553 passed. `tests/unittest/serve/api/anthropic/test_messages_behavior.py` is unchanged from #21518 (18 tests, all pass); the route snapshot and the middleware-order snapshot are unchanged.
- New tests (all `cpu_only`, collected through the `unittest/serve` entry of `l0_cpu.yml`): `tests/unittest/serve/api/anthropic/test_serving.py` (20), `tests/unittest/serve/api/anthropic/test_api_router.py` (9), `tests/unittest/serve/_base/test_request_context.py` (10). Rewritten: `tests/unittest/llmapi/apps/test_anthropic_routes.py` (49), `TestCountTokens` in `tests/unittest/llmapi/apps/test_render_callers.py`.
- `LLM_MODELS_ROOT=... PYTHONPATH=tests python3 -m test_common.serve.cpu_tests` (every serve CPU test, one pytest per CPU-list entry): every entry green except `unittest/grpc/openengine/test_frontend_lifecycle.py::test_frontend_group_cleanup[...]` (3 cases), which fail the same way on the base branch without this change (process-group cleanup under load on the CPU host; unrelated to the Anthropic path).
- `pytest tests/unittest/api_stability -q`: 98 passed. `pytest tests/unittest/disaggregated -m cpu_only -q`: 1430 passed, 1 skipped, 2 xfailed. `pytest -rs tests/unittest/llmapi/apps/test_harmony_parsing.py tests/unittest/llmapi/apps/test_harmony_channel_validation.py`: 79 passed, no import skip.
- Two FastAPI versions (0.136.3 and 0.139.2): `test_api_router.py`, `test_serve_routes.py`, `test_anthropic_routes.py`, `test_routing.py` and the two multi-frontend tests: 114 passed on each.
- `bash scripts/run_mypy.sh`: no issues in 23 source files (the three new modules included). `python scripts/legacy_utils.py check-configs`: up to date. `python scripts/legacy_utils.py lint-precommit tensorrt_llm/serve/api/anthropic/serving.py tensorrt_llm/serve/openai_server.py tensorrt_llm/serve/openai_disagg_server.py`: no new violation (it reports one F821 fewer in `openai_server.py` than the baseline; the baseline was already one high on the base branch).
- `git grep -n "from_server(self)" -- tensorrt_llm/serve/api/anthropic/`: nothing. No `json.loads` of a chat response body in `openai_server.py` or `serving.py`; in `openai_disagg_server.py` only `_envelope_message` parses the envelope `_handle_exception` returns.
- Property checks with Hypothesis over generated inputs (local, not committed): the error-type rule, the aggregated error-message extraction against the old JSON round trip, the disaggregated envelope extraction against the old inline chain, the disaggregated error construction, the non-streaming HTTP answer against the batch item for the same backend outcome, and the internal request context's JSON against a Starlette request over the old synthetic scope: no counter-example in 300 examples each; a gate of 12 planted bugs was caught 12 times.
- GPU, left to CI: `test_e2e.py::test_anthropic_messages` and the single-GPU disaggregated block of `l0_a10.yml`.

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

…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>
…s outside-subclass contract

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Move tensorrt_llm/serve/anthropic_adapter.py, anthropic_protocol.py and
anthropic_batches.py whole into tensorrt_llm/serve/api/anthropic/ as
adapter.py, protocol.py and batches.py, and rewrite every import of them
in the repository. The old paths are deleted without a compatibility
shim: no code outside the repository imports them.

Generated change: the three files are copied byte for byte apart from
their import lines; the importers change only in their import lines.

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>
… mark the new package

Add tensorrt_llm/serve/api/anthropic/__init__.py, retarget the two module
imports of the adapter in test_anthropic_adapter.py to the new path, and add
tests/unittest/serve/api/anthropic/test_messages_behavior.py: characterization
tests of what the aggregated and the disaggregated server answer on the
Anthropic routes today (success, streaming, a stream without chunks, rejected
requests, context length, upstream errors, unexpected errors, engine failures,
an unconvertible chat response, and the aggregated Message Batches), with the
served model and the requested model named differently so that each server's
own answer is pinned. The follow-up PR that gives both servers one Anthropic
Messages serving class must keep this file unchanged and green.

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

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

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

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

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

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

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

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

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

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

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

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

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

The two classes receive the handlers that leave OpenAIServer in the next
commit. Their constructors take what those handlers read from the server
(the render-resources factory, the batch item runner), so missing wiring
fails at construction instead of falling back to a default.

The module is registered as a legacy-formatted file before any moved code
lands in it, so the formatters keep the moved methods as they are;
ruff-legacy.toml and the managed blocks of pyproject.toml and
.pre-commit-config.yaml are regenerated from legacy-files.txt.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ndlers out of OpenAIServer

Generated move: anthropic_count_tokens goes to AnthropicCountTokensServing
and _batch_store, the six Message Batches handlers and _no_such_batch go
to AnthropicBatchesServing in tensorrt_llm/serve/api/anthropic/serving.py,
verbatim except for the declared rewrite in count_tokens
(RenderResources.from_server(self) becomes self._render_resources(), the
injected factory). anthropic_messages, _run_batched_message and
_synthetic_request are deleted here and replaced by the serving class and
the request context added in the next commit. The import cleanup is the
formatters' (autoflake, isort, yapf).

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
… aggregated and disaggregated servers

The Anthropic Messages orchestration existed three times (aggregated
messages, the aggregated Message Batches runner, disaggregated messages),
each serializing the chat result, parsing it back and sniffing the error
body for a message; the batch runner entered the chat pipeline through a
synthetic ASGI request. One AnthropicMessagesServing now runs it for any
ChatBackend: the aggregated backend is the typed chat core
create_chat_completion, the disaggregated backend is the entry-point
wrapper's prepare, run and finish steps without the serialization step
(the new _call_entry_point, in the same span). A RequestContext carries
what the chat core reads from a request (headers, state, raw JSON,
disconnect polling); the batch path builds one directly.

The eight aggregated Anthropic routes come from the new router in
tensorrt_llm/serve/api/anthropic/api_router.py, attached through the
router table and gated by the new harmony_model capability as the route
registration was gated. The launcher forwarding routes of an attached
frontend register before the routers, so that they keep winning over the
local Message Batches handlers. Each server keeps its own answers: the
streamed model name and the error-message extraction come from its
backend (stream_model, error_message); an engine failure stays a 500 on
both.

No behavior change. The new modules join the mypy file list; the two
serving-class modules join the multi-GPU trigger table because they carry
the disaggregated messages path.

Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ntext and the router on the real apps

New tests: tests/unittest/serve/api/anthropic/test_serving.py (the serving
class over a fake chat backend: the stream is reframed from the backend's
own iterator, the backend's model and message reach the answer, each
error kind keeps its status, the batch path hands the backend a
non-streaming request and an internal context whose JSON is the converted
request); tests/unittest/serve/_base/test_request_context.py (delegation
to the HTTP request including its msgpack decoding, the internal context,
the disconnect watcher returning at once for it);
tests/unittest/serve/api/anthropic/test_api_router.py (the eight routes
come from the router with the msgpack route class on the LLM roles only,
none on a harmony model or another role, count_tokens decodes a msgpack
body, attaching needs the serving objects). All three carry the cpu_only
marker and run through the unittest/serve entry of the CPU list.

Rewritten on the fixture factory: test_anthropic_routes.py posts to the
real apps and their own validation handler instead of a mirror handler on
bare servers, drives the batch and count-tokens routes through the serving
objects on app.state, and hands the synthetic-request test to
test_request_context.py; TestCountTokens in test_render_callers.py builds
the count-tokens serving over a namespace. The chat-core and SSE-generator
tests follow the renamed parameter; the multi-frontend tests set the
Anthropic serving objects before attaching the routers. The object.__new__
ratchet drops to 2 and 1 sites for the two rewritten files; a layering
contract keeps the Anthropic package from importing a server.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant