Add an extra llama-server arguments box to the model settings - #8702
Conversation
llama-server documents 283 flags and Studio already emits or manages about 115 of them, so the long tail has no way to be set from the app. The pass-through itself already existed end to end (LoadRequest.llama_extra_args, validate_extra_args, per-model persistence, the CLI), it just had no control, which is why the overrides route deliberately preserves the field when the panel saves. Adds the control, and closes the denylist gap that opens once a flag can be pasted in rather than typed on a command line. --tools was already denied because it enables exec_shell_command; --agent is documented as enabling all built-in tools, so it reached the same capability one alias away. Also denied: --tools-runtime (docker:, podman:, ssh:<target>), the MCP server flags, the CORS flags, --media-path, the log redirection flags Studio parses its own startup output through, --slot-save-path, and the flags that exit instead of serving. --slots and --props stay a user choice: Studio reads GET /props and never /slots. The editor checks what is typed against the INSTALLED binary's --help rather than a list shipped with Unsloth, since a custom or newer llama.cpp is exactly the case a bundled list would get wrong. probe_server_capabilities already parsed that map and threw it away; it is now returned by GET /api/inference/llama-flags. A flag the build does not document warns but still loads, an unprobeable build says nothing, and a flag Studio owns is refused with the control that sets it named. A flag that shadows a control is allowed and noted, because the backend appends extras last and reconciles its own sizing, and the CLI has always allowed it. With the box empty the emitted command is byte-identical to before.
…argument error The panel keys per-model settings by repo:QUANT, but the overrides route carries flags stored under the bare repo id into the first per-quant save, so reading only the exact key showed an empty box for an entry that was about to be inherited. Caught by driving the real panel: flags set through the API for unsloth/gemma-3-270m-it-GGUF did not appear. The unknown-argument message pointed every reader at the extra arguments box. Nothing reaching the classifier says whose flag it was, and Unsloth emits its own conditionally on the capability probe, so a binary swapped under a cached probe lands there too and would be sent to a box it never came from.
The row painted a managed flag red and left Load model enabled, so the panel started a load validate_extra_args then refused. Seen on a real Studio: --parallel 8 showed its error and the button stayed live. The row now reports loadability up, and the objection leaves with the row so a model without the box (diffusion) still loads.
The Load button in the model settings goes through use-chat-model-runtime, which builds its payload field by field, so the flags stayed in the config and never reached llama-server. Caught by loading gemma-3-270m from the panel with --top-k 20 in the box and reading the emitted command, which did not have it. Omitted rather than nulled when the config has not read them, so the route still preserves flags set outside this panel.
The forget path said flags have no UI control. They do now; what the sentence was actually about is that an omitted field is preserved, which is what a save that never opened the box relies on.
main's test_a_huge_unterminated_line_is_cheap caught this on the rebase: the two new regexes ran over the whole capture, and _drain_stdout keeps an unterminated line whole, so a 10 MB single line was scanned twice and the classifier went past its 200 ms budget. llama.cpp prints the argument error and exits, so the tail is where it always is.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b425f15dc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const [text, setText] = useState(() => | ||
| formatExtraArgs(config.llamaExtraArgs), | ||
| ); |
There was a problem hiding this comment.
Clear the local argument text when resetting the config
When the user enters extra arguments and then clicks Reset, the parent replaces config with DEFAULT_PER_MODEL_CONFIG, but this local state retains its initial text because it never responds to subsequent config.llamaExtraArgs changes. The textarea therefore continues displaying the old arguments while handleRun sends the reset config without them, so the visible command and the command actually loaded disagree; synchronize this state for an explicit reset or clear it from the reset handler.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
The row does respond to later config.llamaExtraArgs changes: the block below the catalogue effect compares the rendered stored text against the box and re-seeds when they diverge, so Reset (config becomes DEFAULT_PER_MODEL_CONFIG, tokens go from ["--top-k","20"] to none) clears the textarea. The one case it deliberately leaves standing is a box filled from a stored override the panel never owned: Reset does not clear the server's copy, the field stays omitted on save, and the load inherits it, so the box and the command still agree.
| // Sent only once known, and [] is the explicit "launch with none": | ||
| // the flags are llama-server's, so a transformers load never carries them. | ||
| ...(isGguf && loadLlamaExtraArgs !== undefined | ||
| ? { llama_extra_args: loadLlamaExtraArgs ?? [] } | ||
| : {}), |
There was a problem hiding this comment.
Preserve launched arguments in the active runtime config
When a user loads from this panel with Remember disabled, these arguments are sent to llama-server but are never committed to the runtime store that useActiveModelConfig uses, while the non-remembered override is removed. Reopening settings for the still-running model therefore shows an empty box even though the active server is using the arguments, unlike the other click-time load settings recorded after success; retain the effective argument list in the active/loaded config as well as the request.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Leaving this one. With Remember unchecked the user asked for nothing to be stored, and the load stays correct either way: the omission path inherits the resident process's own extras, so a Reload from that empty box relaunches with the same arguments rather than dropping them. Showing values from a config that was explicitly not remembered would also make Reset and the dirty state read against what is actually saved. The durable case, an override set through the API, is what the hydration in eea2da7 covers.
…list grew Widening the denylist is the one part of this change that acts on data already on disk. An override written when --log-file or --slot-save-path was still allowed still holds it, and the two paths that CARRY such a value over rather than receive it were refusing: - the settings save re-validated the carried-over list, so changing Context Length on that model returned 400 naming a flag the user was not editing and cannot reach from that payload - the load path caught the same refusal but degraded to no extras at all, so one name added to the denylist silently took every other flag with it Both now use drop_managed_flags, which removes only the denied names (with their values, or a bare /var/log/x would be read as a positional model path) and re-validates what is left. An argument the caller just sent is still refused loudly; an argument merely carried over from storage is dropped quietly. Also off the event loop: the flag catalogue route awaited probe_server_capabilities inline, and on a cold cache that runs llama-server --help with a 10s timeout. The startup probes were moved to a thread for exactly this reason (test_startup_llama_probe_non_blocking), so the first open of the panel after an update would have stalled every other request. Two new suites: the compatibility one covers every carry-over path with flags an older build allowed, and the platform one runs the Cartesian product of Linux/WSL/Windows/macOS with NVIDIA/multi-GPU/Vulkan/CPU-only, asserting the command is unchanged with the box empty and that an extra arg never moves a placement flag. Twenty thousand random inputs through the box and through Python's shlex also agree on every one that does not contain a backslash against a newline or end in a lone backslash. Those two are deliberate: POSIX 2.2.1 makes an unquoted backslash before a newline a line continuation, which shlex (a lexer, not a shell) does not implement, and a text field that refuses a half-typed trailing escape is worse than one that carries the character. Both are pinned as named tests now. The same run showed an unquoted Windows path loses its separators, which is what every POSIX shell does and not something to change in the splitter, so the hint asks for quotes around backslashes rather than only around spaces.
Windows CI errored 16 tests in setup with ValueError: the environment variable is longer than 32767 characters. pytest puts the whole parameter in the node id and the node id into PYTEST_CURRENT_TEST, and these cases are 100 KB blobs, so the limit is hit before the test body runs. Linux and macOS have no such cap, which is why it went unnoticed. The blobs still exercise the linear-time claim; only their ids are short now.
…ow is hidden Ten fixes from the review round, the two largest of which were the box telling the truth about what will run: - Hydration filled the textarea but left the config undefined, so clicking Load without editing sent no llama_extra_args at all. The route's omission path inherits from a resident same-model process, not from the stored override, so a cold start launched without the flags the box was showing. Confirmed against a live Studio: the override held --numa distribute and the emitted command did not. Hydration now writes the config too. - Collapsing Advanced settings unmounts the row, and its cleanup withdrew the objection while the tokens stayed in the config, so Load was re-enabled for arguments the backend refuses. The panel now holds the flag and retires it on a model change instead. The rest: - StrictMode replays an effect (setup, cleanup, setup), and marking the key before the request left the first fetch cancelled and the second setup returning early, so hydration never ran in development. Marked once a response is in hand. - The lookup checked two literal keys while the overrides route folds identities and falls back from repo:QUANT to the bare repo, so a model whose entry was written under another casing showed an empty box. resolveStoredExtraArgs mirrors that order. - The editor checked the token cap but not the byte cap, so a grammar or JSON schema over 32 KiB was loadable in the UI and refused by validate_extra_args. Measured in UTF-8, which is what the backend counts. - isDefaultConfig did not look at llamaExtraArgs, so a config whose only change was the box read as default and savePerModelConfig deleted the entry it was asked to remember. - Records carrying the new field are stamped v3. This scheme exists so a client from before a field refuses to rewrite records containing it; without the stamp an older cached tab would normalize the arguments back out on its next save. - The catalogue published llama.cpp's removal stubs (--draft-max and friends, listed only to say the argument has been removed), so the editor called a removed flag supported and stayed quiet about a load that then failed. _is_real already existed for this. - probe_ok equated "parsed something" with "read the whole thing". A --help that exits nonzero after partial output leaves a non-empty catalogue, and every flag past the failure point would have been reported as unsupported. The probe's own result is published and used.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: eea2da7f75
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| seen.add(flag); | ||
|
|
||
| if (catalog?.managed.has(flag)) { |
There was a problem hiding this comment.
Block managed flags while the catalog is loading
On a cold panel open, catalog remains null while /llama-flags performs its probe, which can take up to ten seconds; during that window this condition treats even denylisted inputs such as --model or --agent as valid and leaves Load enabled, although the backend will always return 400. The same mismatch persists if the catalog request fails, so managed-flag validation should be available independently of the slow capability probe or Load should remain blocked until it is known.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Leaving this one. The window is the first panel open after an install or an update, the probe now runs off the event loop, and the catalogue is session-cached after that, so it is seconds once per session rather than per open. Blocking Load until it arrives would penalise every cold open to save a 400 that already names the flag, and the backend is the boundary here, not the row. The cases with no probe behind them (a managed flag with a control in this panel, an unclosed quote, the bounds, control characters, a non-numeric value) are all refused without the catalogue.
It only ever bounded the argument-error branches. The dyld branches above it have to keep reading the whole capture, because the pathological part of the input they guard against is the library name inside the line itself (TestTheLibraryNameIsBounded), so a tail would cut off the framing they match on.
|
Cross-platform status, since the Windows job on the staging replica is red for both this branch and main:
The 16 errors are the The timing assertion is pre-existing and marginal. Locally the classifier costs 104.5 ms on main and 104.8 ms on this branch (best of 45 runs each), so the +0.3% is not what puts it over; roughly half the budget already goes on the macOS loader branches scanning the whole capture. Bounding those to a tail takes it to 29 ms, but it also breaks ubuntu-latest, macos-14 and the Playwright job (chromium, firefox, webkit) are green. |
The auto-switch path was the one place a stored override still became an explicit request: model_override_load_kwargs copies llama_extra_args straight into LoadRequest, and an explicit list is refused rather than trimmed, so an override written before a flag was denylisted broke every OpenAI auto-switch and idle reload of that model until someone rewrote the entry by hand. It is sanitized at that seam now, like the inheritance and settings-save paths. The editor also told two small lies and enabled two loads that could not succeed: - --device, -dev, --main-gpu and -mg are stripped from the command whenever the GPU picker owns placement (_strip_device_extra_args), so the "passed after the settings above, so yours wins" reading was wrong for them. They are now called removed, and only while a GPU is actually selected. - --ctx-size nope and -ngl many reach parse_ctx_override and parse_gpu_layers_override, which refuse the load. The row mirrors those checks. - A control character (the usual source being a command copied out of coloured terminal output) is refused by the backend and was loadable here. - A GGUF that turns out to be DiffusionGemma after classification kept its tokens in the loadable config, and the diffusion shim records them without ever passing them to anything. Cleared with the other flags that path already drops. And the trim that keeps a legacy list inside the new bounds no longer leaves a flag without its value: ['--grammar', <33 KiB>] became ['--grammar'], which this validator accepts (it knows the arity of only a few flags) and llama-server then refuses at startup. /validate now carries the arguments too. It already resolves and passes them to the estimator; only the request field was missing, so a --ctx-size or cache override sized the preflight differently from the load it was approving.
for more information, see https://pre-commit.ci
…load Four things a closer read of the review turned up. Hydration ran inside the extra-arguments row, which lives in Advanced settings and is not rendered while that section is collapsed, so a panel opened closed never fetched the stored override and a cold load launched without it. Moved to the panel, which always mounts. The startup auto-load built its request from the local config and left the flags out. There is nothing resident at that point for the omission path to inherit them from, so a remembered list was dropped on exactly the load that most needs it. The preflight check sends them too, or it would approve a command the load then refuses. The flag catalogue was cached for the whole session. A llama.cpp update or a backend switch replaces the binary in place, so a newly added flag kept being reported as unknown. One minute now, plus an explicit drop on both paths that change the binary. Short aliases were parsed out of --help and thrown away, so typing -fa warned that a valid flag was not in this build.
A backslash-newline contributes nothing, so it must not mark the start of a token: a command wrapped and indented under its first line, which is how they are pasted, gained an empty token at that indentation and sent llama-server a bare positional it reads as a model path. The numeric-value check ran once per flag name, but llama.cpp reads the last occurrence and so does parse_gpu_layers_override, so `-ngl 20 -ngl many` left Load enabled for a request that comes back 400. Checked at every occurrence now, reported once. drop_managed_flags put a rejected VALUE into the list its callers join into a warning log. A stored value carrying ANSI escapes belongs in no log line, so it is a placeholder now, and the flag it belonged to is named rather than disappearing silently.
for more information, see https://pre-commit.ci
|
@codex review |
1 similar comment
|
@codex review |
|
Codex Review: Didn't find any major issues. Chef's kiss. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…ration The env-twin sweep took LLAMA_ARG_MMPROJ and LLAMA_ARG_MMPROJ_URL with it. --mmproj is refused in the box because Unsloth resolves the projector itself, but the environment twin is an INPUT: _launch_has_mmproj reads both to know the launch has a projector at all, and that is what keeps the vision and audio state of a model loaded through an inherited one. Scrubbing them cleared that state, which test_gpu_init_crash_message caught on three cases. Only the paravirtual CPU recovery drops them now, as before, where an unpinned projector is the corrupt path it is undoing. The pooling twins stay listed but noted: load_model already pops LLAMA_ARG_POOLING / _RERANKING / _EMBEDDINGS itself. The auto-load failure-gate harness slices chat-adapter.ts and evaluates it, so the stored-arguments hydration added there left three names undefined and every scenario failed on the harness's own missing-symbol assertion. Stubbed neutrally: these scenarios are about the failure gate, not about which flags a model launches with, so fetchLoadExtraArgs answers 'nothing stored' and the sanitizer is identity, leaving the /load payload each scenario asserts on unchanged. Merged origin/main first, so this branch carries #8783 (a deliberate segfault in the probe tests no longer dumps core).
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 12a12d35fc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| len(effective_extra_args), | ||
| model_log_label, | ||
| ) | ||
| effective_extra_args = [] |
There was a problem hiding this comment.
Match diffusion validation to manual
-ngl translation
When a DiffusionGemma request uses Manual GPU Memory and supplies -ngl in llama_extra_args, /load translates that flag into request.gpu_layers before dropping the extra arguments, but /validate drops the list here without performing that translation. For example, gpu_layers: 0 plus -ngl 20 is validated as a zero-VRAM load during active training, then actually launches 20 GPU layers; the inverse combination can incorrectly return 409 for a CPU load. This also affects the UI when diffusion classification was inconclusive while the extra-arguments row was staged, so validation must either apply the same translation or both paths must consistently ignore the flag.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and it is the sharper half of the asymmetry: /validate is the call that approves the switch, so it has to judge the command /load will actually run.
/load translates the last-wins -ngl out of the extras into request.gpu_layers when Manual GPU Memory owns the offload flags, then strips them, and the training guard reads _diffusion_manual_ngl(gpu_memory_mode, gpu_layers). /validate skipped both, so with gpu_layers: 0 and ["-ngl", "20"] the guard saw a zero-layer diffusion split, returned before the VRAM check, and the launch then put twenty layers on the GPU during training. The inverse pairing (gpu_layers: 20, -ngl 0) could 409 a load that never leaves the CPU.
It now runs the same translation and the same strip_shadowing_flags(..., strip_offload = True) under the same manual-only condition, before placement, so the guard receives the same layer count and the same list either way. Auto mode is untouched: there the flag is a pass-through the loader honours, and translating it here would invent a first-class value /load never set.
Three tests in test_validate_diffusion_extra_args.py: -ngl 20 over gpu_layers 0 reaches the guard as 20 with the flag stripped, -ngl 0 over gpu_layers 20 reaches it as 0, and an Auto-mode request keeps both the field and the flag as they were.
| const body = (await res.json()) as ApiLlamaFlagCatalog; | ||
| // An older backend answers the route without the parameter and returns its | ||
| // full catalogue, which carries the same list. | ||
| cachedManaged = { |
There was a problem hiding this comment.
Guard managed-catalog responses across invalidation
If a managed-only request is in flight when a llama.cpp update or backend switch calls invalidateLlamaFlagCatalog, the old request can resume here and repopulate cachedManaged after it was cleared. Unlike the full-catalog request, this path has no generation check or TTL, and its unconditional finally can also clear a newer in-flight request, so the old backend's defaultParallelSlots may remain cached for the rest of the session and incorrectly allow or reject stored --batch-size values. Capture and verify the catalog generation before publishing this response, as the full-catalog path does.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Agreed. The full catalogue reads the generation before the request and checks it before publishing; this path had neither, and its finally was unconditional, so it could also clear a newer request in flight.
The reason it matters is the field I added: the denylist in this answer is Unsloth own and no binary changes it, but defaultParallelSlots travels beside it and that is the EFFECTIVE count, which depends on whether the probed binary supports --kv-unified. An in-flight managed request resolving after an update therefore put the old build slot floor back into the cache the invalidation had just cleared, where it would sit for the rest of the session, and the hidden hydration check would allow a stored --batch-size 2 on a build now serving four slots.
Same shape as the catalogue now: generation captured before the fetch, checked before cachedManaged is written, and the finally clears the in-flight promise only while it is still ours. Guarded by a source-level test alongside the existing catalogue one.
| // Before the obligation is replaced: "--numa --verbose" leaves --numa without | ||
| // the value it needs, and only this point in the walk still knows that. | ||
| noteOwed(pendingOwner, pendingValues); |
There was a problem hiding this comment.
Preserve quoted leading-hyphen values as values
When a value-taking option is followed by an explicitly quoted value that begins with a hyphen, such as --chat-template '- hello', parseExtraArgs removes the quotes and extraArgFlagName classifies the resulting token as another flag. This call therefore reports the preceding option as missing its value and disables Load, even though the backend validator accepts the token list and forwards it verbatim. Preserve enough quote/token-position information to treat an explicitly quoted flag-shaped token as the pending option's value.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Real, and a false refusal, which is the worse direction. parseExtraArgs takes the quotes off, extraArgFlagName then reads - hello as flag-shaped, and the row reported --chat-template as missing its value and disabled Load, while validate_extra_args accepts the list and llama.cpp reads it correctly: for a value-taking option it takes the next argv element without looking at what it starts with.
The tokeniser now records which tokens the user quoted (argv has no room for that, so it stays in the editor) and the diagnostics consult it only in value POSITION. That distinction matters both ways:
--chat-template "- hello"and--grammar "-x"are values, no error, and the value is not reported as an unknown flag either"--top-k" 20is still a flag, because quoting out of habit is common and reading it as a value would refuse a list that runs- a quoted token with no option in front of it has nothing to be the value of, so it is judged as written: unknown to this build, warned about, still passed, which is the answer the backend gives
Six assertions added, including that the token list itself is unchanged and only the quote marks are new.
…typed /load translates an explicit -ngl out of the extra arguments into the first-class field when Manual GPU Memory owns the offload flags, then strips them. /validate did not, so the call that APPROVES the switch judged a different command than the one that runs: a diffusion GGUF asked with gpu_layers 0 and '-ngl 20' was approved as a load that places nothing on any device and cannot compete with training for VRAM, and then launched twenty layers on it; the opposite pairing refused a load that only ever runs on the CPU. Same translation and same strip now, under the same manual-only condition, so both paths size the same launch. The managed flag answer is guarded by the catalogue generation, as the full catalogue already was. Its denylist is Unsloth's own, but defaultParallelSlots travels with it and that is the effective count for the probed binary, so a request already on the wire when llama.cpp is replaced could repopulate the cache the invalidation had just cleared and keep the old build's slot floor for the rest of the session. Its finally is conditional too, or it would clear a newer request's in-flight promise. A quoted value beginning with a hyphen is a value again. parseExtraArgs drops the quotes, so '--chat-template "- hello"' left a flag-shaped token in value position and the row called the option's value missing, disabling Load over a list the backend accepts and llama.cpp reads correctly. The tokeniser records which tokens were quoted and the diagnostics read that only in value POSITION: a flag quoted out of habit is still a flag, and a quoted token with no option in front of it is judged as written.
There was a problem hiding this comment.
💡 Codex Review
https://github.com/unslothai/unsloth/blob/7ddba98a1eccf80fa5ddf43083a727438660f34d/studio/backend/routes/inference.py#L7968
Ignore extra arguments before validating non-GGUF loads
When a Transformers or MLX model is loaded with a shared request payload containing llama_extra_args, this unconditional validation can return 400 for managed flags, bare values, or oversized arguments even though the new LoadRequest contract says the field is ignored for non-GGUF models. Such payloads were previously accepted because the unknown field was ignored, so clients that reuse one load shape across backend types can now fail before model classification; defer validation until config.is_gguf is known (and after diffusion is excluded), or remove the ignored-field guarantee.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // "this config never read them", and the route preserves the stored | ||
| // flags when the field is omitted. Falling back to another model's | ||
| // value, or to null, would clear flags the user set elsewhere. | ||
| const loadLlamaExtraArgs = pendingLoadConfig?.llamaExtraArgs; |
There was a problem hiding this comment.
Carry remembered arguments through runtime-only load paths
When a remembered GGUF is launched through a path that first calls applyModelLoadConfigToRuntime but does not attach that config to the selection, pendingLoadConfig is undefined here and the saved arguments are omitted. This affects the normal Hub launch in hub-page.tsx:1319-1338 and the training handoff in chat-page.tsx:3203-3216: applyPerModelConfigToRuntime has no field for these arguments, and /load only inherits from the same resident model, so a cold load or switch from another model silently runs without the arguments the user remembered. Pass the resolved config on those selections or otherwise carry its argument list into this load.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed on both paths. applyPerModelConfigToRuntime has no field for the launch flags, and /load inherits them only from the SAME resident model, so the Hub launch and the training handoff started cold (or switched from another model) without the arguments the user had remembered.
Both selections now carry the resolved config itself: hub-page.tsx passes rememberedConfig on the selectModel call beside keepSpeculative, and the handoff in chat-page.tsx passes the same object it feeds to applyModelLoadConfigToRuntime. Neither invents one when nothing is remembered, so the field stays absent and a resident model keeps its own flags, which is what the omission path is for.
The settings-page Apply was already fixed this way earlier; these were the two that went through the runtime alone. Guarded by a source-level test next to that one.
| batchFloor: Math.max( | ||
| 2, | ||
| config.nParallel ?? catalog?.defaultParallelSlots ?? 2, | ||
| ), |
There was a problem hiding this comment.
Size the batch floor with effective explicit slots
When the installed llama-server lacks --kv-unified and the user explicitly selects multiple Slots, this uses the requested count even though the backend clamps the launch to one slot. For example, Slots 4 with extra arguments --batch-size 2 is rejected in the editor and disables Load, while _effective_parallel_slots returns 1 and the backend accepts that exact command. The catalogue only publishes the effective default, so expose the clamp capability/effective selected count or leave this check to the backend when an explicit slot count may be clamped.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right, and it is the same gap the published default had, one level along. The default is effective, but an explicit Slots value is chosen in this panel and never passes through the route, so the editor could not apply the clamp without being told it exists: Slots 4 with --batch-size 2 was refused here while _effective_parallel_slots returns 1 and the backend accepts that exact command.
/api/inference/llama-flags now publishes parallel_slots_clamped, computed by asking the same helper the load uses (_effective_parallel_slots(2) == 1), in the same worker thread as the default so nothing new touches the event loop. An unreadable probe reports False, keeping the ask, as everywhere else here.
The three floor sites (the row, the collapsed revalidation, the hidden hydration check) now share one effectiveBatchFloor() so they cannot drift, and a backend that publishes neither field reads as not clamped, which is exactly the behaviour those builds had before. Tests: the clamp helper against a build with and without --kv-unified and against an unreadable probe, the single-slot default still reporting the clamp off the loop, and a source-level check that one definition backs all three call sites.
for more information, see https://pre-commit.ci
… too A launch that only applies the remembered config to the runtime lost the flags. applyPerModelConfigToRuntime has no field for them, and /load inherits them only from the SAME resident model, so the Hub launch and the training handoff ran a cold start, or a switch from another model, without the arguments the user had remembered for it. Both selections now carry the resolved config itself, and neither invents one when nothing is remembered: the field stays absent, which is what lets a resident model keep its own flags. The batch floor had the same shape of gap as the published default did. That default is effective, but an EXPLICIT Slots value chosen in the panel is not: a build without --kv-unified serves one slot however many are asked for, so Slots 4 with '--batch-size 2' was refused in the editor while the backend, which clamps to one, accepts exactly that command. The catalogue now publishes whether this build clamps, computed by asking the same helper the load uses, and the three floor sites share one function so the row and the two hidden checks cannot drift. A backend that publishes neither field reads as 'not clamped', which is the behaviour those builds had before.
…nto flags-editor
|
@codex review |
|
Before/after from two isolated Studio installs, the merge base Top pair, Advanced settings expanded and the box empty. Before, there is no extra-arguments control anywhere on the panel: the field existed only in the API and the CLI, so a per-model Bottom pair, Read from the same two servers that were photographed:
The 404 on the base is the measurement as much as the missing box is: there was no catalogue for a browser to validate a flag against. One thing to flag on the branch itself. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bc09fac33c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # without --kv-unified serves one slot however many were | ||
| # requested, and refusing a batch of 2 against it is a 400 on a | ||
| # command that would have run. | ||
| _effective_parallel_slots(_n_parallel, diffusion_kind = placement.diffusion_kind), |
There was a problem hiding this comment.
Account for embedding slot clamping in the batch check
For an embedding GGUF with a micro-batch below the requested slot count, this checks the extra --batch-size against the pre-clamp slots even though LlamaCppBackend.load_model later reduces embedding slots to the effective micro-batch. For example, n_parallel=4, n_ubatch=2, and extra args --batch-size 2 is rejected with 400 here on a --kv-unified build, while the launcher deterministically clamps to two slots and that command is valid; the equivalent /validate check has the same mismatch. Include the embedding/micro-batch clamp when deriving the floor.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed. load_model reduces the slots to the effective micro-batch for an embedding GGUF before it launches, so n_parallel=4, -ub 2 and a pass-through --batch-size 2 launches at two slots where two is the floor, and both /load and /validate were refusing it.
Both floors now consult the same clamp, in _embedding_clamped_slots, layered on top of _effective_parallel_slots. It is asked only after check_batch_floor has already raised, so the GGUF header read is paid on the way to a 400 rather than on every load, and _is_embedding_gguf answers False for a header that cannot be read: the clamp can only ever turn a refusal into an acceptance, never the reverse, and never on a guess. Tests cover the clamped accept, the chat GGUF that is still refused, defaults clamping nothing, the floor of one slot, and -b 1, which stays refused because llama-server aborts on a batch of 1 at any slot count.
| strip_shadowing_flags, | ||
| ) | ||
|
|
||
| _validate_ngl_override = parse_gpu_layers_override(effective_extra_args) |
There was a problem hiding this comment.
Validate manual offload arguments before stripping them
In manual GPU-memory mode, this new translation runs before the later validate_extra_args call, so syntax that /load rejects can be parsed and removed before validation sees it. For example, /validate accepts llama_extra_args=["--gpu-layers=20"] after translating it to gpu_layers=20 and stripping the token, but /load validates the original list first and returns 400 because llama-server does not accept attached values; malformed values such as -ngl bad can also raise an uncaught ValueError here instead of the intended 400. Validate the explicit list before applying the same translation and strip used by the load path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and it also swallowed the malformed case. Reproduced both: /validate accepted ["--gpu-layers=20"] by translating it to gpu_layers=20 and dropping the token, while /load validated the original list and returned 400 ("llama-server does not read an attached value"), and -ngl bad raised an uncaught ValueError out of the parser rather than a 400.
validate_extra_args now runs on the resolved list before the manual -ngl translation and strip_shadowing_flags, so the preflight judges the same list /load does. Covered in test_validate_diffusion_extra_args.py: the attached form is a 400 naming the two-argument spelling, -ngl bad is a 400 rather than a crash, and a well-formed -ngl 20 still passes.
| if (body.resolved !== undefined) { | ||
| return body.resolved?.llama_extra_args ?? []; |
There was a problem hiding this comment.
Preserve explicit empty arguments during override hydration
This collapses a resolved llama_extra_args: [] marker into the same return value as an override with no argument field, so callers cannot preserve the distinction the backend stores deliberately. When a quant has an explicit empty marker to suppress a bare-repository fallback while the same model is still resident with the old inherited arguments, ModelConfigPage sees stored.length === 0, leaves llamaExtraArgs undefined, and its next Load omits the field; /load then inherits the resident arguments instead of applying the saved clear. Return the field-presence state as well as its tokens so an explicit empty result hydrates to the explicit-clear state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right, and the tombstone exists precisely so that case is distinguishable. fetchLoadExtraArgs and resolveStoredExtraArgs now return {tokens, explicit}, and all three hydrating callers use it: the panel sets llamaExtraArgs to [] rather than leaving it undefined (only when nothing has been typed into the box in the meantime), and the background auto-load and the compare pane send the explicit empty list instead of omitting the field. Omitting it is what let /load inherit the resident model's arguments, which is the list the user had just cleared.
Covered by new cases in llama-extra-args-override-lookup.test.ts (a row carrying [] is explicit and stops the search; a row that matched on another field but carries no argument list is not explicit, so there is no clear to honour) and by the three call sites in llama-extra-args-panel-hydration.test.ts.
Three things the editor and the routes were judging differently from the server. Validate the pass-through list before the manual offload translation rewrites it. In manual GPU memory mode /validate translated "-ngl" out of the list and stripped the token before validate_extra_args ever saw it, so "--gpu-layers=20" passed the preflight and /load answered 400 on the same list, and a malformed "-ngl bad" raised an uncaught ValueError instead of the 400 it was meant to be. Size the batch floor from the slots an embedding GGUF actually serves. --embedding caps the batch at the micro-batch and llama-server aborts when that is below the slot count, so load_model reduces the slots to it before launching: four slots with "-ub 2" and "--batch-size 2" launches at two slots, where two is the floor, and both routes were refusing it. Read only after the floor has already refused, so the header read is paid on the way to a 400 rather than on every load, and only a positively classified embedding GGUF relaxes anything. Keep an explicit empty argument list apart from a missing one when the panel hydrates. The settings page writes an empty list on purpose when the box is cleared for a quant whose bare-repository row still carries arguments; read as "nothing stored" the panel left the field undefined, its next Load omitted it, and /load carried the resident model's arguments over, which is exactly what had been cleared.
There was a problem hiding this comment.
💡 Codex Review
https://github.com/unslothai/unsloth/blob/09bf23e9db5167c7397a56a72114f6c875d39264/studio/backend/core/inference/llama_cpp.py#L15450-L15452
Redact pass-through values before logging
When an allowed pass-through flag carries sensitive data, this INFO log writes every value verbatim to persistent application logs. Extra arguments can include bearer credentials or private template/configuration data, while the nearby command-log helper only redacts Studio's own --api-key; log only flag names/counts or apply secret-aware redaction before emitting the list.
https://github.com/unslothai/unsloth/blob/09bf23e9db5167c7397a56a72114f6c875d39264/studio/backend/routes/inference.py#L8094-L8097
Preserve llama.cpp's all-layers sentinel during translation
In Manual GPU Memory mode, an explicit -ngl -1 reaches this translation even though llama.cpp defines -1 as offload all layers (the repository's embedding launcher uses that exact value for full GPU offload). Assigning it to request.gpu_layers instead invokes Studio's different -1 sentinel—Auto/--fit—and the subsequent strip removes the original flag, so the launched placement can partially offload to CPU instead of honoring the requested all-GPU command; preserve this sentinel's llama.cpp meaning when translating or leave the raw flag intact.
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
for more information, see https://pre-commit.ci
The panel's explicit-clear hydration rests on a backend rule with no test of its own: a save that clears the box for one quant keeps a row holding an empty list, so a load stops there instead of falling through to a legacy bare-repository row that still carries flags. Four cases now pin it: the clear that suppresses the fallback while leaving the bare row for the other quants, an empty save with nothing to suppress storing nothing at all, a fill never writing one, and the flag deciding only what an empty list means. The embedding classification behind the batch floor reads the GGUF header, so run it in a thread: both routes reach it from an async handler that is also serving download progress polls.
…nto flags-editor # Conflicts: # studio/backend/routes/inference.py
|
@codex review |


Users keep asking for the full llama-server parameter surface. The README lists 283 distinct flags and Studio already emits or manages about 115 of them, so a control per flag is not viable and would permanently couple the panel to a CLI that changes every release. This adds a text box for the long tail, per model, at the bottom of Advanced settings.
The pass-through itself already existed end to end:
LoadRequest.llama_extra_args,validate_extra_args, per-model persistence, and the CLI all use it. It just had no UI, which is why the overrides route deliberately preserves the field when the panel saves. With the box empty the emitted command is byte-identical to before.Closing the denylist gap first
A flag that can be pasted from a forum post is not the same threat as one typed on a command line, so the audit came before the box.
--toolswas already denied because it enablesexec_shell_command.--agentis documented upstream as enabling CORS proxy and all built-in tools, so it reached the same capability one alias away. Now denied as well:--agent,-ag,--no-agent,-no-ag--toolswas denied for--tools-runtimedocker:,podman:orssh:<target>, so tools run on a remote host--mcp-servers-config,--mcp-servers-json--cors-origins,--cors-credentials--media-path--log-file,--log-disable--slot-save-path--help,--usage,--version,--list-devices,--cache-list,--completion-bash--slotsand--propsstay the user's own call: Studio readsGET /propsand never/slots.Also added: a 256 token and 32 KiB bound with no empty tokens and no control characters, and a scrub of the
LLAMA_ARG_*twins of every denied flag, since llama.cpp reads those before argv and denying the token alone left the capability reachable.Validating against the installed binary
probe_server_capabilitiesalready parsedllama-server --helpinto a flag to description map and threw it away, keeping only thesupports_*booleans. That map is now returned fromGET /api/inference/llama-flags, so what the editor checks is the binary you have, not a list shipped with Unsloth. A custom or newer llama.cpp is exactly the case a bundled list would get wrong.What the row says:
--parallel is managed by Unsloth Studio and cannot be passed here), matching whatvalidate_extra_argswould refuse, and the Load button is blockedparse_ctx_overrideand friends exist for that), and the CLI has always allowed itOn a failed start,
_classify_llama_start_failurenow names the flag instead of blaming the GGUF or memory. An unknown flag and a rejected value get different messages, since the fix differs, andstoiis translated into "the value is not a number".What driving the real panel caught
Three things the unit tests could not see, all found by installing this branch and loading a model:
repo:QUANTwhile the overrides route carries repo level flags into the first per-quant save, so it now reads both, into the text only, and an untouched config still omits the field so nothing is wiped.--top-k 20sat in the config and never reached llama-server. The panel's own Load goes throughuse-chat-model-runtime, which builds its payload field by field, and onlyshared-composerhad been wired.Testing
shlex.split(posix=True), round-tripping, and the diagnostics--parallel 8blocks the load,--agentis refused,--temppwarns and still loads, and gemma-3-270m loaded with the command ending--jinja --spec-default --top-k 20test_a_huge_unterminated_line_is_cheapfrom main caught the new regexes scanning a 10 MB unterminated line twice; the argument scan is now bounded to the tail, with a testNot in this change
Promoting individual flags to their own controls (CPU threads, RoPE and YaRN,
--override-kv,--cache-reuse,--numa) is a follow up. Sampling is deliberately not promoted: it is already per conversation in the chat settings, and a load time copy would duplicate and mislead.