[Studio] Show non exported models in chat UI (#4892)
* Show non exported models in chat UI * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Distinguish b/w LoRa and full fine tune saves. Cleanup --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Roland Tannous <115670425+rolandtannous@users.noreply.github.com>
This commit is contained in:
parent
a50f61009b
commit
dccc0ebada
10 changed files with 314 additions and 55 deletions
|
|
@ -31,6 +31,7 @@ __all__ = [
|
|||
# Config
|
||||
"ModelConfig",
|
||||
"is_vision_model",
|
||||
"scan_trained_models",
|
||||
"scan_trained_loras",
|
||||
"load_model_defaults",
|
||||
"get_base_model_from_lora",
|
||||
|
|
@ -72,6 +73,7 @@ def __getattr__(name):
|
|||
if name in (
|
||||
"is_vision_model",
|
||||
"ModelConfig",
|
||||
"scan_trained_models",
|
||||
"scan_trained_loras",
|
||||
"load_model_defaults",
|
||||
"get_base_model_from_lora",
|
||||
|
|
@ -79,14 +81,15 @@ def __getattr__(name):
|
|||
from utils.models import (
|
||||
is_vision_model,
|
||||
ModelConfig,
|
||||
scan_trained_loras,
|
||||
scan_trained_models,
|
||||
load_model_defaults,
|
||||
get_base_model_from_lora,
|
||||
)
|
||||
|
||||
globals()["is_vision_model"] = is_vision_model
|
||||
globals()["ModelConfig"] = ModelConfig
|
||||
globals()["scan_trained_loras"] = scan_trained_loras
|
||||
globals()["scan_trained_models"] = scan_trained_models
|
||||
globals()["scan_trained_loras"] = scan_trained_models
|
||||
globals()["load_model_defaults"] = load_model_defaults
|
||||
globals()["get_base_model_from_lora"] = get_base_model_from_lora
|
||||
return globals()[name]
|
||||
|
|
|
|||
|
|
@ -32,8 +32,9 @@ from auth.authentication import get_current_subject
|
|||
# Import backend functions
|
||||
try:
|
||||
from utils.models import (
|
||||
scan_trained_loras,
|
||||
scan_trained_models,
|
||||
scan_exported_models,
|
||||
get_base_model_from_checkpoint,
|
||||
load_model_defaults,
|
||||
get_base_model_from_lora,
|
||||
is_vision_model,
|
||||
|
|
@ -62,8 +63,9 @@ except ImportError:
|
|||
if str(parent_backend) not in sys.path:
|
||||
sys.path.insert(0, str(parent_backend))
|
||||
from utils.models import (
|
||||
scan_trained_loras,
|
||||
scan_trained_models,
|
||||
scan_exported_models,
|
||||
get_base_model_from_checkpoint,
|
||||
load_model_defaults,
|
||||
get_base_model_from_lora,
|
||||
is_vision_model,
|
||||
|
|
@ -791,15 +793,16 @@ async def scan_loras(
|
|||
lora_list = []
|
||||
|
||||
# Scan training outputs
|
||||
trained_loras = scan_trained_loras(outputs_dir = resolved_outputs_dir)
|
||||
for display_name, adapter_path in trained_loras:
|
||||
base_model = get_base_model_from_lora(adapter_path)
|
||||
trained_models = scan_trained_models(outputs_dir = resolved_outputs_dir)
|
||||
for display_name, model_path, model_type in trained_models:
|
||||
base_model = get_base_model_from_checkpoint(model_path)
|
||||
lora_list.append(
|
||||
LoRAInfo(
|
||||
display_name = display_name,
|
||||
adapter_path = adapter_path,
|
||||
adapter_path = model_path,
|
||||
base_model = base_model,
|
||||
source = "training",
|
||||
export_type = model_type,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
101
studio/backend/tests/test_trained_model_scan.py
Normal file
101
studio/backend/tests/test_trained_model_scan.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""Tests for Studio trained-model discovery used by Chat."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import types as _types
|
||||
import importlib
|
||||
|
||||
|
||||
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
|
||||
if _BACKEND_DIR not in sys.path:
|
||||
sys.path.insert(0, _BACKEND_DIR)
|
||||
|
||||
_loggers_stub = _types.ModuleType("loggers")
|
||||
_loggers_stub.get_logger = lambda name: __import__("logging").getLogger(name)
|
||||
sys.modules.setdefault("loggers", _loggers_stub)
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from utils.models.model_config import (
|
||||
ModelConfig,
|
||||
get_base_model_from_checkpoint,
|
||||
get_base_model_from_lora,
|
||||
scan_trained_models,
|
||||
)
|
||||
|
||||
|
||||
def test_scan_trained_models_includes_lora_and_full_finetune_outputs(tmp_path: Path):
|
||||
lora_dir = tmp_path / "unsloth_SmolLM-135M_1775412608"
|
||||
lora_dir.mkdir()
|
||||
(lora_dir / "adapter_config.json").write_text(
|
||||
json.dumps({"base_model_name_or_path": "HuggingFaceTB/SmolLM-135M"})
|
||||
)
|
||||
(lora_dir / "adapter_model.safetensors").write_bytes(b"")
|
||||
|
||||
full_dir = tmp_path / "unsloth_SmolLM-135M_full_1775412609"
|
||||
full_dir.mkdir()
|
||||
(full_dir / "config.json").write_text(
|
||||
json.dumps({"_name_or_path": "HuggingFaceTB/SmolLM-135M"})
|
||||
)
|
||||
(full_dir / "model.safetensors").write_bytes(b"")
|
||||
|
||||
found = {
|
||||
name: (path, model_type)
|
||||
for name, path, model_type in scan_trained_models(str(tmp_path))
|
||||
}
|
||||
|
||||
assert found[lora_dir.name] == (str(lora_dir), "lora")
|
||||
assert found[full_dir.name] == (str(full_dir), "merged")
|
||||
|
||||
|
||||
def test_get_base_model_from_checkpoint_falls_back_to_full_finetune_config(
|
||||
tmp_path: Path,
|
||||
):
|
||||
(tmp_path / "config.json").write_text(
|
||||
json.dumps({"_name_or_path": "HuggingFaceTB/SmolLM-135M"})
|
||||
)
|
||||
(tmp_path / "model.safetensors").write_bytes(b"")
|
||||
|
||||
assert get_base_model_from_checkpoint(str(tmp_path)) == "HuggingFaceTB/SmolLM-135M"
|
||||
|
||||
|
||||
def test_get_base_model_from_lora_rejects_full_finetune_dirs(tmp_path: Path):
|
||||
(tmp_path / "config.json").write_text(
|
||||
json.dumps({"_name_or_path": "HuggingFaceTB/SmolLM-135M"})
|
||||
)
|
||||
(tmp_path / "model.safetensors").write_bytes(b"")
|
||||
|
||||
assert get_base_model_from_lora(str(tmp_path)) is None
|
||||
|
||||
|
||||
@patch("utils.models.model_config.is_audio_input_type", return_value = False)
|
||||
@patch("utils.models.model_config.detect_audio_type", return_value = None)
|
||||
@patch("utils.models.model_config.is_vision_model", return_value = False)
|
||||
def test_model_config_full_finetune_local_path_is_not_lora(
|
||||
_mock_vision,
|
||||
_mock_audio_type,
|
||||
_mock_audio_input,
|
||||
tmp_path: Path,
|
||||
):
|
||||
(tmp_path / "config.json").write_text(
|
||||
json.dumps({"_name_or_path": "unsloth/Qwen3-4B"})
|
||||
)
|
||||
(tmp_path / "model.safetensors").write_bytes(b"")
|
||||
|
||||
config = ModelConfig.from_identifier(str(tmp_path))
|
||||
|
||||
assert config is not None
|
||||
assert config.is_lora is False
|
||||
assert config.base_model is None
|
||||
|
||||
|
||||
def test_scan_trained_loras_aliases_scan_trained_models():
|
||||
utils_models = importlib.import_module("utils.models")
|
||||
core_module = importlib.import_module("core")
|
||||
|
||||
assert utils_models.scan_trained_loras is utils_models.scan_trained_models
|
||||
assert core_module.scan_trained_loras is core_module.scan_trained_models
|
||||
|
|
@ -13,8 +13,9 @@ from .model_config import (
|
|||
detect_audio_type,
|
||||
is_audio_input_type,
|
||||
VALID_AUDIO_TYPES,
|
||||
scan_trained_loras,
|
||||
scan_trained_models,
|
||||
scan_exported_models,
|
||||
get_base_model_from_checkpoint,
|
||||
load_model_defaults,
|
||||
get_base_model_from_lora,
|
||||
load_model_config,
|
||||
|
|
@ -25,6 +26,8 @@ from .model_config import (
|
|||
)
|
||||
from .checkpoints import scan_checkpoints
|
||||
|
||||
scan_trained_loras = scan_trained_models
|
||||
|
||||
__all__ = [
|
||||
"ModelConfig",
|
||||
"GgufVariantInfo",
|
||||
|
|
@ -33,8 +36,10 @@ __all__ = [
|
|||
"detect_audio_type",
|
||||
"is_audio_input_type",
|
||||
"VALID_AUDIO_TYPES",
|
||||
"scan_trained_models",
|
||||
"scan_trained_loras",
|
||||
"scan_exported_models",
|
||||
"get_base_model_from_checkpoint",
|
||||
"load_model_defaults",
|
||||
"get_base_model_from_lora",
|
||||
"load_model_config",
|
||||
|
|
|
|||
|
|
@ -1322,46 +1322,89 @@ def is_embedding_model(model_name: str, hf_token: Optional[str] = None) -> bool:
|
|||
return False
|
||||
|
||||
|
||||
def scan_trained_loras(outputs_dir: str = str(outputs_root())) -> List[Tuple[str, str]]:
|
||||
def _has_model_weight_files(model_dir: Path) -> bool:
|
||||
"""Return True when a directory contains loadable model weights."""
|
||||
for item in model_dir.iterdir():
|
||||
if not item.is_file():
|
||||
continue
|
||||
|
||||
suffix = item.suffix.lower()
|
||||
if suffix == ".safetensors":
|
||||
return True
|
||||
if suffix == ".gguf":
|
||||
return "mmproj" not in item.name.lower()
|
||||
if suffix == ".bin":
|
||||
name = item.name.lower()
|
||||
if (
|
||||
name.startswith("pytorch_model")
|
||||
or name.startswith("model")
|
||||
or name.startswith("adapter_model")
|
||||
or name.startswith("consolidated")
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _detect_training_output_type(model_dir: Path) -> Optional[str]:
|
||||
"""Classify a Studio training output as LoRA or full finetune."""
|
||||
adapter_config = model_dir / "adapter_config.json"
|
||||
adapter_model = model_dir / "adapter_model.safetensors"
|
||||
if adapter_config.exists() or adapter_model.exists():
|
||||
return "lora"
|
||||
|
||||
config_file = model_dir / "config.json"
|
||||
if config_file.exists() and _has_model_weight_files(model_dir):
|
||||
return "merged"
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _looks_like_lora_adapter(model_dir: Path) -> bool:
|
||||
return model_dir.is_dir() and (
|
||||
(model_dir / "adapter_config.json").exists()
|
||||
or any(model_dir.glob("adapter_model*.safetensors"))
|
||||
or any(model_dir.glob("adapter_model*.bin"))
|
||||
)
|
||||
|
||||
|
||||
def scan_trained_models(
|
||||
outputs_dir: str = str(outputs_root()),
|
||||
) -> List[Tuple[str, str, str]]:
|
||||
"""
|
||||
Scan outputs folder for trained LoRA adapters.
|
||||
Scan outputs folder for trained Studio models.
|
||||
|
||||
Returns:
|
||||
List of tuples: [(display_name, adapter_path), ...]
|
||||
|
||||
Example:
|
||||
[
|
||||
("unsloth_Meta-Llama-3.1_...", "./outputs/unsloth_Meta-Llama-3.1_.../"),
|
||||
("my_finetuned_model", "./outputs/my_finetuned_model/"),
|
||||
]
|
||||
List of tuples: [(display_name, model_path, model_type), ...]
|
||||
model_type is "lora" for adapter runs and "merged" for full finetunes.
|
||||
"""
|
||||
trained_loras = []
|
||||
trained_models = []
|
||||
outputs_path = resolve_output_dir(outputs_dir)
|
||||
|
||||
if not outputs_path.exists():
|
||||
logger.warning(f"Outputs directory not found: {outputs_dir}")
|
||||
return trained_loras
|
||||
return trained_models
|
||||
|
||||
try:
|
||||
for item in outputs_path.iterdir():
|
||||
if item.is_dir():
|
||||
# Check if this directory contains a LoRA adapter
|
||||
adapter_config = item / "adapter_config.json"
|
||||
adapter_model = item / "adapter_model.safetensors"
|
||||
model_type = _detect_training_output_type(item)
|
||||
if model_type is None:
|
||||
continue
|
||||
|
||||
if adapter_config.exists() or adapter_model.exists():
|
||||
display_name = item.name
|
||||
adapter_path = str(item)
|
||||
trained_loras.append((display_name, adapter_path))
|
||||
logger.debug(f"Found trained LoRA: {display_name}")
|
||||
display_name = item.name
|
||||
model_path = str(item)
|
||||
trained_models.append((display_name, model_path, model_type))
|
||||
logger.debug("Found trained model: %s (%s)", display_name, model_type)
|
||||
|
||||
# Sort by modification time (newest first)
|
||||
trained_loras.sort(key = lambda x: Path(x[1]).stat().st_mtime, reverse = True)
|
||||
trained_models.sort(key = lambda x: Path(x[1]).stat().st_mtime, reverse = True)
|
||||
|
||||
logger.info(
|
||||
f"Found {len(trained_loras)} trained LoRA adapters in {outputs_dir}"
|
||||
"Found %s trained models in %s",
|
||||
len(trained_models),
|
||||
outputs_dir,
|
||||
)
|
||||
return trained_loras
|
||||
return trained_models
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scanning outputs folder: {e}")
|
||||
|
|
@ -1494,6 +1537,68 @@ def scan_exported_models(
|
|||
return []
|
||||
|
||||
|
||||
def get_base_model_from_checkpoint(checkpoint_path: str) -> Optional[str]:
|
||||
"""Read the base model name from a local training or checkpoint directory."""
|
||||
try:
|
||||
checkpoint_path_obj = Path(checkpoint_path)
|
||||
|
||||
adapter_config_path = checkpoint_path_obj / "adapter_config.json"
|
||||
if adapter_config_path.exists():
|
||||
with open(adapter_config_path, "r") as f:
|
||||
config = json.load(f)
|
||||
base_model = config.get("base_model_name_or_path")
|
||||
if base_model:
|
||||
logger.info(
|
||||
"Detected base model from adapter_config.json: %s", base_model
|
||||
)
|
||||
return base_model
|
||||
|
||||
config_path = checkpoint_path_obj / "config.json"
|
||||
if config_path.exists():
|
||||
with open(config_path, "r") as f:
|
||||
config = json.load(f)
|
||||
for key in ("model_name", "_name_or_path"):
|
||||
base_model = config.get(key)
|
||||
if base_model and str(base_model) != str(checkpoint_path_obj):
|
||||
logger.info(
|
||||
"Detected base model from config.json (%s): %s",
|
||||
key,
|
||||
base_model,
|
||||
)
|
||||
return base_model
|
||||
|
||||
training_args_path = checkpoint_path_obj / "training_args.bin"
|
||||
if training_args_path.exists():
|
||||
try:
|
||||
import torch
|
||||
|
||||
training_args = torch.load(training_args_path)
|
||||
if hasattr(training_args, "model_name_or_path"):
|
||||
base_model = training_args.model_name_or_path
|
||||
logger.info(
|
||||
"Detected base model from training_args.bin: %s", base_model
|
||||
)
|
||||
return base_model
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load training_args.bin: {e}")
|
||||
|
||||
dir_name = checkpoint_path_obj.name
|
||||
if dir_name.startswith("unsloth_"):
|
||||
parts = dir_name.split("_")
|
||||
if len(parts) >= 2:
|
||||
model_parts = parts[1:-1]
|
||||
base_model = "unsloth/" + "_".join(model_parts)
|
||||
logger.info("Detected base model from directory name: %s", base_model)
|
||||
return base_model
|
||||
|
||||
logger.warning(f"Could not detect base model for checkpoint: {checkpoint_path}")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading base model from checkpoint config: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def get_base_model_from_lora(lora_path: str) -> Optional[str]:
|
||||
"""
|
||||
Read the base model name from a LoRA adapter's config.
|
||||
|
|
@ -1502,16 +1607,14 @@ def get_base_model_from_lora(lora_path: str) -> Optional[str]:
|
|||
lora_path: Path to the LoRA adapter directory
|
||||
|
||||
Returns:
|
||||
Base model identifier (e.g., "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit")
|
||||
or None if not found
|
||||
|
||||
Example:
|
||||
>>> get_base_model_from_lora("./outputs/unsloth_Meta-Llama-3.1_.../")
|
||||
"unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit"
|
||||
Base model identifier or None if not found
|
||||
"""
|
||||
try:
|
||||
lora_path_obj = Path(lora_path)
|
||||
|
||||
if not _looks_like_lora_adapter(lora_path_obj):
|
||||
return None
|
||||
|
||||
# Try adapter_config.json first
|
||||
adapter_config_path = lora_path_obj / "adapter_config.json"
|
||||
if adapter_config_path.exists():
|
||||
|
|
@ -1884,7 +1987,11 @@ class ModelConfig:
|
|||
|
||||
# Auto-detect LoRA for local paths (check adapter_config.json on disk)
|
||||
if not is_lora and is_local:
|
||||
detected_base = get_base_model_from_lora(path)
|
||||
detected_base = (
|
||||
get_base_model_from_lora(path)
|
||||
if _looks_like_lora_adapter(Path(path))
|
||||
else None
|
||||
)
|
||||
if detected_base:
|
||||
is_lora = True
|
||||
logger.info(
|
||||
|
|
|
|||
|
|
@ -203,11 +203,22 @@ export function ModelSelector({
|
|||
? lora.name.split("/")[0].trim()
|
||||
: lora.name;
|
||||
// Show type tag instead of base model name
|
||||
const isLocal = lora.source === "local";
|
||||
const isTraining = lora.source === "training";
|
||||
const isExported = lora.source === "exported";
|
||||
const isMerged = lora.exportType === "merged";
|
||||
const tag = isExported
|
||||
? isMerged ? "Merged · Exported" : "LoRA"
|
||||
: "LoRA";
|
||||
const isGguf = lora.exportType === "gguf";
|
||||
const tag = isLocal
|
||||
? isGguf
|
||||
? "GGUF"
|
||||
: "Local"
|
||||
: isTraining && isMerged
|
||||
? "Full finetune"
|
||||
: isExported
|
||||
? isMerged
|
||||
? "Merged · Exported"
|
||||
: "LoRA · Exported"
|
||||
: "LoRA";
|
||||
all.set(lora.id, {
|
||||
...lora,
|
||||
name: displayName,
|
||||
|
|
|
|||
|
|
@ -1330,7 +1330,7 @@ export function LoraModelPicker({
|
|||
<Input
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Search local adapters"
|
||||
placeholder="Search trained models"
|
||||
className="h-9 pl-8"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1339,7 +1339,7 @@ export function LoraModelPicker({
|
|||
<div className="p-1">
|
||||
{grouped.length === 0 ? (
|
||||
<div className="px-2.5 py-2 text-xs text-muted-foreground">
|
||||
No adapters found.
|
||||
No trained models found.
|
||||
</div>
|
||||
) : (
|
||||
grouped.map(([baseModel, adapters], index) => (
|
||||
|
|
@ -1348,9 +1348,11 @@ export function LoraModelPicker({
|
|||
<ListLabel>{baseModel}</ListLabel>
|
||||
{adapters.map((adapter) => {
|
||||
const isLocal = adapter.source === "local";
|
||||
const isTraining = adapter.source === "training";
|
||||
const isExported = adapter.source === "exported";
|
||||
const isMerged = adapter.exportType === "merged";
|
||||
const isGguf = adapter.exportType === "gguf";
|
||||
const isTrainingFull = isTraining && isMerged;
|
||||
const isLocalGgufDir =
|
||||
isLocal &&
|
||||
(isGgufRepo(adapter.id) || isGgufRepo(adapter.name));
|
||||
|
|
@ -1360,6 +1362,8 @@ export function LoraModelPicker({
|
|||
: "Local"
|
||||
: isGguf
|
||||
? "GGUF"
|
||||
: isTrainingFull
|
||||
? "Full"
|
||||
: isExported
|
||||
? isMerged
|
||||
? "Merged"
|
||||
|
|
@ -1369,6 +1373,8 @@ export function LoraModelPicker({
|
|||
? isLocalGgufDir
|
||||
? "GGUF"
|
||||
: "Local"
|
||||
: isTrainingFull
|
||||
? "Full finetune"
|
||||
: isExported
|
||||
? `${tag} · Exported`
|
||||
: tag;
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ type LoraCandidate = {
|
|||
id: string;
|
||||
baseModel: string;
|
||||
updatedAt?: number;
|
||||
exportType?: "lora" | "merged" | "gguf";
|
||||
};
|
||||
|
||||
function normalizeModelRef(value: string | null | undefined): string {
|
||||
|
|
@ -81,12 +82,13 @@ function pickBestLoraForBase(
|
|||
loras: LoraCandidate[],
|
||||
baseModel: string | null,
|
||||
): LoraCandidate | null {
|
||||
if (loras.length === 0) return null;
|
||||
const sorted = [...loras].sort(
|
||||
const adapterOnly = loras.filter((lora) => lora.exportType === "lora");
|
||||
if (adapterOnly.length === 0) return null;
|
||||
const sorted = [...adapterOnly].sort(
|
||||
(a, b) => (b.updatedAt ?? -1) - (a.updatedAt ?? -1),
|
||||
);
|
||||
const normalizedBase = normalizeModelRef(baseModel);
|
||||
if (!normalizedBase) return sorted[0];
|
||||
if (!normalizedBase) return sorted[0] ?? null;
|
||||
|
||||
const exact = sorted.find(
|
||||
(lora) => normalizeModelRef(lora.baseModel) === normalizedBase,
|
||||
|
|
@ -101,7 +103,7 @@ function pickBestLoraForBase(
|
|||
normalizedBase.includes(normalizedLoraBase)
|
||||
);
|
||||
});
|
||||
return partial ?? sorted[0];
|
||||
return partial ?? sorted[0] ?? null;
|
||||
}
|
||||
|
||||
function messageHasImage(message: MessageRecord): boolean {
|
||||
|
|
@ -154,7 +156,8 @@ type CompareModelSelection = {
|
|||
function useIsLoraCompare(): boolean {
|
||||
return useChatRuntimeStore((s) => {
|
||||
const cp = s.params.checkpoint;
|
||||
return cp ? s.loras.some((l) => l.id === cp) : false;
|
||||
const selected = cp ? s.loras.find((l) => l.id === cp) : undefined;
|
||||
return selected?.exportType === "lora";
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -235,7 +238,7 @@ const LoraCompareContent = memo(function LoraCompareContent({
|
|||
<div className="flex min-h-0 flex-col border-t border-border/60 md:border-t-0 md:border-l">
|
||||
<div className="px-3 py-1.5 text-start md:text-end">
|
||||
<span className="text-[10px] font-semibold uppercase tracking-wider text-primary">
|
||||
Fine-tuned (LoRA)
|
||||
Fine-tuned
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-h-0 flex-1">
|
||||
|
|
|
|||
|
|
@ -236,6 +236,25 @@ export function useChatModelRuntime() {
|
|||
// Apply inference defaults on reconnect (page refresh with model already loaded)
|
||||
if (statusRes.inference) {
|
||||
const currentParams = useChatRuntimeStore.getState().params;
|
||||
const reconnectResponse: LoadModelResponse = {
|
||||
status: "already_loaded",
|
||||
model: statusRes.active_model,
|
||||
display_name: statusRes.active_model,
|
||||
is_vision: statusRes.is_vision,
|
||||
is_lora: false,
|
||||
is_gguf: statusRes.is_gguf,
|
||||
is_audio: statusRes.is_audio,
|
||||
audio_type: statusRes.audio_type,
|
||||
has_audio_input: statusRes.has_audio_input,
|
||||
inference: statusRes.inference,
|
||||
context_length: statusRes.context_length,
|
||||
max_context_length: statusRes.max_context_length,
|
||||
native_context_length: statusRes.native_context_length,
|
||||
supports_reasoning: statusRes.supports_reasoning,
|
||||
reasoning_always_on: statusRes.reasoning_always_on,
|
||||
supports_tools: statusRes.supports_tools,
|
||||
speculative_type: statusRes.speculative_type,
|
||||
};
|
||||
setParams(
|
||||
mergeRecommendedInference(currentParams, statusRes, statusRes.active_model),
|
||||
);
|
||||
|
|
@ -340,8 +359,9 @@ export function useChatModelRuntime() {
|
|||
typeof selection === "string" ? false : selection.isDownloaded ?? false;
|
||||
const model = models.find((entry) => entry.id === modelId);
|
||||
const lora = loras.find((entry) => entry.id === modelId);
|
||||
const loraIsAdapter = lora?.exportType === "lora";
|
||||
const isLora =
|
||||
explicitIsLora ?? model?.isLora ?? (lora ? true : false);
|
||||
explicitIsLora ?? model?.isLora ?? loraIsAdapter ?? false;
|
||||
const displayName = model?.name || lora?.name || modelId;
|
||||
const currentCheckpoint =
|
||||
useChatRuntimeStore.getState().params.checkpoint;
|
||||
|
|
@ -355,7 +375,7 @@ export function useChatModelRuntime() {
|
|||
? loras.find((entry) => entry.id === previousCheckpoint)
|
||||
: undefined;
|
||||
const previousIsLora =
|
||||
previousModel?.isLora ?? (previousLora ? true : false);
|
||||
previousModel?.isLora ?? (previousLora?.exportType === "lora");
|
||||
// Covers Unix absolute (/), relative (./ ../), tilde (~/), Windows drive (C:\), UNC (\\server)
|
||||
const isLocal = /^(\/|\.{1,2}[\\/]|~[\\/]|[A-Za-z]:[\\/]|\\\\)/.test(modelId);
|
||||
const isCachedLora = isLora && isLocal;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export function buildChatTourSteps({
|
|||
body: (
|
||||
<>
|
||||
This selects what’s loaded for inference. Hub = base models. Fine-tuned
|
||||
= your LoRA adapters from Studio.
|
||||
= trained Studio outputs, including LoRA adapters and full finetunes.
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
@ -40,9 +40,9 @@ export function buildChatTourSteps({
|
|||
title: "Two tabs",
|
||||
body: (
|
||||
<>
|
||||
Hub: search Hugging Face models. Fine-tuned: adapters (LoRA) you’ve
|
||||
trained locally. If results look off, compare base vs LoRA to see what
|
||||
changed.
|
||||
Hub: search Hugging Face models. Fine-tuned: local Studio outputs you’ve
|
||||
trained or exported. If results look off, compare base vs fine-tuned
|
||||
outputs to see what changed.
|
||||
</>
|
||||
),
|
||||
onEnter: openModelSelector,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue