From c9488241e9e7f26076da093ba7fb4615fc2a76ec Mon Sep 17 00:00:00 2001 From: Lianmin Zheng Date: Sun, 21 Jun 2026 00:51:08 -0700 Subject: [PATCH] [Refactor] Auto-derive CLI args from dataclass fields to eliminate duplication (#28814) --- python/sglang/srt/arg_groups/arg_utils.py | 259 +++++++++++++ python/sglang/srt/server_args.py | 437 +++++++++------------- 2 files changed, 430 insertions(+), 266 deletions(-) create mode 100644 python/sglang/srt/arg_groups/arg_utils.py diff --git a/python/sglang/srt/arg_groups/arg_utils.py b/python/sglang/srt/arg_groups/arg_utils.py new file mode 100644 index 000000000..890fcfdca --- /dev/null +++ b/python/sglang/srt/arg_groups/arg_utils.py @@ -0,0 +1,259 @@ +# Copyright 2023-2024 SGLang Team +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ============================================================================== +"""Utilities for auto-deriving argparse CLI arguments from dataclass fields. + +Usage:: + + from sglang.srt.arg_groups.arg_utils import A, Arg, add_cli_args_from_dataclass + + @dataclasses.dataclass + class ServerArgs: + # Simple fields — bare string is the help text: + host: A[str, "The host of the HTTP server."] = "127.0.0.1" + port: A[int, "The port of the HTTP server."] = 30000 + trust_remote_code: A[bool, "Whether to allow custom models."] = False + tokenizer_path: A[Optional[str], "The path of the tokenizer."] = None + + # Fields with extra metadata — use Arg(...): + model_path: A[str, Arg(help="Path to model weights.", aliases=["--model"])] + load_format: A[str, Arg(help="Format.", choices=CHOICES)] = "auto" + + @staticmethod + def add_cli_args(parser): + add_cli_args_from_dataclass(parser, ServerArgs) + +``A`` is a short alias for ``typing.Annotated``. A bare ``str`` inside the +annotation is equivalent to ``Arg(help=that_string)``. +""" + +from __future__ import annotations + +import dataclasses +import types +from typing import ( + Annotated, + Any, + Callable, + List, + Literal, + Optional, + Union, + get_args, + get_origin, + get_type_hints, +) + +A = Annotated + + +@dataclasses.dataclass(frozen=True) +class Arg: + """CLI argument metadata attached to a dataclass field via ``Annotated``.""" + + help: str = "" + choices: Optional[list] = None + aliases: Optional[List[str]] = None + cli_name: Optional[str] = None + type_parser: Optional[Callable] = None + nargs: Optional[str] = None + required: Optional[bool] = None + action: Optional[Any] = None + action_kwargs: Optional[dict] = None + # When True, this field is skipped by add_cli_args_from_dataclass. + # Use for fields that have no CLI surface (e.g. injected via Python only). + no_cli: bool = False + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + +_MISSING = dataclasses.MISSING + + +def _unwrap_annotated(tp): + """Return (inner_type, Arg | None) from ``Annotated[T, Arg(...)]``. + + Also accepts a bare string as shorthand: ``Annotated[T, "help text"]`` + is equivalent to ``Annotated[T, Arg(help="help text")]``. + """ + origin = get_origin(tp) + if origin is Annotated: + args = get_args(tp) + inner = args[0] + for a in args[1:]: + if isinstance(a, Arg): + return inner, a + if isinstance(a, str): + return inner, Arg(help=a) + return inner, None + return tp, None + + +def _unwrap_optional(tp): + """If tp is Optional[X] (i.e. Union[X, None]), return (X, True). Else (tp, False).""" + origin = get_origin(tp) + is_union = origin is Union or ( + hasattr(types, "UnionType") and origin is types.UnionType + ) + if is_union: + args = get_args(tp) + non_none = [a for a in args if a is not type(None)] + if len(non_none) == 1: + return non_none[0], True + return tp, False + + +def _unwrap_literal(tp): + """If tp is Literal[...], return list of values. Else None.""" + origin = get_origin(tp) + if origin is Literal: + return list(get_args(tp)) + return None + + +def _infer_type_func(tp): + """Map a Python type annotation to an argparse ``type=`` callable.""" + if tp is str: + return str + if tp is int: + return int + if tp is float: + return float + return str + + +def _field_default(field): + """Return the default value for a dataclass field, or _MISSING.""" + if field.default is not _MISSING: + return field.default + if field.default_factory is not _MISSING: + return field.default_factory() + return _MISSING + + +def _field_to_cli_name(name: str) -> str: + """Convert a field name like ``model_path`` to ``--model-path``.""" + return "--" + name.replace("_", "-") + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def add_cli_args_from_dataclass(parser, cls, *, fields: Optional[List[str]] = None): + """Add argparse arguments for every ``A[T, "help"]`` or ``A[T, Arg(...)]`` field. + + Fields without an ``Arg`` or bare-string annotation are silently skipped — + they must still be registered manually (this allows incremental migration). + + Parameters + ---------- + parser : argparse.ArgumentParser + cls : dataclass type + fields : optional list of field names to include. If None, all fields with + ``Arg`` annotations are included. + """ + hints = get_type_hints(cls, include_extras=True) + + for field in dataclasses.fields(cls): + if fields is not None and field.name not in fields: + continue + + hint = hints.get(field.name) + if hint is None: + continue + + raw_type, arg_meta = _unwrap_annotated(hint) + if arg_meta is None: + continue + if arg_meta.no_cli: + continue + + cli_name = arg_meta.cli_name or _field_to_cli_name(field.name) + names = [cli_name] + (arg_meta.aliases or []) + default = _field_default(field) + + # Handle custom action + if arg_meta.action is not None: + kwargs = { + "action": arg_meta.action, + "help": arg_meta.help, + } + if default is not _MISSING: + kwargs["default"] = default + if arg_meta.action_kwargs: + kwargs.update(arg_meta.action_kwargs) + parser.add_argument(*names, **kwargs) + continue + + # Unwrap Optional + inner_type, is_optional = _unwrap_optional(raw_type) + + # Check for Literal — auto-derive choices + literal_vals = _unwrap_literal(inner_type) + if literal_vals is not None: + choices = arg_meta.choices or literal_vals + # Infer type from first literal value + val_type = type(literal_vals[0]) if literal_vals else str + type_func = arg_meta.type_parser or _infer_type_func(val_type) + kwargs = dict(type=type_func, choices=choices, help=arg_meta.help) + if default is not _MISSING: + kwargs["default"] = default + parser.add_argument(*names, **kwargs) + continue + + # Check for List[X] + origin = get_origin(inner_type) + if origin is list or origin is List: + elem_args = get_args(inner_type) + elem_type = elem_args[0] if elem_args else str + type_func = arg_meta.type_parser or _infer_type_func(elem_type) + nargs = arg_meta.nargs or "+" + kwargs = dict( + type=type_func, + nargs=nargs, + help=arg_meta.help, + ) + if arg_meta.choices: + kwargs["choices"] = arg_meta.choices + if default is not _MISSING: + kwargs["default"] = default + parser.add_argument(*names, **kwargs) + continue + + # Bool → store_true + if inner_type is bool: + kwargs = dict(action="store_true", help=arg_meta.help) + if default is not _MISSING: + kwargs["default"] = default + parser.add_argument(*names, **kwargs) + continue + + # Scalar types (str, int, float, etc.) + type_func = arg_meta.type_parser or _infer_type_func(inner_type) + kwargs = dict(type=type_func, help=arg_meta.help) + if arg_meta.choices: + kwargs["choices"] = arg_meta.choices + if arg_meta.nargs: + kwargs["nargs"] = arg_meta.nargs + if default is not _MISSING: + kwargs["default"] = default + if ( + arg_meta.required is True + or (arg_meta.required is None and default is _MISSING) + ) and any(name.startswith("-") for name in names): + kwargs["required"] = True + parser.add_argument(*names, **kwargs) diff --git a/python/sglang/srt/server_args.py b/python/sglang/srt/server_args.py index 606df4f6c..117b2b440 100644 --- a/python/sglang/srt/server_args.py +++ b/python/sglang/srt/server_args.py @@ -32,6 +32,7 @@ from functools import cached_property from typing import Any, Callable, Dict, List, Literal, Optional, Union from sglang.jit_kernel.kv_canary.consts import RealKvHashMode +from sglang.srt.arg_groups.arg_utils import A, Arg, add_cli_args_from_dataclass from sglang.srt.arg_groups.argparse_actions import ( DeprecatedAction, DeprecatedAliasStoreAction, @@ -39,9 +40,7 @@ from sglang.srt.arg_groups.argparse_actions import ( DeprecatedStoreTrueAction, LoRAPathAction, ) -from sglang.srt.configs.linear_attn_model_registry import ( - get_linear_attn_spec_by_arch, -) +from sglang.srt.configs.linear_attn_model_registry import get_linear_attn_spec_by_arch from sglang.srt.connector import ConnectorType from sglang.srt.distributed.device_communicators.mooncake_transfer_engine import ( parse_ib_device_config, @@ -372,51 +371,177 @@ def add_linear_attn_kernel_backend_choices(choices): @dataclasses.dataclass class ServerArgs: - """ - The arguments of the server. + """The arguments of the server. - NOTE: When you add new arguments, please make sure the order - in this class definition the same as the order in the function - `ServerArgs.add_cli_args`. - Please follow the existing style to group the new arguments into related groups or create new groups. + There are two styles for defining arguments. New arguments MUST use the + ``A[T, ...]`` style; the legacy style is being migrated and should not be + used for new additions. + + **Style 1 — ``A[T, ...]`` (required for all new arguments):** + + Each field carries its own CLI metadata. ``A`` is an alias for + ``typing.Annotated``. For simple fields, use a bare string as the + help text. Use ``Arg(...)`` only when extra metadata is needed + (choices, aliases, custom type parser, etc.):: + + # Simple — bare string is the help text: + host: A[str, "The host of the HTTP server."] = "127.0.0.1" + trust_remote_code: A[bool, "Whether to allow custom models."] = False + + # With extra metadata: + load_format: A[str, Arg(help="Format.", choices=LOAD_FORMAT_CHOICES)] = "auto" + model_path: A[str, Arg(help="Path to model.", aliases=["--model"])] + + **Style 2 — Legacy (existing arguments, to be migrated):** + + The field is a plain type annotation with a default, and a separate + ``parser.add_argument(...)`` call in ``add_cli_args`` defines the CLI + surface. When modifying these fields, keep the order in this class + definition consistent with the order in ``add_cli_args``. + + Group new arguments into the appropriate existing section or create a + new section as needed. """ # Model and tokenizer - model_path: str - tokenizer_path: Optional[str] = None - tokenizer_mode: str = "auto" - tokenizer_backend: str = "huggingface" - tokenizer_worker_num: int = 1 - detokenizer_worker_num: int = 1 - skip_tokenizer_init: bool = False - load_format: str = "auto" - model_loader_extra_config: str = "{}" - trust_remote_code: bool = False - context_length: Optional[int] = None - is_embedding: bool = False - prefill_only_disable_kv_cache: bool = False - enable_multimodal: Optional[bool] = None - revision: Optional[str] = None - model_impl: str = "auto" - model_config_parser: str = "auto" + model_path: A[ + str, + Arg( + help="The path of the model weights. This can be a local folder or a Hugging Face repo ID.", + aliases=["--model"], + ), + ] + tokenizer_path: A[Optional[str], "The path of the tokenizer."] = None + tokenizer_mode: A[ + str, + Arg( + help="Tokenizer mode. 'auto' will use the fast tokenizer if available, " + "and 'slow' will always use the slow tokenizer.", + choices=["auto", "slow"], + ), + ] = "auto" + tokenizer_backend: A[ + str, + Arg( + help="Tokenizer backend. 'huggingface' uses the default HuggingFace " + "tokenizers library, and 'fastokens' uses the fastokens library " + "for faster tokenization. Requires the fastokens package to be installed.", + choices=["huggingface", "fastokens"], + ), + ] = "huggingface" + tokenizer_worker_num: A[int, "The worker num of the tokenizer manager."] = 1 + detokenizer_worker_num: A[int, "The worker num of the detokenizer manager."] = 1 + skip_tokenizer_init: A[ + bool, "If set, skip init tokenizer and pass input_ids in generate request." + ] = False + load_format: A[ + str, + Arg( + help="The format of the model weights to load. " + '"auto" will try to load the weights in the safetensors format ' + "and fall back to the pytorch bin format if safetensors format " + "is not available. " + '"pt" will load the weights in the pytorch bin format. ' + '"safetensors" will load the weights in the safetensors format. ' + '"npcache" will load the weights in pytorch format and store ' + "a numpy cache to speed up the loading. " + '"dummy" will initialize the weights with random values, ' + "which is mainly for profiling." + '"gguf" will load the weights in the gguf format. ' + '"bitsandbytes" will load the weights using bitsandbytes ' + "quantization." + '"layered" loads weights layer by layer so that one can quantize a ' + "layer before loading another to make the peak memory envelope " + "smaller.", + choices=LOAD_FORMAT_CHOICES, + ), + ] = "auto" + model_loader_extra_config: A[ + str, + "Extra config for model loader. This will be passed to the model loader corresponding to the chosen load_format.", + ] = "{}" + trust_remote_code: A[ + bool, + "Whether or not to allow for custom models defined on the Hub in their own modeling files.", + ] = False + context_length: A[ + Optional[int], + Arg( + help="The model's maximum context length. Defaults to None (will use the value from the model's config.json instead)." + f"\n\n{human_readable_int.__doc__}", + type_parser=human_readable_int, + ), + ] = None + is_embedding: A[bool, "Whether to use a CausalLM as an embedding model."] = False + enable_multimodal: A[ + Optional[bool], + "Enable the multimodal functionality for the served model. If the model being served is not multimodal, nothing will happen", + ] = None + revision: A[ + Optional[str], + "The specific model version to use. It can be a branch name, a tag name, or a commit id. If unspecified, will use the default version.", + ] = None + model_impl: A[ + str, + Arg( + help=( + "Which implementation of the model to use.\n\n" + '* "auto" will try to use the SGLang implementation if it exists ' + "and fall back to the Transformers implementation if no SGLang " + "implementation is available.\n" + '* "sglang" will use the SGLang model implementation.\n' + '* "transformers" will use the Transformers model ' + '* "mindspore" will use the MindSpore model ' + "implementation.\n" + ) + ), + ] = "auto" + model_config_parser: A[ + str, + Arg( + help=( + 'Which model-config parser to use. "auto" picks "mistral" ' + 'via the is_mistral_model name heuristic, else "hf" ' + "(AutoConfig over config.json). Plugins can register additional " + "parsers via @register_model_config_parser." + ) + ), + ] = "auto" # HTTP server - host: str = "127.0.0.1" - port: int = 30000 - fastapi_root_path: str = "" - grpc_mode: bool = False - skip_server_warmup: bool = False - warmups: Optional[str] = None - nccl_port: Optional[int] = None - checkpoint_engine_wait_weights_before_ready: bool = False + host: A[str, "The host of the HTTP server."] = "127.0.0.1" + port: A[int, "The port of the HTTP server."] = 30000 + fastapi_root_path: A[str, "App is behind a path based routing proxy."] = "" + grpc_mode: A[bool, "If set, use gRPC server instead of HTTP server."] = False + skip_server_warmup: A[bool, "If set, skip warmup."] = False + warmups: A[ + Optional[str], + "Specify custom warmup functions (csv) to run before server starts eg. --warmups=warmup_name1,warmup_name2 will run the functions `warmup_name1` and `warmup_name2` specified in warmup.py before the server starts listening for requests", + ] = None + nccl_port: A[ + Optional[int], + "The port for NCCL distributed environment setup. Defaults to a random port.", + ] = None + checkpoint_engine_wait_weights_before_ready: A[ + bool, + "If set, the server will wait for initial weights to be loaded via checkpoint-engine or other update methods before serving inference requests.", + ] = False # SSL/TLS - ssl_keyfile: Optional[str] = None - ssl_certfile: Optional[str] = None - ssl_ca_certs: Optional[str] = None - ssl_keyfile_password: Optional[str] = None - enable_ssl_refresh: bool = False - enable_http2: bool = False + ssl_keyfile: A[Optional[str], "The file path to the SSL key file."] = None + ssl_certfile: A[Optional[str], "The file path to the SSL certificate file."] = None + ssl_ca_certs: A[Optional[str], "The CA certificates file."] = None + ssl_keyfile_password: A[ + Optional[str], "The password to decrypt the SSL keyfile." + ] = None + enable_ssl_refresh: A[ + bool, + "Enable automatic SSL certificate hot-reloading when cert/key files change on disk. Requires --ssl-certfile and --ssl-keyfile.", + ] = False + enable_http2: A[ + bool, + "Use Granian instead of Uvicorn as the ASGI server, enabling HTTP/1.1 and HTTP/2 auto-negotiation. Clients may use h2c (cleartext HTTP/2) or plain HTTP/1.1. Requires 'pip install sglang[http2]'.", + ] = False # Quantization and data type dtype: str = "auto" @@ -752,6 +877,10 @@ class ServerArgs: enable_mis: bool = False # Optimization/debug options + prefill_only_disable_kv_cache: A[ + bool, + "Skip the physical KV cache allocation for embedding-mode prefill-only workloads. Currently only valid with --is-embedding, --chunked-prefill-size=-1, --disable-radix-cache, an FA prefill backend, and non-FP4 KV cache so the fa_skip_kv_cache path is active (no layer reads or writes the cache). Other prefill-only workloads such as scoring/MIS may benefit from this later once their attention paths stop using paged KV. Scheduler admission accounting is unchanged; per-layer K/V tensors are sized to (page_size, head_num, head_dim) placeholders so GPU memory is not wasted.", + ] = False disable_radix_cache: bool = False disable_cuda_graph_padding: bool = False enable_profile_cuda_graph: bool = False @@ -4728,232 +4857,8 @@ class ServerArgs: @staticmethod def add_cli_args(parser: argparse.ArgumentParser): - # Model and tokenizer - parser.add_argument( - "--model-path", - "--model", - type=str, - help="The path of the model weights. This can be a local folder or a Hugging Face repo ID.", - required=True, - ) - parser.add_argument( - "--tokenizer-path", - type=str, - default=ServerArgs.tokenizer_path, - help="The path of the tokenizer.", - ) - parser.add_argument( - "--tokenizer-mode", - type=str, - default=ServerArgs.tokenizer_mode, - choices=["auto", "slow"], - help="Tokenizer mode. 'auto' will use the fast " - "tokenizer if available, and 'slow' will " - "always use the slow tokenizer.", - ) - parser.add_argument( - "--tokenizer-backend", - type=str, - default=ServerArgs.tokenizer_backend, - choices=["huggingface", "fastokens"], - help="Tokenizer backend. 'huggingface' uses the default HuggingFace " - "tokenizers library, and 'fastokens' uses the fastokens library " - "for faster tokenization. Requires the fastokens package to be installed.", - ) - parser.add_argument( - "--tokenizer-worker-num", - type=int, - default=ServerArgs.tokenizer_worker_num, - help="The worker num of the tokenizer manager.", - ) - parser.add_argument( - "--detokenizer-worker-num", - type=int, - default=ServerArgs.detokenizer_worker_num, - help="The worker num of the detokenizer manager.", - ) - parser.add_argument( - "--skip-tokenizer-init", - action="store_true", - help="If set, skip init tokenizer and pass input_ids in generate request.", - ) - parser.add_argument( - "--load-format", - type=str, - default=ServerArgs.load_format, - choices=LOAD_FORMAT_CHOICES, - help="The format of the model weights to load. " - '"auto" will try to load the weights in the safetensors format ' - "and fall back to the pytorch bin format if safetensors format " - "is not available. " - '"pt" will load the weights in the pytorch bin format. ' - '"safetensors" will load the weights in the safetensors format. ' - '"npcache" will load the weights in pytorch format and store ' - "a numpy cache to speed up the loading. " - '"dummy" will initialize the weights with random values, ' - "which is mainly for profiling." - '"gguf" will load the weights in the gguf format. ' - '"bitsandbytes" will load the weights using bitsandbytes ' - "quantization." - '"layered" loads weights layer by layer so that one can quantize a ' - "layer before loading another to make the peak memory envelope " - "smaller.", - ) - parser.add_argument( - "--model-loader-extra-config", - type=str, - help="Extra config for model loader. " - "This will be passed to the model loader corresponding to the chosen load_format.", - default=ServerArgs.model_loader_extra_config, - ) - parser.add_argument( - "--trust-remote-code", - action="store_true", - help="Whether or not to allow for custom models defined on the Hub in their own modeling files.", - ) - parser.add_argument( - "--context-length", - type=human_readable_int, - default=ServerArgs.context_length, - help="The model's maximum context length. Defaults to None (will use the value from the model's config.json instead)." - + f"\n\n{human_readable_int.__doc__}", - ) - parser.add_argument( - "--is-embedding", - action="store_true", - help="Whether to use a CausalLM as an embedding model.", - ) - parser.add_argument( - "--prefill-only-disable-kv-cache", - action="store_true", - help="Skip the physical KV cache allocation for embedding-mode prefill-only workloads. Currently only valid with --is-embedding, --chunked-prefill-size=-1, --disable-radix-cache, an FA prefill backend, and non-FP4 KV cache so the fa_skip_kv_cache path is active (no layer reads or writes the cache). Other prefill-only workloads such as scoring/MIS may benefit from this later once their attention paths stop using paged KV. Scheduler admission accounting is unchanged; per-layer K/V tensors are sized to (page_size, head_num, head_dim) placeholders so GPU memory is not wasted.", - ) - parser.add_argument( - "--enable-multimodal", - default=ServerArgs.enable_multimodal, - action="store_true", - help="Enable the multimodal functionality for the served model. If the model being served is not multimodal, nothing will happen", - ) - parser.add_argument( - "--revision", - type=str, - default=None, - help="The specific model version to use. It can be a branch " - "name, a tag name, or a commit id. If unspecified, will use " - "the default version.", - ) - parser.add_argument( - "--model-impl", - type=str, - default=ServerArgs.model_impl, - help="Which implementation of the model to use.\n\n" - '* "auto" will try to use the SGLang implementation if it exists ' - "and fall back to the Transformers implementation if no SGLang " - "implementation is available.\n" - '* "sglang" will use the SGLang model implementation.\n' - '* "transformers" will use the Transformers model ' - '* "mindspore" will use the MindSpore model ' - "implementation.\n", - ) - parser.add_argument( - "--model-config-parser", - type=str, - default=ServerArgs.model_config_parser, - help='Which model-config parser to use. "auto" picks "mistral" ' - 'via the is_mistral_model name heuristic, else "hf" ' - "(AutoConfig over config.json). Plugins can register additional " - "parsers via @register_model_config_parser.", - ) - - # HTTP server - parser.add_argument( - "--host", - type=str, - default=ServerArgs.host, - help="The host of the HTTP server.", - ) - parser.add_argument( - "--port", - type=int, - default=ServerArgs.port, - help="The port of the HTTP server.", - ) - parser.add_argument( - "--fastapi-root-path", - type=str, - default=ServerArgs.fastapi_root_path, - help="App is behind a path based routing proxy.", - ) - parser.add_argument( - "--grpc-mode", - action="store_true", - help="If set, use gRPC server instead of HTTP server.", - ) - parser.add_argument( - "--skip-server-warmup", - action="store_true", - help="If set, skip warmup.", - ) - parser.add_argument( - "--warmups", - type=str, - required=False, - help="Specify custom warmup functions (csv) to run before server starts eg. --warmups=warmup_name1,warmup_name2 " - "will run the functions `warmup_name1` and `warmup_name2` specified in warmup.py before the server starts listening for requests", - ) - parser.add_argument( - "--nccl-port", - type=int, - default=ServerArgs.nccl_port, - help="The port for NCCL distributed environment setup. Defaults to a random port.", - ) - parser.add_argument( - "--checkpoint-engine-wait-weights-before-ready", - action="store_true", - help="If set, the server will wait for initial weights to be loaded via checkpoint-engine or other update methods " - "before serving inference requests.", - ) - - # SSL/TLS - parser.add_argument( - "--ssl-keyfile", - type=str, - default=ServerArgs.ssl_keyfile, - help="The file path to the SSL key file.", - ) - parser.add_argument( - "--ssl-certfile", - type=str, - default=ServerArgs.ssl_certfile, - help="The file path to the SSL certificate file.", - ) - parser.add_argument( - "--ssl-ca-certs", - type=str, - default=ServerArgs.ssl_ca_certs, - help="The CA certificates file.", - ) - parser.add_argument( - "--ssl-keyfile-password", - type=str, - default=ServerArgs.ssl_keyfile_password, - help="The password to decrypt the SSL keyfile.", - ) - parser.add_argument( - "--enable-ssl-refresh", - action="store_true", - default=ServerArgs.enable_ssl_refresh, - help="Enable automatic SSL certificate hot-reloading when cert/key " - "files change on disk. Requires --ssl-certfile and --ssl-keyfile.", - ) - parser.add_argument( - "--enable-http2", - action="store_true", - default=ServerArgs.enable_http2, - help="Use Granian instead of Uvicorn as the ASGI server, enabling HTTP/1.1 and " - "HTTP/2 auto-negotiation. Clients may use h2c (cleartext HTTP/2) or plain HTTP/1.1. " - "Requires 'pip install sglang[http2]'.", - ) + # Auto-derived from Annotated[..., Arg(...)] field metadata. + add_cli_args_from_dataclass(parser, ServerArgs) # Quantization and data type parser.add_argument(