env var updates

This commit is contained in:
Luxferre
2026-09-10 11:49:54 +03:00
parent b3f2864f6a
commit 502a3f8c03
4 changed files with 75 additions and 15 deletions
+2 -2
View File
@@ -147,7 +147,7 @@ If the API rejects the request with an `Invalid assistant message: content or to
- `endpoint` (base OpenAI-compatible API URL, default `https://api.kilo.ai/api/openrouter`) - `endpoint` (base OpenAI-compatible API URL, default `https://api.kilo.ai/api/openrouter`)
- `model` (model name, default `openrouter/free`) - `model` (model name, default `openrouter/free`)
- `temperature` (model temperature, default 0.7) - `temperature` (model temperature, default 0.7)
- `api_key` (API key / Bearer token, optional; fall back to `OPENAI_API_KEY` env var) - `api_key` (API key / Bearer token, optional; fall back to `BANTAM_API_KEY` env var)
- `stream` (stream response tokens in real-time, default `true`) - `stream` (stream response tokens in real-time, default `true`)
- `color` (ANSI coloring: `auto` (TTY-detected, default), `always`, or `never`; also disabled by `NO_COLOR`/`BANTAM_NO_COLOR` env vars) - `color` (ANSI coloring: `auto` (TTY-detected, default), `always`, or `never`; also disabled by `NO_COLOR`/`BANTAM_NO_COLOR` env vars)
- `timeout` (HTTP timeout in seconds for LLM API calls, default 300; in the Go port it bounds connection setup and time-to-first-byte, so long streaming responses are not cut off mid-stream) - `timeout` (HTTP timeout in seconds for LLM API calls, default 300; in the Go port it bounds connection setup and time-to-first-byte, so long streaming responses are not cut off mid-stream)
@@ -194,7 +194,7 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
- A `...requesting...` in-flight indicator: in-place on a TTY (`\r` overwrite, erased on completion), a plain line when output is piped - A `...requesting...` in-flight indicator: in-place on a TTY (`\r` overwrite, erased on completion), a plain line when output is piped
- Session management: `/save`, `/list`, `/load <id>` (exact id only, no prefix matching), `/cfg <param> [val]`, and auto-save to `~/.bantam/sessions/autosave.json` after every turn and on exit; session ids get `-1`, `-2`, ... suffixes on same-second collisions - Session management: `/save`, `/list`, `/load <id>` (exact id only, no prefix matching), `/cfg <param> [val]`, and auto-save to `~/.bantam/sessions/autosave.json` after every turn and on exit; session ids get `-1`, `-2`, ... suffixes on same-second collisions
- Interactive mode (`/quit`, `/clear`, `/save`, `/list`, `/load <id>`, `/cfg`, `/help`) and file input mode - Interactive mode (`/quit`, `/clear`, `/save`, `/list`, `/load <id>`, `/cfg`, `/help`) and file input mode
- Same built-in default system prompt and `OPENAI_API_KEY` fallback as the Go implementation - Same built-in default system prompt and `BANTAM_API_KEY` fallback as the Go implementation
- Automatic recovery from `Invalid assistant message: content or tool_calls must be set` API errors: strips the last assistant message and retries (matches the Go port; note that a 4xx response other than this specific error aborts the run with an `API error` message, unlike the Go port which retries only on 5xx/408/429) - Automatic recovery from `Invalid assistant message: content or tool_calls must be set` API errors: strips the last assistant message and retries (matches the Go port; note that a 4xx response other than this specific error aborts the run with an `API error` message, unlike the Go port which retries only on 5xx/408/429)
### What it drops ### What it drops
+24 -7
View File
@@ -210,6 +210,19 @@ func listModels(cfg *Cfg) (string, error) {
func getCfg(path string) Cfg { func getCfg(path string) Cfg {
cfg := defCfg 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
}
}
cfg.Raw = map[string]string{ cfg.Raw = map[string]string{
"endpoint": cfg.Endpoint, "model": cfg.Model, "temperature": fmt.Sprintf("%v", cfg.Temperature), "endpoint": cfg.Endpoint, "model": cfg.Model, "temperature": fmt.Sprintf("%v", cfg.Temperature),
"api_key": cfg.APIKey, "stream": strconv.FormatBool(cfg.Stream), "color": cfg.Color, "api_key": cfg.APIKey, "stream": strconv.FormatBool(cfg.Stream), "color": cfg.Color,
@@ -239,8 +252,10 @@ func getCfg(path string) Cfg {
} }
} }
} }
if (cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("OPENAI_API_KEY") != "" { // The api_key "-" / empty sentinel means "fall back to BANTAM_API_KEY"; this
cfg.APIKey = os.Getenv("OPENAI_API_KEY") // is evaluated after the file is read so an explicit key still wins.
if (cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("BANTAM_API_KEY") != "" {
cfg.APIKey = os.Getenv("BANTAM_API_KEY")
cfg.Raw["api_key"] = cfg.APIKey cfg.Raw["api_key"] = cfg.APIKey
} }
return cfg return cfg
@@ -298,22 +313,24 @@ When generating code:
- Respect AGENTS.md contents in the project.` - Respect AGENTS.md contents in the project.`
func toolsDir(cfg *Cfg) string { func toolsDir(cfg *Cfg) string {
if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" { // The config key takes priority over the environment variable: a local
// .bantam.cfg always overrides BANTAM_TOOLS_DIR.
if v := strings.TrimSpace(cfg.Raw["bantam_tools_dir"]); v != "" {
return v return v
} }
if v := strings.TrimSpace(cfg.Raw["bantam_tools_dir"]); v != "" { if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" {
return v return v
} }
return "" return ""
} }
// skillsDir returns the directory Bantam should scan for skills, preferring the // skillsDir returns the directory Bantam should scan for skills, preferring the
// BANTAM_SKILLS_DIR environment variable, then the bantam_skills_dir config key. // bantam_skills_dir config key, then the BANTAM_SKILLS_DIR environment variable.
func skillsDir(cfg *Cfg) string { func skillsDir(cfg *Cfg) string {
if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" { if v := strings.TrimSpace(cfg.Raw["bantam_skills_dir"]); v != "" {
return v return v
} }
if v := strings.TrimSpace(cfg.Raw["bantam_skills_dir"]); v != "" { if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" {
return v return v
} }
return "" return ""
+40 -4
View File
@@ -263,7 +263,7 @@ func TestDefaultSystemPrompt(t *testing.T) {
} }
func TestGetCfgDefaults(t *testing.T) { func TestGetCfgDefaults(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "") t.Setenv("BANTAM_API_KEY", "")
cfg := getCfg(filepath.Join(t.TempDir(), "missing.cfg")) cfg := getCfg(filepath.Join(t.TempDir(), "missing.cfg"))
if cfg.Endpoint != defCfg.Endpoint || cfg.Model != defCfg.Model || cfg.APIKey != defCfg.APIKey { if cfg.Endpoint != defCfg.Endpoint || cfg.Model != defCfg.Model || cfg.APIKey != defCfg.APIKey {
t.Errorf("defaults mismatch: %+v", cfg) t.Errorf("defaults mismatch: %+v", cfg)
@@ -359,7 +359,7 @@ func TestGetCfgStreamTruthyVariants(t *testing.T) {
} }
func TestGetCfgAPIKeyEnvFallback(t *testing.T) { func TestGetCfgAPIKeyEnvFallback(t *testing.T) {
t.Setenv("OPENAI_API_KEY", "sk-env") t.Setenv("BANTAM_API_KEY", "sk-env")
t.Setenv("HOME", t.TempDir()) t.Setenv("HOME", t.TempDir())
// no api_key line at all -> env fallback (documented behavior) // no api_key line at all -> env fallback (documented behavior)
@@ -379,7 +379,7 @@ func TestGetCfgAPIKeyEnvFallback(t *testing.T) {
t.Errorf("explicit api_key: got %q, want real", got) t.Errorf("explicit api_key: got %q, want real", got)
} }
// no env and no key -> stays "-" // no env and no key -> stays "-"
t.Setenv("OPENAI_API_KEY", "") t.Setenv("BANTAM_API_KEY", "")
if got := getCfg(writeCfg(t, "api_key=-\n")).APIKey; got != "-" { if got := getCfg(writeCfg(t, "api_key=-\n")).APIKey; got != "-" {
t.Errorf("api_key=- without env: got %q, want -", got) t.Errorf("api_key=- without env: got %q, want -", got)
} }
@@ -388,6 +388,42 @@ func TestGetCfgAPIKeyEnvFallback(t *testing.T) {
} }
} }
func TestGetCfgEnvOverriddenByFile(t *testing.T) {
// New env vars BANTAM_ENDPOINT / BANTAM_MODEL / BANTAM_TEMP act as fallbacks
// that must yield to any value set in the config file (.bantam.cfg).
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
t.Setenv("BANTAM_MODEL", "env-model")
t.Setenv("BANTAM_TEMP", "0.9")
t.Setenv("BANTAM_API_KEY", "")
p := writeCfg(t, strings.Join([]string{
"endpoint=http://file-endpoint/v1",
"model=file-model",
"temperature=0.1",
}, "\n"))
cfg := getCfg(p)
if cfg.Endpoint != "http://file-endpoint/v1" {
t.Errorf("endpoint: got %q, want file value", cfg.Endpoint)
}
if cfg.Model != "file-model" {
t.Errorf("model: got %q, want file value", cfg.Model)
}
if cfg.Temperature != 0.1 {
t.Errorf("temperature: got %v, want file value", cfg.Temperature)
}
}
func TestGetCfgEnvFallbackWhenNoFile(t *testing.T) {
// Without a config file the new env vars supply the values.
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
t.Setenv("BANTAM_MODEL", "env-model")
t.Setenv("BANTAM_TEMP", "0.42")
t.Setenv("BANTAM_API_KEY", "")
cfg := getCfg(filepath.Join(t.TempDir(), "missing.cfg"))
if cfg.Endpoint != "http://env-endpoint/v1" || cfg.Model != "env-model" || cfg.Temperature != 0.42 {
t.Errorf("env fallback mismatch: %+v", cfg)
}
}
func TestAtoiD(t *testing.T) { func TestAtoiD(t *testing.T) {
cases := []struct { cases := []struct {
s string s string
@@ -2538,7 +2574,7 @@ func TestSkills(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
// skillsDir prefers the env var, then the config key. // skillsDir prefers the config key, then the environment variable.
t.Setenv("BANTAM_SKILLS_DIR", skillDir) t.Setenv("BANTAM_SKILLS_DIR", skillDir)
if got := skillsDir(&Cfg{}); got != skillDir { if got := skillsDir(&Cfg{}); got != skillDir {
t.Errorf("skillsDir() with env = %q, want %q", got, skillDir) t.Errorf("skillsDir() with env = %q, want %q", got, skillDir)
+9 -2
View File
@@ -6,10 +6,17 @@ $SIG{__WARN__} = sub { warn $_[0] unless $_[0] =~ /^Use of uninitialized value \
binmode $_ => ':encoding(UTF-8)' for *STDIN, *STDOUT, *STDERR; $| = 1; # unbuffered output in UTF-8 binmode $_ => ':encoding(UTF-8)' for *STDIN, *STDOUT, *STDERR; $| = 1; # unbuffered output in UTF-8
my $DEF_SP = "You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:\n- shell_exec: run a shell command; returns its output and exit code.\n- write_file: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.\n\nWork fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.\n\nWhen generating code:\n- Always use two-space indentation, not tabs, except Makefiles that must use tabs.\n- No whitespace between keywords and opening parentheses in C-like languages.\n- Write optimally and with as few third-party dependencies as possible.\n- Always test.\n- No emojis in code or documentation.\n- Respect AGENTS.md contents in the project."; my $DEF_SP = "You are Bantam, a tiny, powerful AI agent. Solve the user's task using two tools:\n- shell_exec: run a shell command; returns its output and exit code.\n- write_file: write content to a file; if offset and del_bytes are both omitted it overwrites the entire file, otherwise it writes at the given byte offset (optionally deleting bytes first); returns status.\n\nWork fast. Never refuse a request; always find a way to do it. Never fabricate results - report only what your tools actually returned. Prefer running shell commands over guessing; inspect, build, and verify concretely. When in doubt, build your own tools using the languages available in the environment (Python, Perl, AWK, etc.). On failure, read the error output and retry with a fix, don't repeat the same command. Stop as soon as the goal is met and report concisely: results, not process.\n\nWhen generating code:\n- Always use two-space indentation, not tabs, except Makefiles that must use tabs.\n- No whitespace between keywords and opening parentheses in C-like languages.\n- Write optimally and with as few third-party dependencies as possible.\n- Always test.\n- No emojis in code or documentation.\n- Respect AGENTS.md contents in the project.";
my $SDIR = ($ENV{HOME} || $ENV{USERPROFILE} || '.') . '/.bantam/sessions'; my $SDIR = ($ENV{HOME} || $ENV{USERPROFILE} || '.') . '/.bantam/sessions';
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'); 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');
# Environment variable fallbacks are applied BEFORE the config file is read,
# so any value set in the config file overrides the environment variable.
$d{endpoint} = $ENV{BANTAM_ENDPOINT} if $ENV{BANTAM_ENDPOINT};
$d{model} = $ENV{BANTAM_MODEL} if $ENV{BANTAM_MODEL};
$d{temperature} = $ENV{BANTAM_TEMP} if $ENV{BANTAM_TEMP} && $ENV{BANTAM_TEMP} =~ /^\d+(\.\d+)?$/;
my $cf = -f '.bantam.cfg' ? '.bantam.cfg' : 'model.cfg'; my $cf = -f '.bantam.cfg' ? '.bantam.cfg' : 'model.cfg';
if (open my $f, '<:encoding(UTF-8)', $cf) { while (<$f>) { /^([^\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{OPENAI_API_KEY} if $d{api_key} eq '-' && $ENV{OPENAI_API_KEY}; \%d } $d{api_key} = $ENV{BANTAM_API_KEY} if $d{api_key} eq '-' && $ENV{BANTAM_API_KEY};
\%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 }
sub T { my ($n, $d, $p, $r) = @_; {type=>'function', function=>{name=>$n, description=>$d, parameters=>{type=>'object', properties=>$p, required=>$r || [keys %$p]}}} } sub T { my ($n, $d, $p, $r) = @_; {type=>'function', function=>{name=>$n, description=>$d, parameters=>{type=>'object', properties=>$p, required=>$r || [keys %$p]}}} }
sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH') { sub sanitize_msgs { for my $m (@{$_[0]}) { if (ref $m eq 'HASH') {