2026-10-14 08:36:39 +03:00
/* json.cpp — native RFC 8259 JSON parser/serializer, request building,
* and non-streaming response parsing. */
2026-10-14 00:01:03 +03:00
#include "bantam.hpp"
2026-10-14 08:36:39 +03:00
#include <cerrno>
#include <climits>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
2026-10-14 00:01:03 +03:00
2026-10-14 08:36:39 +03:00
/* ----------------------------- 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 ;
2026-10-14 00:01:03 +03:00
2026-10-14 08:36:39 +03:00
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 ------------------------ */
2026-10-14 00:01:03 +03:00
/* The tool definitions, identical to the Go reference implementation. */
static const char * TOOLS_JSON = R "JSON([
{ "type" : "function" , "function" : {
"name" : "bash" ,
"description" : "Executes a given bash command in a shell session, returning its output and exit code." ,
"parameters" : { "type" : "object" , "properties" : {
"command" : { "type" : "string" , "description" : "The command to execute" },
"workdir" : { "type" : "string" , "description" : "The working directory to run the command in. Defaults to the current directory." }
}, "required" : [ "command" ]}}},
{ "type" : "function" , "function" : {
"name" : "read" ,
"description" : "Read a file or directory from the local filesystem. For files, returns numbered lines prefixed as `<line>: <content>`. For directories, returns entry names with trailing slashes for subdirectories." ,
"parameters" : { "type" : "object" , "properties" : {
"filePath" : { "type" : "string" , "description" : "The path to the file or directory to read" },
"offset" : { "type" : "integer" , "description" : "The line number to start reading from (1-indexed, defaults to 1)" },
"limit" : { "type" : "integer" , "description" : "The maximum number of lines to read (defaults to 2000)" }
}, "required" : [ "filePath" ]}}},
{ "type" : "function" , "function" : {
"name" : "write" ,
"description" : "Write content to a file. If offset and del_bytes are both omitted, the entire file is overwritten with the new content; otherwise content is written at the given byte offset, optionally deleting bytes first." ,
"parameters" : { "type" : "object" , "properties" : {
"filePath" : { "type" : "string" , "description" : "The path to the file to write" },
"content" : { "type" : "string" , "description" : "The content to write to the file" },
"offset" : { "type" : "integer" , "description" : "Byte offset to start writing from. If omitted together with del_bytes, the whole file is overwritten instead. Defaults to 0 (start of file)." },
"del_bytes" : { "type" : "integer" , "description" : "Bytes to delete starting at offset. If omitted together with offset, the whole file is overwritten instead." }
}, "required" : [ "filePath" , "content" ]}}}
]) JSON ";
2026-10-14 08:36:39 +03:00
void serialize_message_json ( Json & obj , const Message & m ) {
obj = Json :: object ();
2026-10-14 00:01:03 +03:00
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 ()) {
2026-10-14 08:36:39 +03:00
Json arr = Json :: array ();
2026-10-14 00:01:03 +03:00
for ( auto & tc : m . calls ) {
2026-10-14 08:36:39 +03:00
Json t = Json :: object ();
2026-10-14 00:01:03 +03:00
if ( ! tc . id . empty ()) t [ "id" ] = tc . id ;
t [ "type" ] = "function" ;
2026-10-14 08:36:39 +03:00
Json fn = Json :: object ();
2026-10-14 00:01:03 +03:00
fn [ "name" ] = tc . name . empty () ? "" : tc . name ;
fn [ "arguments" ] = tc . arguments . empty () ? "{}" : tc . arguments ;
2026-10-14 08:36:39 +03:00
t [ "function" ] = std :: move ( fn );
arr . push_back ( std :: move ( t ));
2026-10-14 00:01:03 +03:00
}
2026-10-14 08:36:39 +03:00
obj [ "tool_calls" ] = std :: move ( arr );
2026-10-14 00:01:03 +03:00
}
if ( ! m . tool_call_id . empty ()) obj [ "tool_call_id" ] = m . tool_call_id ;
}
2026-10-14 08:36:39 +03:00
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 --------------------- */
2026-10-14 00:01:03 +03:00
std :: string build_request_json ( const Cfg & cfg , const Messages & msgs ,
bool use_tools , bool stream ) {
2026-10-14 08:36:39 +03:00
Json root = Json :: object ();
2026-10-14 00:01:03 +03:00
root [ "model" ] = cfg . model ;
root [ "temperature" ] = cfg . temperature ;
2026-10-14 08:36:39 +03:00
Json arr = Json :: array ();
2026-10-14 00:01:03 +03:00
for ( auto & m : msgs . items ) {
2026-10-14 08:36:39 +03:00
Json mo ;
serialize_message_json ( mo , m );
arr . push_back ( std :: move ( mo ));
2026-10-14 00:01:03 +03:00
}
2026-10-14 08:36:39 +03:00
root [ "messages" ] = std :: move ( arr );
2026-10-14 00:01:03 +03:00
root [ "stream" ] = stream ;
if ( use_tools ) {
2026-10-14 08:36:39 +03:00
Json tools ;
if ( Json :: parse ( TOOLS_JSON , tools )) {
root [ "tools" ] = std :: move ( tools );
}
2026-10-14 00:01:03 +03:00
}
if ( stream ) {
2026-10-14 08:36:39 +03:00
Json so = Json :: object ();
2026-10-14 00:01:03 +03:00
so [ "include_usage" ] = true ;
2026-10-14 08:36:39 +03:00
root [ "stream_options" ] = std :: move ( so );
2026-10-14 00:01:03 +03:00
}
/* Forward non-internal raw params, parsed as JSON when possible. */
for ( auto & kv : cfg . raw ) {
if ( internal_key ( kv . first )) continue ;
2026-10-14 08:36:39 +03:00
Json jv ;
if ( Json :: parse ( kv . second , jv )) {
root [ kv . first ] = std :: move ( jv );
} else {
2026-10-14 00:01:03 +03:00
root [ kv . first ] = kv . second ;
}
}
2026-10-14 08:36:39 +03:00
return root . dump ();
2026-10-14 00:01:03 +03:00
}
bool parse_nonstream_response ( std :: string_view body , Message & out , Usage & usage ) {
out = Message {};
usage = Usage {};
2026-10-14 08:36:39 +03:00
Json root_mut ;
if ( ! Json :: parse ( body , root_mut ) || ! root_mut . is_object ()) return false ;
const Json & root = root_mut ;
const Json & choices = root [ "choices" ];
if ( ! choices . is_array () || choices . empty ()) return false ;
const Json & ch = choices . at ( 0 );
if ( ! ch . is_object ()) return false ;
const Json & msg = ch [ "message" ];
if ( ! msg . is_object ()) return false ;
out . role = msg [ "role" ]. str ( "assistant" );
out . content = msg [ "content" ]. str ();
std :: string reas = msg [ "reasoning_content" ]. str ();
if ( reas . empty ()) reas = msg [ "reasoning" ]. str ();
if ( reas . empty ()) reas = msg [ "thought" ]. str ();
if ( ! reas . empty ()) out . reasoning = std :: move ( reas );
const Json & tcs = msg [ "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 ( "{}" );
2026-10-14 00:01:03 +03:00
}
2026-10-14 08:36:39 +03:00
out . calls . push_back ( std :: move ( t ));
2026-10-14 00:01:03 +03:00
}
}
2026-10-14 08:36:39 +03:00
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 ;
2026-10-14 00:01:03 +03:00
}