Skip to content

Add interactive Agents command builder - #7312

Merged
danielhanchen merged 65 commits into
unslothai:mainfrom
Imagineer99:feat/agents-command-builder
Jul 27, 2026
Merged

Add interactive Agents command builder#7312
danielhanchen merged 65 commits into
unslothai:mainfrom
Imagineer99:feat/agents-command-builder

Conversation

@Imagineer99

@Imagineer99 Imagineer99 commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • replace the static agent command list with an interactive command builder
  • add agent selection with icons, searchable GGUF model selection, cached GGUF discovery, and a separate quantization selector
  • generate one copyable unsloth start command that targets the active Studio instance and keep per-agent documentation close to the selection
  • add dynamic local-subagent setup and invocation guidance for Claude Code, Codex, OpenCode, and Pi
  • keep the command visible higher in the settings view and clarify that GGUF is required specifically for Codex
  • preserve the latest parent-PR fixes for local PATH detection, copy feedback, and OS-aware remote commands

Why

This builds on #7303 by combining its Agents settings content with a compact command-building workflow. Users can select the agent, model, and quantization without navigating a very large dropdown or copying commands from separate cards.

Supported agents can also keep their current parent model while registering the selected local Unsloth model and quantization as a subagent.

Stack note

The #7303 base predates the merged unsloth start improvements from #7313 and local-subagent implementation from #7326. This branch carries those exact upstream changes so the reviewed tree includes the CLI behavior advertised by the UI. They are already present on main and collapse when the stack is merged or rebased.

Screenshot

image
UNSLOSTH.START.EXAMPLES.mp4

Validation

  • npm run typecheck
  • focused ESLint checks for the changed frontend modules
  • focused Biome checks
  • i18n parity check
  • Vite production build
  • full unsloth start test suite: 293 passed; 3 platform-specific tests skipped
  • Python compileall, Ruff, and git diff --check

shimmyshimmer and others added 5 commits July 21, 2026 06:01
Adds a Settings > Agents tab documenting the `unsloth start` command:
quickstart, supported agents with click-to-copy commands, model
selection, common options, remote Studio setup, argument pass-through,
and a dry-run preview. Agent CLIs found on PATH are badged as installed.

Also removes the "New" badge from the System and Chat tabs.
…dark mode

Claude Code and OpenAI Codex now use the Anthropic and OpenAI logos from
the provider-logos registry; agents without an official asset keep the
monogram tile. Also inverts the Ollama and OpenRouter logos in dark mode
so their monochrome marks stay visible.
The in-tab header now reads "Agents (unsloth start)" while the sidebar
label stays "Agents". Reorders the tab to sit below Connections.
…te snippet

- Only probe agent PATH in the desktop app on a loopback backend, so
  Installed badges are not driven by a remote server's environment.
- Show the "none found" note only when detection actually ran and
  returned empty, not when the call failed.
- Share one copy hook that resets its timeout on rapid clicks and clears
  it on unmount.
- Render the Remote Studio snippet with PowerShell syntax on Windows.
- Note that --no-launch can still load a model when --model is set.
- Drop unused quickstart translation keys.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Imagineer99 commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator Author

added harness specific SVG

image

…nslothai#7303)

- Pick the remote snippet shell from the client platform, not the server deviceType
- Single-line the model examples so they paste in POSIX, PowerShell and cmd
- Split the pass-through block into independent one-command copies
- Derive detection visibility instead of clearing state in the effect
- Announce copy success to assistive tech
- Correct the quickstart/model copy: bare start uses the loaded model
…older

- Quote the --model value in the generated and subagent commands so a local
  path with spaces or metacharacters stays a single argument (client-OS aware)
- Pass the saved Hugging Face token to listGgufVariants so gated repos resolve
- Show 'No separate quantization' instead of a stuck 'Loading quantizations...'
  when a model has no variants; clear the failure once a later request succeeds
@Imagineer99
Imagineer99 marked this pull request as ready for review July 22, 2026 12:34
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7712355720

ℹ️ 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".

Comment thread studio/frontend/src/features/settings/tabs/agents-tab.tsx Outdated
Comment thread studio/frontend/src/features/settings/tabs/agents-tab.tsx Outdated
Comment thread studio/frontend/src/features/settings/tabs/agents-tab.tsx Outdated

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 106305140f

ℹ️ 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".

Comment thread studio/frontend/src/features/settings/tabs/agents-tab.tsx Outdated
Imagineer99 and others added 3 commits July 22, 2026 14:27
… model switching (unslothai#7313)

* Improve unsloth start runtime lifecycle

* Remove speculative Gemma prompt override

* Polish model download progress output

* Refine unsloth start status output

* Clarify unsloth readiness banner

* Clarify model reuse and switching output

* Queue model switches behind active inference

* Tighten unsloth start model switching

* Reduce model switch bookkeeping

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Fix Studio re-exec compatibility

* Recheck sidecar reservation after inference drain

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Pass start marker through child environment

* Fix key redaction, switch-waiter ordering, and stop/messaging gaps for PR unslothai#7313

- Redact minted sk-unsloth keys from the startup-failure log tail: the early
  key marker lands in the server log before the model load finishes, so a
  load-phase crash printed a live key to the terminal
- Deregister a finished switch waiter before releasing the swap gate so a
  swap on another event loop cannot count it as still queued and unload the
  model the finished request is about to generate against
- Warn on same-repo quant switches: an explicit variant replaces the resident
  weights for every attached session, but the repo ids match so no switch
  warning was printed
- Note the agent exit code when it is nonzero so the server keep-alive
  message does not read as a successful session
- Use taskkill /T in unsloth studio stop so llama-server children stop too

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* Tighten comments in start, studio, and inference changes

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Han <danielhanchen@gmail.com>
…nd Pi (unslothai#7326)

Bring the local-subagent support onto main. The original change (unslothai#7316) merged
into the stacked pr/daniel-unsloth-start-audit branch rather than main, and unslothai#7313
reached main via squash, so these files never landed on main.

Adds --as-subagent for claude, codex, opencode and pi: the parent agent keeps its
own cloud model while a locally served GGUF is registered as a delegated subagent,
using ephemeral per-session config that never touches the user's real agent config.

Copy link
Copy Markdown
Collaborator Author

@codex review

1 similar comment

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7827fc87e5

ℹ️ 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".

Comment thread studio/frontend/src/features/settings/tabs/agents-tab.tsx
Comment thread studio/frontend/src/features/settings/tabs/agents-tab.tsx Outdated
Comment thread unsloth_cli/commands/start.py Outdated

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1412cbcf3e

ℹ️ 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".

Comment thread studio/frontend/src/features/settings/tabs/agents-tab.tsx
Comment thread unsloth_cli/commands/start.py

Copy link
Copy Markdown
Collaborator Author

@codex review

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b17fdf9bf5

ℹ️ 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".

Comment on lines +299 to +300
return [model.id, model.model_id, model.display_name].some(
(value) => typeof value === "string" && GGUF_NAME_PATTERN.test(value),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require confirmed GGUF weights before listing local models

Do not treat an unset model_format as GGUF merely because an identifier contains gguf. The scanners intentionally leave the format unset for mixed or non-GGUF weights, so a safetensors directory named Foo-GGUF is added to this GGUF-only picker; selecting it generates a command that loads the transformers backend and then fails for Codex at _require_gguf_for_codex. Confirm that the entry contains a primary, non-mmproj GGUF before exposing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and the name fallback had outlived its purpose. It was there for a backend that did not send model_format, but every scanner now sets it, and after the mmproj fix it means exactly "holds a primary, non-mmproj GGUF and no other weights". Guessing from the name contradicted that deliberate signal, so a safetensors folder named Foo-GGUF got in. isLocalGguf now trusts the format alone.

One thing had to change first or this would have been a regression rather than a fix: _dir_model_format globbed only the top level, but HF cache snapshots keep split quants in per-quant subdirectories, so every sharded repo reported no format and would have vanished from the picker the moment the fallback went. It takes a recursive flag now, used for snapshots.

Checking that also turned up a gap in my own earlier mmproj fix: _scan_models_dir carries a parallel copy of the rule that I had not updated, so an mmproj-only folder under ./models was still classed as GGUF. Fixed there too.

Comment thread studio/backend/routes/models.py Outdated
Comment on lines +3065 to +3066
# Nothing complete anywhere: keep the newest rather than dropping the id.
return candidates[0][1] if candidates else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid publishing an incomplete snapshot as a load target

When an inactive cache contains only an interrupted split GGUF, every candidate fails _completed_gguf_variants, but this fallback still publishes the newest partial snapshot as load_id. The new command builder substitutes that path for the repo ID, so a copied unsloth start command attempts to load incomplete weights and fails; omit the load ID (or the picker entry) when no snapshot contains a complete variant.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and my stated reason for that fallback ("keep the newest rather than dropping the id") does not hold up. Dropping the id is not a loss of information: the command builder then uses the repo id, which fetches the missing shards, whereas the partial path loads incomplete weights and fails. It returns None when no snapshot holds a complete quant. Test covers the single half-downloaded snapshot case.

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9fd4a01e63

ℹ️ 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".

// and no other weights, so an unset format means "not GGUF", not "unknown". Do not
// guess from the name: a safetensors folder called Foo-GGUF would load the
// transformers backend and then fail the GGUF-only agents.
return (model.model_format ?? "").toLowerCase() === "gguf";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude standalone projector files from GGUF choices

When a scan root contains a standalone mmproj*.gguf, both _scan_models_dir and _scan_lmstudio_dir still assign it model_format = "gguf" unconditionally (routes/models.py lines 352-367, 472-488, and 538-551). This predicate consequently admits a vision projector with no model weights into the new picker, and the copied unsloth start --model <mmproj path> command cannot serve it. Filter projector filenames here or classify standalone files consistently with the new directory filtering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed at all three sites. I had applied the mmproj rule to directories twice and still left the standalone-file branches assigning model_format = "gguf" unconditionally, so a loose mmproj-*.gguf was offered as a model. All three now gate on _is_main_gguf_filename, including the LM Studio publisher/model.gguf branch, which classifies separately from the flat one. Tests cover the flat and nested layouts.

Comment on lines +257 to +258
const key = model.toLowerCase();
if (seen.has(key)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve case-sensitive local model paths

On case-sensitive filesystems, cachedRepos also contains the absolute IDs returned by localGgufEntries, but this shared add function lowercases every identifier as though it were a Hugging Face repo. Distinct models such as /models/Foo/model.gguf and /models/foo/model.gguf therefore collide and one disappears from the picker, despite the later code treating the path as the local model's identity. Apply case folding only to Hugging Face repo IDs and retain exact path keys.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. cachedRepos is built from the cached repo ids plus localEntries.map((entry) => entry.id), and those ids are absolute paths, so the shared lowercase key folded together two paths that differ only in case. The case-insensitive key was added for Hugging Face ids, where it is correct, and it should never have applied to paths. add now folds only non-path ids, reusing the existing looksLikePath.

"settings.agents.agent",
"settings.agents.model",
"settings.agents.quantization",
"settings.agents.subagent.title",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the subagent search target mounted

When the currently selected agent is Hermes or OpenClaw, SubagentSection returns null, so this indexed label has no rendered data-settings-label target. Selecting the “Use a local model as a subagent” search result while one of those agents is selected causes SettingsDialog to retry for 30 frames and then abandon the scroll without highlighting anything. Either omit conditional entries from the search index or keep an anchor mounted independently of the selected agent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it contradicts the rule I wrote in that same block. SubagentSection returns null when the agent is not one of the four that support subagents, and the only data-settings-label for that key lives inside it, so the hit had nothing to scroll to. Removed the key rather than mounting an anchor for a section that genuinely does not apply to those agents.

Worth flagging while I was in there: options.title, remote.title, passthrough.title and dryRun.title have no rendered label at all, so they have the same defect more severely. Those predate this PR and I have left them alone rather than widen the diff, but they should be either removed or given anchors.

Comment thread studio/backend/routes/models.py Outdated
# Classify from the snapshot's own weights. A GGUF repo without a -GGUF
# suffix is common, and leaving this unset makes every consumer guess from
# the name; the snapshot is already resolved just above.
model_format = _dir_model_format(Path(snapshot), recursive = True) if snapshot else None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid a duplicate recursive scan in auto-switch indexing

With model auto-switch enabled and the five-second resolver cache expired, _build_index calls _scan_hf_cache, so this new recursive format classification walks every cached snapshot before _local_gguf_entry immediately calls list_local_gguf_variants, which recursively walks the same snapshot again. Non-GGUF repositories are especially costly because both passes must exhaust the tree, making an OpenAI request wait for duplicate filesystem scans across every configured HF cache. Skip format classification for resolver scans or reuse its result to avoid the second traversal.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, and this is a cost I introduced last round when I made the classification recursive. Nothing on the resolver path reads model_format: _local_gguf_entry goes straight to list_local_gguf_variants. So _scan_hf_cache takes classify_format now and _scan_hf_once passes False, which removes the walk entirely there rather than trying to share its result. Verified by counting _dir_model_format calls: 0 through _build_index, still 1 through the route path, so the API keeps its classification.

Comment on lines +306 to +307
if (!(model.id && isLocalGguf(model))) {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Filter partial HF snapshots out of local GGUF entries

When an inactive HF cache contains an interrupted sharded GGUF download, _scan_hf_cache sets partial = true but still gives the newest snapshot a path ID and classifies it as GGUF as soon as one shard exists. This new consumer ignores the partial field and adds that path alongside the cached repo row; selecting the path makes variant discovery treat the incomplete shard set as downloaded and generates a command that fails during load. This also bypasses _repo_gguf_load_id, which deliberately withholds a path when no snapshot is complete so the repo ID can fetch the missing shards.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and it is the same conclusion as the load_id item, applied one layer up. localGgufEntries now skips partial entries, so an interrupted sharded download is offered only as the cached repo row, which fetches the missing shards. The frontend LocalModelInfo did not carry partial, so I added it to match the backend model rather than reading an untyped field; typecheck caught that.

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: 4a070df435

ℹ️ 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".

Resolve the agents-tab conflict from unslothai#7462 by keeping the model picker and
adopting its UI font scale tokens, so the tab still honours the font size setting.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3a17f7b6da

ℹ️ 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".

Comment on lines +263 to +264
try:
aliases.extend(p.name for p in snapshots.iterdir() if p.is_dir())

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Do not remap complete revision IDs to a partial snapshot

When an inactive HF cache contains an older complete snapshot and a newer interrupted download, _scan_hf_cache builds this repo's entry from _resolve_hf_cache_realpath, which selects the newest directory without checking shard completeness. Aliasing every revision basename to that one entry means a durable subagent ID for the older complete revision now resolves to the partial snapshot, so its next request attempts to load missing shards and fails. Build aliases per complete revision, or ensure the shared entry points to a complete snapshot before adding these aliases.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. The scan resolves a repo through _resolve_hf_cache_realpath, which takes the newest snapshot by mtime with no completeness check, so aliasing every revision onto that entry could redirect a pin naming an older complete revision onto a half-downloaded one and break a request that works today. Sibling aliases are now added only when the resolved target passes snapshot_variants_all_complete. The primary alias for the scanned revision is unchanged; that one pointing at its own snapshot is the pre-existing behaviour and out of scope here.

The completeness walk only runs when the repo actually has more than one revision dir, so the common single-revision case stays free. That keeps the duplicate-scan cost you raised last round from creeping back onto the request path.

Comment on lines +3081 to +3083
for _, snapshot in candidates:
if inventory_scan._completed_gguf_variants(Path(snapshot)):
return snapshot

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Exclude incomplete variants from snapshot load IDs

When an inactive-cache snapshot contains one complete quant and another partially downloaded split quant, _completed_gguf_variants is nonempty, so this returns the whole snapshot as load_id. The Agents tab then enumerates that local path, whose local GGUF scanner treats the partial shard group as a downloaded variant; selecting it generates a command that asks llama-server to load missing shards and fails. Either return only snapshots whose advertised variants are all complete, or propagate the completed-variant set so incomplete quants are filtered from the picker.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, "at least one complete quant" was the wrong predicate, since the picker enumerates the whole directory rather than the quant that happened to be complete. New snapshot_variants_all_complete compares what list_local_gguf_variants would advertise against _completed_gguf_variants, and a snapshot only qualifies when every advertised quant is fully present. I used the variant lister rather than revision.files on purpose: split quants live in per-quant subdirectories, so a label taken from the bare filename would not match, and both of these derive labels from extract_quant_label over paths relative to the snapshot, so the sets compare directly.

raw_id,
getattr(info, "model_id", None),
getattr(info, "display_name", None),
*_public_aliases(raw_id),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Restrict revision aliases to Hugging Face cache entries

This calls _public_aliases for every scanner result, although that helper treats any model whose parent directory is named snapshots as a revision of one repository. If a custom scan folder such as /data/snapshots contains separate GGUF model directories model-a and model-b, processing the first directory aliases both sibling names to its entry; setdefault then prevents the real second model from replacing that alias, so requests for model-b silently load model-a. Only expand sibling revision names for verified hf_cache entries under a models--* repository directory.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and this is the worst of the three: it silently serves the wrong model rather than failing. _public_aliases keyed off parent.name == "snapshots" alone, so a scan folder like /data/snapshots holding model-a and model-b aliased both names to whichever was processed first, and setdefault then locked the real second model out. It now also requires the grandparent to be a models--* repo directory, which only a real cache repo has. Verified end to end both ways: a genuine cache repo still aliases rev-old to rev-new, and the scan-folder case indexes model-a only.

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e967dc93ea

ℹ️ 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".

),
].filter(Boolean);
return (
<SelectItem key={variant.quant} value={variant.quant}>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exclude incomplete GGUF variants from generated commands

When a cached repo contains an interrupted split quantization, list_cached_gguf still returns the repo and snapshot_variants_all_complete only suppresses its load_id; this selector nevertheless makes every returned quantization selectable. Choosing the incomplete quant generates --model repo:quant, and the load path can resolve shard 1 from the cache as a local GGUF before discovering that the remaining shards are absent, so the copied command fails instead of downloading a complete variant. Propagate the partial/completeness state to this list and disable or omit incomplete variants.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed, and checking it turned up that the obvious fix would have been inert. The hub service does compute per-variant partial, but /gguf-variants re-packs into models.models.GgufVariantsResponse, whose GgufVariantDetail has no such field, so it was dropped before reaching the client and a frontend filter alone would have silently done nothing.

So partial is carried through the response model and the repack, and the quantization list filters on it. Confirmed the flag survives serialization rather than assuming it: a variant marked partial now round-trips as partial: true instead of being stripped.

Comment on lines +411 to +413
model_format = (
_dir_model_format(Path(snapshot), recursive = True)
if snapshot and classify_format

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep recursive cache scans off the async request path

When the user has a large Hugging Face cache, this classification calls _dir_model_format(..., recursive=True) synchronously for every cached repository reached by the async /api/models/local route. In particular, each non-GGUF snapshot requires rglob to walk its entire directory tree before concluding that no GGUF exists, and opening the new Agents tab invokes this endpoint immediately, so one settings visit can block the backend event loop and delay unrelated inference/API requests. Reuse the file inventory already produced by the cache scan, cache the classification, or offload the recursive work from the request thread.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair, /api/models/local is async def and this ran synchronously per cached repo. I could not take the "reuse the file inventory" option: _scan_hf_cache globs models--* directly and has no revision file list to reuse.

Instead the walk is now bounded to one level (*/*.gguf), which is where split quants actually live, so the unbounded rglob is gone and a non-GGUF snapshot no longer has to exhaust its tree to conclude there is no GGUF. That keeps the sharded-repo correctness the recursion was added for without the event-loop exposure.

Comment on lines +280 to +281
if snapshot_variants_all_complete(load_path):
aliases.extend(others)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve old snapshot aliases when the newest revision is partial

When an inactive-cache model was loaded by path and a durable subagent config therefore stores its snapshot basename, adding a newer but incomplete snapshot makes _scan_hf_cache select that new snapshot while this branch refuses to alias the older revision. Because the scanner emits only one entry per repo, the older complete snapshot is not indexed separately either; subsequent requests using the saved old revision no longer resolve and can fall through to whatever unrelated model is currently loaded. Index each complete sibling revision to its own load path, or retain the old alias without redirecting it to the partial snapshot.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and my previous fix traded a loud failure for a silent one, which is worse. Refusing to alias left the old revision unindexed entirely, so a saved pin fell through to whatever happens to be loaded.

Rebuilt along the lines you suggest: each complete sibling revision is now indexed to its OWN load path via _sibling_revision_entries, instead of aliasing anything onto the scanned entry. A pin naming an older revision resolves to that revision, a newer partial snapshot is skipped rather than substituted, and the models--* guard stays so a scan folder called snapshots is not mistaken for revisions. Verified end to end on exactly your scenario: newest snapshot half-downloaded, rev-old still resolves and its load_path is the old directory.

@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: f79c63e525

ℹ️ 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".

@danielhanchen
danielhanchen merged commit 7f0910f into unslothai:main Jul 27, 2026
45 checks passed
danielhanchen added a commit to danielhanchen/unsloth-staging-2 that referenced this pull request Jul 27, 2026
local_model_resolver.py conflicted where both sides appended after
_build_index: main added _sibling_revision_entries (unslothai#7312) and this branch
added invalidate_index. Kept both, with main's helper next to the caller that
needs it. Taking either side alone leaves _build_index calling a name that no
longer exists.
danielhanchen pushed a commit to Souravrajvi0/unsloth that referenced this pull request Jul 27, 2026
unslothai#7375

unslothai#7312 landed _dir_model_format(recursive = True) and made _scan_hf_cache classify
every snapshot from its own weights, which covers the suffixless GGUF repo this
branch was hinting plus the per-quant subdirectory layout the flat probe missed.
Keeping a second implementation here only conflicted with it, and the branch copy
was the weaker one: it ran on inactive caches alone and globbed the snapshot root,
so a repo storing its quants under UD-Q2_K_XL/ still came back unclassified.

Reverts this branch to main for routes/models.py. The frontend contract test for
the routing keeps its coverage, since it pins the picker against a stubbed row.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants