more env fixes

This commit is contained in:
Luxferre
2026-09-10 12:28:00 +03:00
parent b0a16e99e1
commit d9c7bb4407
4 changed files with 157 additions and 65 deletions
+1 -1
View File
@@ -142,7 +142,7 @@ If the API rejects the request with an `Invalid assistant message: content or to
### Model configuration parameters ### Model configuration parameters
(shared by all implementations; the config file — `.bantam.cfg` takes priority over `model.cfg` when both exist — is plain `key=value` with `#` comments. Values defined in the config file override corresponding `BANTAM_*` environment variables, which in turn override built-in defaults.) (shared by all implementations; `.bantam.cfg` takes priority over `model.cfg` when both exist — both are plain `key=value` with `#` comments. Values defined in `.bantam.cfg` override `BANTAM_*` environment variables; when `.bantam.cfg` is absent, `BANTAM_*` environment variables override `model.cfg` and built-in defaults.)
- `endpoint` (base OpenAI-compatible API URL, default `https://api.kilo.ai/api/openrouter`; falls back to `BANTAM_ENDPOINT` env var) - `endpoint` (base OpenAI-compatible API URL, default `https://api.kilo.ai/api/openrouter`; falls back to `BANTAM_ENDPOINT` env var)
- `model` (model name, default `openrouter/free`; falls back to `BANTAM_MODEL` env var) - `model` (model name, default `openrouter/free`; falls back to `BANTAM_MODEL` env var)
+85 -58
View File
@@ -208,64 +208,7 @@ func listModels(cfg *Cfg) (string, error) {
return b.String(), nil return b.String(), nil
} }
func getCfg(path string) Cfg { func parseCfgFile(path string, cfg *Cfg) {
cfg := defCfg
// Environment variable fallbacks. These are applied BEFORE the config file is
// read so that any value explicitly set in the config file overrides them.
if v := strings.TrimSpace(os.Getenv("BANTAM_ENDPOINT")); v != "" {
cfg.Endpoint = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_MODEL")); v != "" {
cfg.Model = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_TEMP")); v != "" {
if f, e := strconv.ParseFloat(v, 64); e == nil {
cfg.Temperature = f
}
} else if v := strings.TrimSpace(os.Getenv("BANTAM_TEMPERATURE")); v != "" {
if f, e := strconv.ParseFloat(v, 64); e == nil {
cfg.Temperature = f
}
}
if v := strings.TrimSpace(os.Getenv("BANTAM_STREAM")); v != "" {
cfg.Stream = v == "true" || v == "1" || v == "yes"
}
if v := strings.TrimSpace(os.Getenv("BANTAM_COLOR")); v != "" {
cfg.Color = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_TIMEOUT")); v != "" {
cfg.Timeout = atoiD(v, cfg.Timeout)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_SHELL_TIMEOUT")); v != "" {
cfg.ShellTimeout = atoiD(v, cfg.ShellTimeout)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_MAX_AL_ITERATIONS")); v != "" {
cfg.MaxALIterations = atoiD(v, cfg.MaxALIterations)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_CONTEXT_WINDOW")); v != "" {
cfg.ContextWindow = atoiD(v, cfg.ContextWindow)
}
re := "high"
if v := strings.TrimSpace(os.Getenv("BANTAM_REASONING_EFFORT")); v != "" {
re = v
}
cfg.Raw = map[string]string{
"endpoint": cfg.Endpoint, "model": cfg.Model, "temperature": fmt.Sprintf("%v", cfg.Temperature),
"api_key": cfg.APIKey, "stream": strconv.FormatBool(cfg.Stream), "color": cfg.Color,
"timeout": strconv.Itoa(cfg.Timeout), "shell_timeout": strconv.Itoa(cfg.ShellTimeout),
"max_al_iterations": strconv.Itoa(cfg.MaxALIterations),
"context_window": strconv.Itoa(cfg.ContextWindow),
"reasoning_effort": re,
}
if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" {
cfg.Raw["bantam_tools_dir"] = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" {
cfg.Raw["bantam_skills_dir"] = v
}
if d, err := os.ReadFile(path); err == nil { if d, err := os.ReadFile(path); err == nil {
for _, ln := range strings.Split(string(d), "\n") { for _, ln := range strings.Split(string(d), "\n") {
ln = strings.TrimSpace(ln) ln = strings.TrimSpace(ln)
@@ -287,6 +230,90 @@ func getCfg(path string) Cfg {
} }
} }
} }
}
func applyEnvCfg(cfg *Cfg) {
if v := strings.TrimSpace(os.Getenv("BANTAM_ENDPOINT")); v != "" {
cfg.Endpoint = v
cfg.Raw["endpoint"] = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_MODEL")); v != "" {
cfg.Model = v
cfg.Raw["model"] = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_TEMP")); v != "" {
if f, e := strconv.ParseFloat(v, 64); e == nil {
cfg.Temperature = f
cfg.Raw["temperature"] = v
}
} else if v := strings.TrimSpace(os.Getenv("BANTAM_TEMPERATURE")); v != "" {
if f, e := strconv.ParseFloat(v, 64); e == nil {
cfg.Temperature = f
cfg.Raw["temperature"] = v
}
}
if v := strings.TrimSpace(os.Getenv("BANTAM_STREAM")); v != "" {
cfg.Stream = v == "true" || v == "1" || v == "yes"
cfg.Raw["stream"] = strconv.FormatBool(cfg.Stream)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_COLOR")); v != "" {
cfg.Color = v
cfg.Raw["color"] = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_TIMEOUT")); v != "" {
cfg.Timeout = atoiD(v, cfg.Timeout)
cfg.Raw["timeout"] = strconv.Itoa(cfg.Timeout)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_SHELL_TIMEOUT")); v != "" {
cfg.ShellTimeout = atoiD(v, cfg.ShellTimeout)
cfg.Raw["shell_timeout"] = strconv.Itoa(cfg.ShellTimeout)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_MAX_AL_ITERATIONS")); v != "" {
cfg.MaxALIterations = atoiD(v, cfg.MaxALIterations)
cfg.Raw["max_al_iterations"] = strconv.Itoa(cfg.MaxALIterations)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_CONTEXT_WINDOW")); v != "" {
cfg.ContextWindow = atoiD(v, cfg.ContextWindow)
cfg.Raw["context_window"] = strconv.Itoa(cfg.ContextWindow)
}
if v := strings.TrimSpace(os.Getenv("BANTAM_REASONING_EFFORT")); v != "" {
cfg.Raw["reasoning_effort"] = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" {
cfg.Raw["bantam_tools_dir"] = v
}
if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" {
cfg.Raw["bantam_skills_dir"] = v
}
}
func getCfg(path string) Cfg {
cfg := defCfg
cfg.Raw = map[string]string{
"endpoint": cfg.Endpoint, "model": cfg.Model, "temperature": fmt.Sprintf("%v", cfg.Temperature),
"api_key": cfg.APIKey, "stream": strconv.FormatBool(cfg.Stream), "color": cfg.Color,
"timeout": strconv.Itoa(cfg.Timeout), "shell_timeout": strconv.Itoa(cfg.ShellTimeout),
"max_al_iterations": strconv.Itoa(cfg.MaxALIterations),
"context_window": strconv.Itoa(cfg.ContextWindow),
"reasoning_effort": "high",
}
if filepath.Base(path) == "model.cfg" {
// When .bantam.cfg is absent, model.cfg provides base defaults,
// and BANTAM_* environment variables override them.
parseCfgFile(path, &cfg)
applyEnvCfg(&cfg)
} else {
// If a model.cfg exists in the same directory, read it first as base defaults.
modelCfgPath := filepath.Join(filepath.Dir(path), "model.cfg")
if _, err := os.Stat(modelCfgPath); err == nil {
parseCfgFile(modelCfgPath, &cfg)
}
// Environment variables override model.cfg.
applyEnvCfg(&cfg)
// The explicit override file (.bantam.cfg) takes highest priority and overrides env vars.
parseCfgFile(path, &cfg)
}
// The api_key "-" / empty sentinel means "fall back to BANTAM_API_KEY"; this // The api_key "-" / empty sentinel means "fall back to BANTAM_API_KEY"; this
// is evaluated after the file is read so an explicit key still wins. // is evaluated after the file is read so an explicit key still wins.
+69 -2
View File
@@ -420,7 +420,8 @@ func TestGetCfgEnvOverriddenByFile(t *testing.T) {
t.Setenv("BANTAM_TOOLS_DIR", "/env/tools") t.Setenv("BANTAM_TOOLS_DIR", "/env/tools")
t.Setenv("BANTAM_SKILLS_DIR", "/env/skills") t.Setenv("BANTAM_SKILLS_DIR", "/env/skills")
p := writeCfg(t, strings.Join([]string{ p := filepath.Join(t.TempDir(), ".bantam.cfg")
if err := os.WriteFile(p, []byte(strings.Join([]string{
"endpoint=http://file-endpoint/v1", "endpoint=http://file-endpoint/v1",
"model=file-model", "model=file-model",
"temperature=0.1", "temperature=0.1",
@@ -433,7 +434,9 @@ func TestGetCfgEnvOverriddenByFile(t *testing.T) {
"reasoning_effort=high", "reasoning_effort=high",
"bantam_tools_dir=/file/tools", "bantam_tools_dir=/file/tools",
"bantam_skills_dir=/file/skills", "bantam_skills_dir=/file/skills",
}, "\n")) }, "\n")), 0644); err != nil {
t.Fatal(err)
}
cfg := getCfg(p) cfg := getCfg(p)
if cfg.Endpoint != "http://file-endpoint/v1" { if cfg.Endpoint != "http://file-endpoint/v1" {
t.Errorf("endpoint: got %q, want file value", cfg.Endpoint) t.Errorf("endpoint: got %q, want file value", cfg.Endpoint)
@@ -512,6 +515,70 @@ func TestGetCfgEnvFallbackWhenNoFile(t *testing.T) {
} }
} }
func TestGetCfgModelCfgOverriddenByEnvWhenBantamCfgAbsent(t *testing.T) {
clearBantamEnv(t)
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
t.Setenv("BANTAM_MODEL", "env-model")
t.Setenv("BANTAM_TEMP", "0.9")
t.Setenv("BANTAM_TOOLS_DIR", "/env/tools")
t.Setenv("BANTAM_SKILLS_DIR", "/env/skills")
dir := t.TempDir()
modelCfg := filepath.Join(dir, "model.cfg")
if err := os.WriteFile(modelCfg, []byte("endpoint=http://file-endpoint/v1\nmodel=file-model\ntemperature=0.1\nbantam_tools_dir=/file/tools\nbantam_skills_dir=/file/skills\n"), 0644); err != nil {
t.Fatal(err)
}
// When .bantam.cfg is absent, model.cfg yields to existing BANTAM_* env vars:
cfg := getCfg(modelCfg)
if cfg.Endpoint != "http://env-endpoint/v1" {
t.Errorf("endpoint: got %q, want env value", cfg.Endpoint)
}
if cfg.Model != "env-model" {
t.Errorf("model: got %q, want env value", cfg.Model)
}
if cfg.Temperature != 0.9 {
t.Errorf("temperature: got %v, want env value", cfg.Temperature)
}
if toolsDir(&cfg) != "/env/tools" {
t.Errorf("toolsDir: got %q, want /env/tools", toolsDir(&cfg))
}
if skillsDir(&cfg) != "/env/skills" {
t.Errorf("skillsDir: got %q, want /env/skills", skillsDir(&cfg))
}
}
func TestGetCfgBantamCfgOverridesEnvAndModelCfg(t *testing.T) {
clearBantamEnv(t)
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
t.Setenv("BANTAM_MODEL", "env-model")
t.Setenv("BANTAM_TEMP", "0.9")
dir := t.TempDir()
modelCfg := filepath.Join(dir, "model.cfg")
if err := os.WriteFile(modelCfg, []byte("endpoint=http://model-endpoint/v1\nmodel=model-model\ncolor=never\n"), 0644); err != nil {
t.Fatal(err)
}
bantamCfg := filepath.Join(dir, ".bantam.cfg")
if err := os.WriteFile(bantamCfg, []byte("model=bantam-model\n"), 0644); err != nil {
t.Fatal(err)
}
cfg := getCfg(bantamCfg)
// bantam.cfg overrides env and model.cfg for model:
if cfg.Model != "bantam-model" {
t.Errorf("model: got %q, want bantam-model", cfg.Model)
}
// endpoint falls back to env:
if cfg.Endpoint != "http://env-endpoint/v1" {
t.Errorf("endpoint: got %q, want env-endpoint", cfg.Endpoint)
}
// color falls back to model.cfg since not in bantam.cfg or env:
if cfg.Color != "never" {
t.Errorf("color: got %q, want never from model.cfg", cfg.Color)
}
}
func TestAtoiD(t *testing.T) { func TestAtoiD(t *testing.T) {
cases := []struct { cases := []struct {
s string s string
+2 -4
View File
@@ -8,8 +8,7 @@ my $DEF_SP = "You are Bantam, a tiny, powerful AI agent. Solve the user's task u
my $SDIR = ($ENV{HOME} || $ENV{USERPROFILE} || '.') . '/.bantam/sessions'; my $SDIR = ($ENV{HOME} || $ENV{USERPROFILE} || '.') . '/.bantam/sessions';
sub cfg { sub cfg {
my %d = (endpoint=>'https://api.kilo.ai/api/openrouter', model=>'openrouter/free', temperature=>0.7, api_key=>'-', timeout=>300, shell_timeout=>120, max_al_iterations=>1000, reasoning_effort=>'high'); my %d = (endpoint=>'https://api.kilo.ai/api/openrouter', model=>'openrouter/free', temperature=>0.7, api_key=>'-', timeout=>300, shell_timeout=>120, max_al_iterations=>1000, reasoning_effort=>'high');
# Environment variable fallbacks are applied BEFORE the config file is read, if (-f 'model.cfg' && open my $mf, '<:encoding(UTF-8)', 'model.cfg') { while (<$mf>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } }
# so any value set in the config file overrides the environment variable.
$d{endpoint} = $ENV{BANTAM_ENDPOINT} if $ENV{BANTAM_ENDPOINT}; $d{endpoint} = $ENV{BANTAM_ENDPOINT} if $ENV{BANTAM_ENDPOINT};
$d{model} = $ENV{BANTAM_MODEL} if $ENV{BANTAM_MODEL}; $d{model} = $ENV{BANTAM_MODEL} if $ENV{BANTAM_MODEL};
my $t = $ENV{BANTAM_TEMP} // $ENV{BANTAM_TEMPERATURE}; my $t = $ENV{BANTAM_TEMP} // $ENV{BANTAM_TEMPERATURE};
@@ -18,8 +17,7 @@ sub cfg {
$d{shell_timeout} = $ENV{BANTAM_SHELL_TIMEOUT} if defined $ENV{BANTAM_SHELL_TIMEOUT} && $ENV{BANTAM_SHELL_TIMEOUT} =~ /^\d+$/; $d{shell_timeout} = $ENV{BANTAM_SHELL_TIMEOUT} if defined $ENV{BANTAM_SHELL_TIMEOUT} && $ENV{BANTAM_SHELL_TIMEOUT} =~ /^\d+$/;
$d{max_al_iterations} = $ENV{BANTAM_MAX_AL_ITERATIONS} if defined $ENV{BANTAM_MAX_AL_ITERATIONS} && $ENV{BANTAM_MAX_AL_ITERATIONS} =~ /^\d+$/; $d{max_al_iterations} = $ENV{BANTAM_MAX_AL_ITERATIONS} if defined $ENV{BANTAM_MAX_AL_ITERATIONS} && $ENV{BANTAM_MAX_AL_ITERATIONS} =~ /^\d+$/;
$d{reasoning_effort} = $ENV{BANTAM_REASONING_EFFORT} if $ENV{BANTAM_REASONING_EFFORT}; $d{reasoning_effort} = $ENV{BANTAM_REASONING_EFFORT} if $ENV{BANTAM_REASONING_EFFORT};
my $cf = -f '.bantam.cfg' ? '.bantam.cfg' : 'model.cfg'; if (-f '.bantam.cfg' && open my $bf, '<:encoding(UTF-8)', '.bantam.cfg') { while (<$bf>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } }
if (open my $f, '<:encoding(UTF-8)', $cf) { while (<$f>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } }
$d{api_key} = $ENV{BANTAM_API_KEY} if ($d{api_key} eq '-' || !$d{api_key}) && $ENV{BANTAM_API_KEY}; $d{api_key} = $ENV{BANTAM_API_KEY} if ($d{api_key} eq '-' || !$d{api_key}) && $ENV{BANTAM_API_KEY};
\%d } \%d }
sub filter_text { my $s = shift // ''; $s =~ s/[^\x20\t\n\p{L}\p{N}\p{P}\p{S}\p{M}\p{Zs}]//g; $s } sub filter_text { my $s = shift // ''; $s =~ s/[^\x20\t\n\p{L}\p{N}\p{P}\p{S}\p{M}\p{Zs}]//g; $s }