/* config.cpp — configuration file parsing, env precedence, model discovery. */ #include "bantam.hpp" #include #include #include #include #include #include #include #include #include #include using json = nlohmann::json; /* ----------------------------- 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 *file_keys) { std::ifstream f(std::string(path), std::ios::binary); if (!f) return; std::string line; while (std::getline(f, line)) { std::string_view s(line); 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 &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 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 lines; { std::ifstream f(std::string(path), std::ios::binary); std::string content((std::istreambuf_iterator(f)), std::istreambuf_iterator()); 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())); } std::ofstream out(std::string(path), std::ios::binary); if (!out) return -1; for (auto &line : lines) out << line; return 0; } 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; int cw = 0; try { auto root = json::parse(body); if (!root.is_object()) return 0; json list = nullptr; if (root.contains("data")) list = root["data"]; else if (root.contains("models")) list = root["models"]; if (list.is_array()) { static const char *keys[] = {"context_window", "context_length", "max_context_length", "max_model_len", "context_size", "max_tokens", "max_input_tokens", nullptr}; for (auto &item : list) { if (cw != 0) break; if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue; std::string id = item["id"].get(); if (!id.empty() && (id == cfg.model || strcasecmp(id.c_str(), cfg.model.c_str()) == 0)) { for (int k = 0; keys[k]; k++) { if (item.contains(keys[k]) && item[keys[k]].is_number_integer()) { int v = item[keys[k]].get(); if (v > 0) { cw = v; break; } } } } } } } catch (...) { return 0; } return cw; } int fetch_context_window(Cfg &cfg) { static std::map 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"; } std::string out; try { auto root = json::parse(body); if (!root.is_object()) { *errout = -1; return "invalid JSON from /models"; } json list = nullptr; if (root.contains("data")) list = root["data"]; else if (root.contains("models")) list = root["models"]; bool any = false; if (list.is_array()) { for (auto &item : list) { if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue; std::string id = item["id"].get(); 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) return "No models returned by the endpoint."; } catch (...) { *errout = -1; return "invalid JSON from /models"; } *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"); }