2026-10-14 00:01:03 +03:00
|
|
|
/* config.cpp — configuration file parsing, env precedence, model discovery. */
|
|
|
|
|
#include "bantam.hpp"
|
|
|
|
|
#include <cctype>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <set>
|
2026-10-14 08:36:39 +03:00
|
|
|
#include <strings.h>
|
2026-10-14 00:01:03 +03:00
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
|
|
/* ----------------------------- raw map -------------------------------- */
|
|
|
|
|
static void cfg_raw_set(Cfg &c, std::string_view k, std::string_view v) {
|
|
|
|
|
c.raw[std::string(k)] = std::string(v);
|
|
|
|
|
}
|
|
|
|
|
static void cfg_init(Cfg &c) {
|
|
|
|
|
c = Cfg{};
|
|
|
|
|
c.endpoint = "https://api.kilo.ai/api/openrouter";
|
|
|
|
|
c.model = "openrouter/free";
|
|
|
|
|
c.api_key = "-";
|
|
|
|
|
c.temperature = 0.7;
|
|
|
|
|
c.timeout = 300;
|
|
|
|
|
c.shell_timeout = 120;
|
|
|
|
|
c.max_al_iterations = 1000;
|
|
|
|
|
c.stream = 1;
|
|
|
|
|
c.force_stream = -1;
|
|
|
|
|
c.color = "auto";
|
|
|
|
|
c.context_window = 262144;
|
|
|
|
|
c.max_tool_res = 65536;
|
|
|
|
|
cfg_raw_set(c, "endpoint", c.endpoint);
|
|
|
|
|
cfg_raw_set(c, "model", c.model);
|
|
|
|
|
cfg_raw_set(c, "temperature", "0.7");
|
|
|
|
|
cfg_raw_set(c, "api_key", c.api_key);
|
|
|
|
|
cfg_raw_set(c, "stream", "true");
|
|
|
|
|
cfg_raw_set(c, "color", "auto");
|
|
|
|
|
cfg_raw_set(c, "timeout", "300");
|
|
|
|
|
cfg_raw_set(c, "shell_timeout", "120");
|
|
|
|
|
cfg_raw_set(c, "max_al_iterations", "1000");
|
|
|
|
|
cfg_raw_set(c, "context_window", "262144");
|
|
|
|
|
cfg_raw_set(c, "max_tool_res", "65536");
|
|
|
|
|
cfg_raw_set(c, "reasoning_effort", "high");
|
|
|
|
|
}
|
|
|
|
|
const std::string *cfg_raw_get(const Cfg &c, std::string_view k) {
|
|
|
|
|
auto it = c.raw.find(std::string(k));
|
|
|
|
|
return it == c.raw.end() ? nullptr : &it->second;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool internal_key(std::string_view k) {
|
|
|
|
|
static const char *keys[] = {
|
|
|
|
|
"endpoint", "model", "temperature", "stream", "api_key", "timeout",
|
|
|
|
|
"shell_timeout", "max_al_iterations", "color", "context_window", "max_tool_res",
|
|
|
|
|
"bantam_tools_dir", "bantam_skills_dir", nullptr};
|
|
|
|
|
for (int i = 0; keys[i]; i++) if (k == keys[i]) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ----------------------------- parsing -------------------------------- */
|
|
|
|
|
/* parse_cfg_file records every key it sees in *file_keys so that the
|
|
|
|
|
* config file can take precedence over environment variables. */
|
|
|
|
|
static void parse_cfg_file(std::string_view path, Cfg &c, std::set<std::string> *file_keys) {
|
2026-10-14 08:36:39 +03:00
|
|
|
std::string content;
|
|
|
|
|
if (!read_file_str(path, content)) return;
|
|
|
|
|
size_t pos = 0, n = content.size();
|
|
|
|
|
while (pos < n) {
|
|
|
|
|
size_t nl = content.find('\n', pos);
|
|
|
|
|
std::string_view s = (nl == std::string::npos)
|
|
|
|
|
? std::string_view(content).substr(pos)
|
|
|
|
|
: std::string_view(content).substr(pos, nl - pos);
|
|
|
|
|
pos = (nl == std::string::npos) ? n : nl + 1;
|
2026-10-14 00:01:03 +03:00
|
|
|
while (!s.empty() && std::isspace((unsigned char)s.front())) s.remove_prefix(1);
|
|
|
|
|
if (s.empty() || s.front() == '#') continue;
|
|
|
|
|
auto eq = s.find('=');
|
|
|
|
|
if (eq == std::string_view::npos) continue;
|
|
|
|
|
std::string_view k = s.substr(0, eq);
|
|
|
|
|
std::string_view v = s.substr(eq + 1);
|
|
|
|
|
while (!k.empty() && std::isspace((unsigned char)k.back())) k.remove_suffix(1);
|
|
|
|
|
while (!v.empty() && std::isspace((unsigned char)v.front())) v.remove_prefix(1);
|
|
|
|
|
while (!v.empty() && std::isspace((unsigned char)v.back())) v.remove_suffix(1);
|
|
|
|
|
cfg_raw_set(c, k, v);
|
|
|
|
|
if (file_keys) file_keys->insert(std::string(k));
|
|
|
|
|
if (k == "endpoint") c.endpoint = std::string(v);
|
|
|
|
|
else if (k == "model") c.model = std::string(v);
|
|
|
|
|
else if (k == "api_key") c.api_key = std::string(v);
|
|
|
|
|
else if (k == "temperature") c.temperature = std::strtod(std::string(v).c_str(), nullptr);
|
|
|
|
|
else if (k == "timeout") c.timeout = to_int_val(v, c.timeout);
|
|
|
|
|
else if (k == "shell_timeout") c.shell_timeout = to_int_val(v, c.shell_timeout);
|
|
|
|
|
else if (k == "max_al_iterations") c.max_al_iterations = to_int_val(v, c.max_al_iterations);
|
|
|
|
|
else if (k == "stream") c.stream = (v == "true" || v == "1" || v == "yes");
|
|
|
|
|
else if (k == "color") c.color = std::string(v);
|
|
|
|
|
else if (k == "context_window") c.context_window = to_int_val(v, c.context_window);
|
|
|
|
|
else if (k == "max_tool_res") c.max_tool_res = to_int_val(v, c.max_tool_res);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Environment variables act as a FALLBACK: they only fill in values that the
|
|
|
|
|
* config file did not explicitly set. The config file (.bantam.cfg) is the
|
|
|
|
|
* single source of truth. */
|
|
|
|
|
static void apply_env_cfg(Cfg &c, const std::set<std::string> &file_keys) {
|
|
|
|
|
auto ev = [&](const char *key, const char *envname) -> const char * {
|
|
|
|
|
if (file_keys.count(key)) return nullptr;
|
|
|
|
|
const char *e = std::getenv(envname);
|
|
|
|
|
return (e && *e) ? e : nullptr;
|
|
|
|
|
};
|
|
|
|
|
const char *e;
|
|
|
|
|
if ((e = ev("endpoint", "BANTAM_ENDPOINT"))) { c.endpoint = e; cfg_raw_set(c, "endpoint", e); }
|
|
|
|
|
if ((e = ev("model", "BANTAM_MODEL"))) { c.model = e; cfg_raw_set(c, "model", e); }
|
|
|
|
|
if ((e = ev("temperature", "BANTAM_TEMP"))) { c.temperature = std::strtod(e, nullptr); cfg_raw_set(c, "temperature", e); }
|
|
|
|
|
else if ((e = ev("temperature", "BANTAM_TEMPERATURE"))) { c.temperature = std::strtod(e, nullptr); cfg_raw_set(c, "temperature", e); }
|
|
|
|
|
if ((e = ev("stream", "BANTAM_STREAM"))) { c.stream = (std::strcmp(e, "true") == 0 || std::strcmp(e, "1") == 0 || std::strcmp(e, "yes") == 0); cfg_raw_set(c, "stream", e); }
|
|
|
|
|
if ((e = ev("color", "BANTAM_COLOR"))) { c.color = e; cfg_raw_set(c, "color", e); }
|
|
|
|
|
if ((e = ev("timeout", "BANTAM_TIMEOUT"))) { c.timeout = to_int_val(e, c.timeout); cfg_raw_set(c, "timeout", e); }
|
|
|
|
|
if ((e = ev("shell_timeout", "BANTAM_SHELL_TIMEOUT"))) { c.shell_timeout = to_int_val(e, c.shell_timeout); cfg_raw_set(c, "shell_timeout", e); }
|
|
|
|
|
if ((e = ev("max_al_iterations", "BANTAM_MAX_AL_ITERATIONS"))) { c.max_al_iterations = to_int_val(e, c.max_al_iterations); cfg_raw_set(c, "max_al_iterations", e); }
|
|
|
|
|
if ((e = ev("context_window", "BANTAM_CONTEXT_WINDOW"))) { c.context_window = to_int_val(e, c.context_window); cfg_raw_set(c, "context_window", e); }
|
|
|
|
|
if ((e = ev("max_tool_res", "BANTAM_MAX_TOOL_RES"))) { c.max_tool_res = to_int_val(e, c.max_tool_res); cfg_raw_set(c, "max_tool_res", e); }
|
|
|
|
|
if ((e = ev("reasoning_effort", "BANTAM_REASONING_EFFORT"))) cfg_raw_set(c, "reasoning_effort", e);
|
|
|
|
|
if ((e = ev("bantam_tools_dir", "BANTAM_TOOLS_DIR"))) cfg_raw_set(c, "bantam_tools_dir", e);
|
|
|
|
|
if ((e = ev("bantam_skills_dir", "BANTAM_SKILLS_DIR"))) cfg_raw_set(c, "bantam_skills_dir", e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Cfg get_cfg(std::string_view path) {
|
|
|
|
|
Cfg c;
|
|
|
|
|
cfg_init(c);
|
|
|
|
|
std::set<std::string> file_keys;
|
|
|
|
|
parse_cfg_file(path, c, &file_keys);
|
|
|
|
|
apply_env_cfg(c, file_keys);
|
|
|
|
|
/* api_key is usually only supplied via the environment; let it override a
|
|
|
|
|
* placeholder ("-" or empty) left in the config file. */
|
|
|
|
|
if (c.api_key.empty() || c.api_key == "-") {
|
|
|
|
|
const char *k = std::getenv("BANTAM_API_KEY");
|
|
|
|
|
if (k && *k) { c.api_key = k; cfg_raw_set(c, "api_key", k); }
|
|
|
|
|
}
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int set_cfg(std::string_view path, std::string_view key, std::string_view val) {
|
|
|
|
|
std::vector<std::string> lines;
|
|
|
|
|
{
|
2026-10-14 08:36:39 +03:00
|
|
|
std::string content;
|
|
|
|
|
read_file_str(path, content);
|
2026-10-14 00:01:03 +03:00
|
|
|
size_t i = 0, n = content.size();
|
|
|
|
|
while (i < n) {
|
|
|
|
|
size_t j = content.find('\n', i);
|
|
|
|
|
if (j == std::string::npos) { lines.push_back(content.substr(i)); break; }
|
|
|
|
|
lines.push_back(content.substr(i, j - i + 1));
|
|
|
|
|
i = j + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
bool found = false;
|
|
|
|
|
for (auto &line : lines) {
|
|
|
|
|
std::string_view sv(line);
|
|
|
|
|
size_t k = 0;
|
|
|
|
|
while (k < sv.size() && std::isspace((unsigned char)sv[k])) k++;
|
|
|
|
|
if (k < sv.size() && sv[k] == '#') continue;
|
|
|
|
|
auto eq = sv.find('=');
|
|
|
|
|
if (eq == std::string_view::npos) continue;
|
|
|
|
|
std::string_view keyPart = sv.substr(k, eq - k);
|
|
|
|
|
while (!keyPart.empty() && std::isspace((unsigned char)keyPart.back())) keyPart.remove_suffix(1);
|
|
|
|
|
if (keyPart == key) {
|
|
|
|
|
line = sfmt("%s=%s\n", std::string(key).c_str(), std::string(val).c_str());
|
|
|
|
|
found = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!found) {
|
|
|
|
|
if (!lines.empty()) {
|
|
|
|
|
std::string &last = lines.back();
|
|
|
|
|
if (!last.empty() && last.back() != '\n') last += '\n';
|
|
|
|
|
}
|
|
|
|
|
lines.push_back(sfmt("%s=%s\n", std::string(key).c_str(), std::string(val).c_str()));
|
|
|
|
|
}
|
2026-10-14 08:36:39 +03:00
|
|
|
std::string out_buf;
|
|
|
|
|
for (auto &line : lines) out_buf += line;
|
|
|
|
|
return write_file_str(path, out_buf) ? 0 : -1;
|
2026-10-14 00:01:03 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string config_path() {
|
|
|
|
|
/* .bantam.cfg is the sole configuration file. */
|
|
|
|
|
return ".bantam.cfg";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ----------------------------- model discovery ------------------------ */
|
|
|
|
|
static int query_models_context_window(Cfg &cfg) {
|
|
|
|
|
std::string url = cfg.endpoint;
|
|
|
|
|
while (!url.empty() && url.back() == '/') url.pop_back();
|
|
|
|
|
url += "/models";
|
|
|
|
|
std::string body;
|
|
|
|
|
long code = 0;
|
|
|
|
|
std::string err;
|
|
|
|
|
if (http_get(cfg, url, body, code, err) != 0) return 0;
|
|
|
|
|
if (code >= 400) return 0;
|
2026-10-14 08:36:39 +03:00
|
|
|
Json root_mut;
|
|
|
|
|
if (!Json::parse(body, root_mut) || !root_mut.is_object()) return 0;
|
|
|
|
|
const Json &root = root_mut;
|
|
|
|
|
const Json &list = root.contains("data") ? root["data"] : root["models"];
|
|
|
|
|
if (!list.is_array()) return 0;
|
|
|
|
|
static const char *keys[] = {"context_window", "context_length", "max_context_length",
|
|
|
|
|
"max_model_len", "context_size", "max_tokens", "max_input_tokens", nullptr};
|
2026-10-14 00:01:03 +03:00
|
|
|
int cw = 0;
|
2026-10-14 08:36:39 +03:00
|
|
|
for (auto &item : list.aval) {
|
|
|
|
|
if (cw != 0) break;
|
|
|
|
|
if (!item.is_object()) continue;
|
|
|
|
|
std::string id = item["id"].str();
|
|
|
|
|
if (!id.empty() && (id == cfg.model || strcasecmp(id.c_str(), cfg.model.c_str()) == 0)) {
|
|
|
|
|
for (int k = 0; keys[k]; k++) {
|
|
|
|
|
int v = item[keys[k]].as_int();
|
|
|
|
|
if (v > 0) { cw = v; break; }
|
2026-10-14 00:01:03 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-14 08:36:39 +03:00
|
|
|
}
|
2026-10-14 00:01:03 +03:00
|
|
|
return cw;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int fetch_context_window(Cfg &cfg) {
|
|
|
|
|
static std::map<std::string, int> cache;
|
|
|
|
|
std::string key = cfg.endpoint + std::string(1, '\0') + cfg.model;
|
|
|
|
|
auto it = cache.find(key);
|
|
|
|
|
if (it != cache.end()) return it->second;
|
|
|
|
|
int cw = query_models_context_window(cfg);
|
|
|
|
|
if (cw > 0) { cache[key] = cw; return cw; }
|
|
|
|
|
const std::string *v = cfg_raw_get(cfg, "context_window");
|
|
|
|
|
if (v && !v->empty()) return to_int_val(*v, 262144);
|
|
|
|
|
return 262144;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::string list_models(Cfg &cfg, int *errout) {
|
|
|
|
|
std::string url = cfg.endpoint;
|
|
|
|
|
while (!url.empty() && url.back() == '/') url.pop_back();
|
|
|
|
|
url += "/models";
|
|
|
|
|
std::string body;
|
|
|
|
|
long code = 0;
|
|
|
|
|
std::string err;
|
|
|
|
|
if (http_get(cfg, url, body, code, err) != 0) { *errout = -1; return err.empty() ? "request failed" : err; }
|
|
|
|
|
if (code >= 400) { *errout = -1; return "HTTP error from /models"; }
|
2026-10-14 08:36:39 +03:00
|
|
|
Json root_mut;
|
|
|
|
|
if (!Json::parse(body, root_mut) || !root_mut.is_object()) {
|
2026-10-14 00:01:03 +03:00
|
|
|
*errout = -1;
|
|
|
|
|
return "invalid JSON from /models";
|
|
|
|
|
}
|
2026-10-14 08:36:39 +03:00
|
|
|
const Json &root = root_mut;
|
|
|
|
|
const Json &list = root.contains("data") ? root["data"] : root["models"];
|
|
|
|
|
std::string out;
|
|
|
|
|
bool any = false;
|
|
|
|
|
if (list.is_array()) {
|
|
|
|
|
for (auto &item : list.aval) {
|
|
|
|
|
if (!item.is_object()) continue;
|
|
|
|
|
std::string id = item["id"].str();
|
|
|
|
|
if (id.empty()) continue;
|
|
|
|
|
any = true;
|
|
|
|
|
if (id == cfg.model) str_appendf(out, "* %s\n", id.c_str());
|
|
|
|
|
else str_appendf(out, " %s\n", id.c_str());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!any) {
|
|
|
|
|
*errout = 0;
|
|
|
|
|
return "No models returned by the endpoint.";
|
|
|
|
|
}
|
2026-10-14 00:01:03 +03:00
|
|
|
*errout = 0;
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string cfg_or_env(const Cfg &c, std::string_view key, const char *env) {
|
|
|
|
|
const std::string *v = cfg_raw_get(c, key);
|
|
|
|
|
if (v && !v->empty()) return *v;
|
|
|
|
|
const char *e = std::getenv(env);
|
|
|
|
|
if (e && *e) return e;
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
std::string tools_dir(const Cfg &c) { return cfg_or_env(c, "bantam_tools_dir", "BANTAM_TOOLS_DIR"); }
|
|
|
|
|
std::string skills_dir(const Cfg &c) { return cfg_or_env(c, "bantam_skills_dir", "BANTAM_SKILLS_DIR"); }
|