* Add Agents settings tab for unsloth start 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. * Use official brand logos for agents, invert Ollama and OpenRouter in 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. * Title Agents tab "Agents (unsloth start)" and move it below Connections The in-tab header now reads "Agents (unsloth start)" while the sidebar label stays "Agents". Reorders the tab to sit below Connections. * Address review: guard PATH detection, fix copy timeout, OS-aware remote 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. * Add interactive Agents command builder * Add local subagent command guidance * Add official coding agent icons * Use client OS for remote commands, fix copy a11y and model wording (#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 * Shell-quote the model, forward the HF token, and fix the quant placeholder - 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 * Fix Agents command discovery and routing * Unsloth start improvements: download progress, server reuse, and safe model switching (#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 #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> * Unsloth start: add local subagents for Claude Code, Codex, OpenCode and Pi (#7326) Bring the local-subagent support onto main. The original change (#7316) merged into the stacked pr/daniel-unsloth-start-audit branch rather than main, and #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. * Fix Agents builder defaults and flag validation * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix Agents variant and provider fallbacks * Fix local model and Pi subagent edge cases * Agents tab: flag the Codex row when the loaded model is not GGUF * Agents tab: target the active Studio server, wrap narrow rows, index the tab's search terms * Agents tab: build copied commands from the browser-reachable Studio and show the key placeholder * Preserve cache load ids and path variants in built commands for PR #7312 A GGUF outside the active Hugging Face cache only loads by its snapshot path, so keep that load_id for --model while still listing the row by repo id. Path based models carry their quant in --gguf-variant rather than a ":variant" suffix, and the active selection now keeps the variant inference status reports for them. * Agents tab: index the intro for agent-name searches and keep long commands inside the panel * List GGUF variants from the cache the command loads from for PR #7312 A snapshot outside the active Hugging Face cache was offering the remote variant list, so a quant absent from that snapshot could be selected and the generated command would fail to load it. * Agents tab: omit --api-key so the CLI can replay a saved key for the base * Agents tab: label the indexed heading rows and fall back to the active desktop API base * Agents tab: name every supported agent in the indexed intro for PR #7303 * Send the cached GGUF load path and fix the agents tab search targets for PR #7312 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Tighten the agents tab comments for PR #7303 * Build the agents tab example commands from the active Studio base for PR #7303 * Keep the resident model on its active cache load for PR #7312 * Tighten the agents tab and cached GGUF comments for PR #7312 * Take the agent command shell from the Studio host for PR #7303 * Stop emitting snapshot paths as --model and keep unsloth start searchable for PR #7312 * Pick the command shell from where the CLI runs for PR #7303 * Match a path load by its advertised id and follow the resident model for PR #7312 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Keep an explicit quantization and retire superseded native-grant labels for PR #7312 * Scope the remembered quant, stop following unloaded models and keep local GGUF paths for PR #7312 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Stop shadowing the path classifier, match snapshot ordering and sequence status polls for PR #7312 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Release stale native-grant picks, keep local GGUF identities and index snapshot aliases for PR #7312 * Index inactive-cache snapshots, widen local GGUF detection and clear retired quants for PR #7312 * Classify cached repos by snapshot, merge repo ids case-insensitively and keep loose GGUFs variantless for PR #7312 * Fix snapshot alias, partial split and mmproj-only handling for PR #7312 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Trust scanned model_format and drop incomplete snapshot ids for PR #7312 * Exclude mmproj and partial downloads, keep path case and drop duplicate scan for PR #7312 * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Restrict revision aliases and require complete snapshot variants for PR #7312 * Index revisions individually and hide partial variants for PR #7312 --------- Co-authored-by: shimmyshimmer <107991372+shimmyshimmer@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com> Co-authored-by: oobabooga <oobabooga4@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
300 lines
12 KiB
Python
300 lines
12 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Pydantic schemas for Model Management API"""
|
|
|
|
from pydantic import BaseModel, Field
|
|
from typing import Optional, List, Dict, Any, Literal
|
|
|
|
ModelType = Literal["text", "vision", "audio", "embeddings"]
|
|
|
|
|
|
class CheckpointInfo(BaseModel):
|
|
"""Information about a discovered checkpoint directory."""
|
|
|
|
display_name: str = Field(..., description = "User-friendly checkpoint name (folder name)")
|
|
path: str = Field(..., description = "Full path to the checkpoint directory")
|
|
loss: Optional[float] = Field(None, description = "Training loss at this checkpoint")
|
|
|
|
|
|
class ModelCheckpoints(BaseModel):
|
|
"""A training run and its associated checkpoints."""
|
|
|
|
name: str = Field(..., description = "Training run folder name")
|
|
checkpoints: List[CheckpointInfo] = Field(
|
|
default_factory = list,
|
|
description = "List of checkpoints for this training run (final + intermediate)",
|
|
)
|
|
base_model: Optional[str] = Field(
|
|
None,
|
|
description = "Base model name from adapter_config.json or config.json",
|
|
)
|
|
peft_type: Optional[str] = Field(
|
|
None,
|
|
description = "PEFT type (e.g. LORA) if adapter training, None for full fine-tune",
|
|
)
|
|
lora_rank: Optional[int] = Field(
|
|
None,
|
|
description = "LoRA rank (r) if applicable",
|
|
)
|
|
is_quantized: bool = Field(
|
|
False,
|
|
description = "Whether the model uses BNB quantization (e.g. bnb-4bit)",
|
|
)
|
|
|
|
|
|
class CheckpointListResponse(BaseModel):
|
|
"""Response for listing available checkpoints in an outputs directory."""
|
|
|
|
outputs_dir: str = Field(..., description = "Directory that was scanned")
|
|
models: List[ModelCheckpoints] = Field(
|
|
default_factory = list,
|
|
description = "List of training runs with their checkpoints",
|
|
)
|
|
|
|
|
|
class ExportSizeResponse(BaseModel):
|
|
"""Model fp16/bf16-equivalent size; size fields are null when unknown."""
|
|
|
|
model: str = Field(..., description = "Model id or path the estimate was computed for")
|
|
fp16_bytes: Optional[int] = Field(
|
|
None,
|
|
description = "Estimated FP16/BF16-equivalent on-disk size in bytes, or null if unknown",
|
|
)
|
|
total_params: Optional[int] = Field(
|
|
None,
|
|
description = "Estimated total parameter count (fp16_bytes // 2), or null if unknown",
|
|
)
|
|
source: str = Field(
|
|
"unavailable",
|
|
description = "How the estimate was derived (e.g. safetensors, config, local, vllm, unavailable)",
|
|
)
|
|
|
|
|
|
class ModelDetails(BaseModel):
|
|
"""Model configuration and metadata; used for both list and detail views"""
|
|
|
|
id: str = Field(..., description = "Model identifier")
|
|
model_name: Optional[str] = Field(
|
|
None, description = "Model identifier (alias for id, for backward compatibility)"
|
|
)
|
|
name: Optional[str] = Field(None, description = "Display name for the model")
|
|
config: Optional[Dict[str, Any]] = Field(None, description = "Model configuration dictionary")
|
|
is_vision: bool = Field(False, description = "Whether model is a vision model")
|
|
is_embedding: bool = Field(
|
|
False, description = "Whether model is an embedding/sentence-transformer model"
|
|
)
|
|
is_lora: bool = Field(False, description = "Whether model is a LoRA adapter")
|
|
is_gguf: bool = Field(False, description = "Whether model is a GGUF model (llama.cpp format)")
|
|
is_mlx: bool = Field(
|
|
False, description = "Whether model is served via the MLX backend (Apple Silicon)"
|
|
)
|
|
is_audio: bool = Field(False, description = "Whether model is a TTS audio model")
|
|
audio_type: Optional[str] = Field(None, description = "Audio codec type: snac, csm, bicodec, dac")
|
|
has_audio_input: bool = Field(False, description = "Whether model accepts audio input (ASR)")
|
|
model_type: Optional[ModelType] = Field(
|
|
None, description = "Collapsed model modality: text, vision, audio, or embeddings"
|
|
)
|
|
base_model: Optional[str] = Field(None, description = "Base model if this is a LoRA adapter")
|
|
max_position_embeddings: Optional[int] = Field(
|
|
None, description = "Maximum context length supported by the model"
|
|
)
|
|
model_size_bytes: Optional[int] = Field(
|
|
None, description = "Total size of model weight files in bytes"
|
|
)
|
|
|
|
|
|
class LoRAInfo(BaseModel):
|
|
"""LoRA adapter or exported model information"""
|
|
|
|
display_name: str = Field(..., description = "Display name for the LoRA")
|
|
adapter_path: str = Field(..., description = "Path to the LoRA adapter or exported model")
|
|
base_model: Optional[str] = Field(None, description = "Base model identifier")
|
|
source: Optional[str] = Field(None, description = "'training' or 'exported'")
|
|
export_type: Optional[str] = Field(
|
|
None, description = "'lora', 'merged', or 'gguf' (for exports)"
|
|
)
|
|
|
|
|
|
class LoRAScanResponse(BaseModel):
|
|
"""Response schema for scanning trained LoRA adapters"""
|
|
|
|
loras: List[LoRAInfo] = Field(default_factory = list, description = "List of found LoRA adapters")
|
|
outputs_dir: str = Field(..., description = "Directory that was scanned")
|
|
|
|
|
|
class ModelListResponse(BaseModel):
|
|
"""Response schema for listing models"""
|
|
|
|
models: List[ModelDetails] = Field(default_factory = list, description = "List of models")
|
|
default_models: List[str] = Field(default_factory = list, description = "List of default model IDs")
|
|
|
|
|
|
class GgufVariantDetail(BaseModel):
|
|
"""A single GGUF quantization variant in a HuggingFace repo."""
|
|
|
|
filename: str = Field(..., description = "GGUF filename (e.g., 'gemma-3-4b-it-Q4_K_M.gguf')")
|
|
quant: str = Field(..., description = "Quantization label (e.g., 'Q4_K_M')")
|
|
size_bytes: int = Field(0, description = "File size in bytes")
|
|
download_size_bytes: int = Field(0, description = "Total bytes needed to download this variant")
|
|
downloaded: bool = Field(
|
|
False, description = "Whether this variant is already in the local HF cache"
|
|
)
|
|
update_available: bool = Field(
|
|
False, description = "Whether a newer version of this variant is available on HF"
|
|
)
|
|
partial: bool = Field(
|
|
False,
|
|
description = "Whether this variant is an interrupted download. The hub service "
|
|
"already computes it; carry it through so callers can hide a quant whose shards "
|
|
"are incomplete instead of offering one that cannot load.",
|
|
)
|
|
|
|
|
|
class GgufVariantsResponse(BaseModel):
|
|
"""Response for listing GGUF quantization variants in a HuggingFace repo."""
|
|
|
|
repo_id: str = Field(..., description = "HuggingFace repo ID")
|
|
variants: List[GgufVariantDetail] = Field(
|
|
default_factory = list, description = "Available GGUF variants"
|
|
)
|
|
has_vision: bool = Field(
|
|
False, description = "Whether the model has vision support (mmproj files)"
|
|
)
|
|
default_variant: Optional[str] = Field(
|
|
None, description = "Recommended default quantization variant"
|
|
)
|
|
context_length: Optional[int] = Field(
|
|
None,
|
|
description = "Native max context from GGUF metadata; set once a variant is downloaded",
|
|
)
|
|
|
|
|
|
class LocalModelInfo(BaseModel):
|
|
"""Discovered local model candidate."""
|
|
|
|
id: str = Field(..., description = "Identifier to use for loading/training")
|
|
display_name: str = Field(..., description = "Display label")
|
|
path: str = Field(..., description = "Local path where model data was discovered")
|
|
source: Literal["models_dir", "hf_cache", "lmstudio", "custom"] = Field(
|
|
...,
|
|
description = "Discovery source",
|
|
)
|
|
model_id: Optional[str] = Field(
|
|
None,
|
|
description = "HF repo id for cached models, e.g. org/model",
|
|
)
|
|
active_cache: Optional[bool] = Field(
|
|
None,
|
|
description = "Whether an HF model belongs to the current download cache.",
|
|
)
|
|
partial: bool = Field(
|
|
False,
|
|
description = "Whether the cached model has an incomplete download.",
|
|
)
|
|
model_format: Optional[str] = Field(
|
|
None,
|
|
description = "Detected weights format ('gguf' when known). Lets the UI "
|
|
"classify scanned folders whose name lacks a -GGUF suffix.",
|
|
)
|
|
updated_at: Optional[float] = Field(
|
|
None,
|
|
description = "Unix timestamp of latest observed update",
|
|
)
|
|
|
|
|
|
class LocalModelListResponse(BaseModel):
|
|
"""Response schema for listing local/cached models."""
|
|
|
|
models_dir: str = Field(..., description = "Directory scanned for custom local models")
|
|
hf_cache_dir: Optional[str] = Field(
|
|
None,
|
|
description = "HF cache root that was scanned",
|
|
)
|
|
lmstudio_dirs: List[str] = Field(
|
|
default_factory = list,
|
|
description = "LM Studio model directories that were scanned",
|
|
)
|
|
models: List[LocalModelInfo] = Field(
|
|
default_factory = list,
|
|
description = "Discovered local/cached models",
|
|
)
|
|
|
|
|
|
class AddScanFolderRequest(BaseModel):
|
|
"""Request body for adding a custom scan folder."""
|
|
|
|
path: str = Field(..., description = "Absolute or relative directory path to scan for models")
|
|
|
|
|
|
class ScanFolderInfo(BaseModel):
|
|
"""A registered custom model scan folder."""
|
|
|
|
id: int = Field(..., description = "Database row ID")
|
|
path: str = Field(..., description = "Normalized absolute path")
|
|
created_at: str = Field(..., description = "ISO 8601 creation timestamp")
|
|
|
|
|
|
class BrowseEntry(BaseModel):
|
|
"""A directory entry surfaced by the folder browser."""
|
|
|
|
name: str = Field(..., description = "Entry name (basename, not full path)")
|
|
has_models: bool = Field(
|
|
False,
|
|
description = (
|
|
"Hint that the directory likely contains models "
|
|
"(*.gguf, *.safetensors, config.json, or HF-style "
|
|
"`models--*` subfolders). Used by the UI to highlight "
|
|
"promising candidates; the scanner itself is authoritative."
|
|
),
|
|
)
|
|
hidden: bool = Field(
|
|
False,
|
|
description = "Name starts with a dot (e.g. `.cache`)",
|
|
)
|
|
|
|
|
|
class BrowseFoldersResponse(BaseModel):
|
|
"""Response schema for the folder browser endpoint."""
|
|
|
|
current: str = Field(..., description = "Absolute path of the directory just listed")
|
|
parent: Optional[str] = Field(
|
|
None,
|
|
description = (
|
|
"Parent directory of `current`, or null if `current` is the "
|
|
"filesystem root. The frontend uses this to render an `Up` row."
|
|
),
|
|
)
|
|
entries: List[BrowseEntry] = Field(
|
|
default_factory = list,
|
|
description = (
|
|
"Subdirectories of `current`. Sorted with model-bearing "
|
|
"directories first, then alphabetically case-insensitive; "
|
|
"hidden entries come last within each group."
|
|
),
|
|
)
|
|
suggestions: List[str] = Field(
|
|
default_factory = list,
|
|
description = (
|
|
"Handy starting points (home, HF cache, already-registered "
|
|
"scan folders). Rendered as quick-pick chips above the list."
|
|
),
|
|
)
|
|
truncated: bool = Field(
|
|
False,
|
|
description = (
|
|
"True when the listing was capped because the directory had "
|
|
"more subfolders than the server is willing to enumerate in "
|
|
"one request. The UI should show a hint telling the user to "
|
|
"narrow their path."
|
|
),
|
|
)
|
|
model_files_here: int = Field(
|
|
0,
|
|
description = (
|
|
"Count of GGUF/safetensors files immediately inside "
|
|
"``current``. Used by the UI to surface a hint on leaf "
|
|
"model directories (which otherwise look `empty` because "
|
|
"they contain only files, no subdirectories)."
|
|
),
|
|
)
|