Compare commits
2
Commits
502a3f8c03
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9c7bb4407 | ||
|
|
b0a16e99e1 |
@@ -142,22 +142,22 @@ 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)
|
(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`)
|
- `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`)
|
- `model` (model name, default `openrouter/free`; falls back to `BANTAM_MODEL` env var)
|
||||||
- `temperature` (model temperature, default 0.7)
|
- `temperature` (model temperature, default 0.7; falls back to `BANTAM_TEMP` or `BANTAM_TEMPERATURE` env var)
|
||||||
- `api_key` (API key / Bearer token, optional; fall back to `BANTAM_API_KEY` env var)
|
- `api_key` (API key / Bearer token, optional; falls back to `BANTAM_API_KEY` env var; set to `-` or left blank in config file to explicitly defer to `BANTAM_API_KEY`)
|
||||||
- `stream` (stream response tokens in real-time, default `true`)
|
- `stream` (stream response tokens in real-time, default `true`; falls back to `BANTAM_STREAM` env var)
|
||||||
- `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`; disabled by `NO_COLOR`/`BANTAM_NO_COLOR` env vars, falls back to `BANTAM_COLOR` env var)
|
||||||
- `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; falls back to `BANTAM_TIMEOUT` env var)
|
||||||
- `shell_timeout` (timeout in seconds for `shell_exec` commands, default 120)
|
- `shell_timeout` (timeout in seconds for `shell_exec` commands, default 120; falls back to `BANTAM_SHELL_TIMEOUT` env var)
|
||||||
- `max_al_iterations` (max tool-call loop iterations per `AL()` invocation, default 1000)
|
- `max_al_iterations` (max tool-call loop iterations per `AL()` invocation, default 1000; falls back to `BANTAM_MAX_AL_ITERATIONS` env var)
|
||||||
- `context_window` (context window size in tokens, auto-discovered from `/models` API if available, fallback to this setting, default 200000)
|
- `context_window` (context window size in tokens, auto-discovered from `/models` API if available, fallback to this setting, default 200000; falls back to `BANTAM_CONTEXT_WINDOW` env var)
|
||||||
- `reasoning_effort` (reasoning effort level, forwarded to chat completions API, default `high`)
|
- `reasoning_effort` (reasoning effort level, forwarded to chat completions API, default `high`; falls back to `BANTAM_REASONING_EFFORT` env var)
|
||||||
|
|
||||||
- `bantam_tools_dir` (optional path to a directory of extra shell tools; the Go port appends `"Extra shell tools can be found at <dir>"` to the system prompt at startup when set. The `BANTAM_TOOLS_DIR` environment variable overrides this and is checked first; if neither is set, nothing is appended. Note: this is an agent-internal hint, not forwarded to the API.)
|
- `bantam_tools_dir` (optional path to a directory of extra shell tools; the Go port appends `"Extra shell tools can be found at <dir>"` to the system prompt at startup when set. When unset in the config file, it falls back to the `BANTAM_TOOLS_DIR` environment variable; if neither is set, nothing is appended. Note: this is an agent-internal hint, not forwarded to the API.)
|
||||||
- `bantam_skills_dir` (optional path to a directory of reusable *skills*; each skill lives in its own subdirectory as `<skill_name>/SKILL.md`. When set, the Go port appends `"Skills may be discovered and invoked from <dir>"` to the system prompt at startup, and the `/skill <skill_name> [prompt]` command loads a skill's `SKILL.md` and runs it as a prompt. The `BANTAM_SKILLS_DIR` environment variable overrides this and is checked first; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead. Note: this is an agent-internal hint, not forwarded to the API.)
|
- `bantam_skills_dir` (optional path to a directory of reusable *skills*; each skill lives in its own subdirectory as `<skill_name>/SKILL.md`. When set, the Go port appends `"Skills may be discovered and invoked from <dir>"` to the system prompt at startup, and the `/skill <skill_name> [prompt]` command loads a skill's `SKILL.md` and runs it as a prompt. When unset in the config file, it falls back to the `BANTAM_SKILLS_DIR` environment variable; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead. Note: this is an agent-internal hint, not forwarded to the API.)
|
||||||
|
|
||||||
The Go port also supports SOCKS5 proxying via the `SOCKS_PROXY` (or `socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080` or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
|
The Go port also supports SOCKS5 proxying via the `SOCKS_PROXY` (or `socks_proxy`) environment variable (e.g. `SOCKS_PROXY=socks5://127.0.0.1:1080` or `SOCKS_PROXY=127.0.0.1:1080`), falling back to standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
|
||||||
|
|
||||||
@@ -223,11 +223,11 @@ MicroBantam (`mb`) is a compressed Perl 5 reference implementation of the same a
|
|||||||
|
|
||||||
The `extras/` directory contains small, dependency-light shell scripts that extend Bantam without changing its core. Because Bantam's built-in tools are `shell_exec` and `write_file`, these helpers can be invoked directly by the agent through `shell_exec` to give it real-world capabilities (web search, live weather) that the base model alone does not have. They are plain `/bin/sh` scripts depending only on `curl` (and `jq` where noted), so the agent can discover and run them just like any other command.
|
The `extras/` directory contains small, dependency-light shell scripts that extend Bantam without changing its core. Because Bantam's built-in tools are `shell_exec` and `write_file`, these helpers can be invoked directly by the agent through `shell_exec` to give it real-world capabilities (web search, live weather) that the base model alone does not have. They are plain `/bin/sh` scripts depending only on `curl` (and `jq` where noted), so the agent can discover and run them just like any other command.
|
||||||
|
|
||||||
If you keep your own collection of helper scripts, point Bantam at them with the `BANTAM_TOOLS_DIR` environment variable or the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`). When either is defined (environment variable taking precedence over the config key), the Go port appends the line `Extra shell tools can be found at <dir>` to the system prompt at startup, so the agent is aware of where to look for them. The `extras/` scripts shipped here are just examples of what such a directory can contain.
|
If you keep your own collection of helper scripts, point Bantam at them with the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_TOOLS_DIR` environment variable. When either is defined (the config file setting taking precedence over the environment variable), the Go port appends the line `Extra shell tools can be found at <dir>` to the system prompt at startup, so the agent is aware of where to look for them. The `extras/` scripts shipped here are just examples of what such a directory can contain.
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
A *skill* is a reusable, self-contained instruction bundle the agent can load on demand. Each skill is a directory containing a `SKILL.md` file with the skill's prompt/instructions. Point Bantam at a skills directory with the `BANTAM_SKILLS_DIR` environment variable or the `bantam_skills_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`); the environment variable takes precedence. When either is set, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and the agent can run a skill with:
|
A *skill* is a reusable, self-contained instruction bundle the agent can load on demand. Each skill is a directory containing a `SKILL.md` file with the skill's prompt/instructions. Point Bantam at a skills directory with the `bantam_skills_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_SKILLS_DIR` environment variable; the config file setting takes precedence. When either is set, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and the agent can run a skill with:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/skill # list every skill under the configured skills directory
|
/skill # list every skill under the configured skills directory
|
||||||
@@ -320,11 +320,11 @@ The default system prompt instructs the agent to respect `AGENTS.md` contents in
|
|||||||
|
|
||||||
### How do I tell Bantam about extra shell tools?
|
### How do I tell Bantam about extra shell tools?
|
||||||
|
|
||||||
Set the `BANTAM_TOOLS_DIR` environment variable (or the `bantam_tools_dir` key in the config file — `.bantam.cfg` if present, else `model.cfg`) to a directory containing your helper scripts. When defined, the Go port appends `Extra shell tools can be found at <dir>` to the system prompt at startup, making the agent aware of them. The environment variable takes precedence over the config key; if neither is set, nothing is appended.
|
Set the `bantam_tools_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_TOOLS_DIR` environment variable to a directory containing your helper scripts. When defined, the Go port appends `Extra shell tools can be found at <dir>` to the system prompt at startup, making the agent aware of them. The config file key takes precedence over the environment variable; if neither is set, nothing is appended.
|
||||||
|
|
||||||
### How do I give Bantam reusable skills?
|
### How do I give Bantam reusable skills?
|
||||||
|
|
||||||
Set the `BANTAM_SKILLS_DIR` environment variable (or the `bantam_skills_dir` key in the config file — `.bantam.cfg` if present, else `model.cfg`) to a directory where each subdirectory is a skill containing a `SKILL.md` file. When defined, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and you (or the agent) can run a skill with `/skill <name> [prompt]`. The environment variable takes precedence over the config key; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead.
|
Set the `bantam_skills_dir` key in the config file (`.bantam.cfg` if present, else `model.cfg`) or the `BANTAM_SKILLS_DIR` environment variable to a directory where each subdirectory is a skill containing a `SKILL.md` file. When defined, the Go port appends `Skills may be discovered and invoked from <dir>` to the system prompt at startup, and you (or the agent) can run a skill with `/skill <name> [prompt]`. The config file key takes precedence over the environment variable; if neither is set, `/skill` accepts an absolute path to a skill directory or `SKILL.md` file instead.
|
||||||
|
|
||||||
### Is there any common config place for Bantam?
|
### Is there any common config place for Bantam?
|
||||||
|
|
||||||
|
|||||||
@@ -208,29 +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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 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)
|
||||||
@@ -252,6 +230,91 @@ 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.
|
||||||
if(cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("BANTAM_API_KEY") != "" {
|
if(cfg.APIKey == "" || cfg.APIKey == "-") && os.Getenv("BANTAM_API_KEY") != "" {
|
||||||
|
|||||||
+189
-10
@@ -262,8 +262,20 @@ func TestDefaultSystemPrompt(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearBantamEnv(t *testing.T) {
|
||||||
|
for _, k := range []string{
|
||||||
|
"BANTAM_ENDPOINT", "BANTAM_MODEL", "BANTAM_TEMP", "BANTAM_TEMPERATURE",
|
||||||
|
"BANTAM_API_KEY", "BANTAM_STREAM", "BANTAM_COLOR", "BANTAM_NO_COLOR",
|
||||||
|
"BANTAM_TIMEOUT", "BANTAM_SHELL_TIMEOUT", "BANTAM_MAX_AL_ITERATIONS",
|
||||||
|
"BANTAM_CONTEXT_WINDOW", "BANTAM_REASONING_EFFORT", "BANTAM_TOOLS_DIR",
|
||||||
|
"BANTAM_SKILLS_DIR",
|
||||||
|
} {
|
||||||
|
t.Setenv(k, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGetCfgDefaults(t *testing.T) {
|
func TestGetCfgDefaults(t *testing.T) {
|
||||||
t.Setenv("BANTAM_API_KEY", "")
|
clearBantamEnv(t)
|
||||||
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)
|
||||||
@@ -280,6 +292,7 @@ func TestGetCfgDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCfgParsesFile(t *testing.T) {
|
func TestGetCfgParsesFile(t *testing.T) {
|
||||||
|
clearBantamEnv(t)
|
||||||
p := writeCfg(t, strings.Join([]string{
|
p := writeCfg(t, strings.Join([]string{
|
||||||
"endpoint=http://localhost:9999/v1",
|
"endpoint=http://localhost:9999/v1",
|
||||||
"model=test-model",
|
"model=test-model",
|
||||||
@@ -317,6 +330,7 @@ func TestGetCfgParsesFile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCfgIgnoresCommentsBlankAndInvalid(t *testing.T) {
|
func TestGetCfgIgnoresCommentsBlankAndInvalid(t *testing.T) {
|
||||||
|
clearBantamEnv(t)
|
||||||
p := writeCfg(t, strings.Join([]string{
|
p := writeCfg(t, strings.Join([]string{
|
||||||
"# comment",
|
"# comment",
|
||||||
"",
|
"",
|
||||||
@@ -345,6 +359,7 @@ func TestGetCfgIgnoresCommentsBlankAndInvalid(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCfgStreamTruthyVariants(t *testing.T) {
|
func TestGetCfgStreamTruthyVariants(t *testing.T) {
|
||||||
|
clearBantamEnv(t)
|
||||||
for _, tc := range []struct{ v, want string }{
|
for _, tc := range []struct{ v, want string }{
|
||||||
{"true", "true"}, {"1", "true"}, {"yes", "true"},
|
{"true", "true"}, {"1", "true"}, {"yes", "true"},
|
||||||
{"false", "false"}, {"TRUE", "false"}, {"0", "false"},
|
{"false", "false"}, {"TRUE", "false"}, {"0", "false"},
|
||||||
@@ -359,6 +374,7 @@ func TestGetCfgStreamTruthyVariants(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCfgAPIKeyEnvFallback(t *testing.T) {
|
func TestGetCfgAPIKeyEnvFallback(t *testing.T) {
|
||||||
|
clearBantamEnv(t)
|
||||||
t.Setenv("BANTAM_API_KEY", "sk-env")
|
t.Setenv("BANTAM_API_KEY", "sk-env")
|
||||||
t.Setenv("HOME", t.TempDir())
|
t.Setenv("HOME", t.TempDir())
|
||||||
|
|
||||||
@@ -389,17 +405,38 @@ func TestGetCfgAPIKeyEnvFallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCfgEnvOverriddenByFile(t *testing.T) {
|
func TestGetCfgEnvOverriddenByFile(t *testing.T) {
|
||||||
// New env vars BANTAM_ENDPOINT / BANTAM_MODEL / BANTAM_TEMP act as fallbacks
|
// File values override environment variables.
|
||||||
// that must yield to any value set in the config file (.bantam.cfg).
|
clearBantamEnv(t)
|
||||||
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
|
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
|
||||||
t.Setenv("BANTAM_MODEL", "env-model")
|
t.Setenv("BANTAM_MODEL", "env-model")
|
||||||
t.Setenv("BANTAM_TEMP", "0.9")
|
t.Setenv("BANTAM_TEMP", "0.9")
|
||||||
t.Setenv("BANTAM_API_KEY", "")
|
t.Setenv("BANTAM_STREAM", "false")
|
||||||
p := writeCfg(t, strings.Join([]string{
|
t.Setenv("BANTAM_COLOR", "never")
|
||||||
|
t.Setenv("BANTAM_TIMEOUT", "45")
|
||||||
|
t.Setenv("BANTAM_SHELL_TIMEOUT", "15")
|
||||||
|
t.Setenv("BANTAM_MAX_AL_ITERATIONS", "50")
|
||||||
|
t.Setenv("BANTAM_CONTEXT_WINDOW", "100000")
|
||||||
|
t.Setenv("BANTAM_REASONING_EFFORT", "low")
|
||||||
|
t.Setenv("BANTAM_TOOLS_DIR", "/env/tools")
|
||||||
|
t.Setenv("BANTAM_SKILLS_DIR", "/env/skills")
|
||||||
|
|
||||||
|
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",
|
||||||
}, "\n"))
|
"stream=true",
|
||||||
|
"color=always",
|
||||||
|
"timeout=300",
|
||||||
|
"shell_timeout=120",
|
||||||
|
"max_al_iterations=1000",
|
||||||
|
"context_window=200000",
|
||||||
|
"reasoning_effort=high",
|
||||||
|
"bantam_tools_dir=/file/tools",
|
||||||
|
"bantam_skills_dir=/file/skills",
|
||||||
|
}, "\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)
|
||||||
@@ -410,18 +447,136 @@ func TestGetCfgEnvOverriddenByFile(t *testing.T) {
|
|||||||
if cfg.Temperature != 0.1 {
|
if cfg.Temperature != 0.1 {
|
||||||
t.Errorf("temperature: got %v, want file value", cfg.Temperature)
|
t.Errorf("temperature: got %v, want file value", cfg.Temperature)
|
||||||
}
|
}
|
||||||
|
if !cfg.Stream {
|
||||||
|
t.Errorf("stream: got %v, want true from file", cfg.Stream)
|
||||||
|
}
|
||||||
|
if cfg.Color != "always" {
|
||||||
|
t.Errorf("color: got %q, want always from file", cfg.Color)
|
||||||
|
}
|
||||||
|
if cfg.Timeout != 300 {
|
||||||
|
t.Errorf("timeout: got %d, want 300 from file", cfg.Timeout)
|
||||||
|
}
|
||||||
|
if cfg.ShellTimeout != 120 {
|
||||||
|
t.Errorf("shell_timeout: got %d, want 120 from file", cfg.ShellTimeout)
|
||||||
|
}
|
||||||
|
if cfg.MaxALIterations != 1000 {
|
||||||
|
t.Errorf("max_al_iterations: got %d, want 1000 from file", cfg.MaxALIterations)
|
||||||
|
}
|
||||||
|
if cfg.ContextWindow != 200000 {
|
||||||
|
t.Errorf("context_window: got %d, want 200000 from file", cfg.ContextWindow)
|
||||||
|
}
|
||||||
|
if cfg.Raw["reasoning_effort"] != "high" {
|
||||||
|
t.Errorf("reasoning_effort: got %q, want high from file", cfg.Raw["reasoning_effort"])
|
||||||
|
}
|
||||||
|
if toolsDir(&cfg) != "/file/tools" {
|
||||||
|
t.Errorf("toolsDir: got %q, want /file/tools", toolsDir(&cfg))
|
||||||
|
}
|
||||||
|
if skillsDir(&cfg) != "/file/skills" {
|
||||||
|
t.Errorf("skillsDir: got %q, want /file/skills", skillsDir(&cfg))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCfgEnvFallbackWhenNoFile(t *testing.T) {
|
func TestGetCfgEnvFallbackWhenNoFile(t *testing.T) {
|
||||||
// Without a config file the new env vars supply the values.
|
clearBantamEnv(t)
|
||||||
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
|
t.Setenv("BANTAM_ENDPOINT", "http://env-endpoint/v1")
|
||||||
t.Setenv("BANTAM_MODEL", "env-model")
|
t.Setenv("BANTAM_MODEL", "env-model")
|
||||||
t.Setenv("BANTAM_TEMP", "0.42")
|
t.Setenv("BANTAM_TEMP", "0.42")
|
||||||
|
t.Setenv("BANTAM_STREAM", "false")
|
||||||
|
t.Setenv("BANTAM_COLOR", "never")
|
||||||
|
t.Setenv("BANTAM_TIMEOUT", "45")
|
||||||
|
t.Setenv("BANTAM_SHELL_TIMEOUT", "15")
|
||||||
|
t.Setenv("BANTAM_MAX_AL_ITERATIONS", "50")
|
||||||
|
t.Setenv("BANTAM_CONTEXT_WINDOW", "100000")
|
||||||
|
t.Setenv("BANTAM_REASONING_EFFORT", "low")
|
||||||
|
t.Setenv("BANTAM_TOOLS_DIR", "/env/tools")
|
||||||
|
t.Setenv("BANTAM_SKILLS_DIR", "/env/skills")
|
||||||
t.Setenv("BANTAM_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 != "http://env-endpoint/v1" || cfg.Model != "env-model" || cfg.Temperature != 0.42 {
|
if cfg.Endpoint != "http://env-endpoint/v1" || cfg.Model != "env-model" || cfg.Temperature != 0.42 {
|
||||||
t.Errorf("env fallback mismatch: %+v", cfg)
|
t.Errorf("env fallback mismatch: %+v", cfg)
|
||||||
}
|
}
|
||||||
|
if cfg.Stream {
|
||||||
|
t.Errorf("stream: got %v, want false from env", cfg.Stream)
|
||||||
|
}
|
||||||
|
if cfg.Color != "never" {
|
||||||
|
t.Errorf("color: got %q, want never from env", cfg.Color)
|
||||||
|
}
|
||||||
|
if cfg.Timeout != 45 || cfg.ShellTimeout != 15 || cfg.MaxALIterations != 50 || cfg.ContextWindow != 100000 {
|
||||||
|
t.Errorf("numeric env fallback mismatch: %+v", cfg)
|
||||||
|
}
|
||||||
|
if cfg.Raw["reasoning_effort"] != "low" {
|
||||||
|
t.Errorf("reasoning_effort: got %q, want low from env", cfg.Raw["reasoning_effort"])
|
||||||
|
}
|
||||||
|
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 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) {
|
||||||
@@ -1446,6 +1601,7 @@ func TestCompactHappyPath(t *testing.T) {
|
|||||||
// ---------- setCfg and LLM parameter forwarding ----------
|
// ---------- setCfg and LLM parameter forwarding ----------
|
||||||
|
|
||||||
func TestSetCfgUpdatesAndAppends(t *testing.T) {
|
func TestSetCfgUpdatesAndAppends(t *testing.T) {
|
||||||
|
clearBantamEnv(t)
|
||||||
p := filepath.Join(t.TempDir(), "model.cfg")
|
p := filepath.Join(t.TempDir(), "model.cfg")
|
||||||
if err := os.WriteFile(p, []byte("model=old-model\ntemperature=0.5\n"), 0644); err != nil {
|
if err := os.WriteFile(p, []byte("model=old-model\ntemperature=0.5\n"), 0644); err != nil {
|
||||||
t.Fatalf("WriteFile: %v", err)
|
t.Fatalf("WriteFile: %v", err)
|
||||||
@@ -1471,6 +1627,7 @@ func TestSetCfgUpdatesAndAppends(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLLMForwardsRelevantParameters(t *testing.T) {
|
func TestLLMForwardsRelevantParameters(t *testing.T) {
|
||||||
|
clearBantamEnv(t)
|
||||||
var received map[string]any
|
var received map[string]any
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
json.NewDecoder(r.Body).Decode(&received)
|
json.NewDecoder(r.Body).Decode(&received)
|
||||||
@@ -2575,9 +2732,12 @@ func TestSkills(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// skillsDir prefers the config key, then the environment variable.
|
// skillsDir prefers the config key, then the environment variable.
|
||||||
t.Setenv("BANTAM_SKILLS_DIR", skillDir)
|
t.Setenv("BANTAM_SKILLS_DIR", "/env/skills")
|
||||||
if got := skillsDir(&Cfg{}); got != skillDir {
|
if got := skillsDir(&Cfg{Raw: map[string]string{"bantam_skills_dir": skillDir}}); got != skillDir {
|
||||||
t.Errorf("skillsDir() with env = %q, want %q", got, skillDir)
|
t.Errorf("skillsDir() with config overriding env = %q, want %q", got, skillDir)
|
||||||
|
}
|
||||||
|
if got := skillsDir(&Cfg{Raw: map[string]string{}}); got != "/env/skills" {
|
||||||
|
t.Errorf("skillsDir() with env fallback = %q, want %q", got, "/env/skills")
|
||||||
}
|
}
|
||||||
t.Setenv("BANTAM_SKILLS_DIR", "")
|
t.Setenv("BANTAM_SKILLS_DIR", "")
|
||||||
if got := skillsDir(&Cfg{Raw: map[string]string{"bantam_skills_dir": skillDir}}); got != skillDir {
|
if got := skillsDir(&Cfg{Raw: map[string]string{"bantam_skills_dir": skillDir}}); got != skillDir {
|
||||||
@@ -2623,6 +2783,7 @@ func TestSkills(t *testing.T) {
|
|||||||
|
|
||||||
// #19: bare /skill lists skills under the configured dir, or reports none.
|
// #19: bare /skill lists skills under the configured dir, or reports none.
|
||||||
func TestListSkills(t *testing.T) {
|
func TestListSkills(t *testing.T) {
|
||||||
|
t.Setenv("BANTAM_SKILLS_DIR", "")
|
||||||
base := t.TempDir()
|
base := t.TempDir()
|
||||||
skillDir := filepath.Join(base, "skills")
|
skillDir := filepath.Join(base, "skills")
|
||||||
if err := os.MkdirAll(filepath.Join(skillDir, "alpha"), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Join(skillDir, "alpha"), 0755); err != nil {
|
||||||
@@ -2669,3 +2830,21 @@ func TestListSkills(t *testing.T) {
|
|||||||
listSkills(&Cfg{})
|
listSkills(&Cfg{})
|
||||||
os.Stdout.Close()
|
os.Stdout.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestToolsDir(t *testing.T) {
|
||||||
|
t.Setenv("BANTAM_TOOLS_DIR", "/env/tools")
|
||||||
|
if got := toolsDir(&Cfg{Raw: map[string]string{"bantam_tools_dir": "/config/tools"}}); got != "/config/tools" {
|
||||||
|
t.Errorf("toolsDir() with config overriding env = %q, want /config/tools", got)
|
||||||
|
}
|
||||||
|
if got := toolsDir(&Cfg{Raw: map[string]string{}}); got != "/env/tools" {
|
||||||
|
t.Errorf("toolsDir() with env fallback = %q, want /env/tools", got)
|
||||||
|
}
|
||||||
|
t.Setenv("BANTAM_TOOLS_DIR", "")
|
||||||
|
if got := toolsDir(&Cfg{Raw: map[string]string{"bantam_tools_dir": "/config/tools"}}); got != "/config/tools" {
|
||||||
|
t.Errorf("toolsDir() with config = %q, want /config/tools", got)
|
||||||
|
}
|
||||||
|
if got := toolsDir(&Cfg{}); got != "" {
|
||||||
|
t.Errorf("toolsDir() empty = %q, want empty", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,17 @@ 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};
|
||||||
$d{temperature} = $ENV{BANTAM_TEMP} if $ENV{BANTAM_TEMP} && $ENV{BANTAM_TEMP} =~ /^\d+(\.\d+)?$/;
|
my $t = $ENV{BANTAM_TEMP} // $ENV{BANTAM_TEMPERATURE};
|
||||||
my $cf = -f '.bantam.cfg' ? '.bantam.cfg' : 'model.cfg';
|
$d{temperature} = $t if defined $t && $t =~ /^\d+(\.\d+)?$/;
|
||||||
if (open my $f, '<:encoding(UTF-8)', $cf) { while (<$f>) { /^([^\s=]+)\s*=\s*(.+)$/ and $d{$1} = $2; } }
|
$d{timeout} = $ENV{BANTAM_TIMEOUT} if defined $ENV{BANTAM_TIMEOUT} && $ENV{BANTAM_TIMEOUT} =~ /^\d+$/;
|
||||||
$d{api_key} = $ENV{BANTAM_API_KEY} if $d{api_key} eq '-' && $ENV{BANTAM_API_KEY};
|
$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{reasoning_effort} = $ENV{BANTAM_REASONING_EFFORT} if $ENV{BANTAM_REASONING_EFFORT};
|
||||||
|
if (-f '.bantam.cfg' && open my $bf, '<:encoding(UTF-8)', '.bantam.cfg') { while (<$bf>) { /^([^\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 }
|
\%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]}}} }
|
||||||
|
|||||||
Reference in New Issue
Block a user