Skip to content

Feat/model picker per model config v2 - #7207

Merged
danielhanchen merged 154 commits into
unslothai:mainfrom
Sneakr:feat/model-picker-per-model-config-v2
Jul 21, 2026
Merged

Feat/model picker per model config v2#7207
danielhanchen merged 154 commits into
unslothai:mainfrom
Sneakr:feat/model-picker-per-model-config-v2

Conversation

@Sneakr

@Sneakr Sneakr commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Reintroduce model picker per model config

This resubmits the model picker per model config feature from #6647. That PR was squash merged as 8cbdfbe and then reverted on main in 1c7bce4, so the feature is currently not in main.

This branch is the same feature branch including danielhanchen's follow up fixes, plus four additional bug fixes:

  • The context length control now stores the exact value the user sets. Before, setting the slider to the native maximum was converted to null, which made the slider jump back and made Reload load the previous context instead of the chosen one.
  • When resolving a GGUF without a variant, the picker now prefers the first split of a sharded model instead of the largest file. Later splits do not carry the chat template metadata, so a sharded model could silently lose its default template.
  • The template and metadata hooks clear their previous result when a retry starts, so a stale error is no longer shown while a new fetch is in flight.
  • The native path token and its lease expiry are now recorded together when a load commits. Before, the expiry was written by only one load path and even when a load did not start, so a reload could be blocked with an expired file message for a still valid token.

Sneakr and others added 30 commits June 24, 2026 18:22
Relocate model-selector + its support files from components/assistant-ui
into a self-contained features/model-picker feature (own barrel), mirroring
the modular Hub layout. Pure move + import repoint; no behaviour change.
Superset PerModelConfig (customContextLength, kvCacheDtype, speculativeType,
specDraftNMax, tensorParallel, chatTemplateOverride, trustRemoteCode) persisted
to localStorage (unsloth_model_configs) with schema versioning + LRU budget.
KV-dtype and speculative value sets match main's sidebar (no q4_0/ngram-simple).
Reuses features/hub/lib/model-identity for normalization; adds storage-key layer
and applyPerModelConfigToRuntime (sets tensorParallel, which the old PR omitted).
New studio/backend/picker package (schemas/service/routes) mounted at /api/picker:
- POST /api/picker/validate-chat-template (Jinja syntax validation, no false positives)
- GET  /api/picker/chat-template/{model_name} (default template from tokenizer_config.json,
  reusing get_cache_path/resolve_cached_repo_id_case; graceful null, no model-code exec)
Frontend api/templates.ts client + hooks/use-model-defaults lazy cache. No backend
changes to the existing inference load route (per-model load fields already supported).
Picker now sources cached + local models from useHubInventory (the Hub's shared
store) via a thin adapter, replacing its own /api/models/* fetchers + module
caches. Hub, download manager, and picker now share one source of truth, so
completed downloads reflect in the picker automatically. Partial/live-download
rows are filtered from the cached lists (unchanged rendering). Local naming/search
preserved via additive LocalInventoryRow modelId/displayName. Variant expander,
scan-folder management, recommended-fit, search, external providers untouched.

Known minor: cached 'Downloaded date' sort tiebreak degrades to alphabetical
(hub cached rows carry no mtime); default 'recent' (load-time) sort preserved.
Picking a (non-external) model now opens an in-picker config view built from
main's current load controls (context length, KV cache dtype, speculative
decoding, draft tokens, tensor parallel) plus a chat-template editor backed by
the picker validate/default endpoints. 'Remember for this model' persists the
config per model+variant; Run forwards the config to the existing load flow via
meta.config. External models bypass the step. Two-view orchestration lives in
model-selector (single interception point); pickers.tsx call sites untouched.
trustRemoteCode dropped from PerModelConfig to preserve main's per-load consent.
handleCheckpointChange threads meta.config into the selection; stageOrLoad and
the autoload/Hub-run paths now apply the picker config (explicit pick or saved
remembered config) via applyPerModelConfigToRuntime before staging/loading, with
keepSpeculative set so a remembered speculative mode survives the model switch.
Replaces the old remembered-load-settings seeding (resolveInitialConfig now the
single source). SelectedModelInput carries config.
The load knobs (context, KV cache, speculative, draft tokens, tensor parallel)
and the chat-template editor now live only in the picker config step. The sheet's
Model section keeps the staged Load/Cancel flow (config is applied at pick time);
sampling params, system prompt, and RAG are unchanged. Deletes the superseded
remembered-load-settings module + the store's applyRememberedLoadSettings action,
removes the now-dead sheet state/imports, and points the settings reset at
unsloth_model_configs. Delete-cleanup deferred (stale config is LRU-capped).
…odel section

The downloaded-variant gear (ModelLoadSettingsAction) staged a model straight
into the right-sidebar Run-settings flow -- the old 'configure before load' path
now fully replaced by the in-picker config step. Removed the gear + its component.
Also gate the sheet's 'Model' section to staged picks only (pendingSelection):
after the load-knob strip its content is staged-only, so it was rendering an
empty section header whenever a model was merely loaded.
…bled

After the load-config UI moved into the picker, the store's per-model setters
(setKvCacheDtype/setSpeculativeType/setSpecDraftNMax/setTensorParallel/
setCustomContextLength/setChatTemplateOverride) had zero callers
(applyPerModelConfigToRuntime writes via setState), and the sheet's
modelControlsDisabled was unreferenced. Verified dead across the whole tree.
Root cause: with Settings > Chat > 'Load on selection' turned OFF, the config
step's load went down the deferred-staging path -- opening the right sidebar with
'<model> is staged, not loaded yet / Choose Load model'. The in-picker config step
IS the deliberate load action, so its Load now loads immediately (or downloads +
auto-loads when not cached) regardless of the toggle. Renamed the button
'Run model' -> 'Load model' to match. Native/dropped picks still honor the toggle.
…nly load flow

The in-picker config step (and the Hub Run button) now fully supersede the old
stage-to-sidebar flow, so the Load-on-selection toggle is removed everywhere:
- chat stageOrLoad: every pick loads immediately, or downloads + auto-loads when
  not cached (the previous default behaviour, now universal).
- hub Run: drops the stage branch; downloaded GGUFs load directly with their saved
  per-model config (no collision with the chat config step — both end at selectModel).
- store: removed loadOnSelection field/setter/key/default; Settings>Chat toggle and
  its settings-reset entry removed.
- staged sidebar section is now a download-progress view (auto-loads on completion).
No manual staging remains; stageModel is used only for background auto-load downloads.
…through config flow

Read the embedded tokenizer.chat_template from GGUF files (read_gguf_chat_template
in gguf_metadata) and use it as the per-model default. Plumb gguf_variant through
the picker service, /api/picker/chat-template route, frontend templates API, and
use-model-defaults so the right variant's template is fetched.

Also refine the picker config-page/model-selector wiring, drop the dead
ggufNativeContextLength runtime path, and add the per-model-config storage keys to
the settings prefs export.
…e it has no effect

Resolve the default chat template for safetensors models: prefer the modern
chat_template.jinja, fall back to the tokenizer_config.json chat_template field,
then chat_template.json (multimodal processor), then the GGUF embedded template.
Applied to local dirs, the HF cache snapshot scan, and the HF remote fetch.

Hide the chat-template editor in the picker for safetensors models — the override
is only applied at load by the GGUF/llama.cpp backend, so editing it on safetensors
currently has no effect. GGUF keeps the editor. Nothing removed; the dialog stays
for when the safetensors apply path is wired up in a later branch.
…ceeds

Set unsloth_model_configs_migrated only once writeMap confirms the migrated
map persisted, so a quota/storage failure no longer marks migration done and
silently drops the user's pre-existing remembered settings — the next load retries.
Sync 59 upstream commits. Resolve the lone pickers.tsx conflict by fully adopting upstream quick-eject (unslothai#6654) into the refactored picker: thread onEject + RemoveCircleIcon + floating pill through HubModelPicker, wire it from model-selector, and guard the footer eject button to non-hub tabs so the hub tab shows only the pill.
…ig' into feat/model-picker-per-model-config

# Conflicts:
#	studio/backend/picker/service.py
Apply remembered per-model configs consistently from picker and Hub loads, keep default configs from overriding standing speculative settings, add config access for direct local GGUF files, and support saving or forgetting active model settings without a reload.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

1 similar comment
@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: 54568b8dad

ℹ️ 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/backend/picker/service.py Outdated
Comment on lines +392 to +395
for rel in _JINJA_TEMPLATE_PATHS:
template = _download_text(rel)
if template and template.strip():
return template

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 Skip over-cap remote Jinja templates before selecting them

For an uncached Hub repo with a chat_template.jinja between 64 KiB and 4 MiB plus a valid tokenizer_config.json, this returns the oversized Jinja immediately. The route then drops it at its 64 KiB response cap, so it never falls through to the valid tokenizer template (unlike the local path, which skips oversized Jinja files). Apply MAX_CHAT_TEMPLATE_BYTES to raw Jinja downloads or continue searching when its extracted template exceeds that limit.

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 fixed in 774a860. The remote path only bounded raw chat_template.jinja downloads at MAX_TEMPLATE_METADATA_BYTES (4 MiB) and returned the first non-empty Jinja unconditionally, while the route drops any template over MAX_CHAT_TEMPLATE_BYTES (64 KiB), so an uncached repo with a 64 KiB-4 MiB Jinja plus a valid smaller tokenizer_config.json returned no template. It now gates the extracted Jinja on MAX_CHAT_TEMPLATE_BYTES and continues searching when over the cap, matching the local _chat_template_from_jinja_file gate; the 4 MiB bound stays for JSON files that embed a small template. Added a regression test in tests/test_picker_service.py.

danielhanchen and others added 2 commits July 20, 2026 17:19
The remote chat-template resolver bounded raw chat_template.jinja downloads
only by MAX_TEMPLATE_METADATA_BYTES (4 MiB), then returned the first
non-empty Jinja unconditionally. The picker route drops any template larger
than MAX_CHAT_TEMPLATE_BYTES (64 KiB), so an uncached repo whose
chat_template.jinja sits between 64 KiB and 4 MiB returned no template at
all, even when a valid smaller tokenizer_config.json template existed. The
local path already skips oversized .jinja files and falls through.

Gate the extracted Jinja on MAX_CHAT_TEMPLATE_BYTES and continue searching
when it exceeds the cap, matching _chat_template_from_jinja_file. The 4 MiB
download bound stays for JSON files that merely embed a small template. Adds
a regression test that a big Jinja plus a valid tokenizer config resolves to
the tokenizer template.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: e493c30861

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

Copy link
Copy Markdown
Member

Status from my side: this is ready. Codex approves the current head (e493c30) and every inline item is addressed.

All PR-specific CI is green. The only red checks are the three Core (HF=...) matrices, and those are a pre-existing main-wide failure, not something this branch introduces:

  • The failure is AttributeError: module 'unsloth_zoo' has no attribute 'vision_utils' at tests/test_vision_collator_audio.py:510.
  • This PR changes only studio/, tests/studio/, and one .github workflow. It does not touch tests/test_vision_collator_audio.py or any unsloth_zoo code.
  • The same three Core matrices fail identically on unrelated open PRs (for example Windows torch release preservation and a torch 2.11 default across installers #7256), which confirms the drift is main-wide.

So the Core reds are not merge-blocking for this branch; they will clear once unsloth_zoo main is realigned.

Resolves the pickers.tsx import conflict from unslothai#7266 (malformed HF token must
not empty the Recommended list). The PR consolidated the picker imports onto
the @/features/hub barrel, so the resolution keeps that structure and adds
main's new hfApiToken there (also re-exported from the hub barrel) rather than
reintroducing the deep per-module import paths. The accessToken = hfApiToken(
hfToken) fix from unslothai#7266 merges in unchanged.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: fd29d97d9c

ℹ️ 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 and others added 3 commits July 21, 2026 04:01
The v1->v2 localStorage migration (unsloth_load_settings ->
unsloth_model_configs) runs on every store read, so it must migrate exactly
once and never re-run, duplicate, or clobber a newer per-model config on a
reload or restart. That was covered only by a manual proof, so add durable
guards:

- Source-contract test pinning the three idempotency layers (the in-memory
  legacyMigrationChecked guard, the persistent unsloth_model_configs_migrated
  flag set in every terminal branch, and the non-overwriting Object.hasOwn
  merge-skip) plus the readMap invocation. Reddens if any layer is dropped.

- Playwright model-config E2E: promote the legacy-migration step to a gating
  check (soft_fail, which gates under the CI STUDIO_UI_STRICT=1) that the
  migrated value is preserved and the flag is set, then reload again with a
  fresh legacy seed present and assert the stored key set is unchanged, so a
  second reload cannot re-migrate, duplicate, or clobber.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: 3554bd824e

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

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 9988b2a9c5

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

Resolves the hub-page.tsx import conflict from unslothai#7271 (immediate tab
navigation). Keeps the PR's chat + hf-token-store imports and adds main's new
hub-feed-store import (isChannelEntryFresh, useHubFeedStore, used by the
merged tab-navigation body); drops main's duplicate hf-token-store import
since the PR already imports hfApiToken/useHfTokenStore from ./stores.
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

1 similar comment
@danielhanchen

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: e021145f0f

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

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.

5 participants