Ctrl K
ComunidadGuías…

internal/autogen/identity.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"regexp"
	"strings"
)

// Identity is what a gguf says it is, read out of its own header rather than
// out of its filename.
//
// The Models table groups on two keys: files sharing a ModelKey are the SAME
// model (one row, one pill per quant), and rows sharing a FamilyKey are
// finetunes of one base (one collapsible group). Deriving those from the name
// means guessing where the model stops and the quant starts, which is a losing
// game - every publisher spells a quant differently and some spell it in a way
// no pattern can read ("Qwen3.8-27B-mix-q-k-mtp"), so one model quietly becomes
// several rows. The header does not guess: convert_hf_to_gguf writes the source
// repo's own basename / size / finetune, and they survive any rename.
//
// Both keys are EMPTY when the header carries nothing usable - roughly a third
// of real files, since diffusion ggufs and hand-converted models write no
// identity KVs at all. That is not a failure: it is the caller's signal to fall
// back to the id-derived rules (ModelBaseKey / FamilyKey), which is all we ever
// had before. Keys are opaque - compare them, never parse or display them.
type Identity struct {
	ModelKey  string
	FamilyKey string

	// QuantLabel is the tensor-derived quantization ("Q4_K", "IQ4_XS mix"),
	// empty when the tensors can't be judged. It is a FALLBACK for display: a
	// filename that names its own quant keeps that spelling, because "UD-Q4_K_XL"
	// is what the user downloaded and "Q5_K mix" - though truer - is not.
	QuantLabel string
}

// identityJunk matches a KV value that identifies nothing: a bare commit hash
// (LFM2.5 ships general.finetune = "799e37a4...", Nanbeige a hex blob as its
// general.name), or a word describing where the file came from rather than
// which model it is. Left in the key, each of those splits a model's quants
// across rows as surely as a bad regex did - "Qwen3.8-27B-MXFP4" declares
// finetune "source" and is otherwise the same model as every other quant.
var (
	identityHexRe  = regexp.MustCompile(`^[0-9a-f]{16,}$`)
	identityJunkRe = regexp.MustCompile(`^(?:source|hf|gguf|converted|quantized|imatrix|main|model)$`)
	identityWordRe = regexp.MustCompile(`[^a-z0-9.]+`)
)

// normIdentity folds a header value to a comparable key fragment: case, spaces,
// underscores and dashes all stop mattering, so "Qwen2.5-Vl-7B-Instruct" and
// "qwen2.5_vl_7b_instruct" are one model. Returns "" for a value that says
// nothing about which model this is.
func normIdentity(s string) string {
	s = strings.Trim(identityWordRe.ReplaceAllString(strings.ToLower(strings.TrimSpace(s)), "-"), "-")
	if s == "" || identityHexRe.MatchString(s) || identityJunkRe.MatchString(s) {
		return ""
	}
	return s
}

// dropSizeLabel removes the parameter count from a basename. Publishers
// disagree about whether it belongs there - Qwen's own conversion writes
// basename "Qwen3.8" + size_label "27B" while unsloth's writes basename
// "Qwen3.8-27B" + the same size label - and the size is appended to the key
// separately, so leaving it in makes two spellings of one model.
func dropSizeLabel(base, size string) string {
	if base == "" || size == "" {
		return base
	}
	parts := strings.Split(base, "-")
	kept := parts[:0]
	for _, p := range parts {
		if p != size {
			kept = append(kept, p)
		}
	}
	return strings.Trim(strings.Join(kept, "-"), "-")
}

// identityFrom is the pure half of IdentityOf: header in, keys out.
//
// The row key is base + size + finetune and the family key is base + size, so
// every quant of one model lands on one row and every finetune of one base
// lands in one group. The finetune has to be IN the row key: Qwen3-4B-Instruct
// and Qwen3-4B-Thinking share a basename and a size, and are not the same model.
//
// A file with a size label and nothing else identifies nothing (a size is not a
// name), so both keys stay empty and the caller falls back to the id rules.
func identityFrom(m Metadata) Identity {
	id := Identity{QuantLabel: m.QuantLabel}
	size := normIdentity(m.SizeLabel)
	base := normIdentity(m.BaseName)
	if base == "" {
		// Some converters write only general.name, which is the same string with
		// the words title-cased and spaced ("Qwen3 4B Instruct 2507"). It carries
		// the finetune and size inline, so it can only serve as the whole key.
		if n := normIdentity(m.GeneralName); n != "" {
			return Identity{ModelKey: n, FamilyKey: dropSizeLabel(n, size), QuantLabel: m.QuantLabel}
		}
		return id
	}
	base = dropSizeLabel(base, size)
	if base == "" {
		return id
	}
	id.FamilyKey = strings.Trim(base+"-"+size, "-")
	id.ModelKey = strings.Trim(id.FamilyKey+"-"+normIdentity(m.FineTune), "-")
	return id
}

// IdentityOf reads a gguf's identity through the metadata cache, so the answer
// costs one map lookup after the first call per file. Never errors: an
// unreadable file identifies nothing, same as a header that says nothing.
func IdentityOf(path string) Identity {
	m, err := ReadGgufMetadataCached(path)
	if err != nil {
		return Identity{}
	}
	return identityFrom(m)
}