From 502a3f8c034890d4aad91018b3c139b83b445e30 Mon Sep 17 00:00:00 2001 From: Luxferre Date: Thu, 10 Sep 2026 11:49:54 +0300 Subject: [PATCH] env var updates --- README.md | 4 ++-- main.go | 31 ++++++++++++++++++++++++------- main_test.go | 44 ++++++++++++++++++++++++++++++++++++++++---- mb | 11 +++++++++-- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e569585..9de1b98 100644 --- a/README.md +++ b/README.md @@ -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`) - `model` (model name, default `openrouter/free`) - `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`) - `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) @@ -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 - Session management: `/save`, `/list`, `/load ` (exact id only, no prefix matching), `/cfg [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 `, `/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) ### What it drops diff --git a/main.go b/main.go index 275bb95..edd2134 100644 --- a/main.go +++ b/main.go @@ -210,6 +210,19 @@ func listModels(cfg *Cfg) (string, error) { func getCfg(path string) 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 + } + } 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, @@ -239,8 +252,10 @@ func getCfg(path string) Cfg { } } } - if (cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("OPENAI_API_KEY") != "" { - cfg.APIKey = os.Getenv("OPENAI_API_KEY") + // 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. + if (cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("BANTAM_API_KEY") != "" { + cfg.APIKey = os.Getenv("BANTAM_API_KEY") cfg.Raw["api_key"] = cfg.APIKey } return cfg @@ -298,22 +313,24 @@ When generating code: - Respect AGENTS.md contents in the project.` 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 } - if v := strings.TrimSpace(cfg.Raw["bantam_tools_dir"]); v != "" { + if v := strings.TrimSpace(os.Getenv("BANTAM_TOOLS_DIR")); v != "" { return v } return "" } // 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 { - if v := strings.TrimSpace(os.Getenv("BANTAM_SKILLS_DIR")); v != "" { + if v := strings.TrimSpace(cfg.Raw["bantam_skills_dir"]); 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 "" diff --git a/main_test.go b/main_test.go index 1638dca..0d4fcc6 100644 --- a/main_test.go +++ b/main_test.go @@ -263,7 +263,7 @@ func TestDefaultSystemPrompt(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")) if cfg.Endpoint != defCfg.Endpoint || cfg.Model != defCfg.Model || cfg.APIKey != defCfg.APIKey { t.Errorf("defaults mismatch: %+v", cfg) @@ -359,7 +359,7 @@ func TestGetCfgStreamTruthyVariants(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()) // 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) } // 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 != "-" { 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) { cases := []struct { s string @@ -2538,7 +2574,7 @@ func TestSkills(t *testing.T) { 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) if got := skillsDir(&Cfg{}); got != skillDir { t.Errorf("skillsDir() with env = %q, want %q", got, skillDir) diff --git a/mb b/mb index fe5366d..f78485d 100755 --- a/mb +++ b/mb @@ -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 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'; -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'; 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 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') {