# SPDX-License-Identifier: AGPL-3.0-only # Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 """Shared backend utilities.""" import os import structlog from loggers import get_logger from contextlib import contextmanager from pathlib import Path import shutil import tempfile logger = get_logger(__name__) # ── Client-safe error helpers ─────────────────────────────────── # Never return raw exception text to clients; log server-side, return generic. def safe_error_detail(error: Exception, fallback: str = "An internal error occurred") -> str: """Map an exception to a generic, client-safe message (never raw ``str(error)``, which can leak paths). Log the real exception server-side. """ text = str(error).lower() if ( isinstance(error, (ConnectionError, TimeoutError)) or "connection" in text or "timed out" in text or "timeout" in text ): return "Could not reach an upstream service. Please try again." if "out of memory" in text or "cuda error" in text: return "Ran out of memory. Try a smaller model or shorter input." return fallback def safe_curated_detail(error: Exception, fallback: str = "An internal error occurred") -> str: """Client-safe text for curated domain/validation exceptions. Keeps the message (paths stripped) instead of a generic fallback; for known exception types only (use ``safe_error_detail`` for generic ``Exception``). """ from utils.native_path_leases import redact_native_paths msg = redact_native_paths(str(error)).strip() return msg or fallback def log_and_http_error( error: Exception, status_code: int, public_message: str, *, event: str = "request_failed", log = None, ): """Log ``error`` in full server-side and return an ``HTTPException`` whose ``detail`` is only ``public_message`` -- never the raw exception text. Usage: raise log_and_http_error(e, 500, "Failed to start training") """ from fastapi import HTTPException # exc_info=error works for both structlog and stdlib loggers. (log or logger).error(f"{event}: {error}", exc_info = error) return HTTPException(status_code = status_code, detail = public_message) @contextmanager def without_hf_auth(): """ Temporarily disable HuggingFace authentication. Usage: with without_hf_auth(): # Code that should run without cached tokens model_info(model_name, token=None) """ saved_env = {} env_vars = ["HF_TOKEN", "HUGGINGFACE_HUB_TOKEN", "HF_HOME"] for var in env_vars: if var in os.environ: saved_env[var] = os.environ[var] del os.environ[var] saved_disable = os.environ.get("HF_HUB_DISABLE_IMPLICIT_TOKEN") os.environ["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = "1" # Move token files aside temporarily token_files = [] token_locations = [ Path.home() / ".cache" / "huggingface" / "token", Path.home() / ".huggingface" / "token", ] for token_loc in token_locations: if token_loc.exists(): temp = tempfile.NamedTemporaryFile(delete = False) temp.close() shutil.move(str(token_loc), temp.name) token_files.append((token_loc, temp.name)) try: yield finally: # Restore tokens for original, temp in token_files: try: original.parent.mkdir(parents = True, exist_ok = True) shutil.move(temp, str(original)) except Exception as e: logger.error(f"Failed to restore token {original}: {e}") # Restore env for var, value in saved_env.items(): os.environ[var] = value if saved_disable is not None: os.environ["HF_HUB_DISABLE_IMPLICIT_TOKEN"] = saved_disable else: os.environ.pop("HF_HUB_DISABLE_IMPLICIT_TOKEN", None) def format_error_message(error: Exception, model_name: str) -> str: """ Format a user-friendly error message for common load issues. Args: error: The exception that occurred model_name: Name of the model being loaded """ error_str = str(error).lower() model_short = model_name.split("/")[-1] if "/" in model_name else model_name if "repository not found" in error_str or "404" in error_str: return f"Model '{model_short}' not found. Check the model name." if "401" in error_str or "unauthorized" in error_str: return f"Authentication failed for '{model_short}'. Please provide a valid HF token." if "gated" in error_str or "access to model" in error_str: return f"Model '{model_short}' requires authentication. Please provide a valid HF token." if "invalid user token" in error_str: return "Invalid HF token. Please check your token and try again." if ( "memory" in error_str or "cuda" in error_str or "mlx" in error_str or "out of memory" in error_str ): from utils.hardware import get_device device = get_device() device_label = {"cuda": "GPU", "mlx": "Apple Silicon GPU", "cpu": "system"}.get( device.value, "GPU" ) return f"Not enough {device_label} memory to load '{model_short}'. Try a smaller model or free memory." return str(error)