2025-11-17 22:05:44 +01:00
#include "common.h"
#include "server-http.h"
2025-11-24 14:41:53 +01:00
#include "server-common.h"
2025-11-17 22:05:44 +01:00
#include <cpp-httplib/httplib.h>
2026-05-08 15:23:04 +02:00
#include <cstdlib>
2025-11-17 22:05:44 +01:00
#include <functional>
2026-05-08 15:23:04 +02:00
#include <future>
2025-11-17 22:05:44 +01:00
#include <string>
#include <thread>
2026-03-27 11:25:55 -05:00
#ifdef LLAMA_BUILD_WEBUI
2025-11-17 22:05:44 +01:00
// auto generated files (see README.md for details)
2026-03-31 15:44:26 +02:00
#include "index.html.hpp"
#include "bundle.js.hpp"
#include "bundle.css.hpp"
2025-11-17 22:05:44 +01:00
#include "loading.html.hpp"
2026-03-27 11:25:55 -05:00
#endif
2025-11-17 22:05:44 +01:00
//
// HTTP implementation using cpp-httplib
//
class server_http_context :: Impl {
public :
std :: unique_ptr < httplib :: Server > srv ;
};
server_http_context :: server_http_context ()
: pimpl ( std :: make_unique < server_http_context :: Impl > ())
{}
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 ) {
const char * value = std :: getenv ( name );
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-05-14 13:05:52 +03:00
SRV_INF ( "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
if ( params . ssl_file_key != "" && params . ssl_file_cert != "" ) {
2026-05-14 13:05:52 +03:00
SRV_INF ( "running with SSL: key = %s, cert = %s \n " , params . ssl_file_key . c_str (), params . ssl_file_cert . c_str ());
2025-11-18 16:10:47 +08:00
srv . reset (
2025-11-17 22:05:44 +01:00
new httplib :: SSLServer ( params . ssl_file_cert . c_str (), params . ssl_file_key . c_str ())
);
} else {
2026-05-14 13:05:52 +03:00
SRV_INF ( "%s" , "running without SSL \n " );
2025-11-18 16:10:47 +08:00
srv . reset ( new 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" }});
srv -> set_logger ( log_server_request );
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-03-28 01:12:43 +01:00
srv -> set_socket_options ([ reuse_port = params . reuse_port ]( 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 ) {
auto key = params . api_keys [ 0 ];
std :: string substr = key . substr ( std :: max (( int )( key . length () - 4 ), 0 ));
2026-05-14 13:05:52 +03:00
SRV_INF ( "api_keys: ****%s \n " , substr . c_str ());
2025-11-17 22:05:44 +01:00
} else if ( params . api_keys . size () > 1 ) {
2026-05-14 13:05:52 +03:00
SRV_INF ( "api_keys: %zu keys loaded \n " , params . api_keys . size ());
2025-11-17 22:05:44 +01:00
}
//
// Middlewares
//
auto middleware_validate_api_key = [ api_keys = params . api_keys ]( const httplib :: Request & req , httplib :: Response & res ) {
static const std :: unordered_set < std :: string > public_endpoints = {
"/health" ,
"/v1/health" ,
"/models" ,
"/v1/models" ,
2026-04-01 21:32:15 +02:00
"/" ,
"/index.html" ,
"/bundle.js" ,
"/bundle.css" ,
2025-11-17 22:05:44 +01:00
};
// If API key is not set, skip validation
if ( api_keys . empty ()) {
return true ;
}
2026-04-01 21:32:15 +02:00
// If path is public or static file, skip validation
if ( public_endpoints . find ( req . path ) != public_endpoints . end ()) {
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
2025-11-17 22:05:44 +01:00
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 ) {
bool ready = is_ready . load ();
if ( ! ready ) {
2026-03-27 11:25:55 -05:00
#ifdef LLAMA_BUILD_WEBUI
2025-11-17 22:05:44 +01:00
auto tmp = string_split < std :: string > ( req . path , '.' );
if ( req . path == "/" || tmp . back () == "html" ) {
res . status = 503 ;
2025-12-22 14:23:34 +01:00
res . set_content ( reinterpret_cast < const char *> ( loading_html ), loading_html_len , "text/html; charset=utf-8" );
2026-03-27 11:25:55 -05:00
} else
#endif
{
2025-12-22 14:23:34 +01:00
// no endpoints is allowed to be accessed when the server is not ready
// this is to prevent any data races or inconsistent states
2025-11-17 22:05:44 +01:00
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"
);
}
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 ;
});
int n_threads_http = params . n_threads_http ;
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
n_threads_http = std :: max ( params . n_parallel + 4 , ( int32_t ) std :: thread :: hardware_concurrency () - 1 );
2025-11-17 22:05:44 +01:00
}
2026-05-14 13:05:52 +03:00
SRV_INF ( "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
size_t max_threads = ( size_t ) n_threads_http + 1024 ;
return new httplib :: ThreadPool ( n_threads_http , max_threads );
};
2025-11-17 22:05:44 +01:00
//
// Web UI setup
//
if ( ! params . webui ) {
2026-05-14 13:05:52 +03:00
SRV_INF ( "%s" , "the WebUI is disabled \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
bool is_found = srv -> set_mount_point ( params . api_prefix + "/" , params . public_path );
if ( ! is_found ) {
2026-05-14 13:05:52 +03:00
SRV_ERR ( "static assets path not found: %s \n " , params . public_path . c_str ());
2025-11-17 22:05:44 +01:00
return 1 ;
}
} else {
2026-03-27 11:25:55 -05:00
#ifdef LLAMA_BUILD_WEBUI
2025-11-17 22:05:44 +01:00
// using embedded static index.html
2026-03-31 15:44:26 +02:00
srv -> Get ( params . api_prefix + "/" , []( const httplib :: Request & /*req*/ , httplib :: Response & res ) {
// COEP and COOP headers, required by pyodide (python interpreter)
res . set_header ( "Cross-Origin-Embedder-Policy" , "require-corp" );
res . set_header ( "Cross-Origin-Opener-Policy" , "same-origin" );
res . set_content ( reinterpret_cast < const char *> ( index_html ), index_html_len , "text/html; charset=utf-8" );
return false ;
});
srv -> Get ( params . api_prefix + "/bundle.js" , []( const httplib :: Request & /*req*/ , httplib :: Response & res ) {
res . set_content ( reinterpret_cast < const char *> ( bundle_js ), bundle_js_len , "application/javascript; charset=utf-8" );
return false ;
});
srv -> Get ( params . api_prefix + "/bundle.css" , []( const httplib :: Request & /*req*/ , httplib :: Response & res ) {
res . set_content ( reinterpret_cast < const char *> ( bundle_css ), bundle_css_len , "text/css; charset=utf-8" );
2025-11-17 22:05:44 +01:00
return false ;
});
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
auto & srv = pimpl -> srv ;
bool was_bound = false ;
bool is_sock = false ;
if ( string_ends_with ( std :: string ( hostname ), ".sock" )) {
is_sock = true ;
2026-05-14 13:05:52 +03:00
SRV_INF ( "%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-05-14 13:05:52 +03:00
SRV_INF ( "%s" , "binding port with default address family \n " );
2025-11-17 22:05:44 +01:00
// bind HTTP listen port
if ( port == 0 ) {
int bound_port = srv -> bind_to_any_port ( hostname );
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
thread = std :: thread ([ this ]() { pimpl -> srv -> listen_after_bind (); });
srv -> wait_until_ready ();
listening_address = is_sock ? string_format ( "unix://%s" , hostname . c_str ())
: string_format ( "http://%s:%d" , hostname . c_str (), port );
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 );
}
}
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 ) {
params [ key ] = value ;
}
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 ;
set_headers ( res , response -> headers );
std :: string content_type = response -> content_type ;
// convert to shared_ptr as both chunked_content_provider() and on_complete() need to use it
2025-12-22 14:23:34 +01:00
std :: shared_ptr < server_http_req > q_ptr = std :: move ( request );
2025-11-17 22:05:44 +01:00
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 {
std :: string chunk ;
bool has_next = response -> next ( chunk );
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 {
2025-11-17 22:05:44 +01:00
response . reset (); // trigger the destruction of the response object
2025-12-22 14:23:34 +01:00
request . reset (); // trigger the destruction of the request object
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 );
}
}
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 );
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 {
result += cap ? ( char ) std :: toupper ( c ) : ( char ) c ;
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 ;
}
}
void server_http_context :: register_gcp_compat () {
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 ()) {
auto health_handler = handlers . find ( "/health" );
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 ;
});
}