* studio: extract param count from model name as fallback When HuggingFace API doesn't return totalParams for a model, extract the param count from the model name (e.g. "Qwen3-0.6B" -> "0.6B", "Llama-3.2-1B-Instruct" -> "1B"). Applied to both the recommended list and HF search results. * studio: read GGUF context_length via fast header parser, set max tokens - Fast GGUF metadata reader (~30-55ms) parses only KV header, skips tensor data and large arrays (tokenizer vocab etc) - Extracts context_length and chat_template from GGUF metadata - Returns context_length in LoadResponse for frontend to use - Frontend sets maxTokens to actual context_length for GGUFs (e.g. 262144 for Qwen3.5-9B, 131072 for Qwen2.5-7B) - Max Tokens slider shows "Max" and is locked for GGUFs - Auto-load path also uses actual context_length from load response - Toast auto-dismiss (5s) and close button for auto-load toast * studio: GGUF TTS audio support (from PR #4318) Add GGUF TTS audio generation via llama-server. When a GGUF model loads, the backend probes its vocabulary to detect audio codecs (SNAC/BiCodec/DAC/CSM/Whisper). If detected, the codec is pre-loaded and the model is reported as audio to the frontend. During chat, TTS models route to the audio generation path which sends a per-codec prompt to llama-server's /completion endpoint, extracts generated tokens/text, and decodes to WAV using AudioCodecManager. Also strips base64 audio data from prior assistant messages to prevent context overflow. Co-authored-by: Manan Shah <mananshah511@gmail.com> * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Remove package-lock.json from tracking * studio: per-model inference defaults, GGUF max tokens fix, reasoning toggle - Add inference_defaults.json with per-model-family sampling parameters for ~50 families (Qwen3.5, Qwen3, Gemma-3, Llama-3, DeepSeek, etc.). Values sourced from unslothai/docs and Ollama params blobs. - Family-based lookup in inference_config.py: extracts model family from identifier, matches against patterns (longest match first), merges with priority: model-specific YAML > family JSON > default.yaml. - Fix GGUF Max Tokens slider locked at "Max": store ggufContextLength separately from maxTokens so the slider is adjustable (step=64). - Fix Ministral YAML: top_p was literal string "default", now 0.95. - Add reasoning toggle for thinking models (Qwen3.5, Qwen3, DeepSeek-R1, DeepSeek-V3.1, etc.): detect enable_thinking support from GGUF chat template metadata, pass --jinja to llama-server, send chat_template_kwargs per-request. Frontend shows "Reasoning is ON/OFF" pill button next to attachment button in composer. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio: remove default system prompt injection Backend was injecting "You are a helpful AI assistant." when no system prompt was provided. Neither unslothai/docs nor Ollama specify a default system prompt for most models. Now defaults to empty string, letting the model's own chat template handle system behavior. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio: use lightbulb icons and "Think" label for reasoning toggle Lightbulb on when thinking enabled, lightbulb-off when disabled. Label is just "Think" in both states; grayed out styling when off. * studio: fix HTML file upload breaking chat Replace SimpleTextAttachmentAdapter with custom TextAttachmentAdapter (excludes text/html) and HtmlAttachmentAdapter that strips tags via DOMParser, removing scripts/styles and extracting readable text content instead of dumping raw HTML markup into the conversation. * studio: show chat template in Configuration panel Display the model's Jinja2 chat template in a new "Chat Template" section under Settings (now open by default). For GGUFs, reads from GGUF metadata; for safetensors, reads from tokenizer.chat_template. Template is editable with a "Restore default chat template" button that appears when modified. Section only shows when a model with a chat template is loaded. * studio: editable chat template with Apply & Reload Chat template section now functional: - Editing the template shows "Apply & Reload" (reloads model with custom template) and "Revert changes" buttons - For GGUFs: writes template to temp .jinja file, passes --chat-template-file to llama-server on reload - For non-GGUF: passes chat_template_override in load request - Settings section now open by default - selectModel supports forceReload to reload same model * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * studio: fix DeepSeek reasoning detection and auto-load metadata - Set _model_identifier before _read_gguf_metadata so DeepSeek "thinking" template detection works (was always None before) - Populate ggufContextLength, supportsReasoning, reasoningEnabled, defaultChatTemplate in autoLoadSmallestModel GGUF path * studio: add spacing before BETA badge in navbar Add gap-1.5 on the logo Link container to space the BETA label from the wordmark. Co-authored-by: Imagineer99 <Imagineer99@users.noreply.github.com> * studio: vertically center BETA badge with logo --------- Co-authored-by: Manan Shah <mananshah511@gmail.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Imagineer99 <Imagineer99@users.noreply.github.com>
177 lines
5.9 KiB
Python
177 lines
5.9 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
|
|
|
|
"""
|
|
Inference configuration loading utilities.
|
|
|
|
This module provides functions to load inference parameters (temperature, top_p, top_k, min_p)
|
|
from model YAML configuration files, with fallback to default.yaml.
|
|
Includes family-based lookup from inference_defaults.json for GGUF models.
|
|
"""
|
|
|
|
from pathlib import Path
|
|
from typing import Dict, Any, Optional
|
|
import json
|
|
import yaml
|
|
import structlog
|
|
from loggers import get_logger
|
|
|
|
from utils.models.model_config import load_model_defaults
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
# ── Family-based inference defaults (loaded once, cached) ──────────────
|
|
|
|
_FAMILY_DEFAULTS: Optional[Dict[str, Any]] = None
|
|
_FAMILY_PATTERNS: Optional[list] = None
|
|
|
|
|
|
def _load_family_defaults():
|
|
"""Load and cache inference_defaults.json."""
|
|
global _FAMILY_DEFAULTS, _FAMILY_PATTERNS
|
|
if _FAMILY_DEFAULTS is not None:
|
|
return
|
|
|
|
json_path = (
|
|
Path(__file__).parent.parent.parent
|
|
/ "assets"
|
|
/ "configs"
|
|
/ "inference_defaults.json"
|
|
)
|
|
try:
|
|
with open(json_path, "r", encoding = "utf-8") as f:
|
|
data = json.load(f)
|
|
_FAMILY_DEFAULTS = data.get("families", {})
|
|
_FAMILY_PATTERNS = data.get("patterns", [])
|
|
except Exception as e:
|
|
logger.warning(f"Failed to load inference_defaults.json: {e}")
|
|
_FAMILY_DEFAULTS = {}
|
|
_FAMILY_PATTERNS = []
|
|
|
|
|
|
def get_family_inference_params(model_id: str) -> Dict[str, Any]:
|
|
"""
|
|
Look up recommended inference parameters by model family.
|
|
|
|
Extracts the model family from the identifier (e.g. "unsloth/Qwen3.5-9B-GGUF" -> "qwen3.5")
|
|
and returns the matching parameters from inference_defaults.json.
|
|
|
|
Args:
|
|
model_id: Model identifier (e.g. "unsloth/Qwen3.5-9B-GGUF")
|
|
|
|
Returns:
|
|
Dict with inference params, or empty dict if no family match.
|
|
"""
|
|
_load_family_defaults()
|
|
|
|
if not _FAMILY_PATTERNS or not _FAMILY_DEFAULTS:
|
|
return {}
|
|
|
|
# Normalize: lowercase, strip org prefix
|
|
normalized = model_id.lower()
|
|
if "/" in normalized:
|
|
normalized = normalized.split("/", 1)[1]
|
|
|
|
# Match against patterns (ordered longest-match-first in the JSON)
|
|
for pattern in _FAMILY_PATTERNS:
|
|
if pattern in normalized:
|
|
params = _FAMILY_DEFAULTS.get(pattern, {})
|
|
if params:
|
|
return dict(params)
|
|
|
|
return {}
|
|
|
|
|
|
def _has_specific_yaml(model_identifier: str) -> bool:
|
|
"""Check if a model has its own YAML config (not just default.yaml)."""
|
|
from utils.models.model_config import _REVERSE_MODEL_MAPPING
|
|
|
|
script_dir = Path(__file__).parent.parent.parent
|
|
defaults_dir = script_dir / "assets" / "configs" / "model_defaults"
|
|
|
|
# Check the mapping
|
|
if model_identifier.lower() in _REVERSE_MODEL_MAPPING:
|
|
return True
|
|
|
|
# Check for exact filename match
|
|
model_filename = model_identifier.replace("/", "_") + ".yaml"
|
|
for config_path in defaults_dir.rglob(model_filename):
|
|
if config_path.is_file():
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def load_inference_config(model_identifier: str) -> Dict[str, Any]:
|
|
"""
|
|
Load inference configuration parameters for a model.
|
|
|
|
Priority chain:
|
|
1. Model-specific YAML (if it exists and has inference params)
|
|
2. Family-based defaults from inference_defaults.json
|
|
3. default.yaml fallback
|
|
|
|
Args:
|
|
model_identifier: Model identifier (e.g., "unsloth/llama-3-8b-bnb-4bit")
|
|
|
|
Returns:
|
|
Dictionary containing inference parameters:
|
|
{
|
|
"temperature": float,
|
|
"top_p": float,
|
|
"top_k": int,
|
|
"min_p": float
|
|
}
|
|
"""
|
|
# Load model defaults to get inference parameters
|
|
model_defaults = load_model_defaults(model_identifier)
|
|
|
|
# Load default.yaml for fallback values
|
|
script_dir = Path(__file__).parent.parent.parent
|
|
defaults_dir = script_dir / "assets" / "configs" / "model_defaults"
|
|
default_config_path = defaults_dir / "default.yaml"
|
|
|
|
default_inference = {}
|
|
if default_config_path.exists():
|
|
try:
|
|
with open(default_config_path, "r", encoding = "utf-8") as f:
|
|
default_config = yaml.safe_load(f) or {}
|
|
default_inference = default_config.get("inference", {})
|
|
except Exception as e:
|
|
logger.warning(f"Failed to load default.yaml: {e}")
|
|
|
|
# Family-based defaults from inference_defaults.json
|
|
family_params = get_family_inference_params(model_identifier)
|
|
|
|
model_inference = model_defaults.get("inference", {})
|
|
|
|
# If the model has its own YAML config, those values take priority over family defaults.
|
|
# If it only fell back to default.yaml, family defaults take priority.
|
|
has_own_yaml = _has_specific_yaml(model_identifier)
|
|
|
|
def _get_param(key, hardcoded_default):
|
|
if has_own_yaml:
|
|
# Model-specific YAML wins, then family fills gaps, then default.yaml
|
|
val = model_inference.get(key)
|
|
if val is not None and isinstance(val, (int, float)):
|
|
return val
|
|
if key in family_params:
|
|
return family_params[key]
|
|
return default_inference.get(key, hardcoded_default)
|
|
else:
|
|
# No model-specific YAML: family wins, then default.yaml
|
|
if key in family_params:
|
|
return family_params[key]
|
|
return default_inference.get(key, hardcoded_default)
|
|
|
|
inference_config = {
|
|
"temperature": _get_param("temperature", 0.7),
|
|
"top_p": _get_param("top_p", 0.95),
|
|
"top_k": _get_param("top_k", -1),
|
|
"min_p": _get_param("min_p", 0.01),
|
|
"trust_remote_code": model_inference.get(
|
|
"trust_remote_code", default_inference.get("trust_remote_code", False)
|
|
),
|
|
}
|
|
|
|
return inference_config
|