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. # Bantam (C++20 port) — POSIX-compatible Makefile.
# Targets: Linux, FreeBSD, macOS, Haiku (any POSIX make: bmake, gmake, smake). # Targets: Linux, FreeBSD, macOS, Haiku (any POSIX make: bmake, gmake, smake).
# Dependencies: libcurl and GNU readline (found via standard include/lib # Dependencies: libcurl and GNU readline (found via standard include/lib
# paths) for HTTP and interactive line editing. JSON is handled by the vendored # paths) for HTTP and interactive line editing. JSON is handled natively in
# single-header nlohmann/json.hpp (no extra dependency). # json.cpp (no extra or vendored dependency).
# If your headers/libs live in a non-standard prefix, override, e.g.: # If your headers/libs live in a non-standard prefix, override, e.g.:
# make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl -lreadline" # make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl -lreadline"
# #
# 32-bit build: make 32bit # 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" # Large file support on 32-bit: make CPPFLAGS="-D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE"
CXX = c++ 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 CPPFLAGS = -I/usr/local/include -I. -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE
LDFLAGS = -s
LDLIBS = -L/usr/local/lib -lcurl -lreadline LDLIBS = -L/usr/local/lib -lcurl -lreadline
# Location of the AddressSanitizer/UBSan runtime libraries plus a replacement # 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 OBJS = main.o config.o util.o json.o llm.o tools.o session.o render.o editor.o
bantam: $(OBJS) 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 $(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 $(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 $(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 $(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 $(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 $(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 $(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 $(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 $(CXX) $(CXXFLAGS) $(CPPFLAGS) -c editor.cpp
clean: clean:
@@ -64,12 +67,12 @@ install: bantam
# optimized build. ASANDIR points the gcc driver (-B) at the runtime libs and # 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. # the replacement libasan_preinit.o, and sets an rpath so they load at run time.
debug: 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 # 32-bit build target
# Usage: make 32bit # Usage: make 32bit
# This builds a 32-bit binary with large file support enabled. # This builds a 32-bit binary with large file support enabled.
32bit: 32bit:
$(MAKE) CXXFLAGS="$(CXXFLAGS) -m32" LDFLAGS="-m32" bantam $(MAKE) CXXFLAGS="$(CXXFLAGS) -m32" LDFLAGS="$(LDFLAGS) -m32" bantam
.PHONY: clean install debug 32bit .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. `std::map`, `std::span`) instead of raw memory areas.
- Eliminate hand-rolled allocation/string code; rely on RAII so there is far - Eliminate hand-rolled allocation/string code; rely on RAII so there is far
less room for memory-allocation related vulnerabilities. less room for memory-allocation related vulnerabilities.
- Depend only on **libcurl** (HTTP), **GNU readline** (interactive line - Depend only on **libcurl** (HTTP) and **GNU readline** (interactive line
editing) and the vendored single-header **nlohmann/json** library (vendored editing); JSON parsing/serialization and Markdown rendering are implemented
under `nlohmann/json.hpp`); the unsafe json-c C API is no longer used. natively in C++20 with no external or vendored library bloat, and the unsafe
json-c C API is no longer used.
## Repository layout ## 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 util.cpp string building, UTF-8, prompt-injection filter, color, IDs, MD5
config.cpp config file parsing, env precedence, /models discovery 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 llm.cpp libcurl HTTP client, OpenCode headers, backoff, SSE streaming
tools.cpp bash/read/write tools, oversized-result offloading tools.cpp bash/read/write tools, oversized-result offloading
session.cpp message copy, session save/load, compaction 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) editor.cpp GNU readline line editor + history (+ term helpers)
main.cpp REPL, agentic loop, tool dispatch, commands main.cpp REPL, agentic loop, tool dispatch, commands
Makefile POSIX-compatible build Makefile POSIX-compatible build
nlohmann/ vendored nlohmann/json single-header (v3.12.0)
README.md this document README.md this document
``` ```
@@ -61,8 +61,8 @@ README.md this document
- A C++20 compiler (`c++` / `g++` / `clang++`) and a POSIX `make` - A C++20 compiler (`c++` / `g++` / `clang++`) and a POSIX `make`
(`bmake`, `gmake`, `smake`, …). (`bmake`, `gmake`, `smake`, …).
- **libcurl** development packages, available via the standard include / - **libcurl** development packages, available via the standard include /
library search paths. The JSON layer is a vendored single-header library search paths. The JSON layer is implemented natively in `json.cpp`
(`nlohmann/json.hpp`) and needs no separate dependency. and needs no separate dependency.
- **GNU readline** development packages (`libreadline-dev` on Debian/Ubuntu, - **GNU readline** development packages (`libreadline-dev` on Debian/Ubuntu,
`readline` on Fedora/macOS) for the interactive prompt, history and line `readline` on Fedora/macOS) for the interactive prompt, history and line
editing. On common systems: editing. On common systems:
@@ -99,7 +99,7 @@ make CPPFLAGS="-I/opt/include" LDLIBS="-L/opt/lib -lcurl"
## Building ## Building
```bash ```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 clean # remove build artifacts
make install # install to $(DESTDIR)/usr/local/bin/bantam 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 wrapped in RAII types (`CurlHeaders`, `StreamCtx`) that free themselves on
scope exit. There is no hand-rolled allocator and no manual `free` of message 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 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 unlikely. A lightweight, native RFC 8259 JSON engine (`struct Json` in
serialization instead of the unsafe json-c C API. `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 ## Credits
+68 -3
View File
@@ -11,22 +11,83 @@
* std::map, std::span) instead of raw memory areas. * std::map, std::span) instead of raw memory areas.
* - Eliminate hand-rolled allocation/string code; rely on RAII so there * - Eliminate hand-rolled allocation/string code; rely on RAII so there
* is far less room for memory-allocation related vulnerabilities. * is far less room for memory-allocation related vulnerabilities.
* - Depend only on libcurl (HTTP) plus the vendored single-header * - Depend only on libcurl (HTTP) and GNU readline (interactive line
* nlohmann/json library; the unsafe json-c C API is no longer used. * 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. */ * Feature macros: we rely on POSIX.1-2008 interfaces. */
#ifndef BANTAM_HPP #ifndef BANTAM_HPP
#define BANTAM_HPP #define BANTAM_HPP
#include <cstdarg> #include <cstdarg>
#include <cstddef>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <memory> #include <memory>
#include <span> #include <span>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <utility>
#include <vector> #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). */ /* Markdown renderer state (code-fence tracking). */
struct MdState { struct MdState {
bool in_code = false; bool in_code = false;
@@ -98,6 +159,8 @@ extern volatile int g_interrupted; /* SIGINT flag */
void setup_signal_handler(); void setup_signal_handler();
int is_terminal(int fd); int is_terminal(int fd);
int to_int_val(std::string_view s, int def); 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, ...); std::string sfmt(const char *fmt, ...);
void str_appendf(std::string &s, 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); 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, std::string build_request_json(const Cfg &cfg, const Messages &msgs,
bool use_tools, bool stream); bool use_tools, bool stream);
bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage); bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage);
+38 -45
View File
@@ -4,14 +4,10 @@
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <nlohmann/json.hpp>
#include <fstream>
#include <set> #include <set>
#include <strings.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <strings.h>
using json = nlohmann::json;
/* ----------------------------- raw map -------------------------------- */ /* ----------------------------- raw map -------------------------------- */
static void cfg_raw_set(Cfg &c, std::string_view k, std::string_view v) { 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 /* parse_cfg_file records every key it sees in *file_keys so that the
* config file can take precedence over environment variables. */ * 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) { 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); std::string content;
if (!f) return; if (!read_file_str(path, content)) return;
std::string line; size_t pos = 0, n = content.size();
while (std::getline(f, line)) { while (pos < n) {
std::string_view s(line); 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); while (!s.empty() && std::isspace((unsigned char)s.front())) s.remove_prefix(1);
if (s.empty() || s.front() == '#') continue; if (s.empty() || s.front() == '#') continue;
auto eq = s.find('='); 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) { int set_cfg(std::string_view path, std::string_view key, std::string_view val) {
std::vector<std::string> lines; std::vector<std::string> lines;
{ {
std::ifstream f(std::string(path), std::ios::binary); std::string content;
std::string content((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>()); read_file_str(path, content);
size_t i = 0, n = content.size(); size_t i = 0, n = content.size();
while (i < n) { while (i < n) {
size_t j = content.find('\n', i); 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())); 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); std::string out_buf;
if (!out) return -1; for (auto &line : lines) out_buf += line;
for (auto &line : lines) out << line; return write_file_str(path, out_buf) ? 0 : -1;
return 0;
} }
std::string config_path() { std::string config_path() {
@@ -189,31 +188,25 @@ static int query_models_context_window(Cfg &cfg) {
std::string err; std::string err;
if (http_get(cfg, url, body, code, err) != 0) return 0; if (http_get(cfg, url, body, code, err) != 0) return 0;
if (code >= 400) return 0; if (code >= 400) return 0;
int cw = 0; Json root_mut;
try { if (!Json::parse(body, root_mut) || !root_mut.is_object()) return 0;
auto root = json::parse(body); const Json &root = root_mut;
if (!root.is_object()) return 0; const Json &list = root.contains("data") ? root["data"] : root["models"];
json list = nullptr; if (!list.is_array()) return 0;
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", static const char *keys[] = {"context_window", "context_length", "max_context_length",
"max_model_len", "context_size", "max_tokens", "max_input_tokens", nullptr}; "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 (cw != 0) break;
if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue; if (!item.is_object()) continue;
std::string id = item["id"].get<std::string>(); std::string id = item["id"].str();
if (!id.empty() && (id == cfg.model || strcasecmp(id.c_str(), cfg.model.c_str()) == 0)) { if (!id.empty() && (id == cfg.model || strcasecmp(id.c_str(), cfg.model.c_str()) == 0)) {
for (int k = 0; keys[k]; k++) { for (int k = 0; keys[k]; k++) {
if (item.contains(keys[k]) && item[keys[k]].is_number_integer()) { int v = item[keys[k]].as_int();
int v = item[keys[k]].get<int>();
if (v > 0) { cw = v; break; } if (v > 0) { cw = v; break; }
} }
} }
} }
}
}
} catch (...) { return 0; }
return cw; return cw;
} }
@@ -238,28 +231,28 @@ std::string list_models(Cfg &cfg, int *errout) {
std::string err; std::string err;
if (http_get(cfg, url, body, code, err) != 0) { *errout = -1; return err.empty() ? "request failed" : 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"; } 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; 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; bool any = false;
if (list.is_array()) { if (list.is_array()) {
for (auto &item : list) { for (auto &item : list.aval) {
if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue; if (!item.is_object()) continue;
std::string id = item["id"].get<std::string>(); std::string id = item["id"].str();
if (id.empty()) continue; if (id.empty()) continue;
any = true; any = true;
if (id == cfg.model) str_appendf(out, "* %s\n", id.c_str()); if (id == cfg.model) str_appendf(out, "* %s\n", id.c_str());
else 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."; if (!any) {
} catch (...) { *errout = 0;
*errout = -1; return "No models returned by the endpoint.";
return "invalid JSON from /models";
} }
*errout = 0; *errout = 0;
return out; 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 "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. */ /* The tool definitions, identical to the Go reference implementation. */
static const char *TOOLS_JSON = R"JSON([ static const char *TOOLS_JSON = R"JSON([
{"type":"function","function":{ {"type":"function","function":{
@@ -32,120 +500,134 @@ static const char *TOOLS_JSON = R"JSON([
},"required":["filePath","content"]}}} },"required":["filePath","content"]}}}
])JSON"; ])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; obj["role"] = m.role.empty() ? "" : m.role;
if (!m.content.empty()) obj["content"] = m.content; if (!m.content.empty()) obj["content"] = m.content;
if (!m.reasoning.empty()) obj["reasoning_content"] = m.reasoning; if (!m.reasoning.empty()) obj["reasoning_content"] = m.reasoning;
if (!m.calls.empty()) { if (!m.calls.empty()) {
json arr = json::array(); Json arr = Json::array();
for (auto &tc : m.calls) { for (auto &tc : m.calls) {
json t; Json t = Json::object();
if (!tc.id.empty()) t["id"] = tc.id; if (!tc.id.empty()) t["id"] = tc.id;
t["type"] = "function"; t["type"] = "function";
json fn; Json fn = Json::object();
fn["name"] = tc.name.empty() ? "" : tc.name; fn["name"] = tc.name.empty() ? "" : tc.name;
fn["arguments"] = tc.arguments.empty() ? "{}" : tc.arguments; fn["arguments"] = tc.arguments.empty() ? "{}" : tc.arguments;
t["function"] = fn; t["function"] = std::move(fn);
arr.push_back(t); 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; 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, std::string build_request_json(const Cfg &cfg, const Messages &msgs,
bool use_tools, bool stream) { bool use_tools, bool stream) {
json root = json::object(); Json root = Json::object();
root["model"] = cfg.model; root["model"] = cfg.model;
root["temperature"] = cfg.temperature; root["temperature"] = cfg.temperature;
json arr = json::array(); Json arr = Json::array();
for (auto &m : msgs.items) { for (auto &m : msgs.items) {
json mo; Json mo;
serialize_message(mo, m); serialize_message_json(mo, m);
arr.push_back(mo); arr.push_back(std::move(mo));
} }
root["messages"] = arr; root["messages"] = std::move(arr);
root["stream"] = stream; root["stream"] = stream;
if (use_tools) { if (use_tools) {
try { Json tools;
json tools = json::parse(TOOLS_JSON); if (Json::parse(TOOLS_JSON, tools)) {
root["tools"] = tools; root["tools"] = std::move(tools);
} catch (...) {} }
} }
if (stream) { if (stream) {
json so = json::object(); Json so = Json::object();
so["include_usage"] = true; so["include_usage"] = true;
root["stream_options"] = so; root["stream_options"] = std::move(so);
} }
/* Forward non-internal raw params, parsed as JSON when possible. */ /* Forward non-internal raw params, parsed as JSON when possible. */
for (auto &kv : cfg.raw) { for (auto &kv : cfg.raw) {
if (internal_key(kv.first)) continue; if (internal_key(kv.first)) continue;
try { Json jv;
json jv = json::parse(kv.second); if (Json::parse(kv.second, jv)) {
root[kv.first] = jv; root[kv.first] = std::move(jv);
} catch (...) { } else {
root[kv.first] = kv.second; root[kv.first] = kv.second;
} }
} }
return root.dump(-1, ' ', false, json::error_handler_t::replace); return root.dump();
}
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;
} }
bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage) { bool parse_nonstream_response(std::string_view body, Message &out, Usage &usage) {
out = Message{}; out = Message{};
usage = Usage{}; usage = Usage{};
try { Json root_mut;
auto root = json::parse(body); if (!Json::parse(body, root_mut) || !root_mut.is_object()) return false;
if (!root.is_object() || !root.contains("choices") || const Json &root = root_mut;
!root["choices"].is_array() || root["choices"].empty()) const Json &choices = root["choices"];
return false; if (!choices.is_array() || choices.empty()) return false;
auto ch = root["choices"][0]; const Json &ch = choices.at(0);
if (!ch.is_object() || !ch.contains("message") || !ch["message"].is_object()) if (!ch.is_object()) return false;
return false; const Json &msg = ch["message"];
auto msg = ch["message"]; if (!msg.is_object()) return false;
out.role = json_str(msg, "role", "assistant"); out.role = msg["role"].str("assistant");
out.content = json_str(msg, "content"); out.content = msg["content"].str();
std::string reas = json_str(msg, "reasoning_content"); std::string reas = msg["reasoning_content"].str();
if (reas.empty()) reas = json_str(msg, "reasoning"); if (reas.empty()) reas = msg["reasoning"].str();
if (reas.empty()) reas = json_str(msg, "thought"); if (reas.empty()) reas = msg["thought"].str();
if (!reas.empty()) out.reasoning = std::move(reas); if (!reas.empty()) out.reasoning = std::move(reas);
if (msg.contains("tool_calls") && msg["tool_calls"].is_array()) { const Json &tcs = msg["tool_calls"];
for (auto &tc : msg["tool_calls"]) { if (tcs.is_array()) {
for (auto &tc : tcs.aval) {
if (!tc.is_object()) continue; if (!tc.is_object()) continue;
ToolCall t; ToolCall t;
t.id = json_str(tc, "id"); t.id = tc["id"].str();
if (tc.contains("function") && tc["function"].is_object()) { const Json &fn = tc["function"];
auto fn = tc["function"]; if (fn.is_object()) {
t.name = json_str(fn, "name"); t.name = fn["name"].str();
t.arguments = json_str(fn, "arguments", "{}"); t.arguments = fn["arguments"].str("{}");
} }
out.calls.push_back(std::move(t)); out.calls.push_back(std::move(t));
} }
} }
if (root.contains("usage") && root["usage"].is_object()) { const Json &u = root["usage"];
auto u = root["usage"]; if (u.is_object()) {
usage.prompt_tokens = json_int(u, "prompt_tokens"); usage.prompt_tokens = u["prompt_tokens"].as_int();
usage.completion_tokens = json_int(u, "completion_tokens"); usage.completion_tokens = u["completion_tokens"].as_int();
usage.total_tokens = json_int(u, "total_tokens"); usage.total_tokens = u["total_tokens"].as_int();
if (u.contains("prompt_tokens_details") && u["prompt_tokens_details"].is_object()) const Json &ptd = u["prompt_tokens_details"];
usage.cached_tokens = json_int(u["prompt_tokens_details"], "cached_tokens"); if (ptd.is_object()) {
if (usage.cached_tokens == 0) usage.cached_tokens = ptd["cached_tokens"].as_int();
usage.cached_tokens = json_int(u, "cached_tokens");
} }
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; return true;
} catch (...) {
return false;
}
} }
+47 -57
View File
@@ -1,16 +1,14 @@
/* llm.cpp — libcurl HTTP client, Fibonacci backoff, /* llm.cpp — libcurl HTTP client, Fibonacci backoff,
* streaming SSE parser and the llm() dispatch function. */ * streaming SSE parser and the llm() dispatch function. */
#include "bantam.hpp" #include "bantam.hpp"
#include <unistd.h>
#include <array> #include <array>
#include <chrono> #include <cstdio>
#include <thread> #include <cstdlib>
#include <cstring> #include <cstring>
#include <ctime>
#include <curl/curl.h> #include <curl/curl.h>
#include <memory> #include <memory>
#include <nlohmann/json.hpp> #include <unistd.h>
using json = nlohmann::json;
/* ----------------------------- memory callback ------------------------ */ /* ----------------------------- memory callback ------------------------ */
static size_t mem_cb(char *ptr, size_t sz, size_t nm, void *ud) { 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) { static void stream_handle_data(StreamCtx *sc, const std::string &data) {
if (data == "[DONE]") { sc->done = 1; return; } if (data == "[DONE]") { sc->done = 1; return; }
try { Json d_mut;
json d = json::parse(data); if (!Json::parse(data, d_mut) || !d_mut.is_object()) return;
if (!d.is_object()) return; const Json &d = d_mut;
if (d.contains("usage") && d["usage"].is_object()) { const Json &u = d["usage"];
auto u = d["usage"]; if (u.is_object()) {
int pt = (u.contains("prompt_tokens") && u["prompt_tokens"].is_number_integer()) int pt = u["prompt_tokens"].as_int();
? u["prompt_tokens"].get<int>() : 0; int tt = u["total_tokens"].as_int();
int tt = (u.contains("total_tokens") && u["total_tokens"].is_number_integer())
? u["total_tokens"].get<int>() : 0;
if (pt > 0 || tt > 0) { if (pt > 0 || tt > 0) {
sc->last_usage.prompt_tokens = pt; sc->last_usage.prompt_tokens = pt;
sc->last_usage.completion_tokens = sc->last_usage.completion_tokens = u["completion_tokens"].as_int();
(u.contains("completion_tokens") && u["completion_tokens"].is_number_integer())
? u["completion_tokens"].get<int>() : 0;
sc->last_usage.total_tokens = tt; sc->last_usage.total_tokens = tt;
if (u.contains("prompt_tokens_details") && u["prompt_tokens_details"].is_object()) { const Json &ptd = u["prompt_tokens_details"];
auto ptd = u["prompt_tokens_details"]; if (ptd.is_object()) {
if (ptd.contains("cached_tokens") && ptd["cached_tokens"].is_number_integer()) int ct_val = ptd["cached_tokens"].as_int();
sc->last_usage.cached_tokens = ptd["cached_tokens"].get<int>(); if (ct_val > 0) sc->last_usage.cached_tokens = ct_val;
} }
if (sc->last_usage.cached_tokens == 0 && if (sc->last_usage.cached_tokens == 0) {
u.contains("cached_tokens") && u["cached_tokens"].is_number_integer()) int ct_val = u["cached_tokens"].as_int();
sc->last_usage.cached_tokens = u["cached_tokens"].get<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["model"].is_string()) {
if (!d.contains("choices") || !d["choices"].is_array() || d["choices"].empty()) return; sc->last_usage.model = d["model"].str();
auto ch = d["choices"][0]; }
if (!ch.is_object() || !ch.contains("delta") || !ch["delta"].is_object()) return; const Json &choices = d["choices"];
auto delta = ch["delta"]; 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; std::string rc = delta["reasoning_content"].str();
if (delta.contains("reasoning_content") && delta["reasoning_content"].is_string()) if (rc.empty()) rc = delta["reasoning"].str();
rc = delta["reasoning_content"].get<std::string>(); if (rc.empty()) rc = delta["thought"].str();
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>();
if (!rc.empty()) { if (!rc.empty()) {
stream_maybe_erase(sc); stream_maybe_erase(sc);
if (!sc->in_reasoning) { if (!sc->in_reasoning) {
@@ -253,9 +247,7 @@ static void stream_handle_data(StreamCtx *sc, const std::string &data) {
cprint(rc, std::array{2}); cprint(rc, std::array{2});
sc->reas += rc; sc->reas += rc;
} }
std::string ct; std::string ct = delta["content"].str();
if (delta.contains("content") && delta["content"].is_string())
ct = delta["content"].get<std::string>();
if (!ct.empty()) { if (!ct.empty()) {
stream_maybe_erase(sc); stream_maybe_erase(sc);
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; } 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); stream_maybe_erase(sc);
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; } if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
if (sc->live_idx == -1) { if (sc->live_idx == -1) {
flush_tbl(sc); flush_tbl(sc);
if (!sc->line_buf.empty()) { render_md_line_print(sc->line_buf, sc->md); sc->line_buf.clear(); } 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; if (!tc.is_object()) continue;
int idx = (tc.contains("index") && tc["index"].is_number_integer()) int idx = tc["index"].as_int();
? tc["index"].get<int>() : 0; std::string id = tc["id"].str();
std::string id, name, args; std::string name, args;
if (tc.contains("id") && tc["id"].is_string()) id = tc["id"].get<std::string>(); const Json &fn = tc["function"];
if (tc.contains("function") && tc["function"].is_object()) { if (fn.is_object()) {
auto fn = tc["function"]; name = fn["name"].str();
if (fn.contains("name") && fn["name"].is_string()) name = fn["name"].get<std::string>(); args = fn["arguments"].str();
if (fn.contains("arguments") && fn["arguments"].is_string()) args = fn["arguments"].get<std::string>();
} }
stream_upsert_tc(sc, idx, id, name, args); stream_upsert_tc(sc, idx, id, name, args);
} }
step_live(sc); step_live(sc);
} }
} catch (...) {
return;
}
} }
static int stream_feed(StreamCtx *sc, const char *data, size_t len, 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() ---------------------------------- */ /* ----------------------------- llm() ---------------------------------- */
static void sleep_interruptible(int secs, volatile int *interrupted) { static void sleep_interruptible(int secs, volatile int *interrupted) {
struct timespec ts{0, 100000000L};
for (int i = 0; i < secs * 10; i++) { for (int i = 0; i < secs * 10; i++) {
if (*interrupted) return; 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_POSTFIELDSIZE, (long)payload.size());
curl_easy_setopt(curl.c, CURLOPT_HTTPHEADER, headers.list); curl_easy_setopt(curl.c, CURLOPT_HTTPHEADER, headers.list);
set_proxy(curl.c); set_proxy(curl.c);
curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, cfg.timeout > 0 ? cfg.timeout : 30); curl_easy_setopt(curl.c, CURLOPT_CONNECTTIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 30));
if (!stream) curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, cfg.timeout > 0 ? cfg.timeout : 300); if (!stream) curl_easy_setopt(curl.c, CURLOPT_TIMEOUT, (long)(cfg.timeout > 0 ? cfg.timeout : 300));
int attempt, rc = -1; int attempt, rc = -1;
for (attempt = 0; attempt <= nf; attempt++) { for (attempt = 0; attempt <= nf; attempt++) {
+20 -33
View File
@@ -2,21 +2,18 @@
#include "bantam.hpp" #include "bantam.hpp"
#include <array> #include <array>
#include <cctype> #include <cctype>
#include <cerrno>
#include <climits> #include <climits>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
#include <curl/curl.h> #include <curl/curl.h>
#include <dirent.h> #include <dirent.h>
#include <fstream>
#include <nlohmann/json.hpp>
#include <span> #include <span>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
#include <vector> #include <vector>
using json = nlohmann::json;
/* ----------------------------- usage / pct ---------------------------- */ /* ----------------------------- usage / pct ---------------------------- */
static int ctx_tokens(Usage &u) { static int ctx_tokens(Usage &u) {
if (u.context_tokens > u.prompt_tokens) return u.context_tokens; 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 -------------------------- */ /* ----------------------------- sanitize args -------------------------- */
static std::string sanitize_arguments(std::string_view args) { static std::string sanitize_arguments(std::string_view args) {
std::string f = filter_text(args); std::string f = filter_text(args);
json o = nullptr; Json o;
try { o = json::parse(f); } catch (...) { o = nullptr; } if (Json::parse(f, o) && o.is_object()) return f;
if (o.is_object()) return f; Json w = Json::object();
json w = json::object();
w["invalid_raw"] = f; w["invalid_raw"] = f;
return w.dump(-1, ' ', false, json::error_handler_t::replace); return w.dump();
} }
/* ----------------------------- system prompt -------------------------- */ /* ----------------------------- system prompt -------------------------- */
@@ -106,20 +102,13 @@ static std::string build_system_prompt(Cfg &cfg) {
} }
/* ----------------------------- json arg helpers ----------------------- */ /* ----------------------------- json arg helpers ----------------------- */
static std::string get_str(const json &j, const char *k) { 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 j[k].str();
return "";
} }
static int get_int(const json &j, const char *k, int def = 0) { static int get_int(const Json &j, const char *k, int def = 0) {
if (j.contains(k) && j[k].is_number_integer()) { return j[k].as_int(def);
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;
}
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"); std::string v = get_str(a, "filePath");
if (v.empty()) v = get_str(a, "path"); if (v.empty()) v = get_str(a, "path");
return v; return v;
@@ -197,12 +186,12 @@ static int AL(Cfg &cfg, Messages &msgs, Usage &out, volatile int *interrupted, s
printf("\n"); printf("\n");
} }
std::string res; int sty = 2; std::string res; int sty = 2;
json a = nullptr; Json a_mut;
try { a = json::parse(astr); } catch (...) { a = nullptr; } if (!Json::parse(astr, a_mut) || !a_mut.is_object()) {
if (!a.is_object()) {
res = sfmt("[tool error: invalid JSON args for %s: parse error. Raw: %s]", tc.name.c_str(), astr.c_str()); res = sfmt("[tool error: invalid JSON args for %s: parse error. Raw: %s]", tc.name.c_str(), astr.c_str());
sty = 31; sty = 31;
} else { } else {
const Json &a = a_mut;
std::string fn = tc.name; std::string fn = tc.name;
if (fn == "bash" || fn == "shell_exec") { if (fn == "bash" || fn == "shell_exec") {
std::string cmd = get_str(a, "command"); 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('/'); auto sl = fp.find_last_of('/');
if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl)); if (sl != std::string::npos) mkdir_p(std::string_view(fp).substr(0, sl));
else mkdir_p("."); else mkdir_p(".");
std::ofstream of(fp, std::ios::binary | std::ios::trunc); if (!write_file_str(fp, c)) { res = sfmt("[tool error: write %s: %s]", fp.c_str(), strerror(errno)); sty = 31; }
if (!of) { 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 { of.write(c.data(), (std::streamsize)c.size()); res = sfmt("Successfully wrote %zu bytes to %s", c.size(), fp.c_str()); }
} else { } else {
int werr = 0; std::string c = filter_text(cv); int werr = 0; std::string c = filter_text(cv);
res = write_file(fp, offset, del, c, &werr); 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)); return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
if (S_ISDIR(st.st_mode)) if (S_ISDIR(st.st_mode))
return sfmt("[cannot read skill \"%s\": Is a directory]", name.c_str()); return sfmt("[cannot read skill \"%s\": Is a directory]", name.c_str());
std::ifstream f(path, std::ios::binary); std::string data;
if (!f) return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno)); if (!read_file_str(path, data))
std::string data((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>()); return sfmt("[cannot read skill \"%s\": %s]", name.c_str(), strerror(errno));
while (!data.empty() && (data.back() == '\n' || data.back() == '\r')) data.pop_back(); while (!data.empty() && (data.back() == '\n' || data.back() == '\r')) data.pop_back();
std::string out = data; std::string out = data;
if (!prompt.empty()) { out += "\n\n"; out += prompt; } 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}); cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
return 1; return 1;
} }
std::ifstream f(p, std::ios::binary); std::string u;
if (!f) { if (!read_file_str(p, u)) {
cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31}); cprint("Error: file '", std::array{31}); cprint(p, std::array{31}); cprint("' not found.\n", std::array{31});
return 1; 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++; size_t a = 0; while (a < u.size() && (u[a] == ' ' || u[a] == '\n' || u[a] == '\r' || u[a] == '\t')) a++;
u = u.substr(a); u = u.substr(a);
if (!u.empty() && u[0] == '!') { run_direct_shell(u.substr(1), cfg.shell_timeout); return 0; } 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. */ /* session.cpp — session persistence, loading, compaction. */
#include "bantam.hpp" #include "bantam.hpp"
#include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime> #include <ctime>
#include <dirent.h> #include <dirent.h>
#include <fstream>
#include <memory> #include <memory>
#include <nlohmann/json.hpp>
#include <sys/stat.h> #include <sys/stat.h>
#include <unistd.h> #include <unistd.h>
using json = nlohmann::json;
static const char *compaction_prompt = 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."; "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 ---------------------------------- */ /* ----------------------------- paths ---------------------------------- */
static std::string home_dir() { static std::string home_dir() {
const char *h = std::getenv("HOME"); 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 id = (!sid.empty()) ? std::string(sid) : project_id();
std::string d = sessions_dir(); std::string d = sessions_dir();
std::string path = d + "/" + id + ".json"; std::string path = d + "/" + id + ".json";
json root = json::object(); Json root = Json::object();
root["id"] = id; root["id"] = id;
time_t now = time(nullptr); time_t now = time(nullptr);
struct tm tm_buf{}; struct tm tm_buf{};
@@ -89,17 +39,14 @@ void save_session(const Messages &msgs, std::string_view sid,
root["created"] = ts; root["created"] = ts;
std::string sum = summary_of(msgs); std::string sum = summary_of(msgs);
root["summary"] = sum; root["summary"] = sum;
json arr = json::array(); Json arr = Json::array();
for (auto &m : msgs.items) { for (auto &m : msgs.items) {
json mo; Json mo;
serialize_msg_full(mo, m); serialize_message_json(mo, m);
arr.push_back(mo); arr.push_back(std::move(mo));
}
root["messages"] = arr;
{
std::ofstream f(path, std::ios::binary);
if (f) f << root.dump(2, ' ', false, json::error_handler_t::replace);
} }
root["messages"] = std::move(arr);
write_file_str(path, root.dump(2));
if (out_id) *out_id = id; if (out_id) *out_id = id;
if (out_summary) *out_summary = sum; if (out_summary) *out_summary = sum;
} }
@@ -121,30 +68,39 @@ std::vector<Session> sessions_load() {
closedir(dir); 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) { for (auto &nm : files) {
std::string full = d + "/" + nm; std::string full = d + "/" + nm;
std::ifstream f(full, std::ios::binary); std::string buf;
if (!f) continue; if (!read_file_str(full, buf)) continue;
std::string buf((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>()); Json root_mut;
json root; if (!Json::parse(buf, root_mut) || !root_mut.is_object()) continue;
try { root = json::parse(buf); } catch (...) { continue; } const Json &root = root_mut;
if (!root.is_object()) continue;
Session s; Session s;
if (root.contains("id") && root["id"].is_string()) s.id = root["id"].get<std::string>(); s.id = root["id"].str();
if (root.contains("created") && root["created"].is_string()) s.created = root["created"].get<std::string>(); s.created = root["created"].str();
if (root.contains("summary") && root["summary"].is_string()) s.summary = root["summary"].get<std::string>(); s.summary = root["summary"].str();
if (root.contains("messages") && root["messages"].is_array()) { const Json &marr = root["messages"];
for (auto &mo : root["messages"]) { if (marr.is_array()) {
Message m; parse_msg_full(mo, m); s.messages.items.push_back(std::move(m)); 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)"; if (s.summary.empty()) s.summary = "(no summary)";
ss.push_back(std::move(s)); ss.push_back(std::move(s));
} }
/* sort by id descending (newest first) */ /* sort by id descending (newest first) */
std::sort(ss.begin(), ss.end(), for (size_t i = 1; i < ss.size(); i++) {
[](const Session &a, const Session &b) { return a.id > b.id; }); for (size_t j = i; j > 0 && ss[j - 1].id < ss[j].id; j--) {
std::swap(ss[j - 1], ss[j]);
}
}
return ss; return ss;
} }
+25 -16
View File
@@ -1,10 +1,10 @@
/* tools.cpp — bash/read/write tools and oversized result offloading. */ /* tools.cpp — bash/read/write tools and oversized result offloading. */
#include "bantam.hpp" #include "bantam.hpp"
#include <algorithm>
#include <array> #include <array>
#include <cstring>
#include <fstream>
#include <cerrno> #include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <dirent.h> #include <dirent.h>
#include <fcntl.h> #include <fcntl.h>
#include <poll.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); 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; std::string b;
for (auto &nm : raw_names) { for (auto &nm : raw_names) {
std::string full = p + "/" + nm; 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; return b;
} }
/* regular file */ /* 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 (!f) { *style = 31; return sfmt("[tool error: read %s: %s]", p.c_str(), strerror(errno)); }
if (offset < 1) offset = 1; if (offset < 1) offset = 1;
if (limit <= 0) limit = 2000; if (limit <= 0) limit = 2000;
std::string b; std::string b;
std::string line; char *line_buf = nullptr;
size_t cap = 0;
ssize_t n;
int line_num = 0, read_count = 0; int line_num = 0, read_count = 0;
while (std::getline(f, line)) { while ((n = getline(&line_buf, &cap, f)) >= 0) {
line_num++; line_num++;
if (line_num < offset) continue; if (line_num < offset) continue;
read_count++; read_count++;
while (!line.empty() && (line.back() == '\n' || line.back() == '\r')) line.pop_back(); size_t len = (size_t)n;
std::string_view lview = utf8_truncate(line, 2000); 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); str_appendf(b, "%d: ", line_num);
b.append(lview); b.append(lview);
b += '\n'; b += '\n';
if (read_count >= limit) break; if (read_count >= limit) break;
} }
free(line_buf);
std::fclose(f);
if (!b.empty()) b.pop_back(); /* trailing newline */ if (!b.empty()) b.pop_back(); /* trailing newline */
*style = 2; *style = 2;
return b; return b;
@@ -180,9 +189,10 @@ std::string write_file(std::string_view path, int offset, int del_bytes,
*err = 1; *err = 1;
return sfmt("[tool error: write %s: Is a directory]", p.c_str()); return sfmt("[tool error: write %s: Is a directory]", p.c_str());
} }
std::ifstream f(p, std::ios::binary); if (!read_file_str(p, data)) {
if (!f) { *err = 1; return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno)); } *err = 1;
data.assign((std::istreambuf_iterator<char>(f)), std::istreambuf_iterator<char>()); return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno));
}
} else { } else {
auto slash = p.find_last_of('/'); auto slash = p.find_last_of('/');
if (slash != std::string::npos) mkdir_p(std::string_view(p).substr(0, slash)); 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(data, 0, prefix_len);
nd.append(content); nd.append(content);
if (suffix_len) nd.append(data, end_del, suffix_len); if (suffix_len) nd.append(data, end_del, suffix_len);
{ if (!write_file_str(p, nd)) {
std::ofstream out(p, std::ios::binary | std::ios::trunc); *err = 1;
if (!out) { *err = 1; return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno)); } return sfmt("[tool error: write %s: %s]", p.c_str(), strerror(errno));
out.write(nd.data(), (std::streamsize)nd.size());
} }
return sfmt("Successfully wrote %zu bytes to %s", clen, p.c_str()); 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; 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 --------------------------------- */ /* ----------------------------- signal --------------------------------- */
static void sigint_handler(int sig) { static void sigint_handler(int sig) {
(void)sig; (void)sig;