feat: support GGUF export for non-PEFT models + fix venv_t5 switching for local checkpoints (#4455)

* feat: support full model GGUF export, disable incompatible methods in UI

* fix: resolve base model from config.json for venv_t5 export switching

* feat: detect BNB-quantized models and disable all export methods for quantized non-PEFT checkpoints

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

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

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

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

* fix: relocate Ollama Modelfile alongside GGUFs during non-PEFT export cleanup

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

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

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
This commit is contained in:
Roland Tannous 2026-03-20 12:13:18 +04:00 committed by GitHub
commit ebe45981dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 362 additions and 14 deletions

View file

@ -76,6 +76,18 @@ def scan_checkpoints(
elif config_file.exists():
cfg = json.loads(config_file.read_text())
metadata["base_model"] = cfg.get("_name_or_path")
# Detect BNB quantization from config.json (present in both cases)
if config_file.exists():
if "cfg" not in dir():
cfg = json.loads(config_file.read_text())
quant_cfg = cfg.get("quantization_config")
if (
isinstance(quant_cfg, dict)
and quant_cfg.get("quant_method") == "bitsandbytes"
):
metadata["is_quantized"] = True
logger.info("Detected BNB-quantized model: %s", item.name)
except Exception:
pass

View file

@ -92,6 +92,24 @@ def _resolve_base_model(model_name: str) -> str:
except Exception as exc:
logger.debug("Could not read %s: %s", adapter_cfg_path, exc)
# --- config.json fallback (works for both LoRA and full fine-tune) ------
config_json_path = local_path / "config.json"
if config_json_path.is_file():
try:
with open(config_json_path) as f:
cfg = json.load(f)
# Unsloth writes "model_name"; HF writes "_name_or_path"
base = cfg.get("model_name") or cfg.get("_name_or_path")
if base and base != str(local_path):
logger.info(
"Resolved checkpoint '%s' → base model '%s' (via config.json)",
model_name,
base,
)
return base
except Exception as exc:
logger.debug("Could not read %s: %s", config_json_path, exc)
# --- Only try the heavier fallback for local directories ----------------
if local_path.is_dir():
try:
@ -126,6 +144,27 @@ def _check_tokenizer_config_needs_v5(model_name: str) -> bool:
if model_name in _tokenizer_class_cache:
return _tokenizer_class_cache[model_name]
# --- Check local tokenizer_config.json first ---------------------------
local_path = Path(model_name)
local_tc = local_path / "tokenizer_config.json"
if local_tc.is_file():
try:
with open(local_tc) as f:
data = json.load(f)
tokenizer_class = data.get("tokenizer_class", "")
result = tokenizer_class in _TRANSFORMERS_5_TOKENIZER_CLASSES
if result:
logger.info(
"Local check: %s uses tokenizer_class=%s (requires transformers 5.x)",
model_name,
tokenizer_class,
)
_tokenizer_class_cache[model_name] = result
return result
except Exception as exc:
logger.debug("Could not read %s: %s", local_tc, exc)
# --- Fall back to fetching from HuggingFace ----------------------------
import urllib.request
url = f"https://huggingface.co/{model_name}/raw/main/tokenizer_config.json"