char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
ee0ea03ad
char/aliceai-t5-moe
1#include "common.h" 2#include "http.h" 3#include "server-http.h" 4#include "server-common.h" 5#include "ui.h" 6 7#include <cpp-httplib/httplib.h> 8 9#include <functional> 10#include <future> 11#include <memory> 12#include <string> 13#include <thread> 14 15// 16// HTTP implementation using cpp-httplib 17// 18 19class server_http_context::Impl { 20public : 21 std::unique_ptr < httplib::Server > srv ; 22}; 23 24server_http_context::server_http_context () 25 :pimpl (std::make_unique < Impl > ()) 26{} 27 28server_http_context::~server_http_context ()= default ; 29 30static void log_server_request (const httplib::Request & req ,const httplib::Response & res ) { 31// skip logging requests that are regularly sent, to avoid log spam 32if (req .path == "/health" 33|| req .path == "/v1/health" 34|| req .path == "/models" 35|| req .path == "/v1/models" 36|| req .path == "/props" 37|| req .path == "/metrics" 38 ) { 39return ; 40 } 41 42// reminder: this function is not covered by httplib's exception handler; if someone does more complicated stuff, think about wrapping it in try-catch 43 44SRV_TRC ("done request: %s %s %s %d\n" ,req .method .c_str (),req .path .c_str (),req .remote_addr .c_str (),res .status ); 45 46SRV_DBG ("request: %s\n" ,req .body .c_str ()); 47SRV_DBG ("response: %s\n" ,res .body .c_str ()); 48} 49 50// returns true if the Origin header value's host is localhost / 127.0.0.1 / ::1 (any port) 51static bool origin_is_localhost (const std::string & origin ) { 52try { 53const std::string host = common_http_parse_url (origin ).host ; 54return host == "localhost" || host == "127.0.0.1" || host == "::1" ; 55 }catch (const std::exception & ) { 56return false; 57 } 58} 59 60// For Google Cloud Platform deployment compatibility 61struct gcp_params { 62bool enabled ; 63 std::string path_health ; 64 std::string path_predict ; 65int port ; 66 67// Ref: https://docs.cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements#aip-variables 68gcp_params () { 69enabled = getenv ("AIP_MODE" ,"" )== "PREDICTION" ; 70path_health = getenv ("AIP_HEALTH_ROUTE" ,"" , true);// default: using the route defined in server.cpp 71path_predict = getenv ("AIP_PREDICT_ROUTE" ,"/predict" , true); 72port = std::stoi (getenv ("AIP_HTTP_PORT" ,"8080" )); 73 } 74 75static std::string getenv (const char * name ,const std::string & default_value ,bool ensure_leading_slash = false) { 76const auto * value = std::getenv (name ); 77if (value == nullptr || value [0 ]== '\0' ) { 78return default_value ; 79 } 80 std::string val = value ; 81if (ensure_leading_slash && !val .empty ()&& val [0 ]!= '/' ) { 82val .insert (val .begin (),'/' ); 83 } 84return val ; 85 } 86}; 87 88bool server_http_context::init (const common_params & params ) { 89const gcp_params gcp ; 90 91path_prefix = params .api_prefix ; 92port = params .port ; 93hostname = params .hostname ; 94 95if (gcp .enabled ) { 96SRV_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 ); 97 98if (port != gcp .port ) { 99SRV_WRN ("Google Cloud Platform compat: overriding server port %d with AIP_HTTP_PORT %d\n" ,port ,gcp .port ); 100 } 101 102port = gcp .port ; 103 } 104 105auto & srv = pimpl -> srv ; 106 107#ifdef CPPHTTPLIB_OPENSSL_SUPPORT 108if (!params .ssl_file_key .empty ()&& !params .ssl_file_cert .empty ()) { 109SRV_TRC ("running with SSL: key = %s, cert = %s\n" ,params .ssl_file_key .c_str (),params .ssl_file_cert .c_str ()); 110srv = std::make_unique < httplib::SSLServer > ( 111params .ssl_file_cert .c_str (),params .ssl_file_key .c_str () 112 ); 113is_ssl = true; 114 }else { 115SRV_TRC ("%s" ,"running without SSL\n" ); 116srv = std::make_unique < httplib::Server > (); 117 } 118#else 119if (params .ssl_file_key != "" && params .ssl_file_cert != "" ) { 120SRV_ERR ("%s" ,"the server is built without SSL support\n" ); 121return false; 122 } 123srv .reset (new httplib::Server ()); 124#endif 125 126srv -> set_default_headers ({{"Server" ,"llama.cpp" }}); 127// srv->set_logger(log_server_request); // TODO @ngxson : this is too spamy, no very useful; improve it in the future 128srv -> set_exception_handler ([](const httplib::Request & , httplib::Response & res ,const std::exception_ptr & ep ) { 129// this is fail-safe; exceptions should already handled by `ex_wrapper` 130 131 std::string message ; 132try { 133 std::rethrow_exception (ep ); 134 }catch (const std::exception & e ) { 135message = e .what (); 136 }catch (...) { 137message = "Unknown Exception" ; 138 } 139 140res .status = 500 ; 141res .set_content (message ,"text/plain" ); 142SRV_ERR ("got exception: %s\n" ,message .c_str ()); 143 }); 144 145srv -> set_error_handler ([](const httplib::Request & , httplib::Response & res ) { 146if (res .status == 404 ) { 147res .set_content ( 148safe_json_to_str (json { 149 {"error" , { 150 {"message" ,"File Not Found" }, 151 {"type" ,"not_found_error" }, 152 {"code" ,404 } 153 }} 154 }), 155"application/json; charset=utf-8" 156 ); 157 } 158// for other error codes, we skip processing here because it's already done by res->error() 159 }); 160 161// set timeouts and change hostname and port 162srv -> set_read_timeout (params .timeout_read ); 163srv -> set_write_timeout (params .timeout_write ); 164srv -> set_socket_options ([reuse_port = params .reuse_port ](const socket_t sock ) { 165 httplib::set_socket_opt (sock ,SOL_SOCKET ,SO_REUSEADDR ,1 ); 166if (reuse_port ) { 167#ifdef SO_REUSEPORT 168 httplib::set_socket_opt (sock ,SOL_SOCKET ,SO_REUSEPORT ,1 ); 169#else 170SRV_WRN ("%s" ,"SO_REUSEPORT is not supported\n" ); 171#endif 172 } 173 }); 174 175if (params .api_keys .size ()== 1 ) { 176const auto key = params .api_keys [0 ]; 177const std::string substr = key .substr (std::max (static_cast < int > (key .length ()- 4 ),0 )); 178SRV_TRC ("api_keys: ****%s\n" ,substr .c_str ()); 179 }else if (params .api_keys .size ()> 1 ) { 180SRV_TRC ("api_keys: %zu keys loaded\n" ,params .api_keys .size ()); 181 } 182 183// 184// Middlewares 185// 186 187// Frontend paths - all embedded UI assets 188static const std::unordered_set < std::string > frontend_paths = []() { 189 std::unordered_set < std::string > paths {"/" }; 190for (const llama_ui_asset & a :llama_ui_get_assets ()) { 191paths .insert ("/" + a .name ); 192 } 193return paths ; 194 }(); 195 196// Public endpoints - API routes plus all embedded UI assets 197static const std::unordered_set < std::string > get_public_endpoints = []() { 198 std::unordered_set < std::string > endpoints { 199"/health" , 200"/v1/health" , 201 }; 202endpoints .insert (frontend_paths .begin (),frontend_paths .end ()); 203return endpoints ; 204 }(); 205 206auto middleware_validate_api_key = [api_keys = params .api_keys ](const httplib::Request & req , httplib::Response & res ) { 207// If API key is not set, skip validation 208if (api_keys .empty ()) { 209return true; 210 } 211 212// If path is public or a UI asset, skip validation 213if (get_public_endpoints .count (req .path )) { 214return true; 215 } 216 217// Check for API key in the Authorization header 218 std::string req_api_key = req .get_header_value ("Authorization" ); 219if (req_api_key .empty ()) { 220// retry with anthropic header 221req_api_key = req .get_header_value ("X-Api-Key" ); 222 } 223 224// remove the "Bearer " prefix if needed 225static std::string prefix = "Bearer " ; 226if (req_api_key .substr (0 ,prefix .size ())== prefix ) { 227req_api_key = req_api_key .substr (prefix .size ()); 228 } 229 230// validate the API key 231if (std::find (api_keys .begin (),api_keys .end (),req_api_key )!= api_keys .end ()) { 232return true;// API key is valid 233 } 234 235// API key is invalid or not provided 236res .status = 401 ; 237res .set_content ( 238safe_json_to_str (json { 239 {"error" , { 240 {"message" ,"Invalid API Key" }, 241 {"type" ,"authentication_error" }, 242 {"code" ,401 } 243 }} 244 }), 245"application/json; charset=utf-8" 246 ); 247 248SRV_WRN ("%s" ,"unauthorized: Invalid API Key\n" ); 249 250return false; 251 }; 252 253auto middleware_server_state = [this ](const httplib::Request & req , httplib::Response & res ) { 254if (!is_ready .load ()) { 255if (frontend_paths .count (req .path )) { 256return true;// frontend asset, allow it to load and show "loading" 257 } 258// no endpoints are allowed to be accessed when the server is not ready 259// this is to prevent any data races or inconsistent states 260res .status = 503 ; 261res .set_content ( 262safe_json_to_str (json { 263 {"error" , { 264 {"message" ,"Loading model" }, 265 {"type" ,"unavailable_error" }, 266 {"code" ,503 } 267 }} 268 }), 269"application/json; charset=utf-8" 270 ); 271return false; 272 } 273return true; 274 }; 275 276// register server middlewares 277srv -> set_pre_routing_handler ([& params ,middleware_validate_api_key ,middleware_server_state ](const httplib::Request & req , httplib::Response & res ) { 278if (params .cors_credentials && params .cors_origins == "*" ) { 279// special case: echo back the Origin header to allow any origin to access the server with credentials 280res .set_header ("Access-Control-Allow-Origin" ,req .get_header_value ("Origin" )); 281 }else if (params .cors_origins == "localhost" ) { 282// special case: only reflect the Origin header if it is a localhost origin 283 std::string origin = req .get_header_value ("Origin" ); 284if (!origin .empty ()&& origin_is_localhost (origin )) { 285res .set_header ("Access-Control-Allow-Origin" ,origin ); 286 }else if (!origin .empty ()) { 287SRV_WRN ("(CORS) skip non-localhost origin: %s\n" ,origin .c_str ()); 288 } 289 }else { 290res .set_header ("Access-Control-Allow-Origin" ,params .cors_origins ); 291 } 292// If this is OPTIONS request, skip validation because browsers don't include Authorization header 293if (req .method == "OPTIONS" ) { 294res .set_header ("Access-Control-Allow-Credentials" ,params .cors_credentials ?"true" :"false" ); 295res .set_header ("Access-Control-Allow-Methods" ,params .cors_methods ); 296res .set_header ("Access-Control-Allow-Headers" ,params .cors_headers ); 297res .set_content ("" ,"text/html" );// blank response, no data 298return httplib::Server ::HandlerResponse ::Handled ;// skip further processing 299 } 300if (!middleware_server_state (req ,res )) { 301return httplib::Server ::HandlerResponse ::Handled ; 302 } 303if (!middleware_validate_api_key (req ,res )) { 304return httplib::Server ::HandlerResponse ::Handled ; 305 } 306return httplib::Server ::HandlerResponse ::Unhandled ; 307 }); 308 309auto n_threads_http = params .n_threads_http ; 310if (n_threads_http < 1 ) { 311// +4 threads for monitoring, health and some threads reserved for MCP and other tasks in the future 312n_threads_http = std::max (params .n_parallel + 4 ,static_cast < int32_t > (std::thread::hardware_concurrency ()- 1 )); 313 } 314SRV_TRC ("using %d threads for HTTP server\n" ,n_threads_http ); 315srv -> new_task_queue = [n_threads_http ] { 316// spawn n_threads_http fixed thread (always alive), while allow up to 1024 max possible additional threads 317// when n_threads_http is used, server will create new "dynamic" threads that will be destroyed after processing each request 318// ref: https://github.com/yhirose/cpp-httplib/pull/2368 319const auto max_threads = static_cast < size_t > (n_threads_http + 1024 ); 320return new httplib::ThreadPool (n_threads_http ,max_threads ); 321 }; 322 323// 324// Web UI setup 325// 326 327// Use new `params.ui` field (backed by old `params.webui` for compat) 328if (!params .ui ) { 329SRV_INF ("%s" ,"The UI is disabled\n" ); 330SRV_INF ("%s" ,"Use --ui/--no-ui (or deprecated --webui/--no-webui) to enable/disable\n" ); 331 }else { 332// register static assets routes 333if (!params .public_path .empty ()) { 334// Set the base directory for serving static files 335if (const auto is_found = srv -> set_mount_point (params .api_prefix + "/" ,params .public_path ); !is_found ) { 336SRV_ERR ("static assets path not found: %s\n" ,params .public_path .c_str ()); 337return false; 338 } 339 }else { 340#if defined(LLAMA_UI_HAS_ASSETS ) 341static auto handle_gzip_header = [](const httplib::Request & req , httplib::Response & res ) { 342if (!llama_ui_use_gzip ()) { 343// no gzip build, skip 344return true; 345 } 346if (req .get_header_value ("Accept-Encoding" ).find ("gzip" )== std::string::npos ) { 347res .status = 415 ;// unsupported media type 348res .set_content ("Error: gzip is not supported by this browser" ,"text/plain" ); 349return false; 350 }else { 351res .set_header ("Content-Encoding" ,"gzip" ); 352 } 353return true; 354 }; 355 356// Hashed assets never change under a given name, so they can be cached forever. 357// `index.html` is the exception: its name is stable while its contents change on 358// every build, and it is what names the hashed asset versions the UI loads. 359static constexpr auto cache_immutable = "public, max-age=31536000, immutable" ; 360static constexpr auto cache_revalidate = "no-cache" ; 361 362// Serves an asset with ETag/304 handling, under the given caching policy. 363auto serve_asset_cached = [](const std::string & name ,bool isolation ,const char * cache_control ) { 364return [name ,isolation ,cache_control ](const httplib::Request & req , httplib::Response & res ) { 365if (!handle_gzip_header (req ,res )) { 366return true;// returns error message 367 } 368const llama_ui_asset * a = llama_ui_find_asset (name ); 369if (!a ) {res .status = 404 ;return false; } 370res .set_header ("ETag" ,a -> etag ); 371if (const std::string & inm = req .get_header_value ("If-None-Match" ); 372 !inm .empty ()&& (inm == a -> etag || inm == std::string ("W/" )+ a -> etag )) { 373res .status = 304 ; 374return false; 375 } 376if (isolation ) { 377res .set_header ("Cross-Origin-Embedder-Policy" ,"require-corp" ); 378res .set_header ("Cross-Origin-Opener-Policy" ,"same-origin" ); 379 } 380res .set_header ("Cache-Control" ,cache_control ); 381res .set_content (reinterpret_cast < const char *> (a -> data ),a -> size ,a -> type .c_str ()); 382return false; 383 }; 384 }; 385 386auto serve_asset_nocache = [](const std::string & name ) { 387return [name ](const httplib::Request & req , httplib::Response & res ) { 388if (!handle_gzip_header (req ,res )) { 389return true;// returns error message 390 } 391const llama_ui_asset * a = llama_ui_find_asset (name ); 392if (!a ) { 393res .status = 404 ; 394return false; 395 } 396res .set_header ("Cache-Control" ,"no-cache" ); 397res .set_content (reinterpret_cast < const char *> (a -> data ),a -> size ,a -> type .c_str ()); 398return false; 399 }; 400 }; 401 402// main index file -- revalidated, so a new build is picked up on the next load 403srv -> Get (params .api_prefix + "/" ,serve_asset_cached ("index.html" , true,cache_revalidate )); 404srv -> Get (params .api_prefix + "/index.html" ,serve_asset_cached ("index.html" , true,cache_revalidate )); 405 406// All remaining assets registered directly from the embedded asset table. 407// PWA revalidation files (sw.js, manifest, version.json) use no-cache; 408// everything else is immutable. 409static const std::unordered_set < std::string > no_cache_names = { 410"sw.js" , 411"manifest.webmanifest" , 412"_app/version.json" , 413"build.json" 414 }; 415 416for (const auto & a :llama_ui_get_assets ()) { 417if (a .name == "index.html" )continue ;// served at "/" and "/index.html" above 418if (no_cache_names .count (a .name )) { 419SRV_DBG ("serve nocache for %s\n" ,a .name .c_str ()); 420srv -> Get (params .api_prefix + "/" + a .name ,serve_asset_nocache (a .name )); 421 }else { 422srv -> Get (params .api_prefix + "/" + a .name ,serve_asset_cached (a .name , false,cache_immutable )); 423 } 424 } 425 426#endif 427 } 428 } 429return true; 430} 431 432bool server_http_context::start () { 433// Bind and listen 434 435const auto & srv = pimpl -> srv ; 436auto was_bound = false; 437auto is_sock = false; 438if (string_ends_with (std::string (hostname ),".sock" )) { 439is_sock = true; 440SRV_TRC ("%s" ,"setting address family to AF_UNIX\n" ); 441srv -> set_address_family (AF_UNIX ); 442// bind_to_port requires a second arg, any value other than 0 should 443// simply get ignored 444was_bound = srv -> bind_to_port (hostname ,8080 ); 445 }else { 446SRV_TRC ("%s" ,"binding port with default address family\n" ); 447// bind HTTP listen port 448if (port == 0 ) { 449const auto bound_port = srv -> bind_to_any_port (hostname ); 450was_bound = (bound_port >=0 ); 451if (was_bound ) { 452port = bound_port ; 453 } 454 }else { 455was_bound = srv -> bind_to_port (hostname ,port ); 456 } 457 } 458 459if (!was_bound ) { 460SRV_ERR ("couldn't bind HTTP server socket, hostname: %s, port: %d\n" ,hostname .c_str (),port ); 461return false; 462 } 463 464// run the HTTP server in a thread 465thread = std::thread ([this ] {pimpl -> srv -> listen_after_bind (); }); 466srv -> wait_until_ready (); 467 468listening_address = is_sock ?string_format ("unix://%s" ,hostname .c_str ()) 469 :string_format ("%s://%s:%d" ,is_ssl ?"https" :"http" ,common_http_format_host (hostname ).c_str (),port ); 470return true; 471} 472 473void server_http_context::stop ()const { 474if (pimpl -> srv ) { 475pimpl -> srv -> stop (); 476 } 477} 478 479static void set_headers (httplib::Response & res ,const std::map < std::string , std::string > & headers ) { 480for (const auto & [key ,value ] :headers ) { 481res .set_header (key ,value ); 482 } 483} 484 485// percent-decode a path component (%XX). path params arrive raw from httplib, unlike query 486// params, so a conv id like "conv::model" sent as "conv%3A%3Amodel" must be decoded here to 487// match the value the client put in the X-Conversation-Id header 488static std::string decode_path_component (const std::string & in ) { 489 std::string out ; 490out .reserve (in .size ()); 491for (size_t i = 0 ;i < in .size ();i ++ ) { 492if (in [i ]== '%' && i + 2 < in .size ()) { 493auto hex = [](char c )-> int { 494if (c >='0' && c <='9' )return c - '0' ; 495if (c >='a' && c <='f' )return c - 'a' + 10 ; 496if (c >='A' && c <='F' )return c - 'A' + 10 ; 497return -1 ; 498 }; 499int hi = hex (in [i + 1 ]); 500int lo = hex (in [i + 2 ]); 501if (hi >=0 && lo >=0 ) { 502out .push_back (char ((hi <<4 ) |lo )); 503i += 2 ; 504continue ; 505 } 506 } 507out .push_back (in [i ]); 508 } 509return out ; 510} 511 512static std::map < std::string , std::string > get_params (const httplib::Request & req ) { 513 std::map < std::string , std::string > params ; 514for (const auto & [key ,value ] :req .params ) { 515params [key ]= value ; 516 } 517for (const auto & [key ,value ] :req .path_params ) { 518params [key ]= decode_path_component (value ); 519 } 520return params ; 521} 522 523static std::map < std::string , std::string > get_headers (const httplib::Request & req ) { 524 std::map < std::string , std::string > headers ; 525for (const auto & [key ,value ] :req .headers ) { 526headers [key ]= value ; 527 } 528return headers ; 529} 530 531static std::string build_query_string (const httplib::Request & req ) { 532 std::string qs ; 533for (const auto & [key ,value ] :req .params ) { 534if (!qs .empty ()) { 535qs += '&' ; 536 } 537qs += httplib::encode_query_component (key )+ "=" + httplib::encode_query_component (value ); 538 } 539return qs ; 540} 541 542// using unique_ptr for request to allow safe capturing in lambdas 543using server_http_req_ptr = std::unique_ptr < server_http_req > ; 544 545static void process_handler_response (server_http_req_ptr && request ,server_http_res_ptr & response , httplib::Response & res ) { 546if (response -> is_stream ()) { 547res .status = response -> status ; 548// Tell Nginx to not buffer any streamed response 549response -> headers ["X-Accel-Buffering" ]= "no" ; 550set_headers (res ,response -> headers ); 551const std::string content_type = response -> content_type ; 552// convert to shared_ptr as both chunked_content_provider() and on_complete() need to use it 553 std::shared_ptr < server_http_req > q_ptr = std::move (request ); 554 std::shared_ptr < server_http_res > r_ptr = std::move (response ); 555 556const auto chunked_content_provider = [response = r_ptr ](size_t , httplib::DataSink & sink )-> bool { 557 std::string chunk ; 558const bool has_next = response -> next (chunk ); 559if (!chunk .empty ()) { 560if (!sink .write (chunk .data (),chunk .size ())) { 561return false; 562 } 563SRV_DBG ("http: streamed chunk: %s\n" ,chunk .c_str ()); 564 } 565if (!has_next ) { 566sink .done (); 567SRV_DBG ("%s" ,"http: stream ended\n" ); 568 } 569return has_next ; 570 }; 571const auto on_complete = [request = q_ptr ,response = r_ptr ](bool )mutable { 572response -> on_complete (); 573response .reset (); 574request .reset (); 575 }; 576res .set_chunked_content_provider (content_type ,chunked_content_provider ,on_complete ); 577 }else { 578res .status = response -> status ; 579set_headers (res ,response -> headers ); 580res .set_content (response -> data ,response -> content_type ); 581response -> on_complete (); 582 } 583} 584 585void server_http_context::get (const std::string & path ,const server_http_context::handler_t & handler )const { 586handlers .emplace (path ,handler ); 587pimpl -> srv -> Get (path_prefix + path , [handler ](const httplib::Request & req , httplib::Response & res ) { 588server_http_req_ptr request = std::make_unique < server_http_req > (server_http_req { 589get_params (req ), 590get_headers (req ), 591req .path , 592build_query_string (req ), 593req .body , 594 {}, 595req .is_connection_closed 596 }); 597server_http_res_ptr response = handler (* request ); 598process_handler_response (std::move (request ),response ,res ); 599 }); 600} 601 602void server_http_context::post (const std::string & path ,const server_http_context::handler_t & handler )const { 603handlers .emplace (path ,handler ); 604pimpl -> srv -> Post (path_prefix + path , [handler ](const httplib::Request & req , httplib::Response & res ) { 605 std::string body = req .body ; 606 std::map < std::string ,uploaded_file > files ; 607 608if (req .is_multipart_form_data ()) { 609// translate text fields to a JSON object and use it as the body 610json form_json = json::object (); 611for (const auto & [key ,field ] :req .form .fields ) { 612if (form_json .contains (key )) { 613// if the key already exists, convert it to an array 614if (!form_json [key ].is_array ()) { 615json existing_value = form_json [key ]; 616form_json [key ]= json::array ({existing_value }); 617 } 618form_json [key ].push_back (field .content ); 619 }else { 620form_json [key ]= field .content ; 621 } 622 } 623body = form_json .dump (); 624 625// populate files from multipart form 626for (const auto & [key ,file ] :req .form .files ) { 627files [key ]= uploaded_file { 628raw_buffer (file .content .begin (),file .content .end ()), 629file .filename , 630file .content_type , 631 }; 632 } 633 } 634 635server_http_req_ptr request = std::make_unique < server_http_req > (server_http_req { 636get_params (req ), 637get_headers (req ), 638req .path , 639build_query_string (req ), 640body , 641 std::move (files ), 642req .is_connection_closed 643 }); 644server_http_res_ptr response = handler (* request ); 645process_handler_response (std::move (request ),response ,res ); 646 }); 647} 648 649void server_http_context::del (const std::string & path ,const server_http_context::handler_t & handler )const { 650handlers .emplace (path ,handler ); 651pimpl -> srv -> Delete (path_prefix + path , [handler ](const httplib::Request & req , httplib::Response & res ) { 652server_http_req_ptr request = std::make_unique < server_http_req > (server_http_req { 653get_params (req ), 654get_headers (req ), 655req .path , 656build_query_string (req ), 657req .body , 658 {}, 659req .is_connection_closed 660 }); 661server_http_res_ptr response = handler (* request ); 662process_handler_response (std::move (request ),response ,res ); 663 }); 664} 665 666// 667// Vertex AI Prediction protocol (AIP_PREDICT_ROUTE) 668// https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements 669// 670 671// Derives the camelCase @requestFormat alias for a registered path. 672// e.g. "/v1/chat/completions" -> "chatCompletions", "/apply-template" -> "applyTemplate" 673static std::string path_to_gcp_format (const std::string & path ) { 674 std::string s = path ; 675if (s .size ()> 3 && s [0 ]== '/' && s [1 ]== 'v' && s [2 ]== '1' ) { 676s = s .substr (3 ); 677 } 678if (!s .empty ()&& s [0 ]== '/' ) { 679s = s .substr (1 ); 680 } 681 std::string result ; 682bool cap = false; 683for (unsigned char c :s ) { 684if (c == ':' )break ;// stop before path parameters 685if (c == '/' || c == '-' || c == '_' ) { 686cap = true; 687 }else { 688result += static_cast < char > (cap ? std::toupper (c ) :c ); 689cap = false; 690 } 691 } 692return result ; 693} 694 695static json parse_gcp_predict_response (const server_http_res_ptr & res ) { 696if (res == nullptr ) { 697throw std::runtime_error ("empty response from internal handler" ); 698 } 699if (res -> is_stream ()) { 700throw std::invalid_argument ("predict route does not support streaming responses" ); 701 } 702if (res -> data .empty ()) { 703return nullptr ; 704 } 705try { 706return json::parse (res -> data ); 707 }catch (...) { 708return res -> data ; 709 } 710} 711 712void server_http_context::register_gcp_compat ()const { 713const gcp_params gcp ; 714 715if (!gcp .enabled ) { 716// do nothing 717return ; 718 } 719 720if (handlers .count (gcp .path_predict )) { 721SRV_ERR ("AIP_PREDICT_ROUTE=%s conflicts with an existing llama-server route\n" ,gcp .path_predict .c_str ()); 722exit (1 ); 723 } 724 725// camelCase alias -> canonical path (first registration wins on collision) 726// e.g. "chatCompletions" -> "/v1/chat/completions" 727 std::unordered_map < std::string , std::string > alias_to_path ; 728for (const auto & [path ,_ ] :handlers ) { 729alias_to_path .emplace (path_to_gcp_format (path ),path ); 730 } 731 732if (!gcp .path_health .empty ()) { 733const auto health_handler = handlers .find ("/health" ); 734GGML_ASSERT (health_handler != handlers .end ()); 735get (gcp .path_health ,health_handler -> second ); 736 } 737 738post (gcp .path_predict , [this ,alias_to_path = std::move (alias_to_path )](const server_http_req & req )-> server_http_res_ptr { 739static const auto build_error = [](const std::string & message ,error_type type )-> json { 740return json {{"error" ,format_error_response (message ,type )}}; 741 }; 742 743json data ; 744try { 745data = json::parse (req .body ); 746 }catch (const std::exception & e ) { 747auto res = std::make_unique < server_http_res > (); 748res -> status = 400 ; 749res -> data = safe_json_to_str ({{"error" ,format_error_response (e .what (),ERROR_TYPE_INVALID_REQUEST )}}); 750return res ; 751 } 752if (!data .is_object ()) { 753auto res = std::make_unique < server_http_res > (); 754res -> status = 400 ; 755res -> data = safe_json_to_str ({{"error" ,format_error_response ("request body must be a JSON object" ,ERROR_TYPE_INVALID_REQUEST )}}); 756return res ; 757 } 758if (!data .contains ("instances" )|| !data .at ("instances" ).is_array ()) { 759auto res = std::make_unique < server_http_res > (); 760res -> status = 400 ; 761res -> data = safe_json_to_str ({{"error" ,format_error_response ("request body must include an array field named instances" ,ERROR_TYPE_INVALID_REQUEST )}}); 762return res ; 763 } 764 765const json & instances = data .at ("instances" ); 766static const size_t MAX_INSTANCES = 128 ; 767if (instances .size ()> MAX_INSTANCES ) { 768auto res = std::make_unique < server_http_res > (); 769res -> status = 400 ; 770res -> data = safe_json_to_str ({{"error" ,format_error_response ("instances array exceeds maximum size of " + std::to_string (MAX_INSTANCES ),ERROR_TYPE_INVALID_REQUEST )}}); 771return res ; 772 } 773 774 std::vector < std::future < json >> futures ; 775futures .reserve (instances .size ()); 776 777for (const auto & instance :instances ) { 778futures .push_back (std::async (std::launch::async , [this ,& req ,& alias_to_path ,instance ]()-> json { 779if (!instance .is_object ()) { 780return build_error ("each instance must be a JSON object" ,ERROR_TYPE_INVALID_REQUEST ); 781 } 782if (!instance .contains ("@requestFormat" )|| !instance .at ("@requestFormat" ).is_string ()) { 783return build_error ("each instance must include a string @requestFormat" ,ERROR_TYPE_INVALID_REQUEST ); 784 } 785 786try { 787json payload = instance ; 788const std::string format = payload .at ("@requestFormat" ).get < std::string > (); 789payload .erase ("@requestFormat" ); 790 791if (payload .contains ("stream" )) { 792SRV_WRN ("%s" ,"ignoring client-provided stream field in instance, streaming is not supported in predict route\n" ); 793payload ["stream" ]= false; 794 } 795 796// accept both camelCase aliases (e.g. "chatCompletions") and direct paths 797 std::string dispatch_path ; 798auto it_alias = alias_to_path .find (format ); 799if (it_alias != alias_to_path .end ()) { 800dispatch_path = it_alias -> second ; 801 }else if (handlers .count (format )) { 802dispatch_path = format ; 803 }else { 804return build_error ("no handler registered for @requestFormat: " + format ,ERROR_TYPE_INVALID_REQUEST ); 805 } 806 807const server_http_req internal_req { 808req .params , 809req .headers , 810path_prefix + dispatch_path , 811req .query_string , 812payload .dump (), 813 {}, 814req .should_stop , 815 }; 816 817server_http_res_ptr internal_res = handlers .at (dispatch_path )(internal_req ); 818return parse_gcp_predict_response (internal_res ); 819 }catch (const std::invalid_argument & e ) { 820return build_error (e .what (),ERROR_TYPE_INVALID_REQUEST ); 821 }catch (const std::exception & e ) { 822return build_error (e .what (),ERROR_TYPE_SERVER ); 823 }catch (...) { 824return build_error ("unknown error" ,ERROR_TYPE_SERVER ); 825 } 826 })); 827 } 828 829json predictions = json::array (); 830for (auto & future :futures ) { 831predictions .push_back (future .get ()); 832 } 833 834auto res = std::make_unique < server_http_res > (); 835res -> data = safe_json_to_str ({{"predictions" ,predictions }}); 836return res ; 837 }); 838}