Friendlier unsupported model errors, show estimated download size

1. Backend: When a model fails with "No config file found" or similar
   unsupported-model errors, wrap the message with "This model is not
   supported yet. Try a different model." instead of showing the raw
   Unsloth exception.

2. Frontend: Compute estimated download size from the HF search API's
   safetensors.parameters dtype breakdown (BF16=2B/param, I32=4B/param,
   F32=4B/param, etc.) and show it in the model picker instead of just
   the param count. For example, Kimi-K2.5 now shows "~554 GB" instead
   of "171B" (which was misleading since 171B params != 171GB download).
This commit is contained in:
Daniel Han 2026-03-16 06:09:38 +00:00
commit f20c7ca54d
3 changed files with 41 additions and 4 deletions

View file

@ -288,7 +288,17 @@ async def load_model(
raise
except Exception as e:
logger.error(f"Error loading model: {e}", exc_info = True)
raise HTTPException(status_code = 500, detail = f"Failed to load model: {str(e)}")
msg = str(e)
# Surface a friendlier message for models that Unsloth cannot load
not_supported_hints = [
"No config file found",
"not yet supported",
"is not supported",
"does not support",
]
if any(h.lower() in msg.lower() for h in not_supported_hints):
msg = f"This model is not supported yet. Try a different model. (Original error: {msg})"
raise HTTPException(status_code = 500, detail = f"Failed to load model: {msg}")
@router.post("/validate", response_model = ValidateModelResponse)

View file

@ -408,8 +408,13 @@ export function HubModelPicker({
() =>
new Map(
results
.filter((result) => result.totalParams)
.map((result) => [result.id, formatCompact(result.totalParams!)]),
.filter((result) => result.totalParams || result.estimatedSizeBytes)
.map((result) => [
result.id,
result.estimatedSizeBytes
? `~${formatBytes(result.estimatedSizeBytes)}`
: formatCompact(result.totalParams!),
]),
),
[results],
);

View file

@ -11,6 +11,7 @@ export interface HfModelResult {
downloads: number;
likes: number;
totalParams?: number;
estimatedSizeBytes?: number;
}
const EXCLUDED_TAGS = new Set([
@ -54,13 +55,33 @@ function withPopularitySort(
return fetch(url, init);
}
/** Bytes per parameter for each dtype. */
const DTYPE_BYTES: Record<string, number> = {
F64: 8, F32: 4, F16: 2, BF16: 2,
I64: 8, I32: 4, I16: 2, I8: 1, U8: 1,
// Quantized types (4-bit)
NF4: 0.5, FP4: 0.5, INT4: 0.5, GPTQ: 0.5,
};
function estimateSizeFromDtypes(
params: Record<string, number> | undefined,
): number | undefined {
if (!params) return undefined;
let total = 0;
for (const [dtype, count] of Object.entries(params)) {
const bpp = DTYPE_BYTES[dtype.toUpperCase()] ?? 2; // default BF16
total += count * bpp;
}
return total > 0 ? total : undefined;
}
function makeMapModel(excludeGguf: boolean) {
return (raw: unknown): HfModelResult | null => {
const m = raw as {
name: string;
downloads: number;
likes: number;
safetensors?: { total: number };
safetensors?: { total: number; parameters?: Record<string, number> };
tags?: string[];
};
const isEmbedding = m.tags?.some((t) => EMBEDDING_TAGS.has(t));
@ -75,6 +96,7 @@ function makeMapModel(excludeGguf: boolean) {
downloads: m.downloads,
likes: m.likes,
totalParams: m.safetensors?.total,
estimatedSizeBytes: estimateSizeFromDtypes(m.safetensors?.parameters),
};
};
}