Replace nlohmann/json with native C++20 JSON engine, cut build RAM/time, fix 32-bit build, enable -O2 -march=native

This commit is contained in:
Luxferre
2026-10-14 08:36:39 +03:00
parent 3e70adeb8b
commit 39d7640a5a
11 changed files with 910 additions and 25919 deletions
+19 -16
View File
@@ -1,18 +1,20 @@
# Bantam (C++20 port) — POSIX-compatible Makefile.
# Targets: Linux, FreeBSD, macOS, Haiku (any POSIX make: bmake, gmake, smake).
# Dependencies: libcurl and GNU readline (found via standard include/lib
# paths) for HTTP and interactive line editing. JSON is handled by the vendored
# single-header nlohmann/json.hpp (no extra dependency).
# paths) for HTTP and interactive line editing. JSON is handled natively in
# json.cpp (no extra or vendored dependency).
# If your headers/libs live in a non-standard prefix, override, e.g.:
# make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl -lreadline"
#
# 32-bit build: make 32bit
# Or: make CXXFLAGS="-m32" LDFLAGS="-m32"
# Or: make CXXFLAGS="-std=c++20 -O2 -m32" LDFLAGS="-m32"
# Large file support on 32-bit: make CPPFLAGS="-D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE"
CXX = c++
CXXFLAGS = -std=c++20 -Wall -Wextra -Wpedantic -O1 -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
STRIP = strip
CXXFLAGS = -std=c++20 -s -Wall -Wextra -Wpedantic -O2 -march=native -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
CPPFLAGS = -I/usr/local/include -I. -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
LDFLAGS = -s
LDLIBS = -L/usr/local/lib -lcurl -lreadline
# Location of the AddressSanitizer/UBSan runtime libraries plus a replacement
@@ -24,33 +26,34 @@ ASANDIR = /tmp/asanlib
OBJS = main.o config.o util.o json.o llm.o tools.o session.o render.o editor.o
bantam: $(OBJS)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -o bantam $(OBJS) $(LDLIBS)
$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(LDFLAGS) -o bantam $(OBJS) $(LDLIBS)
$(STRIP) bantam
main.o: main.cpp bantam.hpp nlohmann/json.hpp
main.o: main.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c main.cpp
config.o: config.cpp bantam.hpp nlohmann/json.hpp
config.o: config.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c config.cpp
util.o: util.cpp bantam.hpp nlohmann/json.hpp
util.o: util.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c util.cpp
json.o: json.cpp bantam.hpp nlohmann/json.hpp
json.o: json.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c json.cpp
llm.o: llm.cpp bantam.hpp nlohmann/json.hpp
llm.o: llm.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c llm.cpp
tools.o: tools.cpp bantam.hpp nlohmann/json.hpp
tools.o: tools.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c tools.cpp
session.o: session.cpp bantam.hpp nlohmann/json.hpp
session.o: session.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c session.cpp
render.o: render.cpp bantam.hpp nlohmann/json.hpp
render.o: render.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c render.cpp
editor.o: editor.cpp bantam.hpp nlohmann/json.hpp
editor.o: editor.cpp bantam.hpp
$(CXX) $(CXXFLAGS) $(CPPFLAGS) -c editor.cpp
clean:
@@ -64,12 +67,12 @@ install: bantam
# optimized build. ASANDIR points the gcc driver (-B) at the runtime libs and
# the replacement libasan_preinit.o, and sets an rpath so they load at run time.
debug:
$(MAKE) CXXFLAGS="$(CXXFLAGS) -g -O0 -fsanitize=address,undefined -B$(ASANDIR) -Wl,-rpath,$(ASANDIR)" bantam
$(MAKE) CXXFLAGS="-std=c++20 -Wall -Wextra -Wpedantic -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -g -O0 -fsanitize=address,undefined -B$(ASANDIR) -Wl,-rpath,$(ASANDIR)" LDFLAGS="" STRIP=":" bantam
# 32-bit build target
# Usage: make 32bit
# This builds a 32-bit binary with large file support enabled.
32bit:
$(MAKE) CXXFLAGS="$(CXXFLAGS) -m32" LDFLAGS="-m32" bantam
$(MAKE) CXXFLAGS="$(CXXFLAGS) -m32" LDFLAGS="$(LDFLAGS) -m32" bantam
.PHONY: clean install debug 32bit
+12 -11
View File
@@ -34,17 +34,18 @@ The whole philosophy of Bantam rests on two principles:
`std::map`, `std::span`) instead of raw memory areas.
- Eliminate hand-rolled allocation/string code; rely on RAII so there is far
less room for memory-allocation related vulnerabilities.
- Depend only on **libcurl** (HTTP), **GNU readline** (interactive line
editing) and the vendored single-header **nlohmann/json** library (vendored
under `nlohmann/json.hpp`); the unsafe json-c C API is no longer used.
- Depend only on **libcurl** (HTTP) and **GNU readline** (interactive line
editing); JSON parsing/serialization and Markdown rendering are implemented
natively in C++20 with no external or vendored library bloat, and the unsafe
json-c C API is no longer used.
## Repository layout
```
bantam.hpp all shared declarations (structs, prototypes)
bantam.hpp all shared declarations (structs, prototypes, native Json type)
util.cpp string building, UTF-8, prompt-injection filter, color, IDs, MD5
config.cpp config file parsing, env precedence, /models discovery
json.cpp request building + non-stream response parsing (nlohmann/json)
json.cpp native RFC 8259 JSON engine, request building + response parsing
llm.cpp libcurl HTTP client, OpenCode headers, backoff, SSE streaming
tools.cpp bash/read/write tools, oversized-result offloading
session.cpp message copy, session save/load, compaction
@@ -52,7 +53,6 @@ render.cpp terminal markdown renderer (native, box-drawing tables)
editor.cpp GNU readline line editor + history (+ term helpers)
main.cpp REPL, agentic loop, tool dispatch, commands
Makefile POSIX-compatible build
nlohmann/ vendored nlohmann/json single-header (v3.12.0)
README.md this document
```
@@ -61,8 +61,8 @@ README.md this document
- A C++20 compiler (`c++` / `g++` / `clang++`) and a POSIX `make`
(`bmake`, `gmake`, `smake`, …).
- **libcurl** development packages, available via the standard include /
library search paths. The JSON layer is a vendored single-header
(`nlohmann/json.hpp`) and needs no separate dependency.
library search paths. The JSON layer is implemented natively in `json.cpp`
and needs no separate dependency.
- **GNU readline** development packages (`libreadline-dev` on Debian/Ubuntu,
`readline` on Fedora/macOS) for the interactive prompt, history and line
editing. On common systems:
@@ -99,7 +99,7 @@ make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl"
## Building
```bash
make # builds ./bantam (c++ -std=c++20 -O2 -s -Wall -Wextra -Wpedantic)
make # builds & strips ./bantam (c++ -std=c++20 -O2 -march=native -s -Wall -Wextra -Wpedantic)
make clean # remove build artifacts
make install # install to $(DESTDIR)/usr/local/bin/bantam
```
@@ -391,8 +391,9 @@ collections use `std::vector`, and curl header lists and streaming state are
wrapped in RAII types (`CurlHeaders`, `StreamCtx`) that free themselves on
scope exit. There is no hand-rolled allocator and no manual `free` of message
or tool-call data, so use-after-free and double-free are structurally
unlikely. The vendored nlohmann/json library is used for all JSON parsing and
serialization instead of the unsafe json-c C API.
unlikely. A lightweight, native RFC 8259 JSON engine (`struct Json` in
`json.cpp`) handles all JSON parsing and serialization using standard C++
containers instead of an unsafe C API or a heavy header-only template library.
## Credits
+68 -3
View File
@@ -11,22 +11,83 @@
* std::map, std::span) instead of raw memory areas.
* - Eliminate hand-rolled allocation/string code; rely on RAII so there
* is far less room for memory-allocation related vulnerabilities.
* - Depend only on libcurl (HTTP) plus the vendored single-header
* nlohmann/json library; the unsafe json-c C API is no longer used.
* - Depend only on libcurl (HTTP) and GNU readline (interactive line
* editing); JSON parsing/serialization and Markdown rendering are
* implemented natively in modern C++20 with zero external bloat.
*
* Feature macros: we rely on POSIX.1-2008 interfaces. */
#ifndef BANTAM_HPP
#define BANTAM_HPP
#include <cstdarg>
#include <cstddef>
#include <cstdint>
#include <map>
#include <memory>
#include <span>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
/* ------------------------------------------------------------------ */
/* Minimal RFC 8259 JSON value, parser and serializer (json.cpp). */
/* ------------------------------------------------------------------ */
enum class JType { Null, Bool, Int, Float, Str, Arr, Obj };
struct Json {
JType type = JType::Null;
bool bval = false;
int64_t ival = 0;
double dval = 0.0;
std::string sval;
std::vector<Json> aval;
std::vector<std::pair<std::string, Json>> oval;
Json();
~Json();
Json(const Json &);
Json(Json &&) noexcept;
Json &operator=(const Json &);
Json &operator=(Json &&) noexcept;
Json(std::nullptr_t);
Json(bool b);
Json(int n);
Json(int64_t n);
Json(double d);
Json(const char *s);
Json(std::string s);
Json(std::string_view s);
static Json object();
static Json array();
bool is_null() const { return type == JType::Null; }
bool is_bool() const { return type == JType::Bool; }
bool is_number_integer() const { return type == JType::Int; }
bool is_number() const { return type == JType::Int || type == JType::Float; }
bool is_string() const { return type == JType::Str; }
bool is_array() const { return type == JType::Arr; }
bool is_object() const { return type == JType::Obj; }
bool empty() const;
size_t size() const;
bool contains(std::string_view k) const;
Json &operator[](std::string_view k);
const Json &operator[](std::string_view k) const;
Json &at(size_t i);
const Json &at(size_t i) const;
void push_back(Json v);
std::string str(std::string_view def = "") const;
int64_t as_int64(int64_t def = 0) const;
int as_int(int def = 0) const;
std::string dump(int indent = -1) const;
static bool parse(std::string_view src, Json &out);
};
/* Markdown renderer state (code-fence tracking). */
struct MdState {
bool in_code = false;
@@ -98,6 +159,8 @@ extern volatile int g_interrupted; /* SIGINT flag */
void setup_signal_handler();
int is_terminal(int fd);
int to_int_val(std::string_view s, int def);
bool read_file_str(std::string_view path, std::string &out);
bool write_file_str(std::string_view path, std::string_view data);
std::string sfmt(const char *fmt, ...);
void str_appendf(std::string &s, const char *fmt, ...);
@@ -133,8 +196,10 @@ std::string tools_dir(const Cfg &c);
std::string skills_dir(const Cfg &c);
/* ------------------------------------------------------------------ */
/* json.cpp — request building & response parsing (nlohmann/json). */
/* json.cpp — JSON engine, request building & response parsing. */
/* ------------------------------------------------------------------ */
void serialize_message_json(Json &obj, const Message &m);
void parse_message_json(const Json &obj, Message &m);
std::string build_request_json(const Cfg &cfg, const Messages &msgs,
bool use_tools, bool stream);
bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage);
+38 -45
View File
@@ -4,14 +4,10 @@
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <nlohmann/json.hpp>
#include <fstream>
#include <set>
#include <strings.h>
#include <sys/stat.h>
#include <unistd.h>
#include <strings.h>
using json = nlohmann::json;
/* ----------------------------- raw map -------------------------------- */
static void cfg_raw_set(Cfg &c, std::string_view k, std::string_view v) {
@@ -62,11 +58,15 @@ bool internal_key(std::string_view k) {
/* 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::ifstream f(std::string(path), std::ios::binary);
if (!f) return;
std::string line;
while (std::getline(f, line)) {
std::string_view s(line);
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;
while (!s.empty() && std::isspace((unsigned char)s.front())) s.remove_prefix(1);
if (s.empty() || s.front() == '#') continue;
auto eq = s.find('=');
@@ -136,8 +136,8 @@ Cfg get_cfg(std::string_view path) {
int set_cfg(std::string_view path, std::string_view key, std::string_view val) {
std::vector<std::string> lines;
{
std::ifstream f(std::string(path), std::ios::binary);
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::string content;
read_file_str(path, content);
size_t i = 0, n = content.size();
while (i < n) {
size_t j = content.find('\n', i);
@@ -168,10 +168,9 @@ int set_cfg(std::string_view path, std::string_view key, std::string_view val) {
}
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 out_buf;
for (auto &line : lines) out_buf += line;
return write_file_str(path, out_buf) ? 0 : -1;
}
std::string config_path() {
@@ -189,31 +188,25 @@ static int query_models_context_window(Cfg &cfg) {
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()) {
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};
for (auto &item : list) {
int cw = 0;
for (auto &item : list.aval) {
if (cw != 0) break;
if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue;
std::string id = item["id"].get<std::string>();
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++) {
if (item.contains(keys[k]) && item[keys[k]].is_number_integer()) {
int v = item[keys[k]].get<int>();
int v = item[keys[k]].as_int();
if (v > 0) { cw = v; break; }
}
}
}
}
}
} catch (...) { return 0; }
return cw;
}
@@ -238,28 +231,28 @@ std::string list_models(Cfg &cfg, int *errout) {
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()) {
*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;
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<std::string>();
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) return "No models returned by the endpoint.";
} catch (...) {
*errout = -1;
return "invalid JSON from /models";
if (!any) {
*errout = 0;
return "No models returned by the endpoint.";
}
*errout = 0;
return out;
+555 -73
View File
@@ -1,9 +1,477 @@
/* json.cpp — request payload building and non-streaming response parsing. */
/* json.cpp — native RFC 8259 JSON parser/serializer, request building,
* and non-streaming response parsing. */
#include "bantam.hpp"
#include <nlohmann/json.hpp>
#include <cerrno>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
using json = nlohmann::json;
/* ----------------------------- Json methods --------------------------- */
Json::Json() = default;
Json::~Json() = default;
Json::Json(const Json &) = default;
Json::Json(Json &&) noexcept = default;
Json &Json::operator=(const Json &) = default;
Json &Json::operator=(Json &&) noexcept = default;
Json::Json(std::nullptr_t) : type(JType::Null) {}
Json::Json(bool b) : type(JType::Bool), bval(b) {}
Json::Json(int n) : type(JType::Int), ival(n), dval(n) {}
Json::Json(int64_t n) : type(JType::Int), ival(n), dval((double)n) {}
Json::Json(double d) : type(JType::Float), ival((int64_t)d), dval(d) {}
Json::Json(const char *s) : type(JType::Str), sval(s ? s : "") {}
Json::Json(std::string s) : type(JType::Str), sval(std::move(s)) {}
Json::Json(std::string_view s) : type(JType::Str), sval(s) {}
Json Json::object() {
Json j;
j.type = JType::Obj;
return j;
}
Json Json::array() {
Json j;
j.type = JType::Arr;
return j;
}
static const Json kNullJson{};
bool Json::empty() const {
if (type == JType::Arr) return aval.empty();
if (type == JType::Obj) return oval.empty();
if (type == JType::Str) return sval.empty();
return true;
}
size_t Json::size() const {
if (type == JType::Arr) return aval.size();
if (type == JType::Obj) return oval.size();
if (type == JType::Str) return sval.size();
return 0;
}
bool Json::contains(std::string_view k) const {
if (type != JType::Obj) return false;
for (auto &kv : oval) {
if (kv.first == k) return true;
}
return false;
}
Json &Json::operator[](std::string_view k) {
if (type != JType::Obj) {
*this = Json::object();
}
for (auto &kv : oval) {
if (kv.first == k) return kv.second;
}
oval.emplace_back(std::string(k), Json{});
return oval.back().second;
}
const Json &Json::operator[](std::string_view k) const {
if (type != JType::Obj) return kNullJson;
for (auto &kv : oval) {
if (kv.first == k) return kv.second;
}
return kNullJson;
}
Json &Json::at(size_t i) {
return aval[i];
}
const Json &Json::at(size_t i) const {
if (type != JType::Arr || i >= aval.size()) return kNullJson;
return aval[i];
}
void Json::push_back(Json v) {
if (type != JType::Arr) {
*this = Json::array();
}
aval.push_back(std::move(v));
}
std::string Json::str(std::string_view def) const {
if (type == JType::Str) return sval;
return std::string(def);
}
int64_t Json::as_int64(int64_t def) const {
if (type == JType::Int) return ival;
return def;
}
int Json::as_int(int def) const {
if (type != JType::Int) return def;
if (ival > INT_MAX) return INT_MAX;
if (ival < INT_MIN) return INT_MIN;
return (int)ival;
}
/* ----------------------------- JSON serializer ------------------------ */
static void dump_str(std::string &out, std::string_view s) {
out += '"';
const unsigned char *p = (const unsigned char *)s.data();
size_t avail = s.size();
while (avail > 0) {
unsigned char c = p[0];
if (c < 0x80) {
p++;
avail--;
switch (c) {
case '"': out += "\\\""; break;
case '\\': out += "\\\\"; break;
case '\b': out += "\\b"; break;
case '\f': out += "\\f"; break;
case '\n': out += "\\n"; break;
case '\r': out += "\\r"; break;
case '\t': out += "\\t"; break;
default:
if (c < 0x20) str_appendf(out, "\\u%04x", (unsigned)c);
else out += (char)c;
break;
}
} else {
size_t pl = 0;
int cp = utf8_decode(p, avail, &pl);
if (cp == 0xFFFD && pl == 1) {
out += "\xEF\xBF\xBD";
p++;
avail--;
} else {
out.append((const char *)p, pl);
p += pl;
avail -= pl;
}
}
}
out += '"';
}
static void dump_rec(std::string &out, const Json &j, int indent, int depth) {
switch (j.type) {
case JType::Null:
out += "null";
break;
case JType::Bool:
out += (j.bval ? "true" : "false");
break;
case JType::Int:
str_appendf(out, "%lld", (long long)j.ival);
break;
case JType::Float:
if (!std::isfinite(j.dval)) out += "null";
else str_appendf(out, "%.15g", j.dval);
break;
case JType::Str:
dump_str(out, j.sval);
break;
case JType::Arr:
if (j.aval.empty()) {
out += "[]";
} else {
out += '[';
for (size_t i = 0; i < j.aval.size(); i++) {
if (i > 0) out += ',';
if (indent >= 0) {
out += '\n';
out.append((size_t)(depth + 1) * (size_t)indent, ' ');
}
dump_rec(out, j.aval[i], indent, depth + 1);
}
if (indent >= 0) {
out += '\n';
out.append((size_t)depth * (size_t)indent, ' ');
}
out += ']';
}
break;
case JType::Obj:
if (j.oval.empty()) {
out += "{}";
} else {
out += '{';
for (size_t i = 0; i < j.oval.size(); i++) {
if (i > 0) out += ',';
if (indent >= 0) {
out += '\n';
out.append((size_t)(depth + 1) * (size_t)indent, ' ');
}
dump_str(out, j.oval[i].first);
out += (indent >= 0) ? ": " : ":";
dump_rec(out, j.oval[i].second, indent, depth + 1);
}
if (indent >= 0) {
out += '\n';
out.append((size_t)depth * (size_t)indent, ' ');
}
out += '}';
}
break;
}
}
std::string Json::dump(int indent) const {
std::string out;
dump_rec(out, *this, indent, 0);
return out;
}
/* ----------------------------- JSON parser ---------------------------- */
struct JParser {
std::string_view src;
size_t pos = 0;
void skip_ws() {
while (pos < src.size()) {
char c = src[pos];
if (c == ' ' || c == '\t' || c == '\n' || c == '\r') pos++;
else break;
}
}
static int hex_val(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
bool parse_hex4(uint32_t &out) {
if (pos + 4 > src.size()) return false;
uint32_t v = 0;
for (int i = 0; i < 4; i++) {
int h = hex_val(src[pos + (size_t)i]);
if (h < 0) return false;
v = (v << 4) | (uint32_t)h;
}
pos += 4;
out = v;
return true;
}
static void append_utf8(std::string &s, uint32_t cp) {
if (cp <= 0x7F) {
s += (char)cp;
} else if (cp <= 0x7FF) {
s += (char)(0xC0 | (cp >> 6));
s += (char)(0x80 | (cp & 0x3F));
} else if (cp <= 0xFFFF) {
s += (char)(0xE0 | (cp >> 12));
s += (char)(0x80 | ((cp >> 6) & 0x3F));
s += (char)(0x80 | (cp & 0x3F));
} else if (cp <= 0x10FFFF) {
s += (char)(0xF0 | (cp >> 18));
s += (char)(0x80 | ((cp >> 12) & 0x3F));
s += (char)(0x80 | ((cp >> 6) & 0x3F));
s += (char)(0x80 | (cp & 0x3F));
} else {
s += "\xEF\xBF\xBD";
}
}
bool parse_string(std::string &out) {
if (pos >= src.size() || src[pos] != '"') return false;
pos++;
out.clear();
while (pos < src.size()) {
unsigned char c = (unsigned char)src[pos++];
if (c == '"') return true;
if (c < 0x20) return false;
if (c != '\\') {
out += (char)c;
continue;
}
if (pos >= src.size()) return false;
char esc = src[pos++];
switch (esc) {
case '"': out += '"'; break;
case '\\': out += '\\'; break;
case '/': out += '/'; break;
case 'b': out += '\b'; break;
case 'f': out += '\f'; break;
case 'n': out += '\n'; break;
case 'r': out += '\r'; break;
case 't': out += '\t'; break;
case 'u': {
uint32_t cp = 0;
if (!parse_hex4(cp)) return false;
if (cp >= 0xD800 && cp <= 0xDBFF) {
if (pos + 6 <= src.size() && src[pos] == '\\' && src[pos + 1] == 'u') {
size_t saved = pos;
pos += 2;
uint32_t low = 0;
if (parse_hex4(low) && low >= 0xDC00 && low <= 0xDFFF) {
cp = 0x10000u + (((cp - 0xD800u) << 10) | (low - 0xDC00u));
} else {
pos = saved;
cp = 0xFFFDu;
}
} else {
cp = 0xFFFDu;
}
} else if (cp >= 0xDC00 && cp <= 0xDFFF) {
cp = 0xFFFDu;
}
append_utf8(out, cp);
break;
}
default:
return false;
}
}
return false;
}
bool parse_number(Json &out) {
size_t start = pos;
if (src[pos] == '-') pos++;
if (pos >= src.size()) return false;
if (src[pos] == '0') {
pos++;
if (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') return false;
} else if (src[pos] >= '1' && src[pos] <= '9') {
while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') pos++;
} else {
return false;
}
bool is_float = false;
if (pos < src.size() && src[pos] == '.') {
is_float = true;
pos++;
if (pos >= src.size() || src[pos] < '0' || src[pos] > '9') return false;
while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') pos++;
}
if (pos < src.size() && (src[pos] == 'e' || src[pos] == 'E')) {
is_float = true;
pos++;
if (pos < src.size() && (src[pos] == '+' || src[pos] == '-')) pos++;
if (pos >= src.size() || src[pos] < '0' || src[pos] > '9') return false;
while (pos < src.size() && src[pos] >= '0' && src[pos] <= '9') pos++;
}
std::string num_str(src.substr(start, pos - start));
if (!is_float) {
errno = 0;
char *end = nullptr;
long long iv = std::strtoll(num_str.c_str(), &end, 10);
if (errno == 0 && end == num_str.c_str() + num_str.size()) {
out = Json((int64_t)iv);
return true;
}
}
errno = 0;
char *end = nullptr;
double dv = std::strtod(num_str.c_str(), &end);
if (end != num_str.c_str() + num_str.size() || !std::isfinite(dv)) return false;
out = Json(dv);
return true;
}
bool parse_value(Json &out, int depth) {
if (depth > 256) return false;
skip_ws();
if (pos >= src.size()) return false;
char c = src[pos];
if (c == 'n') {
if (src.substr(pos, 4) != "null") return false;
pos += 4;
out = Json(nullptr);
return true;
}
if (c == 't') {
if (src.substr(pos, 4) != "true") return false;
pos += 4;
out = Json(true);
return true;
}
if (c == 'f') {
if (src.substr(pos, 5) != "false") return false;
pos += 5;
out = Json(false);
return true;
}
if (c == '"') {
std::string s;
if (!parse_string(s)) return false;
out = Json(std::move(s));
return true;
}
if (c == '[') {
pos++;
skip_ws();
out = Json::array();
if (pos < src.size() && src[pos] == ']') {
pos++;
return true;
}
while (true) {
Json elem;
if (!parse_value(elem, depth + 1)) return false;
out.aval.push_back(std::move(elem));
skip_ws();
if (pos < src.size() && src[pos] == ',') {
pos++;
continue;
}
if (pos < src.size() && src[pos] == ']') {
pos++;
return true;
}
return false;
}
}
if (c == '{') {
pos++;
skip_ws();
out = Json::object();
if (pos < src.size() && src[pos] == '}') {
pos++;
return true;
}
while (true) {
skip_ws();
std::string key;
if (!parse_string(key)) return false;
skip_ws();
if (pos >= src.size() || src[pos] != ':') return false;
pos++;
Json val;
if (!parse_value(val, depth + 1)) return false;
out[key] = std::move(val);
skip_ws();
if (pos < src.size() && src[pos] == ',') {
pos++;
continue;
}
if (pos < src.size() && src[pos] == '}') {
pos++;
return true;
}
return false;
}
}
if (c == '-' || (c >= '0' && c <= '9')) {
return parse_number(out);
}
return false;
}
};
bool Json::parse(std::string_view src, Json &out) {
JParser p{src, 0};
Json tmp;
if (!p.parse_value(tmp, 0)) return false;
p.skip_ws();
if (p.pos != src.size()) return false;
out = std::move(tmp);
return true;
}
/* ----------------------------- message helpers ------------------------ */
/* The tool definitions, identical to the Go reference implementation. */
static const char *TOOLS_JSON = R"JSON([
{"type":"function","function":{
@@ -32,120 +500,134 @@ static const char *TOOLS_JSON = R"JSON([
},"required":["filePath","content"]}}}
])JSON";
static void serialize_message(json &obj, const Message &m) {
void serialize_message_json(Json &obj, const Message &m) {
obj = Json::object();
obj["role"] = m.role.empty() ? "" : m.role;
if (!m.content.empty()) obj["content"] = m.content;
if (!m.reasoning.empty()) obj["reasoning_content"] = m.reasoning;
if (!m.calls.empty()) {
json arr = json::array();
Json arr = Json::array();
for (auto &tc : m.calls) {
json t;
Json t = Json::object();
if (!tc.id.empty()) t["id"] = tc.id;
t["type"] = "function";
json fn;
Json fn = Json::object();
fn["name"] = tc.name.empty() ? "" : tc.name;
fn["arguments"] = tc.arguments.empty() ? "{}" : tc.arguments;
t["function"] = fn;
arr.push_back(t);
t["function"] = std::move(fn);
arr.push_back(std::move(t));
}
obj["tool_calls"] = arr;
obj["tool_calls"] = std::move(arr);
}
if (!m.tool_call_id.empty()) obj["tool_call_id"] = m.tool_call_id;
}
void parse_message_json(const Json &obj, Message &m) {
m = Message{};
if (!obj.is_object()) return;
m.role = obj["role"].str("user");
m.content = obj["content"].str();
m.reasoning = obj["reasoning_content"].str();
m.tool_call_id = obj["tool_call_id"].str();
const Json &tcs = obj["tool_calls"];
if (tcs.is_array()) {
for (auto &tc : tcs.aval) {
if (!tc.is_object()) continue;
ToolCall t;
t.id = tc["id"].str();
const Json &fn = tc["function"];
if (fn.is_object()) {
t.name = fn["name"].str();
t.arguments = fn["arguments"].str();
}
m.calls.push_back(std::move(t));
}
}
}
/* ----------------------------- request / response --------------------- */
std::string build_request_json(const Cfg &cfg, const Messages &msgs,
bool use_tools, bool stream) {
json root = json::object();
Json root = Json::object();
root["model"] = cfg.model;
root["temperature"] = cfg.temperature;
json arr = json::array();
Json arr = Json::array();
for (auto &m : msgs.items) {
json mo;
serialize_message(mo, m);
arr.push_back(mo);
Json mo;
serialize_message_json(mo, m);
arr.push_back(std::move(mo));
}
root["messages"] = arr;
root["messages"] = std::move(arr);
root["stream"] = stream;
if (use_tools) {
try {
json tools = json::parse(TOOLS_JSON);
root["tools"] = tools;
} catch (...) {}
Json tools;
if (Json::parse(TOOLS_JSON, tools)) {
root["tools"] = std::move(tools);
}
}
if (stream) {
json so = json::object();
Json so = Json::object();
so["include_usage"] = true;
root["stream_options"] = so;
root["stream_options"] = std::move(so);
}
/* Forward non-internal raw params, parsed as JSON when possible. */
for (auto &kv : cfg.raw) {
if (internal_key(kv.first)) continue;
try {
json jv = json::parse(kv.second);
root[kv.first] = jv;
} catch (...) {
Json jv;
if (Json::parse(kv.second, jv)) {
root[kv.first] = std::move(jv);
} else {
root[kv.first] = kv.second;
}
}
return root.dump(-1, ' ', false, json::error_handler_t::replace);
}
static std::string json_str(const json &j, const char *k, std::string_view def = "") {
if (j.is_object() && j.contains(k) && j[k].is_string())
return j[k].get<std::string>();
return std::string(def);
}
static int json_int(const json &j, const char *k, int def = 0) {
if (j.is_object() && j.contains(k) && j[k].is_number_integer())
return j[k].get<int>();
return def;
return root.dump();
}
bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage) {
out = Message{};
usage = Usage{};
try {
auto root = json::parse(body);
if (!root.is_object() || !root.contains("choices") ||
!root["choices"].is_array() || root["choices"].empty())
return false;
auto ch = root["choices"][0];
if (!ch.is_object() || !ch.contains("message") || !ch["message"].is_object())
return false;
auto msg = ch["message"];
out.role = json_str(msg, "role", "assistant");
out.content = json_str(msg, "content");
std::string reas = json_str(msg, "reasoning_content");
if (reas.empty()) reas = json_str(msg, "reasoning");
if (reas.empty()) reas = json_str(msg, "thought");
Json root_mut;
if (!Json::parse(body, root_mut) || !root_mut.is_object()) return false;
const Json &root = root_mut;
const Json &choices = root["choices"];
if (!choices.is_array() || choices.empty()) return false;
const Json &ch = choices.at(0);
if (!ch.is_object()) return false;
const Json &msg = ch["message"];
if (!msg.is_object()) return false;
out.role = msg["role"].str("assistant");
out.content = msg["content"].str();
std::string reas = msg["reasoning_content"].str();
if (reas.empty()) reas = msg["reasoning"].str();
if (reas.empty()) reas = msg["thought"].str();
if (!reas.empty()) out.reasoning = std::move(reas);
if (msg.contains("tool_calls") && msg["tool_calls"].is_array()) {
for (auto &tc : msg["tool_calls"]) {
const Json &tcs = msg["tool_calls"];
if (tcs.is_array()) {
for (auto &tc : tcs.aval) {
if (!tc.is_object()) continue;
ToolCall t;
t.id = json_str(tc, "id");
if (tc.contains("function") && tc["function"].is_object()) {
auto fn = tc["function"];
t.name = json_str(fn, "name");
t.arguments = json_str(fn, "arguments", "{}");
t.id = tc["id"].str();
const Json &fn = tc["function"];
if (fn.is_object()) {
t.name = fn["name"].str();
t.arguments = fn["arguments"].str("{}");
}
out.calls.push_back(std::move(t));
}
}
if (root.contains("usage") && root["usage"].is_object()) {
auto u = root["usage"];
usage.prompt_tokens = json_int(u, "prompt_tokens");
usage.completion_tokens = json_int(u, "completion_tokens");
usage.total_tokens = json_int(u, "total_tokens");
if (u.contains("prompt_tokens_details") && u["prompt_tokens_details"].is_object())
usage.cached_tokens = json_int(u["prompt_tokens_details"], "cached_tokens");
if (usage.cached_tokens == 0)
usage.cached_tokens = json_int(u, "cached_tokens");
const Json &u = root["usage"];
if (u.is_object()) {
usage.prompt_tokens = u["prompt_tokens"].as_int();
usage.completion_tokens = u["completion_tokens"].as_int();
usage.total_tokens = u["total_tokens"].as_int();
const Json &ptd = u["prompt_tokens_details"];
if (ptd.is_object()) {
usage.cached_tokens = ptd["cached_tokens"].as_int();
}
usage.model = json_str(root, "model");
if (usage.cached_tokens == 0) {
usage.cached_tokens = u["cached_tokens"].as_int();
}
}
usage.model = root["model"].str();
return true;
} catch (...) {
return false;
}
}
+47 -57
View File
@@ -1,16 +1,14 @@
/* llm.cpp — libcurl HTTP client, Fibonacci backoff,
* streaming SSE parser and the llm() dispatch function. */
#include "bantam.hpp"
#include <unistd.h>
#include <array>
#include <chrono>
#include <thread>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <curl/curl.h>
#include <memory>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
#include <unistd.h>
/* ----------------------------- memory callback ------------------------ */
static size_t mem_cb(char *ptr, size_t sz, size_t nm, void *ud) {
@@ -200,45 +198,41 @@ static void stream_maybe_erase(StreamCtx *sc) {
static void stream_handle_data(StreamCtx *sc, const std::string &data) {
if (data == "[DONE]") { sc->done = 1; return; }
try {
json d = json::parse(data);
if (!d.is_object()) return;
if (d.contains("usage") && d["usage"].is_object()) {
auto u = d["usage"];
int pt = (u.contains("prompt_tokens") && u["prompt_tokens"].is_number_integer())
? u["prompt_tokens"].get<int>() : 0;
int tt = (u.contains("total_tokens") && u["total_tokens"].is_number_integer())
? u["total_tokens"].get<int>() : 0;
Json d_mut;
if (!Json::parse(data, d_mut) || !d_mut.is_object()) return;
const Json &d = d_mut;
const Json &u = d["usage"];
if (u.is_object()) {
int pt = u["prompt_tokens"].as_int();
int tt = u["total_tokens"].as_int();
if (pt > 0 || tt > 0) {
sc->last_usage.prompt_tokens = pt;
sc->last_usage.completion_tokens =
(u.contains("completion_tokens") && u["completion_tokens"].is_number_integer())
? u["completion_tokens"].get<int>() : 0;
sc->last_usage.completion_tokens = u["completion_tokens"].as_int();
sc->last_usage.total_tokens = tt;
if (u.contains("prompt_tokens_details") && u["prompt_tokens_details"].is_object()) {
auto ptd = u["prompt_tokens_details"];
if (ptd.contains("cached_tokens") && ptd["cached_tokens"].is_number_integer())
sc->last_usage.cached_tokens = ptd["cached_tokens"].get<int>();
const Json &ptd = u["prompt_tokens_details"];
if (ptd.is_object()) {
int ct_val = ptd["cached_tokens"].as_int();
if (ct_val > 0) sc->last_usage.cached_tokens = ct_val;
}
if (sc->last_usage.cached_tokens == 0 &&
u.contains("cached_tokens") && u["cached_tokens"].is_number_integer())
sc->last_usage.cached_tokens = u["cached_tokens"].get<int>();
if (sc->last_usage.cached_tokens == 0) {
int ct_val = u["cached_tokens"].as_int();
if (ct_val > 0) sc->last_usage.cached_tokens = ct_val;
}
}
if (d.contains("model") && d["model"].is_string())
sc->last_usage.model = d["model"].get<std::string>();
if (!d.contains("choices") || !d["choices"].is_array() || d["choices"].empty()) return;
auto ch = d["choices"][0];
if (!ch.is_object() || !ch.contains("delta") || !ch["delta"].is_object()) return;
auto delta = ch["delta"];
}
if (d["model"].is_string()) {
sc->last_usage.model = d["model"].str();
}
const Json &choices = d["choices"];
if (!choices.is_array() || choices.empty()) return;
const Json &ch = choices.at(0);
if (!ch.is_object()) return;
const Json &delta = ch["delta"];
if (!delta.is_object()) return;
std::string rc;
if (delta.contains("reasoning_content") && delta["reasoning_content"].is_string())
rc = delta["reasoning_content"].get<std::string>();
if (rc.empty() && delta.contains("reasoning") && delta["reasoning"].is_string())
rc = delta["reasoning"].get<std::string>();
if (rc.empty() && delta.contains("thought") && delta["thought"].is_string())
rc = delta["thought"].get<std::string>();
std::string rc = delta["reasoning_content"].str();
if (rc.empty()) rc = delta["reasoning"].str();
if (rc.empty()) rc = delta["thought"].str();
if (!rc.empty()) {
stream_maybe_erase(sc);
if (!sc->in_reasoning) {
@@ -253,9 +247,7 @@ static void stream_handle_data(StreamCtx *sc, const std::string &data) {
cprint(rc, std::array{2});
sc->reas += rc;
}
std::string ct;
if (delta.contains("content") && delta["content"].is_string())
ct = delta["content"].get<std::string>();
std::string ct = delta["content"].str();
if (!ct.empty()) {
stream_maybe_erase(sc);
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
@@ -274,31 +266,28 @@ static void stream_handle_data(StreamCtx *sc, const std::string &data) {
}
}
}
if (delta.contains("tool_calls") && delta["tool_calls"].is_array()) {
const Json &tcs = delta["tool_calls"];
if (tcs.is_array()) {
stream_maybe_erase(sc);
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
if (sc->live_idx == -1) {
flush_tbl(sc);
if (!sc->line_buf.empty()) { render_md_line_print(sc->line_buf, sc->md); sc->line_buf.clear(); }
}
for (auto &tc : delta["tool_calls"]) {
for (auto &tc : tcs.aval) {
if (!tc.is_object()) continue;
int idx = (tc.contains("index") && tc["index"].is_number_integer())
? tc["index"].get<int>() : 0;
std::string id, name, args;
if (tc.contains("id") && tc["id"].is_string()) id = tc["id"].get<std::string>();
if (tc.contains("function") && tc["function"].is_object()) {
auto fn = tc["function"];
if (fn.contains("name") && fn["name"].is_string()) name = fn["name"].get<std::string>();
if (fn.contains("arguments") && fn["arguments"].is_string()) args = fn["arguments"].get<std::string>();
int idx = tc["index"].as_int();
std::string id = tc["id"].str();
std::string name, args;
const Json &fn = tc["function"];
if (fn.is_object()) {
name = fn["name"].str();
args = fn["arguments"].str();
}
stream_upsert_tc(sc, idx, id, name, args);
}
step_live(sc);
}
} catch (...) {
return;
}
}
static int stream_feed(StreamCtx *sc, const char *data, size_t len,
@@ -365,9 +354,10 @@ static size_t stream_write_cb(char *ptr, size_t sz, size_t nm, void *ud) {
/* ----------------------------- llm() ---------------------------------- */
static void sleep_interruptible(int secs, volatile int *interrupted) {
struct timespec ts{0, 100000000L};
for (int i = 0; i < secs * 10; i++) {
if (*interrupted) return;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
nanosleep(&ts, nullptr);
}
}
@@ -407,8 +397,8 @@ int llm(Cfg &cfg, Messages &msgs, bool use_tools, Message &out, Usage &usage,
curl_easy_setopt(curl.c, CURLOPT_POSTFIELDSIZE, (long)payload.size());
curl_easy_setopt(curl.c, CURLOPT_HTTPHEADER, headers.list);
set_proxy(curl.c);
curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, cfg.timeout > 0 ? cfg.timeout : 30);
if (!stream) curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, cfg.timeout > 0 ? cfg.timeout : 300);
curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 30));
if (!stream) curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 300));
int attempt, rc = -1;
for (attempt = 0; attempt <= nf; attempt++) {
+20 -33
View File
@@ -2,21 +2,18 @@
#include "bantam.hpp"
#include <array>
#include <cctype>
#include <cerrno>
#include <climits>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <curl/curl.h>
#include <dirent.h>
#include <fstream>
#include <nlohmann/json.hpp>
#include <span>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
using json = nlohmann::json;
/* ----------------------------- usage / pct ---------------------------- */
static int ctx_tokens(Usage &u) {
if (u.context_tokens > u.prompt_tokens) return u.context_tokens;
@@ -59,12 +56,11 @@ static bool is_invalid_assistant_err(const char *err) {
/* ----------------------------- sanitize args -------------------------- */
static std::string sanitize_arguments(std::string_view args) {
std::string f = filter_text(args);
json o = nullptr;
try { o = json::parse(f); } catch (...) { o = nullptr; }
if (o.is_object()) return f;
json w = json::object();
Json o;
if (Json::parse(f, o) && o.is_object()) return f;
Json w = Json::object();
w["invalid_raw"] = f;
return w.dump(-1, ' ', false, json::error_handler_t::replace);
return w.dump();
}
/* ----------------------------- system prompt -------------------------- */
@@ -106,20 +102,13 @@ static std::string build_system_prompt(Cfg &cfg) {
}
/* ----------------------------- json arg helpers ----------------------- */
static std::string get_str(const json &j, const char *k) {
if (j.contains(k) && j[k].is_string()) return j[k].get<std::string>();
return "";
static std::string get_str(const Json &j, const char *k) {
return j[k].str();
}
static int get_int(const json &j, const char *k, int def = 0) {
if (j.contains(k) && j[k].is_number_integer()) {
int64_t v = j[k].get<int64_t>();
if (v > INT_MAX) return INT_MAX;
if (v < INT_MIN) return INT_MIN;
return (int)v;
static int get_int(const Json &j, const char *k, int def = 0) {
return j[k].as_int(def);
}
return def;
}
static std::string tool_path_arg(const json &a) {
static std::string tool_path_arg(const Json &a) {
std::string v = get_str(a, "filePath");
if (v.empty()) v = get_str(a, "path");
return v;
@@ -197,12 +186,12 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
printf("\n");
}
std::string res; int sty = 2;
json a = nullptr;
try { a = json::parse(astr); } catch (...) { a = nullptr; }
if (!a.is_object()) {
Json a_mut;
if (!Json::parse(astr, a_mut) || !a_mut.is_object()) {
res = sfmt("[tool error: invalid JSON args for %s: parse error. Raw: %s]", tc.name.c_str(), astr.c_str());
sty = 31;
} else {
const Json &a = a_mut;
std::string fn = tc.name;
if (fn == "bash" || fn == "shell_exec") {
std::string cmd = get_str(a, "command");
@@ -234,9 +223,8 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
auto sl = fp.find_last_of('/');
if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl));
else mkdir_p(".");
std::ofstream of(fp, std::ios::binary | std::ios::trunc);
if (!of) { res = sfmt("[tool error: write %s: %s]", fp.c_str(), strerror(errno)); sty = 31; }
else { of.write(c.data(), (std::streamsize)c.size()); res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); }
if (!write_file_str(fp, c)) { res = sfmt("[tool error: write %s: %s]", fp.c_str(), strerror(errno)); sty = 31; }
else { res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); }
} else {
int werr = 0; std::string c = filter_text(cv);
res = write_file(fp, offset, del, c, &werr);
@@ -322,9 +310,9 @@ static std::string skill_prompt(std::string_view u, Cfg &cfg) {
return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
if (S_ISDIR(st.st_mode))
return sfmt("[cannot read skill \"%s\": Is a directory]", name.c_str());
std::ifstream f(path, std::ios::binary);
if (!f) return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
std::string data((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
std::string data;
if (!read_file_str(path, data))
return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
while (!data.empty() && (data.back() == '\n' || data.back() == '\r')) data.pop_back();
std::string out = data;
if (!prompt.empty()) { out += "\n\n"; out += prompt; }
@@ -394,12 +382,11 @@ int main(int argc, char **argv) {
cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
return 1;
}
std::ifstream f(p, std::ios::binary);
if (!f) {
std::string u;
if (!read_file_str(p, u)) {
cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
return 1;
}
std::string u((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
size_t a = 0; while (a < u.size() && (u[a] == ' ' || u[a] == '\n' || u[a] == '\r' || u[a] == '\t')) a++;
u = u.substr(a);
if (!u.empty() && u[0] == '!') { run_direct_shell(u.substr(1), cfg.shell_timeout); return 0; }
-25526
View File
File diff suppressed because it is too large Load Diff
+33 -77
View File
@@ -1,67 +1,17 @@
/* session.cpp — session persistence, loading, compaction. */
#include "bantam.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <dirent.h>
#include <fstream>
#include <memory>
#include <nlohmann/json.hpp>
#include <sys/stat.h>
#include <unistd.h>
using json = nlohmann::json;
static const char *compaction_prompt =
"You are now acting as a compaction engine. Summarize the preceding conversation concisely but completely, preserving all important facts, decisions, code snippets, tool outputs, errors, and current task state so work can seamlessly continue. Output only the summary.";
/* ----------------------------- messages ------------------------------- */
static void serialize_msg_full(json &obj, const Message &m) {
obj["role"] = m.role.empty() ? "" : m.role;
if (!m.content.empty()) obj["content"] = m.content;
if (!m.reasoning.empty()) obj["reasoning_content"] = m.reasoning;
if (!m.calls.empty()) {
json arr = json::array();
for (auto &tc : m.calls) {
json t;
if (!tc.id.empty()) t["id"] = tc.id;
t["type"] = "function";
json fn;
fn["name"] = tc.name.empty() ? "" : tc.name;
fn["arguments"] = tc.arguments.empty() ? "{}" : tc.arguments;
t["function"] = fn;
arr.push_back(t);
}
obj["tool_calls"] = arr;
}
if (!m.tool_call_id.empty()) obj["tool_call_id"] = m.tool_call_id;
}
static void parse_msg_full(const json &obj, Message &m) {
m = Message{};
if (!obj.is_object()) return;
if (obj.contains("role") && obj["role"].is_string()) m.role = obj["role"].get<std::string>();
else m.role = "user";
if (obj.contains("content") && obj["content"].is_string()) m.content = obj["content"].get<std::string>();
if (obj.contains("reasoning_content") && obj["reasoning_content"].is_string())
m.reasoning = obj["reasoning_content"].get<std::string>();
if (obj.contains("tool_call_id") && obj["tool_call_id"].is_string())
m.tool_call_id = obj["tool_call_id"].get<std::string>();
if (obj.contains("tool_calls") && obj["tool_calls"].is_array()) {
for (auto &tc : obj["tool_calls"]) {
if (!tc.is_object()) continue;
ToolCall t;
if (tc.contains("id") && tc["id"].is_string()) t.id = tc["id"].get<std::string>();
if (tc.contains("function") && tc["function"].is_object()) {
auto fn = tc["function"];
if (fn.contains("name") && fn["name"].is_string()) t.name = fn["name"].get<std::string>();
if (fn.contains("arguments") && fn["arguments"].is_string()) t.arguments = fn["arguments"].get<std::string>();
}
m.calls.push_back(std::move(t));
}
}
}
/* ----------------------------- paths ---------------------------------- */
static std::string home_dir() {
const char *h = std::getenv("HOME");
@@ -79,7 +29,7 @@ void save_session(const Messages &msgs, std::string_view sid,
std::string id = (!sid.empty()) ? std::string(sid) : project_id();
std::string d = sessions_dir();
std::string path = d + "/" + id + ".json";
json root = json::object();
Json root = Json::object();
root["id"] = id;
time_t now = time(nullptr);
struct tm tm_buf{};
@@ -89,17 +39,14 @@ void save_session(const Messages &msgs, std::string_view sid,
root["created"] = ts;
std::string sum = summary_of(msgs);
root["summary"] = sum;
json arr = json::array();
Json arr = Json::array();
for (auto &m : msgs.items) {
json mo;
serialize_msg_full(mo, m);
arr.push_back(mo);
}
root["messages"] = arr;
{
std::ofstream f(path, std::ios::binary);
if (f) f << root.dump(2, ' ', false, json::error_handler_t::replace);
Json mo;
serialize_message_json(mo, m);
arr.push_back(std::move(mo));
}
root["messages"] = std::move(arr);
write_file_str(path, root.dump(2));
if (out_id) *out_id = id;
if (out_summary) *out_summary = sum;
}
@@ -121,30 +68,39 @@ std::vector<Session> sessions_load() {
closedir(dir);
}
}
std::sort(files.begin(), files.end()); /* alphasort (ascending) */
for (size_t i = 1; i < files.size(); i++) {
for (size_t j = i; j > 0 && files[j - 1] > files[j]; j--) {
std::swap(files[j - 1], files[j]);
}
}
for (auto &nm : files) {
std::string full = d + "/" + nm;
std::ifstream f(full, std::ios::binary);
if (!f) continue;
std::string buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
json root;
try { root = json::parse(buf); } catch (...) { continue; }
if (!root.is_object()) continue;
std::string buf;
if (!read_file_str(full, buf)) continue;
Json root_mut;
if (!Json::parse(buf, root_mut) || !root_mut.is_object()) continue;
const Json &root = root_mut;
Session s;
if (root.contains("id") && root["id"].is_string()) s.id = root["id"].get<std::string>();
if (root.contains("created") && root["created"].is_string()) s.created = root["created"].get<std::string>();
if (root.contains("summary") && root["summary"].is_string()) s.summary = root["summary"].get<std::string>();
if (root.contains("messages") && root["messages"].is_array()) {
for (auto &mo : root["messages"]) {
Message m; parse_msg_full(mo, m); s.messages.items.push_back(std::move(m));
s.id = root["id"].str();
s.created = root["created"].str();
s.summary = root["summary"].str();
const Json &marr = root["messages"];
if (marr.is_array()) {
for (auto &mo : marr.aval) {
Message m;
parse_message_json(mo, m);
s.messages.items.push_back(std::move(m));
}
}
if (s.summary.empty()) s.summary = "(no summary)";
ss.push_back(std::move(s));
}
/* sort by id descending (newest first) */
std::sort(ss.begin(), ss.end(),
[](const Session &a, const Session &b) { return a.id > b.id; });
for (size_t i = 1; i < ss.size(); i++) {
for (size_t j = i; j > 0 && ss[j - 1].id < ss[j].id; j--) {
std::swap(ss[j - 1], ss[j]);
}
}
return ss;
}
+25 -16
View File
@@ -1,10 +1,10 @@
/* tools.cpp — bash/read/write tools and oversized result offloading. */
#include "bantam.hpp"
#include <algorithm>
#include <array>
#include <cstring>
#include <fstream>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dirent.h>
#include <fcntl.h>
#include <poll.h>
@@ -123,7 +123,11 @@ std::string read_file_or_dir(std::string_view path, int offset, int limit, int *
}
closedir(dir);
}
std::sort(raw_names.begin(), raw_names.end());
for (size_t i = 1; i < raw_names.size(); i++) {
for (size_t j = i; j > 0 && raw_names[j - 1] > raw_names[j]; j--) {
std::swap(raw_names[j - 1], raw_names[j]);
}
}
std::string b;
for (auto &nm : raw_names) {
std::string full = p + "/" + nm;
@@ -138,24 +142,29 @@ std::string read_file_or_dir(std::string_view path, int offset, int limit, int *
return b;
}
/* regular file */
std::ifstream f(p, std::ios::binary);
FILE *f = std::fopen(p.c_str(), "rb");
if (!f) { *style = 31; return sfmt("[tool error: read %s: %s]", p.c_str(), strerror(errno)); }
if (offset < 1) offset = 1;
if (limit <= 0) limit = 2000;
std::string b;
std::string line;
char *line_buf = nullptr;
size_t cap = 0;
ssize_t n;
int line_num = 0, read_count = 0;
while (std::getline(f, line)) {
while ((n = getline(&line_buf, &cap, f)) >= 0) {
line_num++;
if (line_num < offset) continue;
read_count++;
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) line.pop_back();
std::string_view lview = utf8_truncate(line, 2000);
size_t len = (size_t)n;
while (len > 0 && (line_buf[len - 1] == '\n' || line_buf[len - 1] == '\r')) len--;
std::string_view lview = utf8_truncate(std::string_view(line_buf, len), 2000);
str_appendf(b, "%d: ", line_num);
b.append(lview);
b += '\n';
if (read_count >= limit) break;
}
free(line_buf);
std::fclose(f);
if (!b.empty()) b.pop_back(); /* trailing newline */
*style = 2;
return b;
@@ -180,9 +189,10 @@ std::string write_file(std::string_view path, int offset, int del_bytes,
*err = 1;
return sfmt("[tool error: write %s: Is a directory]", p.c_str());
}
std::ifstream f(p, std::ios::binary);
if (!f) { *err = 1; return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno)); }
data.assign((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>());
if (!read_file_str(p, data)) {
*err = 1;
return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno));
}
} else {
auto slash = p.find_last_of('/');
if (slash != std::string::npos) mkdir_p(std::string_view(p).substr(0, slash));
@@ -201,10 +211,9 @@ std::string write_file(std::string_view path, int offset, int del_bytes,
nd.append(data, 0, prefix_len);
nd.append(content);
if (suffix_len) nd.append(data, end_del, suffix_len);
{
std::ofstream out(p, std::ios::binary | std::ios::trunc);
if (!out) { *err = 1; return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno)); }
out.write(nd.data(), (std::streamsize)nd.size());
if (!write_file_str(p, nd)) {
*err = 1;
return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno));
}
return sfmt("Successfully wrote %zu bytes to %s", clen, p.c_str());
}
+31
View File
@@ -324,6 +324,37 @@ int to_int_val(std::string_view s, int def) {
return (int)v;
}
bool read_file_str(std::string_view path, std::string &out) {
out.clear();
std::string p(path);
FILE *f = std::fopen(p.c_str(), "rb");
if (!f) return false;
char buf[4096];
size_t n;
while ((n = std::fread(buf, 1, sizeof buf, f)) > 0) {
out.append(buf, n);
}
bool ok = (std::ferror(f) == 0);
std::fclose(f);
return ok;
}
bool write_file_str(std::string_view path, std::string_view data) {
std::string p(path);
FILE *f = std::fopen(p.c_str(), "wb");
if (!f) return false;
size_t written = 0;
while (written < data.size()) {
size_t n = std::fwrite(data.data() + written, 1, data.size() - written, f);
if (n == 0) {
std::fclose(f);
return false;
}
written += n;
}
return std::fclose(f) == 0;
}
/* ----------------------------- signal --------------------------------- */
static void sigint_handler(int sig) {
(void)sig;