2025-11-17 22:05:44 +01:00
#include "common.h"
2026-06-30 16:16:44 +02:00
#include "http.h"
2025-11-17 22:05:44 +01:00
#include "server-http.h"
2025-11-24 14:41:53 +01:00
#include "server-common.h"
2026-05-16 02:02:40 +02:00
#include "ui.h"
2025-11-17 22:05:44 +01:00
#include <cpp-httplib/httplib.h>
#include <functional>
2026-05-08 15:23:04 +02:00
#include <future>
2026-05-28 14:00:25 +02:00
#include <memory>
2025-11-17 22:05:44 +01:00
#include <string>
#include <thread>
//
// HTTP implementation using cpp-httplib
//
class server_http_context :: Impl {
public :
std :: unique_ptr < httplib :: Server > srv ;
};
server_http_context :: server_http_context ()
2026-05-28 14:00:25 +02:00
: pimpl ( std :: make_unique < Impl > ())
2025-11-17 22:05:44 +01:00
{}
server_http_context ::~ server_http_context () = default ;
static void log_server_request ( const httplib :: Request & req , const httplib :: Response & res ) {
2026-01-22 19:24:37 +01:00
// skip logging requests that are regularly sent, to avoid log spam
if ( req . path == "/health"
|| req . path == "/v1/health"
|| req . path == "/models"
|| req . path == "/v1/models"
|| req . path == "/props"
|| req . path == "/metrics"
) {
2025-11-17 22:05:44 +01:00
return ;
}
// reminder: this function is not covered by httplib's exception handler; if someone does more complicated stuff, think about wrapping it in try-catch
2026-05-14 13:05:52 +03:00
SRV_TRC ( "done request: %s %s %s %d \n " , req . method . c_str (), req . path . c_str (), req . remote_addr . c_str (), res . status );
2025-11-17 22:05:44 +01:00
SRV_DBG ( "request: %s \n " , req . body . c_str ());
SRV_DBG ( "response: %s \n " , res . body . c_str ());
}
2026-05-08 15:23:04 +02:00
// For Google Cloud Platform deployment compatibility
struct gcp_params {
bool enabled ;
std :: string path_health ;
std :: string path_predict ;
int port ;
// Ref: https://docs.cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements#aip-variables
gcp_params () {
enabled = getenv ( "AIP_MODE" , "" ) == "PREDICTION" ;
path_health = getenv ( "AIP_HEALTH_ROUTE" , "" , true ); // default: using the route defined in server.cpp
path_predict = getenv ( "AIP_PREDICT_ROUTE" , "/predict" , true );
port = std :: stoi ( getenv ( "AIP_HTTP_PORT" , "8080" ));
}
static std :: string getenv ( const char * name , const std :: string & default_value , bool ensure_leading_slash = false ) {
2026-05-28 14:00:25 +02:00
const auto * value = std :: getenv ( name );
2026-05-08 15:23:04 +02:00
if ( value == nullptr || value [ 0 ] == '\0' ) {
return default_value ;
}
std :: string val = value ;
if ( ensure_leading_slash && ! val . empty () && val [ 0 ] != '/' ) {
val . insert ( val . begin (), '/' );
}
return val ;
}
};
2025-11-17 22:05:44 +01:00
bool server_http_context :: init ( const common_params & params ) {
2026-05-08 15:23:04 +02:00
const gcp_params gcp ;
2025-11-17 22:05:44 +01:00
path_prefix = params . api_prefix ;
port = params . port ;
hostname = params . hostname ;
2026-05-08 15:23:04 +02:00
if ( gcp . enabled ) {
2026-06-28 08:52:15 +03:00
SRV_TRC ( "Google Cloud Platform compat: health route = %s, predict route = %s, port = %d \n " , gcp . path_health . c_str (), gcp . path_predict . c_str (), gcp . port );
2026-05-08 15:23:04 +02:00
if ( port != gcp . port ) {
2026-05-14 13:05:52 +03:00
SRV_WRN ( "Google Cloud Platform compat: overriding server port %d with AIP_HTTP_PORT %d \n " , port , gcp . port );
2026-05-08 15:23:04 +02:00
}
port = gcp . port ;
}
2025-11-18 16:10:47 +08:00
auto & srv = pimpl -> srv ;
2025-11-17 22:05:44 +01:00
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
2026-05-28 14:00:25 +02:00
if ( ! params . ssl_file_key . empty () && ! params . ssl_file_cert . empty ()) {
2026-06-28 08:52:15 +03:00
SRV_TRC ( "running with SSL: key = %s, cert = %s \n " , params . ssl_file_key . c_str (), params . ssl_file_cert . c_str ());
2026-05-28 14:00:25 +02:00
srv = std :: make_unique < httplib :: SSLServer > (
params . ssl_file_cert . c_str (), params . ssl_file_key . c_str ()
2025-11-17 22:05:44 +01:00
);
2026-05-27 08:06:30 +03:00
is_ssl = true ;
2025-11-17 22:05:44 +01:00
} else {
2026-06-28 08:52:15 +03:00
SRV_TRC ( "%s" , "running without SSL \n " );
2026-05-28 14:00:25 +02:00
srv = std :: make_unique < httplib :: Server > ();
2025-11-17 22:05:44 +01:00
}
#else
if ( params . ssl_file_key != "" && params . ssl_file_cert != "" ) {
2026-05-14 13:05:52 +03:00
SRV_ERR ( "%s" , "the server is built without SSL support \n " );
2025-11-17 22:05:44 +01:00
return false ;
}
2025-11-18 16:10:47 +08:00
srv . reset ( new httplib :: Server ());
2025-11-17 22:05:44 +01:00
#endif
srv -> set_default_headers ({{ "Server" , "llama.cpp" }});
2026-06-13 00:10:29 +02:00
// srv->set_logger(log_server_request); // TODO @ngxson : this is too spamy, no very useful; improve it in the future
2025-11-17 22:05:44 +01:00
srv -> set_exception_handler ([]( const httplib :: Request & , httplib :: Response & res , const std :: exception_ptr & ep ) {
// this is fail-safe; exceptions should already handled by `ex_wrapper`
std :: string message ;
try {
std :: rethrow_exception ( ep );
} catch ( const std :: exception & e ) {
message = e . what ();
} catch (...) {
message = "Unknown Exception" ;
}
res . status = 500 ;
res . set_content ( message , "text/plain" );
2026-05-14 13:05:52 +03:00
SRV_ERR ( "got exception: %s \n " , message . c_str ());
2025-11-17 22:05:44 +01:00
});
srv -> set_error_handler ([]( const httplib :: Request & , httplib :: Response & res ) {
if ( res . status == 404 ) {
res . set_content (
safe_json_to_str ( json {
{ "error" , {
{ "message" , "File Not Found" },
{ "type" , "not_found_error" },
{ "code" , 404 }
}}
}),
"application/json; charset=utf-8"
);
}
// for other error codes, we skip processing here because it's already done by res->error()
});
// set timeouts and change hostname and port
srv -> set_read_timeout ( params . timeout_read );
srv -> set_write_timeout ( params . timeout_write );
2026-05-28 14:00:25 +02:00
srv -> set_socket_options ([ reuse_port = params . reuse_port ]( const socket_t sock ) {
2026-03-28 08:59:44 +01:00
httplib :: set_socket_opt ( sock , SOL_SOCKET , SO_REUSEADDR , 1 );
2026-03-28 01:12:43 +01:00
if ( reuse_port ) {
#ifdef SO_REUSEPORT
2026-03-28 08:59:44 +01:00
httplib :: set_socket_opt ( sock , SOL_SOCKET , SO_REUSEPORT , 1 );
2026-03-28 01:12:43 +01:00
#else
2026-05-14 13:05:52 +03:00
SRV_WRN ( "%s" , "SO_REUSEPORT is not supported \n " );
2026-03-28 01:12:43 +01:00
#endif
}
});
2025-11-17 22:05:44 +01:00
if ( params . api_keys . size () == 1 ) {
2026-05-28 14:00:25 +02:00
const auto key = params . api_keys [ 0 ];
const std :: string substr = key . substr ( std :: max ( static_cast < int > ( key . length () - 4 ), 0 ));
2026-06-28 08:52:15 +03:00
SRV_TRC ( "api_keys: ****%s \n " , substr . c_str ());
2025-11-17 22:05:44 +01:00
} else if ( params . api_keys . size () > 1 ) {
2026-06-28 08:52:15 +03:00
SRV_TRC ( "api_keys: %zu keys loaded \n " , params . api_keys . size ());
2025-11-17 22:05:44 +01:00
}
//
// Middlewares
//
2026-07-10 14:42:17 +02:00
// Frontend paths - all embedded UI assets
static const std :: unordered_set < std :: string > frontend_paths = []() {
std :: unordered_set < std :: string > paths { "/" };
for ( const llama_ui_asset & a : llama_ui_get_assets ()) {
paths . insert ( "/" + a . name );
}
return paths ;
}();
2026-06-13 11:51:20 +02:00
// Public endpoints - API routes plus all embedded UI assets
static const std :: unordered_set < std :: string > get_public_endpoints = []() {
2026-06-12 15:53:26 +02:00
std :: unordered_set < std :: string > endpoints {
2025-11-17 22:05:44 +01:00
"/health" ,
"/v1/health" ,
"/models" ,
"/v1/models" ,
};
2026-07-10 14:42:17 +02:00
endpoints . insert ( frontend_paths . begin (), frontend_paths . end ());
2026-06-12 15:53:26 +02:00
return endpoints ;
}();
2025-11-17 22:05:44 +01:00
2026-06-12 15:53:26 +02:00
auto middleware_validate_api_key = [ api_keys = params . api_keys ]( const httplib :: Request & req , httplib :: Response & res ) {
2025-11-17 22:05:44 +01:00
// If API key is not set, skip validation
if ( api_keys . empty ()) {
return true ;
}
2026-06-13 14:31:41 +02:00
// If path is public or a UI asset, skip validation
if ( get_public_endpoints . count ( req . path )) {
2025-11-17 22:05:44 +01:00
return true ;
}
2025-11-28 12:57:04 +01:00
// Check for API key in the Authorization header
std :: string req_api_key = req . get_header_value ( "Authorization" );
if ( req_api_key . empty ()) {
// retry with anthropic header
req_api_key = req . get_header_value ( "X-Api-Key" );
}
2025-11-17 22:05:44 +01:00
2025-11-28 12:57:04 +01:00
// remove the "Bearer " prefix if needed
2026-05-28 14:00:25 +02:00
static std :: string prefix = "Bearer " ;
2025-11-28 12:57:04 +01:00
if ( req_api_key . substr ( 0 , prefix . size ()) == prefix ) {
req_api_key = req_api_key . substr ( prefix . size ());
}
// validate the API key
if ( std :: find ( api_keys . begin (), api_keys . end (), req_api_key ) != api_keys . end ()) {
return true ; // API key is valid
2025-11-17 22:05:44 +01:00
}
// API key is invalid or not provided
res . status = 401 ;
res . set_content (
safe_json_to_str ( json {
{ "error" , {
{ "message" , "Invalid API Key" },
{ "type" , "authentication_error" },
{ "code" , 401 }
}}
}),
"application/json; charset=utf-8"
);
2026-05-14 13:05:52 +03:00
SRV_WRN ( "%s" , "unauthorized: Invalid API Key \n " );
2025-11-17 22:05:44 +01:00
return false ;
};
auto middleware_server_state = [ this ]( const httplib :: Request & req , httplib :: Response & res ) {
2026-05-28 14:00:25 +02:00
if ( ! is_ready . load ()) {
2026-07-10 14:42:17 +02:00
if ( frontend_paths . count ( req . path )) {
return true ; // frontend asset, allow it to load and show "loading"
2025-11-17 22:05:44 +01:00
}
2026-05-14 13:21:41 +02:00
// no endpoints are allowed to be accessed when the server is not ready
// this is to prevent any data races or inconsistent states
res . status = 503 ;
res . set_content (
safe_json_to_str ( json {
{ "error" , {
{ "message" , "Loading model" },
{ "type" , "unavailable_error" },
{ "code" , 503 }
}}
}),
"application/json; charset=utf-8"
);
2025-11-17 22:05:44 +01:00
return false ;
}
return true ;
};
// register server middlewares
srv -> set_pre_routing_handler ([ middleware_validate_api_key , middleware_server_state ]( const httplib :: Request & req , httplib :: Response & res ) {
res . set_header ( "Access-Control-Allow-Origin" , req . get_header_value ( "Origin" ));
// If this is OPTIONS request, skip validation because browsers don't include Authorization header
if ( req . method == "OPTIONS" ) {
res . set_header ( "Access-Control-Allow-Credentials" , "true" );
res . set_header ( "Access-Control-Allow-Methods" , "GET, POST" );
res . set_header ( "Access-Control-Allow-Headers" , "*" );
res . set_content ( "" , "text/html" ); // blank response, no data
return httplib :: Server :: HandlerResponse :: Handled ; // skip further processing
}
if ( ! middleware_server_state ( req , res )) {
return httplib :: Server :: HandlerResponse :: Handled ;
}
if ( ! middleware_validate_api_key ( req , res )) {
return httplib :: Server :: HandlerResponse :: Handled ;
}
return httplib :: Server :: HandlerResponse :: Unhandled ;
});
2026-05-28 14:00:25 +02:00
auto n_threads_http = params . n_threads_http ;
2025-11-17 22:05:44 +01:00
if ( n_threads_http < 1 ) {
2026-03-23 12:22:46 +01:00
// +4 threads for monitoring, health and some threads reserved for MCP and other tasks in the future
2026-05-28 14:00:25 +02:00
n_threads_http = std :: max ( params . n_parallel + 4 , static_cast < int32_t > ( std :: thread :: hardware_concurrency () - 1 ));
2025-11-17 22:05:44 +01:00
}
2026-06-28 08:52:15 +03:00
SRV_TRC ( "using %d threads for HTTP server \n " , n_threads_http );
2026-03-23 12:22:46 +01:00
srv -> new_task_queue = [ n_threads_http ] {
// spawn n_threads_http fixed thread (always alive), while allow up to 1024 max possible additional threads
// when n_threads_http is used, server will create new "dynamic" threads that will be destroyed after processing each request
// ref: https://github.com/yhirose/cpp-httplib/pull/2368
2026-05-28 14:00:25 +02:00
const auto max_threads = static_cast < size_t > ( n_threads_http + 1024 );
2026-03-23 12:22:46 +01:00
return new httplib :: ThreadPool ( n_threads_http , max_threads );
};
2025-11-17 22:05:44 +01:00
//
// Web UI setup
//
2026-05-16 02:02:40 +02:00
// Use new `params.ui` field (backed by old `params.webui` for compat)
if ( ! params . ui ) {
SRV_INF ( "%s" , "The UI is disabled \n " );
SRV_INF ( "%s" , "Use --ui/--no-ui (or deprecated --webui/--no-webui) to enable/disable \n " );
2025-11-17 22:05:44 +01:00
} else {
// register static assets routes
if ( ! params . public_path . empty ()) {
// Set the base directory for serving static files
2026-05-28 14:00:25 +02:00
if ( const auto is_found = srv -> set_mount_point ( params . api_prefix + "/" , params . public_path ); ! is_found ) {
2026-05-14 13:05:52 +03:00
SRV_ERR ( "static assets path not found: %s \n " , params . public_path . c_str ());
2026-05-28 14:00:25 +02:00
return false ;
2025-11-17 22:05:44 +01:00
}
} else {
2026-05-23 17:08:22 -04:00
#if defined(LLAMA_UI_HAS_ASSETS)
2026-06-13 16:57:27 +02:00
static auto handle_gzip_header = []( const httplib :: Request & req , httplib :: Response & res ) {
if ( ! llama_ui_use_gzip ()) {
// no gzip build, skip
return true ;
}
if ( req . get_header_value ( "Accept-Encoding" ). find ( "gzip" ) == std :: string :: npos ) {
res . status = 415 ; // unsupported media type
res . set_content ( "Error: gzip is not supported by this browser" , "text/plain" );
return false ;
} else {
res . set_header ( "Content-Encoding" , "gzip" );
}
return true ;
};
2026-06-13 11:51:20 +02:00
auto serve_asset_cached = []( const std :: string & name , bool isolation ) {
return [ name , isolation ]( const httplib :: Request & req , httplib :: Response & res ) {
2026-06-13 16:57:27 +02:00
if ( ! handle_gzip_header ( req , res )) {
return true ; // returns error message
}
2026-06-13 11:51:20 +02:00
const llama_ui_asset * a = llama_ui_find_asset ( name );
if ( ! a ) { res . status = 404 ; return false ; }
2026-05-28 20:21:24 +10:00
res . set_header ( "ETag" , a -> etag );
if ( const std :: string & inm = req . get_header_value ( "If-None-Match" );
2026-06-01 05:21:08 +08:00
! inm . empty () && ( inm == a -> etag || inm == std :: string ( "W/" ) + a -> etag )) {
2026-05-28 20:21:24 +10:00
res . status = 304 ;
return false ;
}
2026-06-13 11:51:20 +02:00
if ( isolation ) {
2026-05-23 17:08:22 -04:00
res . set_header ( "Cross-Origin-Embedder-Policy" , "require-corp" );
2026-06-13 11:51:20 +02:00
res . set_header ( "Cross-Origin-Opener-Policy" , "same-origin" );
2026-05-23 17:08:22 -04:00
}
2026-06-12 15:53:26 +02:00
res . set_header ( "Cache-Control" , "public, max-age=31536000, immutable" );
2026-06-13 11:51:20 +02:00
res . set_content ( reinterpret_cast < const char *> ( a -> data ), a -> size , a -> type . c_str ());
2026-05-23 17:08:22 -04:00
return false ;
};
};
2026-06-13 11:51:20 +02:00
auto serve_asset_nocache = []( const std :: string & name ) {
2026-06-13 16:57:27 +02:00
return [ name ]( const httplib :: Request & req , httplib :: Response & res ) {
if ( ! handle_gzip_header ( req , res )) {
return true ; // returns error message
}
2026-06-13 11:51:20 +02:00
const llama_ui_asset * a = llama_ui_find_asset ( name );
2026-06-12 15:53:26 +02:00
if ( ! a ) {
res . status = 404 ;
return false ;
}
res . set_header ( "Cache-Control" , "no-cache" );
2026-06-13 11:51:20 +02:00
res . set_content ( reinterpret_cast < const char *> ( a -> data ), a -> size , a -> type . c_str ());
2026-06-12 15:53:26 +02:00
return false ;
};
};
2026-06-13 14:31:41 +02:00
// main index file
srv -> Get ( params . api_prefix + "/" , serve_asset_cached ( "index.html" , true ));
srv -> Get ( params . api_prefix + "/index.html" , serve_asset_cached ( "index.html" , true ));
2026-06-13 11:51:20 +02:00
// All remaining assets registered directly from the embedded asset table.
// PWA revalidation files (sw.js, manifest, version.json) use no-cache;
// everything else is immutable.
static const std :: unordered_set < std :: string > no_cache_names = {
"sw.js" ,
"manifest.webmanifest" ,
2026-06-13 14:31:41 +02:00
"_app/version.json" ,
2026-06-13 11:51:20 +02:00
"build.json"
};
for ( const auto & a : llama_ui_get_assets ()) {
if ( a . name == "index.html" ) continue ; // served at "/" and "/index.html" above
if ( no_cache_names . count ( a . name )) {
2026-06-13 14:31:41 +02:00
SRV_DBG ( "serve nocache for %s \n " , a . name . c_str ());
2026-06-13 11:51:20 +02:00
srv -> Get ( params . api_prefix + "/" + a . name , serve_asset_nocache ( a . name ));
2026-06-12 15:53:26 +02:00
} else {
2026-06-13 11:51:20 +02:00
srv -> Get ( params . api_prefix + "/" + a . name , serve_asset_cached ( a . name , false ));
2026-06-12 15:53:26 +02:00
}
2026-06-13 11:51:20 +02:00
}
2026-06-12 15:53:26 +02:00
2026-03-27 11:25:55 -05:00
#endif
2025-11-17 22:05:44 +01:00
}
}
return true ;
}
bool server_http_context :: start () {
// Bind and listen
2026-05-28 14:00:25 +02:00
const auto & srv = pimpl -> srv ;
auto was_bound = false ;
auto is_sock = false ;
2025-11-17 22:05:44 +01:00
if ( string_ends_with ( std :: string ( hostname ), ".sock" )) {
is_sock = true ;
2026-06-28 08:52:15 +03:00
SRV_TRC ( "%s" , "setting address family to AF_UNIX \n " );
2025-11-17 22:05:44 +01:00
srv -> set_address_family ( AF_UNIX );
// bind_to_port requires a second arg, any value other than 0 should
// simply get ignored
was_bound = srv -> bind_to_port ( hostname , 8080 );
} else {
2026-06-28 08:52:15 +03:00
SRV_TRC ( "%s" , "binding port with default address family \n " );
2025-11-17 22:05:44 +01:00
// bind HTTP listen port
if ( port == 0 ) {
2026-05-28 14:00:25 +02:00
const auto bound_port = srv -> bind_to_any_port ( hostname );
2025-11-17 22:05:44 +01:00
was_bound = ( bound_port >= 0 );
if ( was_bound ) {
port = bound_port ;
}
} else {
was_bound = srv -> bind_to_port ( hostname , port );
}
}
if ( ! was_bound ) {
2026-05-14 13:05:52 +03:00
SRV_ERR ( "couldn't bind HTTP server socket, hostname: %s, port: %d \n " , hostname . c_str (), port );
2025-11-17 22:05:44 +01:00
return false ;
}
// run the HTTP server in a thread
2026-05-28 14:00:25 +02:00
thread = std :: thread ([ this ] { pimpl -> srv -> listen_after_bind (); });
2025-11-17 22:05:44 +01:00
srv -> wait_until_ready ();
2026-05-27 08:06:30 +03:00
listening_address = is_sock ? string_format ( "unix://%s" , hostname . c_str ())
2026-06-30 16:16:44 +02:00
: string_format ( "%s://%s:%d" , is_ssl ? "https" : "http" , common_http_format_host ( hostname ). c_str (), port );
2025-11-17 22:05:44 +01:00
return true ;
}
void server_http_context :: stop () const {
if ( pimpl -> srv ) {
pimpl -> srv -> stop ();
}
}
static void set_headers ( httplib :: Response & res , const std :: map < std :: string , std :: string > & headers ) {
for ( const auto & [ key , value ] : headers ) {
res . set_header ( key , value );
}
}
2026-06-26 09:31:29 +02:00
// percent-decode a path component (%XX). path params arrive raw from httplib, unlike query
// params, so a conv id like "conv::model" sent as "conv%3A%3Amodel" must be decoded here to
// match the value the client put in the X-Conversation-Id header
static std :: string decode_path_component ( const std :: string & in ) {
std :: string out ;
out . reserve ( in . size ());
for ( size_t i = 0 ; i < in . size (); i ++ ) {
if ( in [ i ] == '%' && i + 2 < in . size ()) {
auto hex = []( char c ) -> int {
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 ;
};
int hi = hex ( in [ i + 1 ]);
int lo = hex ( in [ i + 2 ]);
if ( hi >= 0 && lo >= 0 ) {
out . push_back ( char (( hi << 4 ) | lo ));
i += 2 ;
continue ;
}
}
out . push_back ( in [ i ]);
}
return out ;
}
2025-11-17 22:05:44 +01:00
static std :: map < std :: string , std :: string > get_params ( const httplib :: Request & req ) {
std :: map < std :: string , std :: string > params ;
for ( const auto & [ key , value ] : req . params ) {
params [ key ] = value ;
}
for ( const auto & [ key , value ] : req . path_params ) {
2026-06-26 09:31:29 +02:00
params [ key ] = decode_path_component ( value );
2025-11-17 22:05:44 +01:00
}
return params ;
}
static std :: map < std :: string , std :: string > get_headers ( const httplib :: Request & req ) {
std :: map < std :: string , std :: string > headers ;
for ( const auto & [ key , value ] : req . headers ) {
headers [ key ] = value ;
}
return headers ;
}
2026-02-24 21:46:06 +01:00
static std :: string build_query_string ( const httplib :: Request & req ) {
std :: string qs ;
for ( const auto & [ key , value ] : req . params ) {
if ( ! qs . empty ()) {
qs += '&' ;
}
qs += httplib :: encode_query_component ( key ) + "=" + httplib :: encode_query_component ( value );
}
return qs ;
}
2025-12-22 14:23:34 +01:00
// using unique_ptr for request to allow safe capturing in lambdas
using server_http_req_ptr = std :: unique_ptr < server_http_req > ;
static void process_handler_response ( server_http_req_ptr && request , server_http_res_ptr & response , httplib :: Response & res ) {
2025-11-17 22:05:44 +01:00
if ( response -> is_stream ()) {
res . status = response -> status ;
2026-06-19 04:01:24 +08:00
// Tell Nginx to not buffer any streamed response
response -> headers [ "X-Accel-Buffering" ] = "no" ;
2025-11-17 22:05:44 +01:00
set_headers ( res , response -> headers );
2026-05-28 14:00:25 +02:00
const std :: string content_type = response -> content_type ;
2025-11-17 22:05:44 +01:00
// convert to shared_ptr as both chunked_content_provider() and on_complete() need to use it
2026-06-26 09:31:29 +02:00
std :: shared_ptr < server_http_req > q_ptr = std :: move ( request );
std :: shared_ptr < server_http_res > r_ptr = std :: move ( response );
const auto chunked_content_provider = [ response = r_ptr ]( size_t , httplib :: DataSink & sink ) -> bool {
2025-11-17 22:05:44 +01:00
std :: string chunk ;
2026-05-28 14:00:25 +02:00
const bool has_next = response -> next ( chunk );
2025-11-17 22:05:44 +01:00
if ( ! chunk . empty ()) {
2026-04-06 13:03:02 +01:00
if ( ! sink . write ( chunk . data (), chunk . size ())) {
return false ;
}
2025-11-17 22:05:44 +01:00
SRV_DBG ( "http: streamed chunk: %s \n " , chunk . c_str ());
}
if ( ! has_next ) {
sink . done ();
SRV_DBG ( "%s" , "http: stream ended \n " );
}
return has_next ;
};
2025-12-22 14:23:34 +01:00
const auto on_complete = [ request = q_ptr , response = r_ptr ]( bool ) mutable {
2026-07-11 12:41:47 +02:00
response -> on_complete ();
response . reset ();
2026-06-26 09:31:29 +02:00
request . reset ();
2025-11-17 22:05:44 +01:00
};
res . set_chunked_content_provider ( content_type , chunked_content_provider , on_complete );
} else {
res . status = response -> status ;
set_headers ( res , response -> headers );
res . set_content ( response -> data , response -> content_type );
2026-07-11 12:41:47 +02:00
response -> on_complete ();
2025-11-17 22:05:44 +01:00
}
}
void server_http_context :: get ( const std :: string & path , const server_http_context :: handler_t & handler ) const {
2026-05-08 15:23:04 +02:00
handlers . emplace ( path , handler );
2025-11-17 22:05:44 +01:00
pimpl -> srv -> Get ( path_prefix + path , [ handler ]( const httplib :: Request & req , httplib :: Response & res ) {
2025-12-22 14:23:34 +01:00
server_http_req_ptr request = std :: make_unique < server_http_req > ( server_http_req {
2025-11-17 22:05:44 +01:00
get_params ( req ),
get_headers ( req ),
req . path ,
2026-02-24 21:46:06 +01:00
build_query_string ( req ),
2025-11-17 22:05:44 +01:00
req . body ,
2026-04-14 11:09:52 +02:00
{},
2025-11-17 22:05:44 +01:00
req . is_connection_closed
});
2025-12-22 14:23:34 +01:00
server_http_res_ptr response = handler ( * request );
process_handler_response ( std :: move ( request ), response , res );
2025-11-17 22:05:44 +01:00
});
}
void server_http_context :: post ( const std :: string & path , const server_http_context :: handler_t & handler ) const {
2026-05-08 15:23:04 +02:00
handlers . emplace ( path , handler );
2025-11-17 22:05:44 +01:00
pimpl -> srv -> Post ( path_prefix + path , [ handler ]( const httplib :: Request & req , httplib :: Response & res ) {
2026-04-14 11:09:52 +02:00
std :: string body = req . body ;
2026-04-27 23:55:00 +02:00
std :: map < std :: string , uploaded_file > files ;
2026-04-14 11:09:52 +02:00
if ( req . is_multipart_form_data ()) {
// translate text fields to a JSON object and use it as the body
json form_json = json :: object ();
for ( const auto & [ key , field ] : req . form . fields ) {
if ( form_json . contains ( key )) {
// if the key already exists, convert it to an array
if ( ! form_json [ key ]. is_array ()) {
json existing_value = form_json [ key ];
form_json [ key ] = json :: array ({ existing_value });
}
form_json [ key ]. push_back ( field . content );
} else {
form_json [ key ] = field . content ;
}
}
body = form_json . dump ();
// populate files from multipart form
for ( const auto & [ key , file ] : req . form . files ) {
2026-04-27 23:55:00 +02:00
files [ key ] = uploaded_file {
raw_buffer ( file . content . begin (), file . content . end ()),
file . filename ,
file . content_type ,
};
2026-04-14 11:09:52 +02:00
}
}
2025-12-22 14:23:34 +01:00
server_http_req_ptr request = std :: make_unique < server_http_req > ( server_http_req {
2025-11-17 22:05:44 +01:00
get_params ( req ),
get_headers ( req ),
req . path ,
2026-02-24 21:46:06 +01:00
build_query_string ( req ),
2026-04-14 11:09:52 +02:00
body ,
std :: move ( files ),
2025-11-17 22:05:44 +01:00
req . is_connection_closed
});
2025-12-22 14:23:34 +01:00
server_http_res_ptr response = handler ( * request );
2026-06-17 18:04:58 +02:00
process_handler_response ( std :: move ( request ), response , res );
});
}
void server_http_context :: del ( const std :: string & path , const server_http_context :: handler_t & handler ) const {
handlers . emplace ( path , handler );
pimpl -> srv -> Delete ( path_prefix + path , [ handler ]( const httplib :: Request & req , httplib :: Response & res ) {
server_http_req_ptr request = std :: make_unique < server_http_req > ( server_http_req {
get_params ( req ),
get_headers ( req ),
req . path ,
build_query_string ( req ),
req . body ,
{},
req . is_connection_closed
});
server_http_res_ptr response = handler ( * request );
2025-12-22 14:23:34 +01:00
process_handler_response ( std :: move ( request ), response , res );
2025-11-17 22:05:44 +01:00
});
}
2026-05-08 15:23:04 +02:00
//
// Vertex AI Prediction protocol (AIP_PREDICT_ROUTE)
// https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements
//
// Derives the camelCase @requestFormat alias for a registered path.
// e.g. "/v1/chat/completions" -> "chatCompletions", "/apply-template" -> "applyTemplate"
static std :: string path_to_gcp_format ( const std :: string & path ) {
std :: string s = path ;
if ( s . size () > 3 && s [ 0 ] == '/' && s [ 1 ] == 'v' && s [ 2 ] == '1' ) {
s = s . substr ( 3 );
}
if ( ! s . empty () && s [ 0 ] == '/' ) {
s = s . substr ( 1 );
}
std :: string result ;
bool cap = false ;
for ( unsigned char c : s ) {
if ( c == ':' ) break ; // stop before path parameters
if ( c == '/' || c == '-' || c == '_' ) {
cap = true ;
} else {
2026-05-28 14:00:25 +02:00
result += static_cast < char > ( cap ? std :: toupper ( c ) : c );
2026-05-08 15:23:04 +02:00
cap = false ;
}
}
return result ;
}
static json parse_gcp_predict_response ( const server_http_res_ptr & res ) {
if ( res == nullptr ) {
throw std :: runtime_error ( "empty response from internal handler" );
}
if ( res -> is_stream ()) {
throw std :: invalid_argument ( "predict route does not support streaming responses" );
}
if ( res -> data . empty ()) {
return nullptr ;
}
try {
return json :: parse ( res -> data );
} catch (...) {
return res -> data ;
}
}
2026-05-28 14:00:25 +02:00
void server_http_context :: register_gcp_compat () const {
2026-05-08 15:23:04 +02:00
const gcp_params gcp ;
if ( ! gcp . enabled ) {
// do nothing
return ;
}
if ( handlers . count ( gcp . path_predict )) {
2026-05-14 13:05:52 +03:00
SRV_ERR ( "AIP_PREDICT_ROUTE=%s conflicts with an existing llama-server route \n " , gcp . path_predict . c_str ());
2026-05-08 15:23:04 +02:00
exit ( 1 );
}
// camelCase alias -> canonical path (first registration wins on collision)
// e.g. "chatCompletions" -> "/v1/chat/completions"
std :: unordered_map < std :: string , std :: string > alias_to_path ;
for ( const auto & [ path , _ ] : handlers ) {
alias_to_path . emplace ( path_to_gcp_format ( path ), path );
}
if ( ! gcp . path_health . empty ()) {
2026-05-28 14:00:25 +02:00
const auto health_handler = handlers . find ( "/health" );
2026-05-08 15:23:04 +02:00
GGML_ASSERT ( health_handler != handlers . end ());
get ( gcp . path_health , health_handler -> second );
}
post ( gcp . path_predict , [ this , alias_to_path = std :: move ( alias_to_path )]( const server_http_req & req ) -> server_http_res_ptr {
static const auto build_error = []( const std :: string & message , error_type type ) -> json {
return json {{ "error" , format_error_response ( message , type )}};
};
json data ;
try {
data = json :: parse ( req . body );
} catch ( const std :: exception & e ) {
auto res = std :: make_unique < server_http_res > ();
res -> status = 400 ;
res -> data = safe_json_to_str ({{ "error" , format_error_response ( e . what (), ERROR_TYPE_INVALID_REQUEST )}});
return res ;
}
if ( ! data . is_object ()) {
auto res = std :: make_unique < server_http_res > ();
res -> status = 400 ;
res -> data = safe_json_to_str ({{ "error" , format_error_response ( "request body must be a JSON object" , ERROR_TYPE_INVALID_REQUEST )}});
return res ;
}
if ( ! data . contains ( "instances" ) || ! data . at ( "instances" ). is_array ()) {
auto res = std :: make_unique < server_http_res > ();
res -> status = 400 ;
res -> data = safe_json_to_str ({{ "error" , format_error_response ( "request body must include an array field named instances" , ERROR_TYPE_INVALID_REQUEST )}});
return res ;
}
const json & instances = data . at ( "instances" );
static const size_t MAX_INSTANCES = 128 ;
if ( instances . size () > MAX_INSTANCES ) {
auto res = std :: make_unique < server_http_res > ();
res -> status = 400 ;
res -> data = safe_json_to_str ({{ "error" , format_error_response ( "instances array exceeds maximum size of " + std :: to_string ( MAX_INSTANCES ), ERROR_TYPE_INVALID_REQUEST )}});
return res ;
}
std :: vector < std :: future < json >> futures ;
futures . reserve ( instances . size ());
for ( const auto & instance : instances ) {
futures . push_back ( std :: async ( std :: launch :: async , [ this , & req , & alias_to_path , instance ]() -> json {
if ( ! instance . is_object ()) {
return build_error ( "each instance must be a JSON object" , ERROR_TYPE_INVALID_REQUEST );
}
if ( ! instance . contains ( "@requestFormat" ) || ! instance . at ( "@requestFormat" ). is_string ()) {
return build_error ( "each instance must include a string @requestFormat" , ERROR_TYPE_INVALID_REQUEST );
}
try {
json payload = instance ;
const std :: string format = payload . at ( "@requestFormat" ). get < std :: string > ();
payload . erase ( "@requestFormat" );
if ( payload . contains ( "stream" )) {
2026-05-14 13:05:52 +03:00
SRV_WRN ( "%s" , "ignoring client-provided stream field in instance, streaming is not supported in predict route \n " );
2026-05-08 15:23:04 +02:00
payload [ "stream" ] = false ;
}
// accept both camelCase aliases (e.g. "chatCompletions") and direct paths
std :: string dispatch_path ;
auto it_alias = alias_to_path . find ( format );
if ( it_alias != alias_to_path . end ()) {
dispatch_path = it_alias -> second ;
} else if ( handlers . count ( format )) {
dispatch_path = format ;
} else {
return build_error ( "no handler registered for @requestFormat: " + format , ERROR_TYPE_INVALID_REQUEST );
}
const server_http_req internal_req {
req . params ,
req . headers ,
path_prefix + dispatch_path ,
req . query_string ,
payload . dump (),
{},
req . should_stop ,
};
server_http_res_ptr internal_res = handlers . at ( dispatch_path )( internal_req );
return parse_gcp_predict_response ( internal_res );
} catch ( const std :: invalid_argument & e ) {
return build_error ( e . what (), ERROR_TYPE_INVALID_REQUEST );
} catch ( const std :: exception & e ) {
return build_error ( e . what (), ERROR_TYPE_SERVER );
} catch (...) {
return build_error ( "unknown error" , ERROR_TYPE_SERVER );
}
}));
}
json predictions = json :: array ();
for ( auto & future : futures ) {
predictions . push_back ( future . get ());
}
auto res = std :: make_unique < server_http_res > ();
res -> data = safe_json_to_str ({{ "predictions" , predictions }});
return res ;
});
}