fix(cli): make the drifted server flag spellings work on both binaries - #1125
Merged
Conversation
`mlxcel serve` (`ServeArgs`) and `mlxcel-server` (`ServerArgs`) are two hand-maintained clap definitions of the same server sharing 112 flag spellings. Four had drifted. The worst was parallel slots: `--n-parallel` worked only on `serve`, `--parallel` only on `mlxcel-server`, so a command line copied between the two failed with `error: unexpected argument '--parallel' found` even though both flags read the same `LLAMA_ARG_N_PARALLEL` env var. The repository already establishes the intended pattern twice in the same structs, on `--draft-model` / `--model-draft` and `--draft-max` / `--draft`, whose doc comments state the goal outright. Each missing spelling is added as a `visible_alias`, matching what the drafter flags do: `--parallel` on `ServeArgs::n_parallel`, `--n-parallel` on `ServerArgs::parallel`, `--predict` on `ServeArgs::n_predict`, `--adapter` on `ServerArgs::lora`. No primary spelling that worked before stops working. The DRY breakers needed a primary chosen rather than an alias added, because the two binaries disagreed on the name itself. The singular `--dry-sequence-breaker` becomes primary on both: it is what llama-server uses, it is what `mlxcel-server` already used, and the flags in this group carry `LLAMA_ARG_*` env vars precisely because llama-server compatibility is the point. The plural `--dry-sequence-breakers` that `mlxcel serve` used to require is kept as a `visible_alias` on both binaries. The unrelated help sentence added by #1118 on `SamplingOptions::dry_multiplier`, and the scope comment in `src/commands/generate.rs`, both named the plural; they now name the primary. The `--n-parallel` / `--parallel` help paragraphs had forked into two wordings of the same text, one per spelling, which is how the name drift stayed invisible. They are reconciled to the more informative `mlxcel-server` wording, with a closing sentence that states the copied-command-line property and is identical on both binaries. `docs/CONTINUOUS_BATCHING.md` no longer presents the two spellings as one per binary. The durable half is `tests/cli_help_consistency.rs`. Comparing the whole flag surface would be noise (`mlxcel serve` legitimately carries `--estimate-memory` and `--force`), so the contract is a named list: `SHARED_SERVER_FLAG_GROUPS` requires both binaries to accept every spelling of each concept, whichever one each makes primary, and `SHARED_SERVER_FLAG_DESCRIPTIONS` requires the help prose to read identically for the subset where identical prose is correct. The two drafter groups are excluded from the prose check because their descriptions name each binary's own primary and alias roles, which are opposite by design. The comparison ignores the signature line and the bracketed `[env:]` / `[default:]` / `[alias:]` annotations by construction, so each binary keeps its own primary spelling, derived value name, and alias annotation. Both new helpers carry their own guard tests, since a matcher that silently matches nothing would make the contract vacuous. Parse-level assertions in `src/main_tests.rs` and the `mlx_server.rs` test module pin that each spelling resolves to the identical field value, mirroring the existing issue #464 alias tests. Validated with `cargo test --profile test-fast --features cuda --bin mlxcel aliases_resolve_identically`, the same selector on `--bin mlxcel-server`, `--test cli_help_consistency`, `cargo clippy --profile test-fast --features cuda --lib --tests -- -D warnings`, and `cargo fmt --all --check`. Closes #1109
Review follow-up for PR #1125. The named-concept list catches a regression of a pairing someone already wrote down; it cannot catch what issue #1109 actually asked for, a future divergence, because a flag added to one binary tomorrow is simply absent from the list and the suite stays green. The reason the issue offered the named list as a fallback was an assumption that a whole-surface comparison would be too noisy. Measuring it killed that assumption: the two binaries share 134 long-name spellings and differ by three (`--estimate-memory` and `--force` on `mlxcel serve`, `--version` on `mlxcel-server`, which carries it where `mlxcel` has it at the top level). `the_two_server_binaries_accept_the_same_flag_surface` compares the two surfaces against `SERVE_ONLY_FLAGS` / `SERVER_ONLY_FLAGS`, so a one-sided flag now fails until someone either adds it to the other binary or records it as deliberate. Both invariants are kept: the surface comparison sees a set of names, not which names are two spellings of one concept, so dropping `--parallel` from one binary and `--n-parallel` from the other in the same change would leave the surfaces equal and only the named list would notice. Three defects in the helpers, all found by review rather than by a failing test. `signature_long_name` did not handle clap's single-token `--flag[=<VALUE>]` rendering, so `--prompt-cache-enabled` and `--apc-enabled` (both already present on both binaries) would have made `shared_flag_entry` panic for a flag that is in fact correct; the marker is now stripped and both spellings are in the guard test. `flag_entry_by_long_name` searched the whole help, and `mlxcel-server` sets `flatten_help`, so its `--help` carries a second flag surface under `mlxcel-server download:` in which `--models-dir` and `--help` already appear twice; `top_level_help` cuts at that heading. `flag_description` dropped `[env: ...]` and `[default: ...]` along with `[alias: ...]`, which would have let two binaries agree on a flag name while disagreeing on its default or its environment variable, the exact property issue #1109 turned on; `flag_env_and_default` compares those two explicitly. `[profile.test-fast]` inherits `release`, so `debug-assertions` is off and clap's own duplicate-name `debug_assert` never runs in the profile this repository verifies with. A colliding `visible_alias` would have been silently last-wins. `serve_flag_names_and_aliases_are_unique` and `server_flag_names_and_aliases_are_unique` walk the built `Command` and assert uniqueness directly, so the guard holds in every profile. Two user-facing claims are narrowed to what is true. The shipped `--parallel` help said "so a command line copied between them parses unchanged"; short forms are still divergent (`mlxcel-server` has `-c` and `-n`, `mlxcel serve` has neither), so it now says "so this flag parses on either binary". `docs/CONTINUOUS_BATCHING.md` no longer attributes the alias to a particular binary and is reflowed. Validated with `cargo test --profile test-fast --features cuda --bin mlxcel tests::serve_` (13 passed), the same on `--bin mlxcel-server tests::` (13 passed), `--test cli_help_consistency` (25 passed, up from 17 on the base), `cargo clippy --profile test-fast --features cuda --lib --bins --tests -- -D warnings`, and `cargo fmt --all --check`. Both new assertions were mutation-checked against the real binaries and restored: removing `visible_alias = "n-parallel"` fails the named-list assertion, and removing `--force` from the allowlist fails the surface assertion with the shape a genuinely new one-sided flag would produce. Refs #1109
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.
Summary
mlxcel serveandmlxcel-serverare two hand-maintained clap definitions of the same server sharing 112 flag spellings, and four had drifted apart. Each missing spelling is added as avisible_alias, the DRY breakers get one primary spelling on both binaries, andtests/cli_help_consistency.rsgrows a named-list contract so a fifth divergence cannot land silently.Related issues
Closes #1109
Type of change
fix(bug fix)Why it mattered
--n-parallelworked only onmlxcel serve,--parallelonly onmlxcel-server, so a command line copied between the two failed witherror: unexpected argument '--parallel' found. Both flags read the sameLLAMA_ARG_N_PARALLELenv var, so the divergence was purely in the name, and an operator who worked around it via the env var would not discover the gap until someone else read the script.README.mdadvertises llama-server compatibility as a migration aid, and three of the four broke it onmlxcel serve.This is drift, not design. The repository already establishes the intended pattern twice in the same structs:
--draft-model/--model-draftand--draft-max/--draftare aliased both ways, and the doc comment on the first states the goal outright ("so commands copied between the two binaries work unchanged"). The four flags below simply never got the same treatment.What changed
Aliases, mirroring the drafter flags.
visible_alias = "parallel"onServeArgs::n_parallel,visible_alias = "n-parallel"onServerArgs::parallel,visible_alias = "predict"onServeArgs::n_predict,visible_alias = "adapter"onServerArgs::lora.visible_aliasrenders the alternate spelling in--help, which is what the existing drafter flags use. No primary spelling changes here, so nothing that worked before stops working.DRY breakers: one primary on both binaries. This row needed a decision rather than an alias, because the two binaries disagreed on the name itself (
--dry-sequence-breakersplural onserve,--dry-sequence-breakersingular onmlxcel-server). The singular becomes primary on both. It is what llama-server uses, it is whatmlxcel-serveralready used, and the flags in this group carryLLAMA_ARG_*env vars precisely because llama-server compatibility is the point. Unlike the drafter flags, there is no competing mlx-lm spelling to honor here, so this pair can do better than the drafters' opposite-primaries compromise and share a single primary. The plural is kept as avisible_aliason both binaries, so the spellingmlxcel serveused to require still parses.Two references to the old primary spelling are updated. The help sentence added by #1118 on
SamplingOptions::dry_multiplierand the scope comment atsrc/commands/generate.rs:921both named the plural; they now name the primary. Both remain accurate: the plural still works, but naming the primary is what keeps the help text pointing at the documented spelling.The forked
--parallelhelp paragraph is reconciled. The two binaries carried the same paragraph in two wordings, each edited to match its own spelling, which is how the name drift stayed invisible. They now share the more informativemlxcel-servertext plus a closing sentence stating the copied-command-line property, identical on both.docs/CONTINUOUS_BATCHING.mdno longer presents the two spellings as one per binary.tests/cli_help_consistency.rs, the durable half. Three assertions, each failing on something the others cannot:the_two_server_binaries_accept_the_same_flag_surfacecompares the whole long-name surface of the two binaries againstSERVE_ONLY_FLAGS/SERVER_ONLY_FLAGS. This is the one that catches a NEW divergence: a flag added to one binary and forgotten on the other fails immediately, with no list to remember to update. The issue offered a named list as a fallback on the assumption that a whole-surface comparison would be too noisy; measuring it killed that assumption. The two binaries share 134 spellings and differ by exactly three, so the allowlist is--estimate-memoryand--forceonmlxcel serve(subcommand-shaped one-shot actions) and--versiononmlxcel-server(mlxcelcarries it at the top level).SHARED_SERVER_FLAG_GROUPSlists six concepts (the four repaired here plus the two drafter pairs) and pins which alternate spellings belong to one concept, which a set comparison cannot express. It is the REGRESSION guard: dropping--parallelfrommlxcel-serverand--n-parallelfrommlxcel servein the same change would leave the two surfaces equal, and only this assertion would notice. It compares sorted sets, so which spelling each binary makes primary stays a free choice.SHARED_SERVER_FLAG_DESCRIPTIONSrequires the help prose, the environment variable, and the default value to match for the four non-drafter concepts. The drafter pairs are excluded because their descriptions name each binary's own primary and alias roles, which are opposite by design, so identical prose there would make one of the two wrong.Six helpers support this.
signature_long_namerequires the exact shape clap renders, so a prose line starting with--cannot anchor an entry, and it strips the[=<VALUE>]marker clap emits as part of the same token for an optional-value flag.flag_entry_by_long_nameanchors on the long name alone, because clap derives the value name from the Rust field name and the two binaries therefore render different ones for the same concept.top_level_helpcuts the help at the first flattened-subcommand heading, becausemlxcel-serversetsflatten_helpand its--helpcarries a second flag surface undermlxcel-server download:in which--models-dirand--helpalready appear twice.all_documented_spellings,flag_description, andflag_env_and_defaultsplit an entry into the three parts the contract treats differently.flag_help_entry's body-slicing loop is factored into a sharedentry_bodyso the two entry finders cannot disagree about where an entry ends.Two clap name-uniqueness guards.
[profile.test-fast]inheritsrelease, sodebug-assertionsis off and clap's own duplicate-namedebug_assertnever runs in the profile this repository verifies with: avisible_aliascolliding with an existing flag would be silently last-wins rather than a panic, and this PR adds four of them.serve_flag_names_and_aliases_are_uniqueandserver_flag_names_and_aliases_are_uniquewalk the builtCommandand assert uniqueness directly, so the guard holds in every profile.CHANGELOG.md: an### Fixedentry under## [Unreleased], because the accepted CLI surface changes on both binaries and one primary spelling changes onmlxcel serve.Parse-level assertions in
src/main_tests.rsand themlx_server.rstest module pin that each spelling resolves to the identical field value, mirroring the existing issue #464 alias tests. Both ends of the table are covered, including--adapter/--loraonmlxcel serve, which was already symmetric.The new assertion is not vacuous
A green contract test proves nothing unless it fails on a real regression, so this was checked three ways:
[=<VALUE>]form, the flattened-subcommand cut (and four inputs it must NOT cut), the surface collector, and the mirrored-entry case where two binaries with opposite primaries and different value names must still compare equal.dropping_a_shared_flag_alias_would_fail_the_spelling_parity_assertionsplices the livemlxcel-server --help, cuts the rendered[alias: --n-parallel]annotation out of the--parallelentry, and asserts the accepted-spelling set then collapses to["--parallel"]. This is the same idiom the existingremoving_the_rendered_alias_annotation_leaves_the_alias_undocumentedtest uses.visible_alias = "n-parallel"fromsrc/bin/mlx_server.rsfails the named-list assertion withleft: ["--parallel"], right: ["--n-parallel", "--parallel"]. Removing--forcefromSERVE_ONLY_FLAGSfails the whole-surface assertion withleft: {"--estimate-memory", "--force"}, right: {"--estimate-memory"}, which is the shape a genuinely new one-sided flag would produce.serve.len() > 100 && server.len() > 100), so it cannot pass by comparing two empty sets if the matcher ever stops resolving entries.Test plan
cargo fmt --all -- --checkcleancargo clippy --profile test-fast --features cuda --lib --bins --tests -- -D warningsclean (exit 0).--binsis included because this PR changes both binary crates. The template's--workspace --all-targetsform was not run: this host builds MLX from source and that invocation does not finish inside the available window.cargo test --profile test-fast --features cuda --bin mlxcel tests::serve_(13 passed)cargo test --profile test-fast --features cuda --bin mlxcel-server tests::(13 passed)cargo test --profile test-fast --features cuda --test cli_help_consistency(25 passed, up from 17 on the base)cargo test --workspace --profile test-fastandcargo deny checknot run here. No dependency, type, or signature changes; the blast radius is two clap structs, their test modules, and one integration test.Known gap, deliberately not closed here
Short forms still diverge:
mlxcel-servercarries-cfor--ctx-sizeand-nfor--predict, andmlxcel servehas neither, somlxcel-server -m X -c 4096 -n 256does not copy across. Both letters are free onmlxcel serve, so closing it is cheap, but it is a different table from the one #1109 enumerated and the contract compares long names only. The shipped help sentence is narrowed accordingly: it now says "so this flag parses on either binary" rather than claiming any copied command line parses unchanged.