Repository navigation
[TRTLLM-17263][refactor] Split the chat handler into a typed core and lift the SSE generator into serve/_base - #21515
Draft
JunyiXu-nv wants to merge 18 commits into
Draft
JunyiXu-nv wants to merge 18 commits into
JunyiXu-nv wants to merge 18 commits into
Conversation
…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>
…efore splitting it Three tests run OpenAIServer.openai_chat through a server built with the real constructor and the fake generator, and pin what it answers today: a request the handler rejects keeps its 400 body byte for byte, an engine failure keeps HTTP 200 with body null (and /health_generate keeps turning that into a 500), a response that cannot be JSON-encoded keeps its 400, and a stream keeps its frames and media type. They pass on the unsplit handler and must still pass once it is split into a typed core and an HTTP wrapper. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…serve/_base/sse.py Generated move: the chat_stream_generator closure of OpenAIServer.openai_chat is lifted to module level in tensorrt_llm/serve/_base/sse.py unchanged, apart from its two free variables, which become the keyword-only parameters `server` (was `self`) and `raw_request`. openai_chat imports it back and passes server=self, raw_request=raw_request at its one call. The generator keeps its `except Exception:` clause and its two `_postprocess_result` reads. tensorrt_llm/serve/_base/sse.py joins legacy-files.txt (its code comes from a legacy-formatted file) and the generated lint configs are regenerated with scripts/legacy_utils.py gen-configs; ruff-legacy-baseline.json gains the module's one recorded F821 (the `anext` builtin ruff does not know), which openai_server.py keeps counting until its baseline is regenerated. The Anthropic stream tests patched the logger of openai_server to check what the generator logs; they now patch the logger of serve/_base/sse.py, where the generator lives. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…ompletion and an HTTP wrapper OpenAIServer.create_chat_completion is the former body of openai_chat with a typed contract: it returns the ChatCompletionResponse for a non-streaming request and the SSE stream as strings for a streaming one, and it raises ServingError (the HTTP status apart from the OpenAI error body) where it used to return an error response, or EngineFatalError after the three side effects of an engine failure. Nothing else in the body changes: the six early rejections raise where they returned, the ValueError and the generic handler raise what they rendered, and the ServingError handler comes first so an early rejection is neither logged nor re-wrapped. openai_chat keeps its name, signature and route and becomes the wrapper that renders the typed result: JSONResponse for the response, a streaming response for the stream, the error body with its status for ServingError, and HTTP 200 with body null after an engine failure, as before. Its own ValueError branch keeps the 400 for a response that cannot be JSON-encoded, which used to fail inside the core. Every caller (the route, /health_generate, the Anthropic messages and batch paths) is unchanged. tensorrt_llm/serve/_base/errors.py holds error_body, ServingError, serving_error and EngineFatalError; create_error_response builds its body with error_body, so a ServingError body and a rendered error can never differ (the over-length-prompt code lives in one place). Tests: the typed core and its errors through a server built with the real constructor; the error builders against create_error_response; the lifted SSE generator with fakes. The three answer-pinning tests added before the split pass unchanged. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
The new error module is written from scratch, so it joins the mypy file list in pyproject.toml; scripts/run_mypy.sh passes with it (the strict global settings apply). tensorrt_llm/serve/_base/sse.py stays out: it is moved legacy code and joins type checking with its package later. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…se in the multimodal prompt test The chat handler now serializes a ChatCompletionResponse and streams any other result of its core, so a stand-in for _create_chat_response that only has model_dump() is no longer rendered as JSON. The test stubs a real ChatCompletionResponse instead; what it checks (the rendered prompt, the decoded image and chat_template_applied reaching the engine) is unchanged. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
…d 400 without a traceback The HTTP wrapper answers a response that cannot be JSON-encoded from its own ValueError branch, which logs nothing; the generic handler would render the same 400 but log a traceback. The answer-pinning test now also asserts that no ERROR record is emitted, so dropping that branch fails the test. Signed-off-by: JunyiXu-nv <219237550+JunyiXu-nv@users.noreply.github.com>
JunyiXu-nv
force-pushed
the
dev-junyix-feat-serve-typed-chat-core
branch
from
November 4, 2026 12:50
22fda4f to
d8c56b5
Compare
1 task done
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #21448 ([TRTLLM-17261][refactor] Add the trtllm-serve router skeleton and move /health, /version and /v1/models into ops.system), which is itself stacked on #21406 ([TRTLLM-17261][test] Add route, middleware-order and shim-identity snapshots and a CPU test seam for trtllm-serve): the first commits below belong to those PRs and disappear from this diff when they merge; this PR's own change is the last 6 commits.
@coderabbitai summary
Description
OpenAIServer.openai_chatreturned an already serializedResponse, so its in-process callers (the Anthropic Messages handler, the Anthropic batch runner,/health_generate) had to parse the JSON back or sniff the error body. This PR splits it in place:create_chat_completion()is the typed core, andopenai_chatkeeps its name, signature and route as a thin HTTP wrapper that renders the typed result exactly as before. The typed errors live in a newtensorrt_llm/serve/_base/errors.py, and the chat SSE generator, a closure insideopenai_chat, is lifted unchanged intotensorrt_llm/serve/_base/sse.py. Later PRs remove the JSON round trips of the Anthropic paths and move the core into a chat serving class; nothing here changes what any client receives.Part of the trtllm-serve refactor (epic TRTLLM-17056): typed return contract and Anthropic convergence (TRTLLM-17263). Builds on the serve router skeleton (#21448) and the CPU test seam and fixture factory (#21406).
Coordination. One test outside the handler's own tests changes:
tests/unittest/llmapi/apps/test_openai_chat_multimodal_prompt.pystubbed_create_chat_responsewith aSimpleNamespacethat only hadmodel_dump(); the wrapper serializes aChatCompletionResponseand streams any other result of the core, so the stub is now a realChatCompletionResponse. That is a tightening of a test seam forced by the new dispatch, not a behavior change: the production_create_chat_responsecan only return aChatCompletionResponse, and what the test checks (the rendered prompt, the decoded image andchat_template_appliedreaching the engine) is unchanged. Open PRs on the same lines, which re-apply on top once this merges: #21392 (Choose the trtllm-serve post-processing path once per server) edits the lifted generator, now intensorrt_llm/serve/_base/sse.py; #21445 (Add public runtime accessors for the remaining serving-frontend private accesses) and #21393 (Add public LLM health queries and a private-access lint for the serving frontends) record a private-access baseline entry keyed to the generator's old scopeOpenAIServer.openai_chat.chat_stream_generator, which becomestensorrt_llm/serve/_base/sse.py/chat_stream_generatorif they land first; and the PRs that add lines insideopenai_chat's body, which is nowcreate_chat_completion— #21414 (warn when a requested output format cannot be enforced), #21111 (take Kimi-K3 chat prompt ids from its renderer), #21062 (fail a request whose generation degenerates into a token run), #21059 (enforceparallel_tool_calls=falseby stopping after the first DSML tool call), #20977 (configure input processor workers), #20739 (add timing metrics for input processing and multimodal encoding), #20936 (Anthropic Messages: carry the engine request id in the message id), #19800 (bound multimodal request CPU memory in trtllm-serve) and #19461 (Expose image, video, and audio prompt-token counts in trtllm-serve responses) — turn anyreturn self.create_error_response(...)they add inside that body intoraise serving_error(...)with the same arguments; everything else in the body re-applies as it is. #21107 (Anthropic Messages: answer an over-length prompt with "prompt is too long", before any stream) edits the same region oftest_anthropic_routes.pyas the fixture retarget below (a textual conflict only).Behavior
No behavior change.
The contract of
OpenAIServer.create_chat_completion(request, raw_request):ChatCompletionResponse(after the optional base64 encoding ofprompt_token_idsfor context-only disaggregated requests), not serialized;data: [DONE];ServingError(the HTTP status apart from the OpenAI error body, whosecodemay becontext_length_exceeded) orEngineFatalError(after the same three side effects as today: traceback logged, generator termination recorded, SIGINT); it never returns an error response and never returnsNone.The wrapper
openai_chatrenders:JSONResponse(result.model_dump())for the response,StreamingResponse(media_type="text/event-stream")for the stream,JSONResponse(e.body.model_dump(), status_code=e.status)for aServingError(byte-identical tocreate_error_responsefor the same arguments), andNoneafter anEngineFatalError. Its ownValueErrorbranch keeps the 400 for a response that cannot be JSON-encoded (JSONResponseencodes in its constructor withallow_nan=False; that used to fail inside the handler's owntry).What an engine failure answers, per caller:
/v1/chat/completionsNone: HTTP 200, bodynullanthropic_messages,_run_batched_messageopenai_chat:None→ 500 / itemerroredhealth_generateNone.status_code→AttributeError→ 500chat_harmonyCppExecutorErrorbranch; 400)_handle_exceptionreturnsNone)Answering 5xx on an engine failure is a separate, later behavior change.
create_error_responsenow builds its body witherror_body(the one copy of the rule that an over-length prompt getscode="context_length_exceeded"while the status stays 400), so aServingErrorbody and a rendered error body can never differ.Moves
Pure-move check
Pure-move check: base d3aacdd, head d8c56b5, tools 517fc468330a
OpenAIServer.openai_chat.chat_stream_generatorOpenAIServer.create_error_responseOpenAIServer.openai_chatOpenAIServer.create_chat_completionAdded
OpenAIServer.create_chat_completion: typed chat core: the body of openai_chat with errors raised as ServingError / EngineFatalError and the typed result returnedModified (declared)
OpenAIServer.create_error_response: body built by serve/_base/errors.py::error_body, shared with ServingError--- base
+++ head
@@ -1,7 +1,4 @@
@staticmethod
def create_error_response(message: str, err_type: str='BadRequestError', status_code: HTTPStatus=HTTPStatus.BAD_REQUEST) -> Response:
return JSONResponse(content=error_response.model_dump(), status_code=status_code.value)
OpenAIServer.openai_chat: thin HTTP wrapper over create_chat_completion; same route, same responses--- base
+++ head
@@ -1,154 +1,13 @@
async def openai_chat(self, request: ChatCompletionRequest, raw_request: Request) -> Response:
return self.create_error_response(str(e))
except Exception as e:
0 definitions moved verbatim; 0 differences; 1 lifted; 1 added; 2 modified (declared); shim exports 0 names
History:
git blame -C -C tensorrt_llm/serve/_base/sse.pyattributes the generator's lines to their original commits.Removed from OpenAIServer
None. The chat SSE generator was a closure inside
openai_chat, not a method; every method ofOpenAIServeris still there, andcreate_chat_completionis new.CI bookkeeping
tensorrt_llm/serve/_base/prefix covers both new files;tensorrt_llm/serve/openai_server.pyis already listed, so the multi-GPU stages run and need theci: full pre-merge approvedlabel).tensorrt_llm/serve/_base/sse.pyadded (legacy formatting group; its code comes fromopenai_server.py);tensorrt_llm/serve/_base/errors.pyis new code and stays in the ruff group; derived configs regenerated withscripts/legacy_utils.py gen-configs;ruff-legacy-baseline.json: one new entrytensorrt_llm/serve/_base/sse.py: {F821: 1}(theanextbuiltin ruff does not know, moved with the generator; theopenai_server.pyentry stays at 3 — never a fulllint-update-violationsin a move PR).tensorrt_llm/serve/_base/errors.pyadded to[tool.mypy] files;sse.pyis moved legacy code and stays out until its package graduates.tests/unittest/serve/are collected by theunittest/serveentry oftests/integration/test_lists/test-db/l0_cpu.yml(line 337) and carrypytest.mark.cpu_only.server_loggerfixture of the four chat-stream tests intests/unittest/llmapi/apps/test_anthropic_routes.pypatchedopenai_server.loggerto check what the generator logs; it now patches the logger oftensorrt_llm.serve._base.sse, where the generator lives (verified by making the generator's log call raise temporarily: the failure test then fails). The remainingpatch("tensorrt_llm.serve.openai_server.logger")intest_responses_store_replay.pytargets Responses code, which did not move.tests/unittest/llmapi/apps/test_openai_chat_multimodal_prompt.pystubbed_create_chat_responsewith aSimpleNamespacethat only hadmodel_dump(); the wrapper serializes aChatCompletionResponseand streams any other result of the core, so the stub is now a realChatCompletionResponse(what the test checks, the rendered prompt and decoded image reaching the engine, is unchanged). Every other_create_chat_responsestub intests/already returns aChatCompletionResponse.Test Coverage
CPU, run from the worktree with the compiled artifacts linked in (
tensorrt_llm.__file__resolved to the worktree):pytest -m cpu_only tests/unittest/serve/test_openai_server_chat_core.py— the three answer-pinning tests (test_http_wrapper_keeps_every_answer,test_unencodable_response_is_still_a_400,test_health_generate_still_fails_with_500_on_engine_failure) run on the unsplit handler before the split: 3 passed; after the split, with the six typed-core tests: 9 passed. The unencodable-response case also pins that nothing is logged at ERROR, so it fails if the wrapper's ownValueErrorbranch is removed and the generic handler (which logs a traceback) renders the 400 instead.pytest -m cpu_only tests/unittest/serve/_base/test_errors.py tests/unittest/serve/_base/test_sse.py— 7 passed.pytest -m cpu_only tests/unittest/serve tests/unittest/llmapi/apps/test_serving_extensions.py tests/unittest/llmapi/apps/test_openai_chat_disagg_multimodal.py tests/unittest/llmapi/apps/test_anthropic_routes.py tests/unittest/llmapi/apps/test_render_callers.py tests/unittest/disaggregated -q— 1759 passed, 1 skipped, 2 xfailed (includes the route snapshot, the middleware-order and layering contracts and theobject.__new__ratchet: no site added or removed).PYTHONPATH=<worktree>/tests python3 -m test_common.serve.cpu_tests(every serve CPU test, one pytest process per test-db entry) — 66 test-db entries, 4210 passed. Two failures on the first full run, neither a defect of the change:test_openai_chat_multimodal_prompt.py::test_rendered_image_request_reaches_the_engine_marked_templated(its_create_chat_responsestub was aSimpleNamespace; fixed here, 4 passed on re-run) andtest_render_standalone.py::TestCpuOnlyStartup::test_the_launcher_loads_a_cuda_stub_only_when_the_driver_is_missing, which assertsshutil.which("trtllm-render")and only fails when the environment'sbin/is not on PATH (1 passed when re-run with it on PATH).LLM_MODELS_ROOT=... pytest -rs tests/unittest/llmapi/apps/test_harmony_parsing.py tests/unittest/llmapi/apps/test_harmony_channel_validation.py— 79 passed, 0 skipped; no "not importable" skip and no channel-validation guard skip.python -c "import tensorrt_llm.serve._base.sse"— imports (every name the generator uses is imported in the new module).bash scripts/run_mypy.sh— "Compiled bindings detected — running full mypy type check. Success: no issues found in 20 source files" (errors.pyincluded).python scripts/legacy_utils.py lint-precommit tensorrt_llm/serve/_base/sse.py tensorrt_llm/serve/openai_server.py— passes (its tip about one resolved F821 inopenai_server.pyis expected);python scripts/legacy_utils.py check-configs— "All generated configs are up to date."pre-commit run --files <every changed file>— clean on the second pass (isort and yapf converge).create_error_responserenders; thecontext_length_exceededcode rule;ServingErrorcarries status, body and message and is not aValueError; the wrapper renders aServingErrorbyte-identically tocreate_error_response, a typed response as 200 JSON ofmodel_dump(), a stream astext/event-streamwith the same frames, an engine failure asNoneand aValueErroras 400 — all pass (60–200 examples each), and a mutation gate of 8 planted bugs (inerrors.pyand in the wrapper) is detected 8/8.Left to CI (GPU):
test_e2e.py::test_openai_misc_example[pytorch](/health_generate200) andtest_e2e.py::test_openai_chat_example[pytorch](l0_a10.yml),test_e2e.py::test_anthropic_messages(l0_h100.yml).New tests:
tests/unittest/serve/test_openai_server_chat_core.py(9),tests/unittest/serve/_base/test_errors.py(4),tests/unittest/serve/_base/test_sse.py(3); all under theunittest/serveentry ofl0_cpu.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 to the best of your knowledge.
Test cases are provided for new code paths (see test instructions)
If PR introduces API changes, an appropriate PR label is added - either
api-compatibleorapi-breaking. Forapi-breaking, includeBREAKINGin the PR title.Any new dependencies have been scanned for license and vulnerabilities
CODEOWNERS updated if ownership changes
Documentation updated as needed
Update tava architecture diagram if there is a significant design change in PR.
The reviewers assigned automatically/manually are appropriate for the PR.
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