Skip to content

Commit cb206cd

Browse files
committed
feat: write llama.ini for llama-server --models-preset llama.ini
1 parent 7757333 commit cb206cd

12 files changed

Lines changed: 268 additions & 165 deletions

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
*
22
!conf
3+
!event
34
!gie
45
!go.mod
56
!go.sum
67
!goinfer.go
78
!infer
9+
!proxy

.gitignore

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
/.continue/
99
/.old-code/
1010
/.roo/
11+
/*.ini*
12+
/*.log*
13+
/*.yml*
1114
/docsite/dist/
1215
/go.work*
1316
/goinfer
14-
/goinfer.ini*
1517
/infer/dist/
1618
/llama-server.exe
17-
/llama-swap.yml
1819
/template.jinja
1920
dist/
20-
llama.log

conf/conf.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,6 @@ const (
5050
// Hex code: C0ffee 15 C001, 50 C0ffee 15 900d. Bad C0de 15 Dead, 101. Cafe 91f7, 90 Cafe, 7e57 C0de.
5151
debugAPIKey = "C0ffee15C00150C0ffee15900dBadC0de15Dead101Cafe91f790Cafe7e57C0de"
5252
unsetAPIKey = "Please ⚠️ Set your private 64-hex-digit API key (32 bytes)"
53-
54-
// GoinferINI is the config filename.
55-
GoinferINI = "goinfer.ini"
56-
// LlamaSwapYML is the llama-swap config filename.
57-
LlamaSwapYML = "llama-swap.yml"
5853
)
5954

6055
// Do not use the bad ports: they are blocked by web browsers,

conf/conf_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func TestWriteMainCfg(t *testing.T) {
8484
t.Setenv("GI_MODELS_DIR", modelsDir)
8585
t.Setenv("GI_LLAMA_EXE", llamaExe)
8686

87-
data, err := cfg.GenFileData(false, true)
87+
data, err := cfg.GenGoinferINI(false, true)
8888
if err != nil {
8989
t.Fatalf("WriteMainCfg failed: %v", err)
9090
}
@@ -107,7 +107,7 @@ func TestWriteSwapCfg(t *testing.T) {
107107
}
108108
cfg.ModelsDir = modelsDir
109109

110-
ymlData, err := cfg.GenSwapYAMLData(false, false)
110+
ymlData, err := cfg.GenLlamaSwapYAML(false, false)
111111
if err != nil {
112112
t.Fatalf("WriteSwapCfg failed: %v ymlData=%s", err, string(ymlData))
113113
}

conf/files.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ func nameWithDir(root, truncated, name string) string {
179179
}
180180

181181
// extractFlags returns the truncated path and the llama-server flags from a file path.
182-
// It first checks for a companion ".args" file; if present, its contents are used as flags.
182+
// It first checks for a companion ".sh" file; if present, its contents are used as flags.
183183
// Otherwise, it parses flags encoded in the filename after an '&' delimiter.
184184
// Returns the truncated path (without extension) and a space-separated flag string.
185185
//
@@ -194,7 +194,7 @@ func extractFlags(path string) (truncated, flags_ string) {
194194
}
195195

196196
// 1. Is there a file containing the command line arguments?
197-
argsFn := filepath.Clean(truncated + ".args")
197+
argsFn := filepath.Clean(truncated + ".sh")
198198
args, err := os.ReadFile(argsFn)
199199
if err == nil {
200200
flags := oneLine(args)
@@ -226,7 +226,7 @@ func extractFlags(path string) (truncated, flags_ string) {
226226
return truncated[:pos], strings.Join(flags, " ")
227227
}
228228

229-
// oneLine converts the `.args` file into a single space-separated string,
229+
// oneLine converts the `.sh` file into a single space-separated string,
230230
// removing trailing backslashes, trimming whitespace, ignoring empty lines or comments.
231231
func oneLine(input []byte) string {
232232
keep := make([]byte, 0, len(input))
@@ -238,13 +238,15 @@ func oneLine(input []byte) string {
238238
}
239239
// Remove leading/trailing whitespace
240240
line = bytes.TrimSpace(line)
241-
// Skip blank lines and comments
242-
if len(line) == 0 || bytes.HasPrefix(line, []byte("#")) {
241+
// Skip blank lines
242+
if len(line) == 0 {
243243
continue
244244
}
245245
// Convert the byte slice to a string before appending.
246-
keep = append(keep, line...)
247-
keep = append(keep, ' ')
246+
if line[0] == '-' {
247+
keep = append(keep, line...)
248+
keep = append(keep, ' ')
249+
}
248250
}
249251

250252
return string(keep)

conf/files_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,9 @@ func TestExtractFlags(t *testing.T) {
8585
t.Parallel()
8686
tmp := t.TempDir()
8787

88-
// .args file present
88+
// .sh file present
8989
modelPath := createGGUFFile(t, tmp, "model1.gguf", 2048)
90-
argsPath := strings.TrimSuffix(modelPath, ".gguf") + ".args"
90+
argsPath := strings.TrimSuffix(modelPath, ".gguf") + ".sh"
9191
err := os.WriteFile(argsPath, []byte("-foo bar -baz qux"), 0o600)
9292
if err != nil {
9393
t.Fatalf("failed to write args file: %v", err)

conf/read.go renamed to conf/goinferini.go

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,22 @@
55
package conf
66

77
import (
8+
"crypto/rand"
9+
"encoding/hex"
810
"errors"
9-
"io"
1011
"log/slog"
1112
"os"
13+
"path/filepath"
1214
"strings"
1315
"syscall"
1416

1517
"github.com/LM4eu/goinfer/gie"
16-
"github.com/LM4eu/goinfer/proxy/config"
1718
"github.com/pelletier/go-toml/v2"
1819
)
1920

21+
// GoinferINI is the config filename.
22+
const GoinferINI = "goinfer.ini"
23+
2024
// ReadGoinferINI loads the configuration file, reads the env vars and verifies the settings.
2125
// Always return a valid configuration, because the receiver may want to write a valid config.
2226
func ReadGoinferINI(noAPIKey bool, extra, start string) (*Cfg, error) {
@@ -80,15 +84,39 @@ func ReadFileData(data []byte, noAPIKey bool, extra, start string) (*Cfg, error)
8084
return cfg, err
8185
}
8286

83-
// ReadSwapFromReader uses the LoadConfigFromReader() from llama-swap project.
84-
func (cfg *Cfg) ReadSwapFromReader(r io.Reader) error {
85-
var err error
86-
cfg.Swap, err = config.LoadConfigFromReader(r)
87-
if err != nil {
88-
slog.Error("Cannot load llama-swap config", "file", LlamaSwapYML, "error", err)
89-
os.Exit(1)
87+
// WriteGoinferINI populates the configuration with defaults, applies environment variables,
88+
// writes the resulting configuration to the given file.
89+
func (cfg *Cfg) WriteGoinferINI(debug, noAPIKey bool) error {
90+
data, err := cfg.GenGoinferINI(debug, noAPIKey)
91+
er := writeWithHeader(GoinferINI, "# Configuration of https://github.com/LM4eu/goinfer\n\n", data)
92+
if er != nil {
93+
if err != nil {
94+
return errors.Join(err, er)
95+
}
96+
return er
97+
}
98+
return err
99+
}
100+
101+
// GenGoinferINI sets the API keys, reads the environment variables,
102+
// fix some settings and writes the result config to a buffer.
103+
func (cfg *Cfg) GenGoinferINI(debug, noAPIKey bool) ([]byte, error) {
104+
cfg.setAPIKey(debug, noAPIKey)
105+
cfg.applyEnvVars()
106+
cfg.trimParamValues()
107+
cfg.fixDefaultModel()
108+
109+
err := cfg.validate(noAPIKey)
110+
111+
data, er := toml.Marshal(&cfg)
112+
if er != nil {
113+
er = gie.Wrap(err, gie.ConfigErr, "failed to yaml.Marshal", "cfg", cfg)
114+
if err != nil {
115+
return data, errors.Join(err, er)
116+
}
117+
return data, er
90118
}
91-
return cfg.ValidateSwap()
119+
return data, err
92120
}
93121

94122
// load the configuration file (if filename not empty).
@@ -190,3 +218,55 @@ func (cfg *Cfg) trimParamValues() {
190218
cfg.Llama.Common = strings.TrimSpace(cfg.Llama.Common)
191219
cfg.Llama.Goinfer = strings.TrimSpace(cfg.Llama.Goinfer)
192220
}
221+
222+
func writeWithHeader(path, header string, data []byte) error {
223+
path = filepath.Clean(path)
224+
file, err := os.Create(path)
225+
if err != nil {
226+
return gie.Wrap(err, gie.ConfigErr, "failed to create file="+path)
227+
}
228+
229+
_, err = file.WriteString(header)
230+
if err == nil {
231+
_, err = file.Write(data)
232+
}
233+
234+
er := file.Close()
235+
if err != nil {
236+
err = er
237+
}
238+
if err != nil {
239+
return gie.Wrap(err, gie.ConfigErr, "failed to write file="+path)
240+
}
241+
242+
return nil
243+
}
244+
245+
func (cfg *Cfg) setAPIKey(debug, noAPIKey bool) {
246+
switch {
247+
case noAPIKey:
248+
cfg.APIKey = unsetAPIKey
249+
slog.Info("Flag -no-api-key => Do not generate API key")
250+
251+
case debug:
252+
cfg.APIKey = debugAPIKey
253+
slog.Warn("API key is DEBUG => security threat")
254+
255+
default:
256+
cfg.APIKey = gen64HexDigits()
257+
slog.Info("Generated random API key")
258+
}
259+
}
260+
261+
func gen64HexDigits() string {
262+
buf := make([]byte, 32)
263+
_, err := rand.Read(buf)
264+
if err != nil {
265+
slog.Warn("Failed to rand.Read", "error", err)
266+
return ""
267+
}
268+
269+
key := make([]byte, 64)
270+
hex.Encode(key, buf)
271+
return string(key)
272+
}

conf/llamacppini.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2025 The contributors of Goinfer.
2+
// This file is part of Goinfer, a LLM proxy under the MIT License.
3+
// SPDX-License-Identifier: MIT
4+
5+
// Package conf reads/writes configuration
6+
package conf
7+
8+
import (
9+
"bytes"
10+
"strings"
11+
)
12+
13+
// LlamaINI is the llama.cpp config filename.
14+
const LlamaINI = "llama.ini"
15+
16+
// WriteLlamaINI generates the llama.cpp configuration.
17+
func WriteLlamaINI(yml []byte) error {
18+
header := `# DO NOT EDIT - This file is generated by Goinfer.
19+
#
20+
# llama.cpp configurations using Model Presets:
21+
#
22+
# llama-server --models-preset ./llama.ini
23+
#
24+
# Each section in this file defines a new preset.
25+
# Keys within a section correspond to command-line arguments (without leading dashes).
26+
# For example, the argument --n-gpu-layer 123 is written as n-gpu-layer = 123.
27+
# Short argument forms (e.g., c, ngl) and environment variable names (e.g., LLAMA_ARG_N_GPU_LAYERS) are also supported as keys.
28+
#
29+
# Doc: https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md#model-presets
30+
31+
version = 1
32+
`
33+
return writeWithHeader(LlamaINI, header, yml)
34+
}
35+
36+
// GenSwapYAMLData generates the llama-swap configuration.
37+
func (cfg *Cfg) GenLlamaINI() []byte {
38+
var out bytes.Buffer
39+
40+
// For each model, set two model settings:
41+
// 1. for the OpenAI endpoints
42+
// 2. for the /completion endpoint (prefix with A_ and hide the model)
43+
for model, mi := range cfg.getInfo() {
44+
out.Write(genModel(model, mi.Path, mi.Flags))
45+
out.Write(genModel(model+":A", mi.Path, cfg.Llama.Goinfer+" "+mi.Flags))
46+
}
47+
48+
return out.Bytes()
49+
}
50+
51+
// Add the model settings within the llama-swap configuration.
52+
func genModel(name, path, flags string) []byte {
53+
out := bytes.NewBufferString(`
54+
[` + name + `]
55+
model = ` + path)
56+
57+
boolean := false
58+
59+
for arg := range strings.FieldsSeq(flags) {
60+
if arg[0] == '-' {
61+
if boolean {
62+
out.WriteString(" true")
63+
}
64+
boolean = true
65+
out.WriteByte('\n')
66+
if len(arg) == 2 {
67+
out.WriteByte(arg[1])
68+
} else {
69+
i := 1
70+
if arg[1] == '-' {
71+
i = 2
72+
}
73+
out.WriteString(arg[i:])
74+
}
75+
out.WriteByte(' ')
76+
out.WriteByte('=')
77+
continue
78+
}
79+
boolean = false
80+
out.WriteByte(' ')
81+
out.WriteString(arg)
82+
}
83+
84+
if boolean {
85+
out.WriteString(" true\n")
86+
} else {
87+
out.WriteByte('\n')
88+
}
89+
90+
return out.Bytes()
91+
}

0 commit comments

Comments
 (0)