Skip to content

Commit f42e195

Browse files
committed
feat(conf): retrieve llama-server flags from shell scripts
1 parent 996edea commit f42e195

4 files changed

Lines changed: 200 additions & 3 deletions

File tree

conf/conf.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type (
3232
ModelsDir string `toml:"models_dir" yaml:"models_dir" comment:"\nGoinfer recursively searches GGUF files in one or multiple folders separated by ':'\nList your GGUF dirs with: locate .gguf | sed -e 's,/[^/]*$,,' | uniq\nenv. var: GI_MODELS_DIR"`
3333
DefaultModel string `toml:"default_model" yaml:"default_model" comment:"\nThe default model name to load at startup\nCan also be set with: ./goinfer -start <model-name>"`
3434
Addr string `toml:"addr" yaml:"addr" comment:"address can be 'host:port' or 'ip:por' or simply ':port' (for host = localhost)"`
35+
Shells []*ModelInfo `toml:"-" yaml:"-"`
3536
}
3637

3738
// Llama holds the inference engine settings.

conf/files.go

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,113 @@ func nameWithDir(root, truncated, name string) string {
199199
return grp + "/" + name
200200
}
201201

202+
// extractModelNameAndFlags search for a llama-server command line
203+
// and extract the model path (flag -m or --model) and the flags after -m|--model.
204+
func extractModelNameAndFlags(fsys fs.FS, shellPath string) (modelPath, flags []byte) {
205+
// shellPath = filepath.Clean(shellPath)
206+
script, err := fs.ReadFile(fsys, shellPath)
207+
if err != nil {
208+
return nil, nil
209+
}
210+
211+
script = searchLlamaServer(script)
212+
if script == nil {
213+
return nil, nil
214+
}
215+
216+
script = searchModelFlag(script)
217+
if script == nil {
218+
return nil, nil
219+
}
220+
221+
// trim any leading whitespace character
222+
script = bytes.TrimSpace(script)
223+
pos := bytes.IndexAny(script, " \t")
224+
if pos < 0 {
225+
modelPath = script
226+
} else {
227+
modelPath = script[:pos]
228+
flags = oneLine(script[pos+1:])
229+
}
230+
231+
slog.Info("Found", "from file", shellPath, "flags", flags)
232+
233+
return modelPath, flags
234+
}
235+
236+
// searchLlamaServer searches for a llama-server command line.
237+
func searchLlamaServer(script []byte) []byte {
238+
for {
239+
var before []byte
240+
var found bool
241+
before, script, found = bytes.Cut(script, []byte("/llama-server"))
242+
if !found {
243+
return nil
244+
}
245+
246+
// check space after "llama-server"
247+
if len(script) == 0 {
248+
return nil
249+
}
250+
251+
if script[0] != ' ' && script[0] != '\t' {
252+
continue
253+
}
254+
255+
// rewind to the beginning of the line
256+
pos := bytes.LastIndexByte(before, '\n')
257+
if pos >= 0 {
258+
before = before[pos+1:]
259+
}
260+
261+
// skip commented lines except shebang (#!)
262+
if len(before) == 0 || before[0] != '#' || (len(before) > 1 && before[1] == '!') {
263+
return script[1:]
264+
}
265+
}
266+
}
267+
268+
// searchModelFlag searches for the flag: --model or -m.
269+
func searchModelFlag(script []byte) []byte {
270+
for {
271+
before, after, found := bytes.Cut(script, []byte("--model"))
272+
if !found {
273+
before, after, found = bytes.Cut(script, []byte("-m"))
274+
if !found {
275+
return nil
276+
}
277+
}
278+
script = after
279+
280+
if len(script) == 0 {
281+
return nil
282+
}
283+
284+
if script[0] != ' ' && script[0] != '\t' {
285+
continue
286+
}
287+
288+
if len(before) == 0 {
289+
return script
290+
}
291+
292+
if before[len(before)-1] != ' ' && before[len(before)-1] != '\t' {
293+
continue
294+
}
295+
296+
// rewind to the beginning of the line
297+
pos := bytes.LastIndexByte(before, '\n')
298+
if pos >= 0 {
299+
before = before[pos+1:]
300+
}
301+
302+
// skip commented lines
303+
if before[0] != '#' {
304+
return script[1:]
305+
}
306+
}
307+
}
308+
202309
// extractFlags returns the truncated path and the llama-server flags from a file path.
203310
// It first checks for a companion ".sh" file; if present, its contents are used as flags.
204311
// Otherwise, it parses flags encoded in the filename after an '&' delimiter.
@@ -255,7 +362,7 @@ func oneLine(input []byte) []byte {
255362
continue // Skip empty lines
256363
}
257364

258-
if line[len(line)-1] == '\'' {
365+
if line[len(line)-1] == '\\' {
259366
line = line[:len(line)-1] // Remove trailing backslash
260367
}
261368

@@ -265,8 +372,10 @@ func oneLine(input []byte) []byte {
265372
continue
266373
}
267374

375+
if len(keep) > 0 {
376+
keep = append(keep, ' ')
377+
}
268378
keep = append(keep, line...)
269-
keep = append(keep, ' ')
270379
}
271380

272381
return keep

conf/files_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"path/filepath"
1010
"strings"
1111
"testing"
12+
"testing/fstest"
1213

1314
"github.com/LM4eu/goinfer/proxy/config"
1415
)
@@ -242,3 +243,61 @@ func Test_nameWithGGUF(t *testing.T) {
242243
})
243244
}
244245
}
246+
247+
const (
248+
script1 = "/path/to/llama-server --host 0.0.0.0 --port 5800 --verbose-prompt --no-warmup " +
249+
" -m /path/model.gguf " +
250+
` --no-mmap --chat-template-kwargs '{"reasoning_effort": "high"}' ` +
251+
" --reasoning-format auto -c 10240 " + " # --no-context-shift"
252+
253+
script2 = `#!/bin/sh
254+
/path/to/llama-server --host 0.0.0.0 --port 5800 --verbose-prompt \
255+
--no-warmup --model /path/model.gguf --no-mmap \
256+
--chat-template-kwargs '{"reasoning_effort": "high"}' \
257+
--reasoning-format auto -c 10240 \
258+
# --no-context-shift
259+
`
260+
script3 = `#!/path/to/llama-server --host 0.0.0.0 --port 5800 \
261+
--verbose-prompt --no-warmup -m /path/model.gguf \
262+
--no-mmap --chat-template-kwargs '{"reasoning_effort": "high"}' \
263+
--reasoning-format auto -c 10240 \
264+
# --no-context-shift
265+
`
266+
)
267+
268+
func Test_extractModelNameAndFlags(t *testing.T) {
269+
t.Parallel()
270+
271+
testFS := fstest.MapFS{
272+
"0.sh": &fstest.MapFile{Data: []byte("")},
273+
"1.sh": &fstest.MapFile{Data: []byte(script1)},
274+
"2.sh": &fstest.MapFile{Data: []byte(script2)},
275+
"3.sh": &fstest.MapFile{Data: []byte(script3)},
276+
"4.sh": &fstest.MapFile{Data: []byte("/path/to/llama-server --models-preset ~/bin/goinfer/models.ini")},
277+
"5.sh": &fstest.MapFile{Data: []byte("llama-server --model ~/model.gguf")},
278+
"6.sh": &fstest.MapFile{Data: []byte("/llama-server --model ~/model.gguf")},
279+
"7.sh": &fstest.MapFile{Data: []byte("/llama-server\t-m\t~/model.gguf\t-c\t0")},
280+
}
281+
282+
tests := []struct{ path, wantModel, wantFlags string }{
283+
{"1.sh", "/path/model.gguf", `--no-mmap --chat-template-kwargs '{"reasoning_effort": "high"}' --reasoning-format auto -c 10240 # --no-context-shift`},
284+
{"2.sh", "/path/model.gguf", `--no-mmap --chat-template-kwargs '{"reasoning_effort": "high"}' --reasoning-format auto -c 10240`},
285+
{"3.sh", "/path/model.gguf", `--no-mmap --chat-template-kwargs '{"reasoning_effort": "high"}' --reasoning-format auto -c 10240`},
286+
{"4.sh", "", ""},
287+
{"5.sh", "", ""},
288+
{"6.sh", "~/model.gguf", ""},
289+
{"7.sh", "~/model.gguf", "-c\t0"},
290+
}
291+
for _, tt := range tests {
292+
t.Run(tt.path, func(t *testing.T) {
293+
t.Parallel()
294+
gotModel, gotFlags := extractModelNameAndFlags(testFS, tt.path)
295+
if string(gotModel) != tt.wantModel {
296+
t.Errorf("extractModelNameAndFlags(%s) = %s, want %s", tt.path, gotModel, tt.wantModel)
297+
}
298+
if string(gotFlags) != tt.wantFlags {
299+
t.Errorf("extractModelNameAndFlags(%s) = %s, want %s", tt.path, gotFlags, tt.wantFlags)
300+
}
301+
})
302+
}
303+
}

conf/info.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ func (cfg *Cfg) updateInfo() {
159159
// cfg.Info. It validates each file using validateFile and warns about errors (logs).
160160
func (cfg *Cfg) search(params map[string]ModelParams, root string) error {
161161
return filepath.WalkDir(root, func(path string, dir fs.DirEntry, err error) error {
162+
fsys := os.DirFS(root)
162163
switch {
163164
case err != nil:
164165
if dir == nil {
@@ -172,8 +173,10 @@ func (cfg *Cfg) search(params map[string]ModelParams, root string) error {
172173
if err != nil {
173174
slog.Warn("skip params file", "path", path, "err", err)
174175
}
175-
case strings.HasSuffix(path, ".gguf"):
176+
case filepath.Ext(path) == ".gguf":
176177
cfg.keepGUFF(root, path)
178+
case filepath.Ext(path) == ".sh":
179+
cfg.keepFlags(fsys, path[len(root):])
177180
default:
178181
}
179182
return nil
@@ -231,3 +234,28 @@ func (cfg *Cfg) keepGUFF(root, path string) {
231234
}
232235
cfg.Info[name] = &mi
233236
}
237+
238+
func (cfg *Cfg) keepFlags(fsys fs.FS, path string) {
239+
modelPath, args := extractModelNameAndFlags(fsys, path)
240+
if modelPath == nil {
241+
return
242+
}
243+
244+
flags := replaceDIR(path, string(args))
245+
246+
mi := ModelInfo{Params: nil, Flags: flags, Path: string(modelPath), Origin: path}
247+
if cfg.Shells == nil {
248+
slog.Debug("Found first", "shell", path)
249+
cfg.Shells = []*ModelInfo{&mi}
250+
return
251+
}
252+
253+
for _, mm := range cfg.Shells {
254+
if mm.Origin == mi.Origin {
255+
slog.Debug("Already present", "shell", path)
256+
return
257+
}
258+
}
259+
260+
cfg.Shells = append(cfg.Shells, &mi)
261+
}

0 commit comments

Comments
 (0)