internal/autogen/inherit.go
Rama main · réplica de lectura del repositorio original.
package autogen
import "strings"
// NoneSentinel is the value a variant writes on an inheriting free-form string
// knob to mean "explicitly nothing" rather than "inherit the model-wide value".
//
// Every free-form string on VariantSpec uses empty => inherit, which leaves a
// variant no way to express "the model pins a chat template, this profile wants
// the gguf's baked-in one". The field carries three states through two, and the
// third one was simply unreachable. Rather than widen each field to *string
// (the PreserveThinking treatment - correct, but it moves the yaml round-trip,
// both DTO directions and every `||` coalesce in the editor), the off state gets
// a name, matching the vocabulary Mmproj already uses ("gpu"/"ram"/"none").
//
// Only free-form and path knobs take the sentinel. The enum knobs already spell
// out their own off state (flashAttn "off", mmap "off", mmproj "none",
// ropeScaling "none", contextShift "off"), and giving those a second spelling
// would be ambiguous, not consistent.
//
// A literal file named "none" is thus unreachable as a bare value; write it as
// "./none" or an absolute path.
const NoneSentinel = "none"
// isNone reports whether s is the "explicitly nothing" sentinel. Case- and
// space-insensitive: it is typed by hand into a text box.
func isNone(s string) bool {
return strings.EqualFold(strings.TrimSpace(s), NoneSentinel)
}
// inheritStr resolves one inheriting free-form string: empty inherits the
// model-wide value, the sentinel forces the knob off, anything else pins.
func inheritStr(variant, model string) string {
switch v := strings.TrimSpace(variant); {
case v == "":
return model
case isNone(v):
return ""
default:
return variant
}
}
// clearNone maps a model-wide sentinel to empty. At model level empty ALREADY
// means "no flag", so "none" there is redundant - but a user who learned the
// word on a variant will write it on the model too, and left alone it would be
// emitted as a literal path.
func clearNone(s string) string {
if isNone(s) {
return ""
}
return s
}
// mergeInheritStrings layers a variant's free-form string knobs over the
// effective override, honouring the sentinel. It is the single place that
// decides which knobs are sentinel-aware; the enum knobs stay in the plain
// non-empty-wins chain in buildProfiles.
func mergeInheritStrings(eff *Override, v *VariantSpec) {
eff.ChatTemplateFile = inheritStr(v.ChatTemplateFile, eff.ChatTemplateFile)
eff.MmprojFile = inheritStr(v.MmprojFile, eff.MmprojFile)
eff.CustomArgs = inheritStr(v.CustomArgs, eff.CustomArgs)
eff.ExtraArgs = inheritStr(v.ExtraArgs, eff.ExtraArgs)
eff.TensorSplit = inheritStr(v.TensorSplit, eff.TensorSplit)
eff.OverrideTensor = inheritStr(v.OverrideTensor, eff.OverrideTensor)
eff.KvKDraft = inheritStr(v.KvKDraft, eff.KvKDraft)
eff.KvVDraft = inheritStr(v.KvVDraft, eff.KvVDraft)
}
// mergeInheritImageStrings is mergeInheritStrings for the sd-server component
// paths, which merge in mergeImageVariant rather than the llama chain. Dropping
// an inherited encoder ("this preset runs without the external T5") was
// unreachable for the same reason.
func mergeInheritImageStrings(o *Override, v *VariantSpec) {
o.VaePath = inheritStr(v.VaePath, o.VaePath)
o.ClipLPath = inheritStr(v.ClipLPath, o.ClipLPath)
o.ClipGPath = inheritStr(v.ClipGPath, o.ClipGPath)
o.T5Path = inheritStr(v.T5Path, o.T5Path)
o.TextEncoderPath = inheritStr(v.TextEncoderPath, o.TextEncoderPath)
o.AudioVaePath = inheritStr(v.AudioVaePath, o.AudioVaePath)
o.LlmVisionPath = inheritStr(v.LlmVisionPath, o.LlmVisionPath)
o.LoraDir = inheritStr(v.LoraDir, o.LoraDir)
}
// NormalizeNone clears a sentinel written at MODEL level on every knob
// mergeInheritStrings / mergeInheritImageStrings cover. At model level empty
// already means "no flag", so the two are equivalent and resolving early keeps
// every consumer honest.
//
// It must run on every door an Override comes in through, not just the config
// file: the launch-command preview builds one straight from the editor's DTO
// and renders it without ever touching LoadGenerateFile, so a sentinel left
// unresolved there renders as --chat-template-file "none" and the box silently
// disagrees with what a save would produce.
//
// Never call it on a VariantSpec. There the sentinel is the whole point.
func NormalizeNone(o *Override) {
o.ChatTemplateFile = clearNone(o.ChatTemplateFile)
o.MmprojFile = clearNone(o.MmprojFile)
o.CustomArgs = clearNone(o.CustomArgs)
o.ExtraArgs = clearNone(o.ExtraArgs)
o.TensorSplit = clearNone(o.TensorSplit)
o.OverrideTensor = clearNone(o.OverrideTensor)
o.KvKDraft = clearNone(o.KvKDraft)
o.KvVDraft = clearNone(o.KvVDraft)
o.VaePath = clearNone(o.VaePath)
o.ClipLPath = clearNone(o.ClipLPath)
o.ClipGPath = clearNone(o.ClipGPath)
o.T5Path = clearNone(o.T5Path)
o.TextEncoderPath = clearNone(o.TextEncoderPath)
o.AudioVaePath = clearNone(o.AudioVaePath)
o.LlmVisionPath = clearNone(o.LlmVisionPath)
o.LoraDir = clearNone(o.LoraDir)
}