Ctrl K
ComunidadGuías…

internal/autogen/customargs.go

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

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"sort"
	"strings"

	"github.com/quartermaster-labs/quartermaster/internal/config"
)

// Custom launch arguments are applied to a rendered command in ONE direction:
// the user's text is never decomposed. For every knob the text sets, the
// generated occurrences of that knob are dropped, and the text is appended
// verbatim at the end, where llama-server's own last-flag-wins rule gives it
// the final say.
//
// The alternative (parsing the text back into structured fields, which is what
// the old launch-parameters box did) is a lossy round trip: every flag needs a
// parser case, every miss lands in the free-form bucket, and flags like -cram
// ended up emitted twice, once from the field and once from the text.

// Token provenance values for CmdToken.Source.
const (
	SourceGenerated = "generated"
	SourceCustom    = "custom"
)

// CmdToken is one token of a launch command with the provenance the editor
// needs to render the composed command without diffing strings. Suppressed
// tokens are generated tokens that a custom knob replaced; they are reported so
// the editor can strike them through or summarise the count.
type CmdToken struct {
	Text       string `json:"text"`
	Source     string `json:"source"`
	Knob       string `json:"knob,omitempty"`
	Suppressed bool   `json:"suppressed,omitempty"`
}

// ComposedCmd is a rendered command in layers.
type ComposedCmd struct {
	// Generated is the command as the generator emitted it (no custom text).
	Generated string `json:"generated"`
	// Effective is what will actually run.
	Effective string `json:"effective"`
	// Custom is the user's text, unchanged.
	Custom string `json:"custom,omitempty"`
	// Lines is the effective command split back into emittable lines. It is
	// byte-identical to the input lines when Custom is empty.
	Lines []string `json:"-"`
	// Tokens is every token of both layers, in order, with provenance.
	Tokens []CmdToken `json:"tokens,omitempty"`
	// Owned lists the knobs the custom text sets, sorted.
	Owned []string `json:"ownedKnobs,omitempty"`
	// Issues is what the editor's flag check found in Tokens. Empty for a
	// command with no custom text, and never consumed by config emission.
	Issues []CmdIssue `json:"issues,omitempty"`
}

// CustomArgsText returns the custom launch-argument text in effect: the new
// CustomArgs field when it carries anything, else the legacy ExtraArgs bucket
// written by the old two-way box. CustomArgsOff suppresses both without
// discarding them, so a disable/enable round trip in the editor loses nothing.
func (o *Override) CustomArgsText() string {
	if o == nil || o.CustomArgsOff {
		return ""
	}
	if strings.TrimSpace(o.CustomArgs) != "" {
		return o.CustomArgs
	}
	return o.ExtraArgs
}

// ClearOwnedFieldsFromText parses the override's effective custom text and
// zeroes the structured fields it shadows. This is the save-time half of the
// ownership rule: dropping the generated flag at emit time is not enough, since
// the stale field would resurface the moment the user deletes the flag from the
// text (issue #38's mmap reset). Unparseable text clears nothing.
//
// It resolves against BOTH flag tables, unlike ComposeCmd, which must be handed
// exactly one. The two answer different questions. Suppression decides what a
// specific binary is launched with, so guessing the table there would emit the
// wrong argv; clearing only zeroes fields on an Override that carries both
// backends' knobs and never uses more than one set, so resolving a diffusion
// model's -H against the llama table's --hf-repo costs nothing but a blank
// field nothing reads. That matters because the save and preview endpoints see
// the JSON body before anything has read the gguf header, and making them
// arch-detect first would put a header read on every keystroke of the editor's
// live preview.
func ClearOwnedFieldsFromText(ov *Override) []string {
	text := ov.CustomArgsText()
	if strings.TrimSpace(text) == "" {
		return nil
	}
	toks, err := config.SanitizeCommand(text)
	if err != nil {
		return nil
	}
	owned := LlamaFlags.OwnedKnobs(toks)
	for k, v := range SdFlags.OwnedKnobs(toks) {
		if v {
			owned[k] = true
		}
	}
	return ClearOwnedFields(ov, owned)
}

// ComposeCmd applies custom launch arguments to generated command lines. An
// empty (or comment-only) custom text is the identity: the lines are returned
// untouched, so a config saved before this feature existed regenerates
// byte-identically.
//
// Unparseable custom text is returned as an error with the command left
// unchanged; the caller decides whether to surface it (the preview does) or to
// emit it anyway and let the spawn fail as it would have before.
func (ft *FlagTable) ComposeCmd(lines []string, custom string) (ComposedCmd, error) {
	gen := strings.Join(lines, " ")
	cc := ComposedCmd{Generated: gen, Effective: gen, Lines: lines, Custom: custom}

	genToks, _ := config.SanitizeCommand(gen)
	appendGen := func(tok string, knob string, suppressed bool) {
		cc.Tokens = append(cc.Tokens, CmdToken{Text: tok, Source: SourceGenerated, Knob: knob, Suppressed: suppressed})
	}

	// Comment-only text is blank: the spawn strips comment lines, so it can
	// suppress nothing and must not turn into an empty-command error.
	if strings.TrimSpace(config.StripComments(custom)) == "" {
		for _, t := range genToks {
			appendGen(t, ft.TokenKnob(t), false)
		}
		return cc, nil
	}

	customToks, err := config.SanitizeCommand(custom)
	if err != nil {
		// Emit the text unchanged (what the old extraArgs path did) so the
		// spawn fails with the backend's own message; callers that can show a
		// dialog (the preview) surface err instead.
		if len(lines) > 0 {
			cc.Lines = append(append([]string{}, lines...), splitCustomLines(custom)...)
			cc.Effective = strings.Join(cc.Lines, " ")
		}
		return cc, err
	}
	if len(customToks) == 0 { // comment-only text
		for _, t := range genToks {
			appendGen(t, ft.TokenKnob(t), false)
		}
		return cc, nil
	}

	owned := ft.OwnedKnobs(customToks)
	for k := range owned {
		cc.Owned = append(cc.Owned, k)
	}
	sort.Strings(cc.Owned)

	out := make([]string, 0, len(lines)+2)
	for _, line := range lines {
		if strings.TrimSpace(line) == "" {
			continue
		}
		toks, err := config.SanitizeCommand(line)
		if err != nil {
			// A line the spawner could not split either; keep it as written and
			// let the same failure happen at spawn time.
			out = append(out, line)
			for _, t := range toks {
				appendGen(t, ft.TokenKnob(t), false)
			}
			continue
		}
		kept := make([]string, 0, len(toks))
		dropped := false
		for i := 0; i < len(toks); i++ {
			tok := toks[i]
			if !strings.HasPrefix(tok, "-") {
				kept = append(kept, tok)
				appendGen(tok, "", false)
				continue
			}
			name, _, inline := SplitFlagToken(tok)
			def, known := ft.Lookup(name)
			if known && def.Knob != "" && owned[def.Knob] {
				dropped = true
				appendGen(tok, def.Knob, true)
				for n := 0; def.Value && !inline && n <= def.ExtraValues && i+1 < len(toks); n++ {
					i++
					appendGen(toks[i], def.Knob, true)
				}
				continue
			}
			kept = append(kept, tok)
			appendGen(tok, ft.TokenKnob(tok), false)
			for n := 0; known && def.Value && !inline && n <= def.ExtraValues && i+1 < len(toks); n++ {
				i++
				kept = append(kept, toks[i])
				appendGen(toks[i], "", false)
			}
		}
		if len(kept) == 0 {
			continue
		}
		if dropped {
			// Only a line that actually lost a token is re-serialised; every
			// other line keeps the emitter's exact bytes.
			out = append(out, joinTokens(kept))
		} else {
			out = append(out, line)
		}
	}

	for _, line := range splitCustomLines(custom) {
		out = append(out, line)
	}
	for _, t := range customToks {
		cc.Tokens = append(cc.Tokens, CmdToken{Text: t, Source: SourceCustom, Knob: ft.TokenKnob(t)})
	}
	if len(out) == 0 {
		return cc, nil
	}
	cc.Lines = out
	cc.Effective = strings.Join(out, " ")
	return cc, nil
}

// ComposeCmd composes against the llama-server table, which is what every
// llama-server caller wants and what the tests were written against.
func ComposeCmd(lines []string, custom string) (ComposedCmd, error) {
	return LlamaFlags.ComposeCmd(lines, custom)
}

// splitCustomLines prepares the user's text for the YAML cmd block: one
// indented line per non-empty source line, CR stripped. Comment lines are kept
// (the spawn's SanitizeCommand drops them, and so does the UI preview).
func splitCustomLines(custom string) []string {
	raw := strings.Split(strings.ReplaceAll(custom, "\r\n", "\n"), "\n")
	out := make([]string, 0, len(raw))
	for _, l := range raw {
		l = strings.TrimRight(l, "\r")
		if strings.TrimSpace(l) == "" {
			continue
		}
		out = append(out, l)
	}
	return out
}

// joinTokens re-serialises a token list for the config file. Only tokens that
// need it are quoted; everything else keeps its spelling, so the round trip
// through SanitizeCommand at spawn time reproduces the same argv.
func joinTokens(toks []string) string {
	var b strings.Builder
	for i, t := range toks {
		if i > 0 {
			b.WriteByte(' ')
		}
		b.WriteString(quoteToken(t))
	}
	return b.String()
}

func quoteToken(tok string) string {
	if tok == "" || strings.ContainsAny(tok, " \t\"") {
		return `"` + strings.ReplaceAll(tok, `"`, "") + `"`
	}
	return tok
}