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:
@@ -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
|
||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
+51
-58
@@ -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;
|
||||||
|
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};
|
||||||
int cw = 0;
|
int cw = 0;
|
||||||
try {
|
for (auto &item : list.aval) {
|
||||||
auto root = json::parse(body);
|
if (cw != 0) break;
|
||||||
if (!root.is_object()) return 0;
|
if (!item.is_object()) continue;
|
||||||
json list = nullptr;
|
std::string id = item["id"].str();
|
||||||
if (root.contains("data")) list = root["data"];
|
if (!id.empty() && (id == cfg.model || strcasecmp(id.c_str(), cfg.model.c_str()) == 0)) {
|
||||||
else if (root.contains("models")) list = root["models"];
|
for (int k = 0; keys[k]; k++) {
|
||||||
if (list.is_array()) {
|
int v = item[keys[k]].as_int();
|
||||||
static const char *keys[] = {"context_window", "context_length", "max_context_length",
|
if (v > 0) { cw = v; break; }
|
||||||
"max_model_len", "context_size", "max_tokens", "max_input_tokens", nullptr};
|
|
||||||
for (auto &item : list) {
|
|
||||||
if (cw != 0) break;
|
|
||||||
if (!item.is_object() || !item.contains("id") || !item["id"].is_string()) continue;
|
|
||||||
std::string id = item["id"].get<std::string>();
|
|
||||||
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>();
|
|
||||||
if (v > 0) { cw = v; break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (...) { return 0; }
|
}
|
||||||
return cw;
|
return cw;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,29 +231,29 @@ 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"; }
|
||||||
std::string out;
|
Json root_mut;
|
||||||
try {
|
if (!Json::parse(body, root_mut) || !root_mut.is_object()) {
|
||||||
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>();
|
|
||||||
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;
|
*errout = -1;
|
||||||
return "invalid JSON from /models";
|
return "invalid JSON from /models";
|
||||||
}
|
}
|
||||||
|
const Json &root = root_mut;
|
||||||
|
const Json &list = root.contains("data") ? root["data"] : root["models"];
|
||||||
|
std::string out;
|
||||||
|
bool any = false;
|
||||||
|
if (list.is_array()) {
|
||||||
|
for (auto &item : list.aval) {
|
||||||
|
if (!item.is_object()) continue;
|
||||||
|
std::string id = item["id"].str();
|
||||||
|
if (id.empty()) continue;
|
||||||
|
any = true;
|
||||||
|
if (id == cfg.model) str_appendf(out, "* %s\n", id.c_str());
|
||||||
|
else str_appendf(out, " %s\n", id.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any) {
|
||||||
|
*errout = 0;
|
||||||
|
return "No models returned by the endpoint.";
|
||||||
|
}
|
||||||
*errout = 0;
|
*errout = 0;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
if (!tc.is_object()) continue;
|
for (auto &tc : tcs.aval) {
|
||||||
ToolCall t;
|
if (!tc.is_object()) continue;
|
||||||
t.id = json_str(tc, "id");
|
ToolCall t;
|
||||||
if (tc.contains("function") && tc["function"].is_object()) {
|
t.id = tc["id"].str();
|
||||||
auto fn = tc["function"];
|
const Json &fn = tc["function"];
|
||||||
t.name = json_str(fn, "name");
|
if (fn.is_object()) {
|
||||||
t.arguments = json_str(fn, "arguments", "{}");
|
t.name = fn["name"].str();
|
||||||
}
|
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()) {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
usage.model = json_str(root, "model");
|
|
||||||
return true;
|
|
||||||
} catch (...) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
if (usage.cached_tokens == 0) {
|
||||||
|
usage.cached_tokens = u["cached_tokens"].as_int();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
usage.model = root["model"].str();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,104 +198,95 @@ 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())
|
if (pt > 0 || tt > 0) {
|
||||||
? u["total_tokens"].get<int>() : 0;
|
sc->last_usage.prompt_tokens = pt;
|
||||||
if (pt > 0 || tt > 0) {
|
sc->last_usage.completion_tokens = u["completion_tokens"].as_int();
|
||||||
sc->last_usage.prompt_tokens = pt;
|
sc->last_usage.total_tokens = tt;
|
||||||
sc->last_usage.completion_tokens =
|
const Json &ptd = u["prompt_tokens_details"];
|
||||||
(u.contains("completion_tokens") && u["completion_tokens"].is_number_integer())
|
if (ptd.is_object()) {
|
||||||
? u["completion_tokens"].get<int>() : 0;
|
int ct_val = ptd["cached_tokens"].as_int();
|
||||||
sc->last_usage.total_tokens = tt;
|
if (ct_val > 0) sc->last_usage.cached_tokens = ct_val;
|
||||||
if (u.contains("prompt_tokens_details") && u["prompt_tokens_details"].is_object()) {
|
}
|
||||||
auto ptd = u["prompt_tokens_details"];
|
if (sc->last_usage.cached_tokens == 0) {
|
||||||
if (ptd.contains("cached_tokens") && ptd["cached_tokens"].is_number_integer())
|
int ct_val = u["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 &&
|
|
||||||
u.contains("cached_tokens") && u["cached_tokens"].is_number_integer())
|
|
||||||
sc->last_usage.cached_tokens = u["cached_tokens"].get<int>();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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())
|
if (!rc.empty()) {
|
||||||
rc = delta["reasoning"].get<std::string>();
|
stream_maybe_erase(sc);
|
||||||
if (rc.empty() && delta.contains("thought") && delta["thought"].is_string())
|
if (!sc->in_reasoning) {
|
||||||
rc = delta["thought"].get<std::string>();
|
if (!sc->content.empty()) {
|
||||||
if (!rc.empty()) {
|
|
||||||
stream_maybe_erase(sc);
|
|
||||||
if (!sc->in_reasoning) {
|
|
||||||
if (!sc->content.empty()) {
|
|
||||||
flush_tbl(sc);
|
|
||||||
if (!sc->line_buf.empty()) { render_md_line_print(sc->line_buf, sc->md); sc->line_buf.clear(); }
|
|
||||||
printf("\n");
|
|
||||||
}
|
|
||||||
cprint("--- reasoning start ---\n", std::array{36});
|
|
||||||
sc->in_reasoning = true;
|
|
||||||
}
|
|
||||||
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>();
|
|
||||||
if (!ct.empty()) {
|
|
||||||
stream_maybe_erase(sc);
|
|
||||||
if (sc->in_reasoning) { cprint("\n--- reasoning end ---\n", std::array{36}); sc->in_reasoning = false; }
|
|
||||||
sc->content += ct;
|
|
||||||
sc->line_buf += ct;
|
|
||||||
while (true) {
|
|
||||||
auto nl = sc->line_buf.find('\n');
|
|
||||||
if (nl == std::string::npos) break;
|
|
||||||
std::string cur = sc->line_buf.substr(0, nl);
|
|
||||||
sc->line_buf.erase(0, nl + 1);
|
|
||||||
if (!sc->md.in_code && !is_table_line(cur)) {
|
|
||||||
flush_tbl(sc);
|
|
||||||
render_md_line_print(cur, sc->md);
|
|
||||||
} else {
|
|
||||||
sc->tbl.push_back(cur);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (delta.contains("tool_calls") && delta["tool_calls"].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);
|
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(); }
|
||||||
|
printf("\n");
|
||||||
}
|
}
|
||||||
for (auto &tc : delta["tool_calls"]) {
|
cprint("--- reasoning start ---\n", std::array{36});
|
||||||
if (!tc.is_object()) continue;
|
sc->in_reasoning = true;
|
||||||
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>();
|
|
||||||
}
|
|
||||||
stream_upsert_tc(sc, idx, id, name, args);
|
|
||||||
}
|
|
||||||
step_live(sc);
|
|
||||||
}
|
}
|
||||||
} catch (...) {
|
cprint(rc, std::array{2});
|
||||||
return;
|
sc->reas += rc;
|
||||||
|
}
|
||||||
|
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; }
|
||||||
|
sc->content += ct;
|
||||||
|
sc->line_buf += ct;
|
||||||
|
while (true) {
|
||||||
|
auto nl = sc->line_buf.find('\n');
|
||||||
|
if (nl == std::string::npos) break;
|
||||||
|
std::string cur = sc->line_buf.substr(0, nl);
|
||||||
|
sc->line_buf.erase(0, nl + 1);
|
||||||
|
if (!sc->md.in_code && !is_table_line(cur)) {
|
||||||
|
flush_tbl(sc);
|
||||||
|
render_md_line_print(cur, sc->md);
|
||||||
|
} else {
|
||||||
|
sc->tbl.push_back(cur);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 : tcs.aval) {
|
||||||
|
if (!tc.is_object()) continue;
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+33
-77
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user