env var updates
This commit is contained in:
@@ -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 <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
|
||||
- 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
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
+40
-4
@@ -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)
|
||||
|
||||
@@ -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') {
|
||||
|
||||
Reference in New Issue
Block a user