Skip to content

fix(cli): make the drifted server flag spellings work on both binaries - #1125

Merged
inureyes merged 4 commits into
mainfrom
fix/issue-1109-server-flag-spelling-parity
Aug 13, 2026
Merged

fix(cli): make the drifted server flag spellings work on both binaries#1125
inureyes merged 4 commits into
mainfrom
fix/issue-1109-server-flag-spelling-parity

Conversation

@inureyes

@inureyes inureyes commented Aug 13, 2026

Copy link
Copy Markdown
Member

Summary

mlxcel serve and mlxcel-server are two hand-maintained clap definitions of the same server sharing 112 flag spellings, and four had drifted apart. Each missing spelling is added as a visible_alias, the DRY breakers get one primary spelling on both binaries, and tests/cli_help_consistency.rs grows 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-parallel worked only on mlxcel serve, --parallel only on mlxcel-server, so a command line copied between the two failed with error: unexpected argument '--parallel' found. Both flags read the same LLAMA_ARG_N_PARALLEL env 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.md advertises llama-server compatibility as a migration aid, and three of the four broke it on mlxcel serve.

This is drift, not design. The repository already establishes the intended pattern twice in the same structs: --draft-model / --model-draft and --draft-max / --draft are 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" on ServeArgs::n_parallel, visible_alias = "n-parallel" on ServerArgs::parallel, visible_alias = "predict" on ServeArgs::n_predict, visible_alias = "adapter" on ServerArgs::lora. visible_alias renders 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-breakers plural on serve, --dry-sequence-breaker singular on mlxcel-server). The singular 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. 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 a visible_alias on both binaries, so the spelling mlxcel serve used to require still parses.

Two references to the old primary spelling are updated. The help sentence added by #1118 on SamplingOptions::dry_multiplier and the scope comment at src/commands/generate.rs:921 both 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 --parallel help 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 informative mlxcel-server text plus a closing sentence stating the copied-command-line property, identical on both. docs/CONTINUOUS_BATCHING.md no 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_surface compares the whole long-name surface of the two binaries against SERVE_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-memory and --force on mlxcel serve (subcommand-shaped one-shot actions) and --version on mlxcel-server (mlxcel carries it at the top level).
  • SHARED_SERVER_FLAG_GROUPS lists 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 --parallel from mlxcel-server and --n-parallel from mlxcel serve in 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_DESCRIPTIONS requires 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_name requires 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_name anchors 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_help cuts the help at the first flattened-subcommand heading, because mlxcel-server sets flatten_help and its --help carries a second flag surface under mlxcel-server download: in which --models-dir and --help already appear twice. all_documented_spellings, flag_description, and flag_env_and_default split an entry into the three parts the contract treats differently. flag_help_entry's body-slicing loop is factored into a shared entry_body so the two entry finders cannot disagree about where an entry ends.

Two clap name-uniqueness guards. [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 visible_alias colliding 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_unique and server_flag_names_and_aliases_are_unique walk the built Command and assert uniqueness directly, so the guard holds in every profile.

CHANGELOG.md: an ### Fixed entry under ## [Unreleased], because the accepted CLI surface changes on both binaries and one primary spelling changes on mlxcel serve.

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. Both ends of the table are covered, including --adapter / --lora on mlxcel 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:

  1. Five guard tests pin the helpers against synthetic input: the four signature shapes clap renders plus the two it must reject, the optional-value [=<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.
  2. dropping_a_shared_flag_alias_would_fail_the_spelling_parity_assertion splices the live mlxcel-server --help, cuts the rendered [alias: --n-parallel] annotation out of the --parallel entry, and asserts the accepted-spelling set then collapses to ["--parallel"]. This is the same idiom the existing removing_the_rendered_alias_annotation_leaves_the_alias_undocumented test uses.
  3. Two one-off live mutations against the real binaries, each restored and the suite re-run green. Removing visible_alias = "n-parallel" from src/bin/mlx_server.rs fails the named-list assertion with left: ["--parallel"], right: ["--n-parallel", "--parallel"]. Removing --force from SERVE_ONLY_FLAGS fails the whole-surface assertion with left: {"--estimate-memory", "--force"}, right: {"--estimate-memory"}, which is the shape a genuinely new one-sided flag would produce.
  4. The whole-surface assertion carries its own floor (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 -- --check clean
  • cargo clippy --profile test-fast --features cuda --lib --bins --tests -- -D warnings clean (exit 0). --bins is included because this PR changes both binary crates. The template's --workspace --all-targets form 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)
  • Both live mutation checks described above, each restored and re-run green
  • cargo test --workspace --profile test-fast and cargo deny check not run here. No dependency, type, or signature changes; the blast radius is two clap structs, their test modules, and one integration test.
  • No real-checkpoint validation. Nothing on the inference path changes; the diff is clap attributes, help text, tests, and one docs line.

Known gap, deliberately not closed here

Short forms still diverge: mlxcel-server carries -c for --ctx-size and -n for --predict, and mlxcel serve has neither, so mlxcel-server -m X -c 4096 -n 256 does not copy across. Both letters are free on mlxcel 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.

`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
@inureyes inureyes added type:bug Bug fixes, error corrections, or issue resolutions priority:medium Medium priority status:review Under review labels Aug 13, 2026
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
@inureyes inureyes added status:done Completed and removed status:review Under review labels Aug 13, 2026
@inureyes
inureyes merged commit 586af16 into main Aug 13, 2026
8 checks passed
@inureyes
inureyes deleted the fix/issue-1109-server-flag-spelling-parity branch August 13, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority:medium Medium priority status:done Completed type:bug Bug fixes, error corrections, or issue resolutions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli): four server flags drift between mlxcel serve and mlxcel-server; --parallel / --n-parallel work on only one binary each

1 participant