Files
bantam-cpp/config.cpp
T

281 lines
12 KiB
C++
Raw Normal View History

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>
#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) {
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); c.context_window_set = true; }
2026-10-14 00:01:03 +03:00
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); c.context_window_set = true; cfg_raw_set(c, "context_window", e); }
2026-10-14 00:01:03 +03:00
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) {
2026-10-15 23:32:13 +03:00
/* Strip a single pair of surrounding quotes so a value containing spaces can
* be passed as e.g. "a b" without the quote characters becoming part of the
* stored value. */
std::string vstr(val);
if (vstr.size() >= 2 &&
((vstr.front() == '"' && vstr.back() == '"') ||
(vstr.front() == '\'' && vstr.back() == '\''))) {
vstr = vstr.substr(1, vstr.size() - 2);
}
2026-10-14 00:01:03 +03:00
std::vector<std::string> lines;
{
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) {
2026-10-15 23:32:13 +03:00
line = sfmt("%s=%s\n", std::string(key).c_str(), vstr.c_str());
2026-10-14 00:01:03 +03:00
found = true;
}
}
if (!found) {
if (!lines.empty()) {
std::string &last = lines.back();
if (!last.empty() && last.back() != '\n') last += '\n';
}
2026-10-15 23:32:13 +03:00
lines.push_back(sfmt("%s=%s\n", std::string(key).c_str(), vstr.c_str()));
2026-10-14 00:01:03 +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;
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;
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 00:01:03 +03:00
return cw;
}
int fetch_context_window(Cfg &cfg) {
if (cfg.context_window_set && cfg.context_window > 0) return cfg.context_window;
2026-10-14 00:01:03 +03:00
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");
int fallback = (v && !v->empty()) ? to_int_val(*v, 262144) : 262144;
cache[key] = fallback;
return fallback;
2026-10-14 00:01:03 +03:00
}
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"; }
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";
}
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"); }