Ctrl K
ComunidadGuías…

internal/config/cmdinfo.go

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

Archivo original ↗Preguntar a KiBot
package config

import (
	"path/filepath"
	"strconv"
	"strings"
	"sync"
)

// CmdInfo is a parsed, read-only view of a rendered launch command.
//
// A model's command is a STRING all the way from autogen's emitter to the
// spawn, so every consumer that wants one fact out of it (which gguf? which
// port? is the slot cache on?) has to take it apart again. Doing that ad hoc
// produced two classes of bug: substring sniffs like
// strings.Contains(cmd, "--spec-type draft-mtp") that silently stop matching
// when the emitter wraps the flag onto its own line, and a full shlex split on
// the request hot path (per-request family lookup / slot-cache gating).
//
// ParseCmd centralizes and memoizes the split, so callers ask token-exact
// questions (Has/Value/Values) instead of scanning raw text, and repeat asks
// for the same command are a map lookup.
//
// A CmdInfo must be treated as immutable — it is shared between callers.
type CmdInfo struct {
	// Argv is the sanitized token list, nil if the command could not be split.
	Argv []string
	// ModelPath is the model file the command loads: the value of -m / --model /
	// --diffusion-model, or vllm's positional `serve <model>` (a gguf or an HF
	// folder), slash-normalized. "" when the command names none (a
	// non-llama.cpp upstream). This is the fork's model "family" key — every
	// variant of one model (ctx tiers, game/judge profiles) is the same file
	// launched with different placement flags.
	ModelPath string
}

// modelPathFlags are the flags whose value is the loaded model file; the first
// one appearing in the command decides. sd-server loads via --diffusion-model,
// qwentts/asr/sam via --model, TTS.cpp via --model-path, llama-server via -m.
// A backend missing from this list has no family key, which is not cosmetic: the
// config editor refuses to save an override for a model whose gguf it cannot
// find ("model has no gguf path to override"), so every emitted backend's model
// flag belongs here. audiocpp_server is the one backend that cannot be served by
// this list at all - it has NO model flag, only a --config file - so its weights
// are read off the model entry's audiocpp: block by server.modelGguf.
var modelPathFlags = []string{"-m", "--model", "--model-path", "--diffusion-model"}

// cmdInfoCache memoizes by raw command string. Commands are stable config data
// (one per model, changing only on reload), so the cache is small and never
// needs per-entry eviction — it is dropped wholesale past a generous cap so a
// pathological caller (many one-off adhoc commands) can't grow it without
// bound.
var (
	cmdInfoMu    sync.RWMutex
	cmdInfoCache = map[string]*CmdInfo{}
)

const cmdInfoCacheMax = 512

// ParseCmd splits a rendered launch command and extracts the facts consumers
// share. It never returns nil; an unparseable command yields a CmdInfo with a
// nil Argv, so callers find no flags and fall back to their defaults exactly as
// they did when each of them called SanitizeCommand itself.
func ParseCmd(cmd string) *CmdInfo {
	cmdInfoMu.RLock()
	info, ok := cmdInfoCache[cmd]
	cmdInfoMu.RUnlock()
	if ok {
		return info
	}

	info = &CmdInfo{}
	if argv, err := SanitizeCommand(cmd); err == nil {
		info.Argv = argv
	}
	if v, ok := info.Value(modelPathFlags...); ok {
		info.ModelPath = filepath.ToSlash(strings.TrimSpace(v))
	} else if v, ok := info.servePositional(); ok {
		info.ModelPath = filepath.ToSlash(strings.TrimSpace(v))
	}

	cmdInfoMu.Lock()
	if len(cmdInfoCache) >= cmdInfoCacheMax {
		cmdInfoCache = map[string]*CmdInfo{}
	}
	cmdInfoCache[cmd] = info
	cmdInfoMu.Unlock()
	return info
}

// servePositional reads the model of a `<exe> serve <model>` command. vllm
// takes its model positionally, so modelPathFlags alone left every vllm entry
// with no family key and no way to be saved from the config editor.
func (c *CmdInfo) servePositional() (string, bool) {
	if len(c.Argv) < 3 || c.Argv[1] != "serve" || strings.HasPrefix(c.Argv[2], "-") {
		return "", false
	}
	return c.Argv[2], true
}

// Has reports whether any of the given flags appears as its own token (or in
// --flag=value form). Token-exact: unlike a substring test it can't be fooled
// by a longer flag that starts the same way, nor defeated by a line break
// between the flag and its value.
func (c *CmdInfo) Has(flags ...string) bool {
	for _, a := range c.Argv {
		for _, f := range flags {
			if a == f || strings.HasPrefix(a, f+"=") {
				return true
			}
		}
	}
	return false
}

// Value returns the value of the first of the given flags that carries one,
// accepting both "--flag value" and "--flag=value". ok is false when no flag
// matched or the match was the final token with nothing after it.
func (c *CmdInfo) Value(flags ...string) (string, bool) {
	for i, a := range c.Argv {
		for _, f := range flags {
			if strings.HasPrefix(a, f+"=") {
				return strings.TrimPrefix(a, f+"="), true
			}
			if a == f {
				if i+1 < len(c.Argv) {
					return c.Argv[i+1], true
				}
				return "", false
			}
		}
	}
	return "", false
}

// Values returns every value given for a flag, in order. Repeatable flags
// (--spec-type, --lora) are accumulated by the backend rather than overridden,
// so a caller asking "is draft-mtp active" must look at all of them.
func (c *CmdInfo) Values(flags ...string) []string {
	var out []string
	for i, a := range c.Argv {
		for _, f := range flags {
			if strings.HasPrefix(a, f+"=") {
				out = append(out, strings.TrimPrefix(a, f+"="))
			} else if a == f && i+1 < len(c.Argv) {
				out = append(out, c.Argv[i+1])
			}
		}
	}
	return out
}

// Int is Value parsed as a base-10 int; ok is false if absent or non-numeric.
func (c *CmdInfo) Int(flags ...string) (int, bool) {
	v, ok := c.Value(flags...)
	if !ok {
		return 0, false
	}
	n, err := strconv.Atoi(v)
	if err != nil {
		return 0, false
	}
	return n, true
}

// HasValue reports whether any of the flags is present with the exact value v.
func (c *CmdInfo) HasValue(v string, flags ...string) bool {
	for _, got := range c.Values(flags...) {
		if got == v {
			return true
		}
	}
	return false
}