diff --git a/docs/cookbook/diffusion/MiniMax/MiniMax-H3.mdx b/docs/cookbook/diffusion/MiniMax/MiniMax-H3.mdx index af5e03200..96e7fe8c3 100644 --- a/docs/cookbook/diffusion/MiniMax/MiniMax-H3.mdx +++ b/docs/cookbook/diffusion/MiniMax/MiniMax-H3.mdx @@ -410,12 +410,19 @@ for variant in 0 1; do done ``` -### Choose a quality profile +### Choose the quality level -`quality` is a request-scoped sampling parameter. One resident server can -switch between all four profiles; an approximate request mounts its audited -Cache-DiT policy at the batch boundary, and a later `lossless` request removes -the hooks before denoising. +`quality` is a request-scoped sampling parameter with two validated levels: + +- `"lossless"` (default): the exact reference path. Output is bit-exact + against the reference implementation and the CI ground truth. +- `"high"`: the audited accelerated path. Quality is guaranteed (the audited + Cache-DiT configuration measures SSIM 0.931 / PSNR 28.16 dB against + `lossless`), but output is no longer bit-identical to the reference. + +One resident server serves both levels; a `quality: "high"` request mounts +its audited Cache-DiT policy at the batch boundary, and a later +`quality: "lossless"` request removes the hooks before denoising. Start the validated server once: @@ -434,13 +441,14 @@ sglang serve \ --port 30010 ``` -Then choose a request tag: +Then choose a request level: - + -Native denoising with no feature-cache approximation. This is the default. +Native denoising with no feature-cache approximation. This is the default; +omitting the field is equivalent. ```json Request field { @@ -452,8 +460,8 @@ Native denoising with no feature-cache approximation. This is the default. -The least aggressive approximate profile. Use it when output should stay -closest to the same-seed lossless trajectory. +The audited accelerated path. Use it when you can trade bit-exactness for +latency while keeping output closest to the same-seed lossless trajectory. ```json Request field { @@ -463,32 +471,6 @@ closest to the same-seed lossless trajectory. - - -The balanced profile: substantially lower latency with a larger change from -the same-seed lossless output. - -```json Request field -{ - "quality": "medium" -} -``` - - - - - -The fastest validated profile and the largest visual deviation. Use it for -latency-sensitive previews and high-throughput generation. - -```json Request field -{ - "quality": "low" -} -``` - - - The measured trade-off is: @@ -497,8 +479,6 @@ The measured trade-off is: | --- | ---: | ---: | ---: | ---: | --- | | `lossless` | 75.10 s | 1.00× | 1.000 | exact | Native reference path | | `high` | 53.70 s | 1.40× | 0.931 | 28.16 dB | Smallest same-seed visual change | -| `medium` | 30.23 s | 2.48× | 0.818 | 20.40 dB | Balanced latency and visual deviation | -| `low` | 25.81 s | 2.91× | 0.794 | 19.25 dB | Largest deviation; fastest preview path | These numbers use 1344×768, 124-frame, 24 fps T2VA with 50 inference steps, video flow shift 12, audio flow shift 3, and three fixed prompt/seed pairs on @@ -506,26 +486,26 @@ video flow shift 12, audio flow shift 3, and three fixed prompt/seed pairs on and a moving close-up portrait. `inference_time_s` is averaged across the three prompts; the quiet-scene point is itself the mean of two repeats. -SSIM and PSNR compare decoded, frame-aligned output with the `lossless` result -for the same prompt and seed. They measure trajectory deviation, not absolute -perceptual quality: an approximate profile can produce a different but still -plausible realization. The profiles also change the joint audio-video denoise +SSIM and PSNR compare decoded, frame-aligned output with the `lossless` +result for the same prompt and seed. They measure trajectory deviation, not +absolute perceptual quality: the `high` path can produce a different but +still plausible realization. It also changes the joint audio-video denoise trajectory, while these two metrics cover video only. -Approximate profiles currently accept only the exact workload and 4×H200 +`quality: "high"` currently accepts only the exact workload and 4×H200 deployment above; other hardware, task modes, request shapes, step counts, or -flow shifts fail before denoising. Offline generation uses the same profile -name, for example `sglang generate --quality medium`. +flow shifts fail before denoising. Offline generation uses the same level +name, for example `sglang generate --quality high`. -`quality` selects a model sampling profile and can change generated content. +`quality` selects a model sampling level and can change generated content. `output_quality` controls only output-file compression; it is a separate field. -For manually tuned Cache-DiT experiments outside that validated profile, omit +For manually tuned Cache-DiT experiments outside that validated path, omit the request `quality` field and set the process-wide environment controls -directly. An explicit `quality: lossless` request overrides those controls and -restores native denoising: +directly. An explicit `quality: "lossless"` request overrides those controls +and restores native denoising: ```bash Command SGLANG_CACHE_DIT_ENABLED=true \ @@ -548,8 +528,8 @@ Cache-DiT skips selected block computation and is approximate. It cannot be combined with FSDP inference or DiT layerwise offload. Breakable CUDA graph execution takes precedence and leaves Cache-DiT disabled. Tune the cache thresholds only after comparing both video and audio quality on the target -task profile. A real B200 request has completed, but the named profiles above -remain fail-closed to the audited 4×H200 workload. +task profile. A real B200 request has completed, but the `quality: "high"` +path above remains fail-closed to the audited 4×H200 workload. ## 6. Runtime feature recipes @@ -646,7 +626,7 @@ real H3 validation runs. - MiniMax-H3 produces the canonical 24 fps output; request duration is expressed through `target.duration_seconds`. - `target.duration_seconds` must be between 4 and 15 seconds, inclusive. The command picker defaults to the verified 5-second profile. -- Use a 768-pixel short edge for the released quality profile. The aligned output dimensions are derived from `target.aspect_ratio`. +- Use a 768-pixel short edge for the released quality recipe. The aligned output dimensions are derived from `target.aspect_ratio`. - `flow_shift` controls video diffusion and `audio_flow_shift` controls audio diffusion. - V2V uses `task: "ref2va"` with a `video` or `video_audio` reference; it is served by the `Ref2VA` partition and is not a separate public task value. - `conditions[].start_time_seconds` selects a non-negative offset for a video reference. Its visual and audio streams are always sought together. diff --git a/docs/docs/sglang-diffusion/api/cli.mdx b/docs/docs/sglang-diffusion/api/cli.mdx index 6c0789947..0cb31ad31 100644 --- a/docs/docs/sglang-diffusion/api/cli.mdx +++ b/docs/docs/sglang-diffusion/api/cli.mdx @@ -105,7 +105,7 @@ Use `sglang generate --help` and `sglang serve --help` for the full argument lis - `--image-path {PATH} [{PATH} ...]`: input image(s) for image-to-video or image-to-image generation - `--num-inference-steps {STEPS}` and `--seed {SEED}` - `--num-outputs-per-prompt {N}` / `--num-outputs {N}`: generate multiple outputs for each prompt. A scalar seed expands as `seed + output_index`. -- `--quality {PROFILE}`: select a model-owned request quality/performance profile. Supported names and deployment constraints are model-specific. +- `--quality {lossless,high}`: request-level quality. `lossless` (default) keeps the exact reference path, bit-exact against the reference implementation; `high` opts into the model-owned validated accelerated path, whose quality stays guaranteed but is not bit-exact. Support and validated deployment constraints are model-specific. - `--height {HEIGHT}`, `--width {WIDTH}`, `--num-frames {N}`, `--fps {FPS}` - `--output-path {PATH}`, `--output-file-name {NAME}`, `--save-output`, `--return-frames` diff --git a/python/sglang/multimodal_gen/configs/pipeline_configs/minimax_h3.py b/python/sglang/multimodal_gen/configs/pipeline_configs/minimax_h3.py index b42b70ea1..d3e6e4d03 100644 --- a/python/sglang/multimodal_gen/configs/pipeline_configs/minimax_h3.py +++ b/python/sglang/multimodal_gen/configs/pipeline_configs/minimax_h3.py @@ -84,7 +84,8 @@ class MiniMaxH3PipelineConfig(PipelineConfig): return getattr(value, "value", value) def validate_quality_deployment(self, server_args) -> None: - """Fail closed unless the resident server matches the measured profile.""" + """Fail closed unless the resident server matches the deployment + audited for quality="high".""" attention_backend = self._server_arg_value(server_args.attention_backend) attention_backend = ( @@ -161,7 +162,7 @@ class MiniMaxH3PipelineConfig(PipelineConfig): } if mismatches: raise ValueError( - "MiniMax-H3 approximate quality profiles are validated only for " + 'MiniMax-H3 quality="high" is validated only for ' f"the strict 4xH200 fl2va deployment; mismatches: {mismatches}" ) diff --git a/python/sglang/multimodal_gen/configs/sample/sampling_params.py b/python/sglang/multimodal_gen/configs/sample/sampling_params.py index c771c3ce8..c5636c507 100644 --- a/python/sglang/multimodal_gen/configs/sample/sampling_params.py +++ b/python/sglang/multimodal_gen/configs/sample/sampling_params.py @@ -51,6 +51,12 @@ def generate_request_id() -> str: return str(uuid.uuid4()) +# Validated request-level quality levels. "lossless" is the exact reference +# path (bit-exact against the CI golden outputs); "high" opts into validated +# accelerated paths whose quality is guaranteed but not bit-exact. +QUALITY_LEVELS: tuple[str, ...] = ("lossless", "high") + + def _sanitize_filename(name: str, replacement: str = "_", max_length: int = 150) -> str: """Create a filesystem- and ffmpeg-friendly filename. @@ -123,9 +129,20 @@ class SamplingParams: ) output_quality: str | None = "default" output_compression: int | None = None - # Model-owned, request-scoped approximate acceleration profile. Models - # that support it must validate the deployment and workload explicitly. - # It intentionally participates in the dynamic-batch signature. + # Model-owned, request-scoped quality level. + # + # - "lossless" (default): the exact reference path. Output is expected to + # be bit-identical to the HF reference implementation and to pass the + # CI golden/ground-truth comparisons. + # - "high": opt into validated accelerated paths. Quality stays + # guaranteed (the intent is to back every such path with mathematical + # acceptance thresholds, e.g. PSNR > 25 against the reference), but + # the output is no longer bit-exact versus the HF reference or the CI + # ground truth. + # + # Models that support "high" must validate the deployment and workload + # explicitly. It intentionally participates in the dynamic-batch + # signature. quality: str = "lossless" # Frame interpolation @@ -408,9 +425,10 @@ class SamplingParams: f"prompt_path must be a txt file, got {self.prompt_path!r}" ) - if not isinstance(self.quality, str) or not self.quality.strip(): + if self.quality not in QUALITY_LEVELS: raise ValueError( - f"quality must be a non-empty string, got {self.quality!r}" + f"quality must be one of {list(QUALITY_LEVELS)}, " + f"got {self.quality!r}" ) # These are always required to be sane regardless of pipeline. @@ -932,9 +950,14 @@ class SamplingParams: add_argument( "--quality", type=str, + choices=list(QUALITY_LEVELS), help=( - "Select a model-owned quality/performance profile. " - "Support and validated deployment constraints are model-specific." + "Request-level quality: 'lossless' (default) keeps the exact " + "reference path, bit-exact against the reference " + "implementation; 'high' opts into the model-owned validated " + "accelerated path, whose quality stays guaranteed but is not " + "bit-exact. Support and validated deployment constraints are " + "model-specific." ), ) add_argument( diff --git a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/constants.py b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/constants.py index 1f59adf35..61b79ed4a 100644 --- a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/constants.py +++ b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/constants.py @@ -27,11 +27,10 @@ MINIMAX_H3_MAX_DURATION_SECONDS = 15.0 # The distilled checkpoint has exactly one positive denoise branch. MINIMAX_H3_DEFAULT_BRANCHES: tuple = ({"name": "cond_1"},) -# Audited 4xH200 T2VA profiles. The tuple is +# Audited 4xH200 T2VA Cache-DiT parameters for quality="high": # (warmup steps, residual-difference threshold, max consecutive cached steps). -MINIMAX_H3_QUALITY_PROFILES: dict[str, tuple[int, float, int] | None] = { - "lossless": None, - "high": (4, 0.04, 1), - "medium": (4, 0.12, 3), - "low": (4, 0.24, 3), -} +# Measured SSIM 0.931 / PSNR 28.16 dB against quality="lossless" on the +# validated workload. quality="lossless" (the default) uses no Cache-DiT +# configuration at all. Process-wide SGLANG_CACHE_DIT_* environment controls +# remain available for manual experiments and are independent of this field. +MINIMAX_H3_HIGH_QUALITY_CACHE_DIT_CONFIG: tuple[int, float, int] = (4, 0.04, 1) diff --git a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/release_metadata.py b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/release_metadata.py index 14e375a3d..475de96aa 100644 --- a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/release_metadata.py +++ b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/release_metadata.py @@ -7,11 +7,9 @@ import math from dataclasses import dataclass from typing import Any, Mapping +from sglang.multimodal_gen.configs.sample.sampling_params import QUALITY_LEVELS from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req from sglang.multimodal_gen.runtime.pipelines_core.stages.base import PipelineStage -from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.constants import ( - MINIMAX_H3_QUALITY_PROFILES, -) from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.resolved_plan import ( minimax_h3_plan_from_batch, ) @@ -150,25 +148,23 @@ class MiniMaxH3PartitionAdmissionStage(PipelineStage): raise ValueError("MiniMax H3 request task must be a non-empty string") self.metadata.canonical_task(task) quality = getattr(batch.sampling_params, "quality", "lossless") - if quality not in MINIMAX_H3_QUALITY_PROFILES: + if quality not in QUALITY_LEVELS: raise ValueError( - f"unsupported MiniMax-H3 quality profile {quality!r}; supported: " - f"{list(MINIMAX_H3_QUALITY_PROFILES)}" + f"quality must be one of {list(QUALITY_LEVELS)}, got {quality!r}" ) - approximate = quality != "lossless" + high_quality = quality == "high" attention_backend = str(server_args.attention_backend or "").strip().lower() if attention_backend == "sage_attn" and not batch.is_warmup: raise ValueError( "MiniMax-H3 does not support SageAttention: the current packed " "varlen path does not preserve model output" ) - if approximate and not batch.is_warmup: + if high_quality and not batch.is_warmup: server_args.pipeline_config.validate_quality_deployment(server_args) plan = minimax_h3_plan_from_batch(batch) if plan is None: raise ValueError( - "MiniMax-H3 approximate quality profiles require a resolved " - "request plan" + 'MiniMax-H3 quality="high" requires a resolved request plan' ) shape = plan.shape actual = { @@ -212,7 +208,7 @@ class MiniMaxH3PartitionAdmissionStage(PipelineStage): ) if not exact or not shifts: raise ValueError( - "MiniMax-H3 approximate quality profiles are validated only for " + 'MiniMax-H3 quality="high" is validated only for ' f"{_MINIMAX_H3_QUALITY_WORKLOAD}; got {actual}" ) return batch diff --git a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/stages/denoising.py b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/stages/denoising.py index 802388c6f..568e82a0c 100644 --- a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/stages/denoising.py +++ b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/stages/denoising.py @@ -24,7 +24,7 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.denoising import ( DenoisingStage, ) from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.constants import ( - MINIMAX_H3_QUALITY_PROFILES, + MINIMAX_H3_HIGH_QUALITY_CACHE_DIT_CONFIG, ) from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.minimax_h3.task_profiles import ( MINIMAX_H3_FL2VA_KEYFRAME_SIGNATURES, @@ -387,7 +387,7 @@ class MiniMaxH3DenoisingStage(DenoisingStage): scheduler=None, pipeline=pipeline, ) - self._minimax_h3_quality_profile = "lossless" + self._minimax_h3_quality = "lossless" self._minimax_h3_cache_mode: str | None = None def _owns_compile_warmup_lifecycle(self) -> bool: @@ -395,7 +395,7 @@ class MiniMaxH3DenoisingStage(DenoisingStage): def _cache_dit_requested(self) -> bool: return ( - getattr(self, "_minimax_h3_quality_profile", "lossless") != "lossless" + getattr(self, "_minimax_h3_quality", "lossless") == "high" or super()._cache_dit_requested() ) @@ -403,19 +403,15 @@ class MiniMaxH3DenoisingStage(DenoisingStage): self, num_inference_steps: int | tuple[int, int], batch: Req ) -> None: quality = getattr(batch.sampling_params, "quality", "lossless") - if quality not in MINIMAX_H3_QUALITY_PROFILES: - raise ValueError(f"unsupported MiniMax-H3 quality profile {quality!r}") explicit_fields = getattr(batch.sampling_params, "_explicit_fields", ()) generic_requested = ( super()._cache_dit_requested() and "quality" not in explicit_fields ) desired_mode = ( - quality - if quality != "lossless" - else ("generic" if generic_requested else None) + "high" if quality == "high" else ("generic" if generic_requested else None) ) current_mode = getattr(self, "_minimax_h3_cache_mode", None) - self._minimax_h3_quality_profile = quality + self._minimax_h3_quality = quality # H3 is monolithic-only, and the scheduler executes one worker batch at # a time. Combined with `quality` in the dynamic-batch signature, this @@ -435,7 +431,7 @@ class MiniMaxH3DenoisingStage(DenoisingStage): def _cache_dit_scm_masks( self, primary_num_steps: int, secondary_num_steps: int | None = None ) -> tuple[str, str, list[int] | None, list[int] | None]: - if getattr(self, "_minimax_h3_quality_profile", "lossless") != "lossless": + if getattr(self, "_minimax_h3_quality", "lossless") == "high": return "none", "dynamic", None, None return super()._cache_dit_scm_masks(primary_num_steps, secondary_num_steps) @@ -447,16 +443,14 @@ class MiniMaxH3DenoisingStage(DenoisingStage): *, secondary: bool = False, ) -> CacheDitConfig: - quality = getattr(self, "_minimax_h3_quality_profile", "lossless") - profile = MINIMAX_H3_QUALITY_PROFILES[quality] - if profile is None or secondary: + if secondary or getattr(self, "_minimax_h3_quality", "lossless") != "high": return super()._build_cache_dit_config( num_inference_steps, steps_computation_mask, scm_policy, secondary=secondary, ) - warmup, threshold, max_cached = profile + warmup, threshold, max_cached = MINIMAX_H3_HIGH_QUALITY_CACHE_DIT_CONFIG return CacheDitConfig( enabled=True, Fn_compute_blocks=1, diff --git a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/video_adapter.py b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/video_adapter.py index 7ab71a609..a51c69099 100644 --- a/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/video_adapter.py +++ b/python/sglang/multimodal_gen/runtime/pipelines_core/stages/model_specific_stages/minimax_h3/video_adapter.py @@ -11,6 +11,7 @@ from typing import TYPE_CHECKING, Any from sglang.multimodal_gen.configs.pipeline_configs.minimax_h3 import ( MiniMaxH3PipelineConfig, ) +from sglang.multimodal_gen.configs.sample.sampling_params import QUALITY_LEVELS from sglang.multimodal_gen.runtime.entrypoints.openai.protocol import ( VideoGenerationsRequest, ) @@ -104,12 +105,14 @@ class MiniMaxH3VideoModelAdapter: request: VideoGenerationsRequest, name: str, ) -> str | None: - value = _parse_extra_value(_extra_value(request, name)) + value = _extra_value(request, name) if value is None: return None - if not isinstance(value, str) or not value.strip(): - raise ValueError(f"{name} must be a non-empty string") - return value.strip().lower() + if value not in QUALITY_LEVELS: + raise ValueError( + f"{name} must be one of {list(QUALITY_LEVELS)}, got {value!r}" + ) + return value @staticmethod def _reject_retired_cfg_fields(kwargs: dict[str, Any]) -> None: diff --git a/python/sglang/multimodal_gen/test/unit/test_minimax_h3_admission.py b/python/sglang/multimodal_gen/test/unit/test_minimax_h3_admission.py index 229f109be..19b8ee681 100644 --- a/python/sglang/multimodal_gen/test/unit/test_minimax_h3_admission.py +++ b/python/sglang/multimodal_gen/test/unit/test_minimax_h3_admission.py @@ -208,6 +208,21 @@ def test_video_adapter_lowers_only_native_fields_and_rejects_cfg(): ) +@pytest.mark.parametrize("bad_quality", ["ultra", "draft", "", 1]) +def test_video_adapter_rejects_invalid_quality(bad_quality): + request = VideoGenerationsRequest( + prompt="contract", + task="t2va", + conditions=[], + target=TARGET, + quality=bad_quality, + ) + with pytest.raises(ValueError, match="quality must be one of"): + MiniMaxH3SamplingParams.lower_video_request_kwargs( + request, {"prompt": request.prompt, "seed": request.seed} + ) + + class _HopperCapability: def to_int(self) -> int: return 90 @@ -289,7 +304,7 @@ def test_quality_admission_fails_closed_outside_validated_request(): with pytest.raises(ValueError, match="does not support SageAttention"): stage.forward(batch, server_args) - batch.sampling_params.quality = "unsupported" + batch.sampling_params.quality = "ultra" server_args.attention_backend = None - with pytest.raises(ValueError, match="unsupported MiniMax-H3 quality profile"): + with pytest.raises(ValueError, match="quality must be one of"): stage.forward(batch, server_args) diff --git a/python/sglang/multimodal_gen/test/unit/test_sampling_params.py b/python/sglang/multimodal_gen/test/unit/test_sampling_params.py index 7beca3bd2..cfdf58f2a 100644 --- a/python/sglang/multimodal_gen/test/unit/test_sampling_params.py +++ b/python/sglang/multimodal_gen/test/unit/test_sampling_params.py @@ -41,12 +41,17 @@ class TestSamplingParamsValidate(unittest.TestCase): with self.assertRaisesRegex(ValueError, r"num_outputs_per_prompt"): SamplingParams(num_outputs_per_prompt=0) - def test_quality_must_be_a_non_empty_profile_name(self): + def test_quality_defaults_to_lossless(self): + self.assertEqual(SamplingParams().quality, "lossless") + + def test_quality_accepts_the_two_validated_levels(self): + self.assertEqual(SamplingParams(quality="lossless").quality, "lossless") self.assertEqual(SamplingParams(quality="high").quality, "high") - with self.assertRaisesRegex(ValueError, r"quality must be a non-empty string"): - SamplingParams(quality="") - with self.assertRaisesRegex(ValueError, r"quality must be a non-empty string"): - SamplingParams(quality=True) # type: ignore[arg-type] + + def test_quality_rejects_invalid_values(self): + for bad in ("ultra", "draft", "fast", "", True, 1): + with self.assertRaisesRegex(ValueError, r"quality must be one of"): + SamplingParams(quality=bad) # type: ignore[arg-type] def test_seed_accepts_int_or_non_empty_int_list(self): self.assertEqual(SamplingParams(seed=7).seed, 7) @@ -235,7 +240,7 @@ class TestSamplingParamsCliArgs(unittest.TestCase): def test_quality_is_request_scoped_cli_arg(self): self.assertNotIn("quality", self._parse_cli_kwargs([])) self.assertEqual( - self._parse_cli_kwargs(["--quality", "medium"])["quality"], "medium" + self._parse_cli_kwargs(["--quality", "high"])["quality"], "high" ) def test_qwen_image_cli_path_preserves_model_defaults(self):