char-fork/llama.cpp
git clone https://git.t4t.associates/char-fork/llama.cpp
56381e407
char/aliceai-t5-moe
1#include "server-common.h" 2#include "http.h" 3#include "server-models.h" 4#include "server-context.h" 5#include "server-stream.h" 6 7#include "build-info.h" 8#include "preset.h" 9#include "download.h" 10#include "http.h" 11#include "subproc.h" 12 13#include <cpp-httplib/httplib.h> // TODO: remove this once we use HTTP client from download.h 14#include <optional> 15 16#include <functional> 17#include <optional> 18#include <algorithm> 19#include <thread> 20#include <mutex> 21#include <condition_variable> 22#include <cstring> 23#include <cstdlib> 24#include <atomic> 25#include <chrono> 26#include <queue> 27#include <filesystem> 28#include <random> 29#include <sstream> 30#include <cstring> 31 32#ifndef _WIN32 33extern char ** environ ; 34#endif 35 36#if defined(__APPLE__ )&& defined(__MACH__ ) 37// macOS: use _NSGetExecutablePath to get the executable path 38#include <mach-o/dyld.h> 39#include <limits.h> 40#endif 41 42#define DEFAULT_STOP_TIMEOUT 10 // seconds 43 44#define CMD_ROUTER_TO_CHILD_EXIT "cmd_router_to_child:exit" 45#define CMD_CHILD_TO_ROUTER_STATE "cmd_child_to_router:state:" // followed by json string 46 47// note: SIGPIPE is ignored by the server 48static void request_child_exit (server_subproc & proc ) { 49FILE * stdin_file = proc .sproc .stdin_file (); 50if (stdin_file ) { 51fprintf (stdin_file ,"%s\n" ,CMD_ROUTER_TO_CHILD_EXIT ); 52fflush (stdin_file ); 53 } 54} 55 56// address for child process, this is needed because router may run on 0.0.0.0 57// ref: https://github.com/ggml-org/llama.cpp/issues/17862 58#define CHILD_ADDR "127.0.0.1" 59 60// single-threaded, watching all child processes at once 61struct server_monitor { 62server_monitor (server_models & models ) :models (models ) { 63th = std::thread ([this ]() {run (); }); 64 } 65 66 ~server_monitor () { 67push ({ cmd_t::QUIT , {},"" ,0 , false }); 68th .join (); 69 } 70 71// thread-safe 72void watch (const std::string & name , std::shared_ptr < server_subproc > proc ,server_child_mode mode ,int port ) { 73child_t c ; 74c .name = name ; 75c .proc = std::move (proc ); 76c .mode = mode ; 77c .port = port ; 78if (!c .proc -> has_output ()) { 79SRV_ERR ("failed to get stdout/stderr of child process for name=%s\n" ,name .c_str ()); 80c .eof = true; 81 } 82push ({ cmd_t::WATCH , std::move (c ),"" ,0 , false }); 83 } 84 85// thread-safe 86void stop (const std::string & name ,int stop_timeout ,bool send_exit ) { 87push ({ cmd_t::STOP , {},name ,stop_timeout ,send_exit }); 88 } 89 90private : 91struct child_t { 92 std::string name ; 93 std::shared_ptr < server_subproc > proc ; 94server_child_mode mode = SERVER_CHILD_MODE_NORMAL ; 95int port = 0 ; 96 std::string buf ;// partial line 97bool eof = false;// output closed, waiting for the process to be reaped 98int64_t deadline = 0 ;// force-kill time in ms, 0 when no stop is pending 99 }; 100 101struct cmd_t { 102enum {WATCH ,STOP ,QUIT }type ; 103child_t child ; 104 std::string name ; 105int stop_timeout ; 106bool send_exit ; 107 }; 108 109void push (cmd_t && cmd ) { 110 { 111 std::lock_guard < std::mutex > lk (mu ); 112cmds .push_back (std::move (cmd )); 113 } 114waiter .wake (); 115 } 116 117// returns true if the loop should exit 118bool handle_commands () { 119 std::deque < cmd_t > batch ; 120 { 121 std::lock_guard < std::mutex > lk (mu ); 122batch .swap (cmds ); 123 } 124for (auto & cmd :batch ) { 125switch (cmd .type ) { 126case cmd_t::WATCH : 127children .push_back (std::move (cmd .child )); 128break ; 129case cmd_t::STOP : 130// the newest child with this name is the one the registry knows 131for (auto it = children .rbegin ();it != children .rend ();++ it ) { 132if (it -> name != cmd .name ) { 133continue ; 134 } 135if (cmd .send_exit && !it -> eof ) { 136request_child_exit (* it -> proc ); 137 } 138it -> deadline = ggml_time_ms ()+ (int64_t )cmd .stop_timeout * 1000 ; 139break ; 140 } 141break ; 142case cmd_t::QUIT : 143return true; 144 } 145 } 146return false; 147 } 148 149// read what the child wrote, forward complete lines 150void read_output (child_t & c ) { 151char chunk [4096 ]; 152while (!c .eof ) { 153int n = c .proc -> read_output (chunk ,sizeof (chunk )); 154if (n < 0 ) { 155c .eof = true; 156break ; 157 } 158if (n == 0 ) { 159break ; 160 } 161c .buf .append (chunk , (size_t )n ); 162size_t start = 0 ; 163while (true) { 164size_t nl = c .buf .find ('\n' ,start ); 165if (nl == std::string::npos ) { 166break ; 167 } 168 std::string line = c .buf .substr (start ,nl + 1 - start ); 169start = nl + 1 ; 170on_line (c ,line ); 171 } 172c .buf .erase (0 ,start ); 173if (c .buf .size ()> max_line ) { 174c .buf .clear ();// a child that never writes a newline must not grow this without bound 175 } 176 } 177if (c .eof && !c .buf .empty ()) { 178on_line (c ,c .buf ); 179c .buf .clear (); 180 } 181 } 182 183void on_line (child_t & c ,const std::string & line ) { 184if (string_starts_with (line ,CMD_CHILD_TO_ROUTER_STATE )) { 185LOG_DBG ("[%5d] %s" ,c .port ,line .c_str ());// prevent spamming the log 186models .handle_child_state (c .name ,line ); 187 }else { 188LOG ("[%5d] %s" ,c .port ,line .c_str ());// forward log 189 } 190 } 191 192void run () { 193while (true) { 194if (handle_commands ()) { 195return ; 196 } 197 198// wait for output, a wakeup, or the next deadline; 199// a child whose output closed is polled for its exit every 50 ms 200int64_t now = ggml_time_ms (); 201int64_t timeout = -1 ; 202for (const auto & c :children ) { 203if (c .eof ) { 204timeout = timeout < 0 ?50 : std::min < int64_t > (timeout ,50 ); 205 } 206if (c .deadline ) { 207int64_t d = std::max < int64_t > (0 ,c .deadline - now ); 208timeout = timeout < 0 ?d : std::min (timeout ,d ); 209 } 210 } 211 std::vector < server_subproc *> procs ; 212 std::vector < child_t *> owners ; 213for (auto & c :children ) { 214if (!c .eof ) { 215procs .push_back (c .proc .get ()); 216owners .push_back (& c ); 217 } 218 } 219 std::vector < bool > ready ; 220waiter .wait (procs ,ready ,timeout ); 221for (size_t i = 0 ;i < owners .size ();i ++ ) { 222if (ready [i ]) { 223read_output (* owners [i ]); 224 } 225 } 226 227// deadlines and exits 228now = ggml_time_ms (); 229for (auto it = children .begin ();it != children .end ();) { 230if (it -> deadline && now >=it -> deadline && !it -> proc -> stopped .load (std::memory_order_acquire )) { 231SRV_WRN ("force-killing model instance name=%s after timeout\n" ,it -> name .c_str ()); 232it -> proc -> terminate (); 233it -> deadline = 0 ; 234 } 235if (it -> eof && !it -> proc -> is_alive ()) { 236int exit_code = it -> proc -> join (); 237it -> proc -> stopped .store (true, std::memory_order_release ); 238models .on_child_exit (it -> name ,it -> proc ,it -> mode ,exit_code ); 239SRV_INF ("instance name=%s exited with status %d\n" ,it -> name .c_str (),exit_code ); 240it = children .erase (it ); 241 }else { 242++ it ; 243 } 244 } 245 } 246 } 247 248static constexpr size_t max_line = 1024 * 1024 ; 249 250server_models & models ; 251 std::mutex mu ; 252 std::deque < cmd_t > cmds ; 253 std::vector < child_t > children ;// monitor thread only 254 server_subproc::waiter waiter ; 255 std::thread th ; 256}; 257 258struct server_lru_sched { 259server_lru_sched (server_models & models ) :models (models ) {} 260 261bool has_capacity (std::unique_lock < std::mutex > & lk ) { 262check_lock (lk ); 263return models .base_params .models_max <=0 264|| count_running ()< (size_t )models .base_params .models_max ; 265 } 266 267// returns "" if no model can be given up 268 std::string pick_victim (std::unique_lock < std::mutex > & lk ) { 269check_lock (lk ); 270 std::string victim ; 271int64_t victim_last_used = 0 ; 272for (const auto & m :models .mapping ) { 273// a busy model is mid-request, one still coming up has no request to finish 274if (m .second .req_count != 0 || !m .second .meta .is_ready_or_sleep ()) { 275continue ; 276 } 277// already on its way out, or a queued request wants it 278if (models .stopping_models .count (m .first )|| find (m .first )) { 279continue ; 280 } 281if (victim .empty ()|| m .second .meta .last_used < victim_last_used ) { 282victim = m .first ; 283victim_last_used = m .second .meta .last_used ; 284 } 285 } 286return victim ; 287 } 288 289// requests wanting the same model share one entry, so they all need only one slot 290// and all get unblocked by the single load that entry performs 291void join (std::unique_lock < std::mutex > & lk ,const std::string & model_id ) { 292check_lock (lk ); 293if (entry_t * e = find (model_id )) { 294e -> n_waiters ++ ; 295SRV_INF ("request for name=%s joined the queue, %d waiting\n" ,model_id .c_str (),e -> n_waiters ); 296return ; 297 } 298queue .push_back ({model_id ,1 , false }); 299SRV_INF ("models_max reached, request for name=%s queued at position %zu\n" , 300model_id .c_str (),queue .size ()); 301 } 302 303void leave (std::unique_lock < std::mutex > & lk ,const std::string & model_id ) { 304check_lock (lk ); 305for (auto it = queue .begin ();it != queue .end ();++ it ) { 306if (it -> model_id == model_id ) { 307if (-- it -> n_waiters <=0 ) { 308queue .erase (it );// last one waiting for this model went away 309 } 310return ; 311 } 312 } 313 } 314 315bool queue_empty (std::unique_lock < std::mutex > & lk ) { 316check_lock (lk ); 317return queue .empty (); 318 } 319 320// true if it is this model's turn to load, and nobody is loading it yet 321bool try_claim (std::unique_lock < std::mutex > & lk ,const std::string & model_id ) { 322check_lock (lk ); 323if (queue .empty ()|| queue .front ().model_id != model_id || queue .front ().loading ) { 324return false; 325 } 326if (!has_capacity (lk )) { 327return false; 328 } 329queue .front ().loading = true; 330return true; 331 } 332 333// on failure the entry is back in line; on success it stays until its waiters leave, 334// so the model coming up is never picked as a victim before they use it 335void claim_done (std::unique_lock < std::mutex > & lk ,const std::string & model_id ,bool ok ) { 336check_lock (lk ); 337if (ok ) { 338return ; 339 } 340for (auto it = queue .begin ();it != queue .end ();++ it ) { 341if (it -> model_id == model_id ) { 342it -> loading = false; 343return ; 344 } 345 } 346 } 347 348// evict idle models while queued requests outnumber the slots that are free or being freed 349// caller must hold models.mutex; never blocks, so it is safe from any thread 350void tick (std::unique_lock < std::mutex > & lk ) { 351check_lock (lk ); 352if (models .base_params .models_max <=0 || queue .empty ()) { 353return ; 354 } 355int n_running = 0 ; 356int n_stopping = 0 ; 357for (const auto & m :models .mapping ) { 358if (m .second .meta .is_running ()) { 359n_running ++ ; 360if (models .stopping_models .count (m .first )) { 361n_stopping ++ ; 362 } 363 } 364 } 365int n_needed = 0 ; 366int n_claimed = 0 ;// claimed the slot, but load() has not spawned yet 367for (const auto & e :queue ) { 368if (!e .loading ) { 369n_needed ++ ; 370continue ; 371 } 372auto it = models .mapping .find (e .model_id ); 373if (it != models .mapping .end ()&& !it -> second .meta .is_running ()) { 374n_claimed ++ ; 375 } 376 } 377int n_free = models .base_params .models_max - n_running + n_stopping - n_claimed ; 378while (n_free < n_needed ) { 379 std::string victim = pick_victim (lk ); 380if (victim .empty ()) { 381return ;// all remaining models are busy, wait for a request to end 382 } 383SRV_INF ("evicting idle LRU name=%s for a queued request\n" ,victim .c_str ()); 384models .request_stop (victim ); 385n_free ++ ; 386 } 387 } 388 389private : 390struct entry_t { 391 std::string model_id ; 392int n_waiters ;// requests waiting for this model 393bool loading ;// one of the waiters is doing the load right now 394 }; 395 396entry_t * find (const std::string & model_id ) { 397for (auto & e :queue ) { 398if (e .model_id == model_id ) { 399return & e ; 400 } 401 } 402return nullptr ; 403 } 404 405void check_lock (std::unique_lock < std::mutex > & lk ) { 406GGML_ASSERT (lk .owns_lock ()&& lk .mutex ()== & models .mutex ); 407 } 408 409size_t count_running () { 410size_t count = 0 ; 411for (const auto & m :models .mapping ) { 412if (m .second .meta .is_running ()) { 413count ++ ; 414 } 415 } 416return count ; 417 } 418 419server_models & models ; 420 std::deque < entry_t > queue ; 421}; 422 423// short loopback budget for the resumable stream router to child JSON calls (probe, lookup, 424// delete). distinct from params.timeout_read/write which only applies to the generation proxy 425static constexpr int STREAM_LOOKUP_TIMEOUT_MS = 250 ; 426 427static std::filesystem::path get_server_exec_path () { 428#if defined(_WIN32 ) 429wchar_t buf [32768 ]= {0 };// Large buffer to handle long paths 430DWORD len = GetModuleFileNameW (nullptr ,buf ,_countof (buf )); 431if (len == 0 || len >=_countof (buf )) { 432throw std::runtime_error ("GetModuleFileNameW failed or path too long" ); 433 } 434return std::filesystem::path (buf ); 435#elif defined(__APPLE__ )&& defined(__MACH__ ) 436char small_path [PATH_MAX ]; 437uint32_t size = sizeof (small_path ); 438 439if (_NSGetExecutablePath (small_path ,& size )== 0 ) { 440// resolve any symlinks to get absolute path 441try { 442return std::filesystem::canonical (std::filesystem::path (small_path )); 443 }catch (...) { 444return std::filesystem::path (small_path ); 445 } 446 }else { 447// buffer was too small, allocate required size and call again 448 std::vector < char > buf (size ); 449if (_NSGetExecutablePath (buf .data (),& size )== 0 ) { 450try { 451return std::filesystem::canonical (std::filesystem::path (buf .data ())); 452 }catch (...) { 453return std::filesystem::path (buf .data ()); 454 } 455 } 456throw std::runtime_error ("_NSGetExecutablePath failed after buffer resize" ); 457 } 458#else 459char path [FILENAME_MAX ]; 460ssize_t count = readlink ("/proc/self/exe" ,path ,FILENAME_MAX ); 461if (count <=0 ) { 462throw std::runtime_error ("failed to resolve /proc/self/exe" ); 463 } 464return std::filesystem::path (std::string (path ,count )); 465#endif 466} 467 468static void unset_reserved_args (common_preset & preset ,bool unset_model_args ) { 469preset .unset_option ("LLAMA_ARG_SSL_KEY_FILE" ); 470preset .unset_option ("LLAMA_ARG_SSL_CERT_FILE" ); 471preset .unset_option ("LLAMA_API_KEY" ); 472preset .unset_option ("LLAMA_ARG_MODELS_DIR" ); 473preset .unset_option ("LLAMA_ARG_MODELS_MAX" ); 474preset .unset_option ("LLAMA_ARG_MODELS_PRESET" ); 475preset .unset_option ("LLAMA_ARG_MODELS_AUTOLOAD" ); 476if (unset_model_args ) { 477preset .unset_option ("LLAMA_ARG_MODEL" ); 478preset .unset_option ("LLAMA_ARG_MMPROJ" ); 479preset .unset_option ("LLAMA_ARG_ALIAS" ); 480preset .unset_option ("LLAMA_ARG_HF_REPO" ); 481 } 482} 483 484#ifdef _WIN32 485static std::string wide_to_utf8 (const wchar_t * ws ) { 486if (!ws || !* ws ) { 487return {}; 488 } 489 490const int len = static_cast < int > (std::wcslen (ws )); 491const int bytes = WideCharToMultiByte (CP_UTF8 ,0 ,ws ,len ,nullptr ,0 ,nullptr ,nullptr ); 492if (bytes == 0 ) { 493return {}; 494 } 495 496 std::string utf8 (bytes ,'\0' ); 497WideCharToMultiByte (CP_UTF8 ,0 ,ws ,len ,utf8 .data (),bytes ,nullptr ,nullptr ); 498 499return utf8 ; 500} 501#endif 502 503static std::vector < std::string > get_environment () { 504 std::vector < std::string > env ; 505 506#ifdef _WIN32 507LPWCH env_block = GetEnvironmentStringsW (); 508if (!env_block ) { 509return env ; 510 } 511for (LPWCH e = env_block ;* e ;e += wcslen (e )+ 1 ) { 512env .emplace_back (wide_to_utf8 (e )); 513 } 514FreeEnvironmentStringsW (env_block ); 515#else 516if (environ == nullptr ) { 517return env ; 518 } 519for (char ** e = environ ;* e != nullptr ;e ++ ) { 520env .emplace_back (* e ); 521 } 522#endif 523 524return env ; 525} 526 527void server_model_meta::update_args (common_preset_context & ctx_preset , std::string bin_path ) { 528// update params 529unset_reserved_args (preset , false); 530preset .set_option (ctx_preset ,"LLAMA_ARG_HOST" ,CHILD_ADDR ); 531preset .set_option (ctx_preset ,"LLAMA_ARG_PORT" , std::to_string (port )); 532preset .set_option (ctx_preset ,"LLAMA_ARG_ALIAS" ,name ); 533// TODO: maybe validate preset before rendering ? 534// render args 535args = preset .to_args (bin_path ); 536 537// unified binary dispatches by subcommand, re-inject it right after the 538// binary path so the child starts as 'llama serve ...' not 'llama ...' 539const char * app_cmd = std::getenv ("LLAMA_APP_CMD" ); 540if (app_cmd != nullptr && app_cmd [0 ]!= '\0' && !bin_path .empty ()) { 541args .insert (args .begin ()+ 1 ,app_cmd ); 542 } 543} 544 545void server_model_meta::update_caps () { 546try { 547common_params params ; 548preset .apply_to_params (params , { 549"LLAMA_ARG_MODEL" , 550"LLAMA_ARG_MODEL_URL" , 551"LLAMA_ARG_MMPROJ" , 552"LLAMA_ARG_MMPROJ_URL" , 553"LLAMA_ARG_MMPROJ_AUTO" , 554"LLAMA_ARG_HF_REPO" , 555"LLAMA_ARG_HF_REPO_FILE" , 556 }); 557params .offline = true; 558common_models_handler handler = common_models_handler_init (params ,LLAMA_EXAMPLE_SERVER ); 559common_models_handler_apply (handler ,params );// note: this won't download the model because offline=true 560if (params .no_mmproj || params .mmproj .path .empty ()) { 561multimodal = { false, false }; 562 }else { 563multimodal = mtmd_get_cap_from_file (params .mmproj .path .c_str ()); 564 } 565 }catch (const std::exception & e ) { 566LOG_WRN ("failed to initialize common_params for multimodal capability detection: %s\n" ,e .what ()); 567multimodal = { false, false }; 568 } 569} 570 571// 572// server_models 573// 574 575server_models::server_models ( 576const common_params & params , 577int argc , 578char ** argv ) 579 :ctx_preset (LLAMA_EXAMPLE_SERVER ), 580base_params (params ), 581base_env (get_environment ()), 582base_preset (ctx_preset .load_from_args (argc ,argv )), 583sched (std::make_unique < server_lru_sched > (* this )), 584monitor (std::make_unique < server_monitor > (* this )) { 585// clean up base preset 586unset_reserved_args (base_preset , true); 587// set binary path 588try { 589bin_path = get_server_exec_path ().string (); 590 }catch (const std::exception & e ) { 591bin_path = argv [0 ]; 592LOG_WRN ("failed to get server executable path: %s\n" ,e .what ()); 593LOG_WRN ("using original argv[0] as fallback: %s\n" ,argv [0 ]); 594 } 595load_models (); 596debug_fake_timing = !common_get_env ("LLAMA_SERVER_DEBUG_FAKE_TIMING" ).empty (); 597} 598 599server_models::~server_models ()= default ; 600 601void server_models::instance_t::request_exit ()const { 602request_child_exit (* subproc ); 603} 604 605void server_models::add_model (server_model_meta && meta ) { 606if (mapping .find (meta .name )!= mapping .end ()) { 607throw std::runtime_error (string_format ("model '%s' appears multiple times" ,meta .name .c_str ())); 608 } 609 610// check model name does not conflict with existing aliases 611for (const auto & [key ,inst ] :mapping ) { 612if (inst .meta .aliases .count (meta .name )) { 613throw std::runtime_error (string_format ("model name '%s' conflicts with alias of model '%s'" , 614meta .name .c_str (),key .c_str ())); 615 } 616 } 617 618// parse aliases from preset's --alias option (comma-separated) 619 std::string alias_str ; 620if (meta .preset .get_option ("LLAMA_ARG_ALIAS" ,alias_str )&& !alias_str .empty ()) { 621for (auto & alias :string_split < std::string > (alias_str ,',' )) { 622alias = string_strip (alias ); 623if (!alias .empty ()) { 624meta .aliases .insert (alias ); 625 } 626 } 627 } 628 629// parse tags from preset's --tags option (comma-separated) 630 std::string tags_str ; 631if (meta .preset .get_option ("LLAMA_ARG_TAGS" ,tags_str )&& !tags_str .empty ()) { 632for (auto & tag :string_split < std::string > (tags_str ,',' )) { 633tag = string_strip (tag ); 634if (!tag .empty ()) { 635meta .tags .insert (tag ); 636 } 637 } 638 } 639 640// validate aliases do not conflict with existing names or aliases 641for (const auto & alias :meta .aliases ) { 642if (mapping .find (alias )!= mapping .end ()) { 643throw std::runtime_error (string_format ("alias '%s' for model '%s' conflicts with existing model name" , 644alias .c_str (),meta .name .c_str ())); 645 } 646for (const auto & [key ,inst ] :mapping ) { 647if (inst .meta .aliases .count (alias )) { 648throw std::runtime_error (string_format ("alias '%s' for model '%s' conflicts with alias of model '%s'" , 649alias .c_str (),meta .name .c_str (),key .c_str ())); 650 } 651 } 652 } 653 654meta .update_args (ctx_preset ,bin_path );// render args 655meta .update_caps (); 656 std::string name = meta .name ; 657mapping [name ]= instance_t { 658/* subproc */ std::make_shared < server_subproc > (), 659/* meta */ std::move (meta ) 660 }; 661} 662 663void server_models::notify_sse (const std::string & event ,const std::string & model_id ,const json & data ) { 664 std::unique_ptr < server_task_result_router > result = std::make_unique < server_task_result_router > (); 665result -> data = { 666 {"model" ,model_id }, 667 {"event" ,event }, 668 }; 669if (!data .is_null ()) { 670result -> data ["data" ]= data ; 671 } 672SRV_DBG ("notifying SSE clients about event '%s' for model '%s': %s\n" ,event .c_str (),model_id .c_str (),safe_json_to_str (result -> data ).c_str ()); 673sse .broadcast (std::move (result )); 674} 675 676void server_models::load_models () { 677// Phase 1: load presets from all sources - pure I/O, no lock needed 678// 1. cached models 679common_presets cached_models = ctx_preset .load_from_cache (); 680SRV_INF ("Loaded %zu cached model presets\n" ,cached_models .size ()); 681// 2. local models from --models-dir 682common_presets local_models ; 683if (!base_params .models_dir .empty ()) { 684local_models = ctx_preset .load_from_models_dir (base_params .models_dir ); 685SRV_INF ("Loaded %zu local model presets from %s\n" ,local_models .size (),base_params .models_dir .c_str ()); 686 } 687// 3. custom-path models from presets 688common_preset global = {}; 689common_presets custom_presets = {}; 690if (!base_params .models_preset .empty ()) { 691custom_presets = ctx_preset .load_from_ini (base_params .models_preset ,global ); 692SRV_INF ("Loaded %zu custom model presets from %s\n" ,custom_presets .size (),base_params .models_preset .c_str ()); 693 } 694 695// cascade, apply global preset first 696cached_models = ctx_preset .cascade (global ,cached_models ); 697local_models = ctx_preset .cascade (global ,local_models ); 698custom_presets = ctx_preset .cascade (global ,custom_presets ); 699 700// note: if a model exists in both cached and local, local takes precedence 701common_presets final_presets ; 702 std::unordered_map < std::string ,server_model_source > source_map ; 703for (const auto & [name ,preset ] :cached_models ) { 704final_presets [name ]= preset ; 705source_map [name ]= SERVER_MODEL_SOURCE_CACHE ; 706 } 707for (const auto & [name ,preset ] :local_models ) { 708final_presets [name ]= preset ; 709source_map [name ]= SERVER_MODEL_SOURCE_MODELS_DIR ; 710 } 711for (const auto & [name ,custom ] :custom_presets ) { 712if (final_presets .find (name )!= final_presets .end ()) { 713final_presets [name ].merge (custom ); 714 }else { 715final_presets [name ]= custom ; 716 } 717source_map [name ]= SERVER_MODEL_SOURCE_PRESET ; 718 } 719 720// overlay router's own CLI args on top of every model preset so that 721// e.g. `llama-server --temp 0` is honoured by all child processes 722for (auto & [name ,preset ] :final_presets ) { 723preset .merge (base_preset ); 724 } 725 726auto get_source = [& ](const std::string & name ) { 727return source_map .count (name ) ?source_map .at (name ) :SERVER_MODEL_SOURCE_PRESET ; 728 }; 729 730// hide cache models whose resolved file is already used by a preset with dedup-cache-models enabled 731 std::set < std::string > hidden_models ; 732 { 733 std::set < std::string > preset_paths ; 734for (const auto & [name ,preset ] :custom_presets ) { 735 std::string val ; 736if (!preset .get_option (COMMON_ARG_PRESET_DEDUP_CACHE_MODELS ,val )|| !common_arg_utils::is_truthy (val )) { 737continue ; 738 } 739 std::string hf_repo ; 740if (!preset .get_option ("LLAMA_ARG_HF_REPO" ,hf_repo )|| hf_repo .empty ()) { 741continue ; 742 } 743 std::string hf_file ; 744preset .get_option ("LLAMA_ARG_HF_FILE" ,hf_file ); 745 std::string path = common_download_resolve_path (hf_repo ,hf_file ); 746if (!path .empty ()) { 747preset_paths .insert (path ); 748 } 749 } 750if (!preset_paths .empty ()) { 751for (const auto & [name ,preset ] :cached_models ) { 752if (get_source (name )!= SERVER_MODEL_SOURCE_CACHE ) { 753continue ;// merged with another source, not a pure cache entry 754 } 755 std::string path = common_download_resolve_path (name ); 756if (!path .empty ()&& preset_paths .count (path )) { 757SRV_INF ("hiding cache model name=%s (deduplicated by a preset)\n" ,name .c_str ()); 758hidden_models .insert (name ); 759 } 760 } 761 } 762 } 763 764// Helpers that read `mapping` - must be called while holding the lock. 765 std::unordered_set < std::string > custom_names ; 766for (const auto & [name ,preset ] :custom_presets )custom_names .insert (name ); 767auto join_set = [](const std::set < std::string > & s ) { 768 std::string result ; 769for (const auto & v :s ) { 770if (!result .empty ())result += ", " ; 771result += v ; 772 } 773return result ; 774 }; 775auto log_available_models = [& ]() { 776SRV_INF ("Available models (%zu) (*: custom preset)\n" ,mapping .size ()); 777for (const auto & [name ,inst ] :mapping ) { 778bool has_custom = custom_names .find (name )!= custom_names .end (); 779 std::string info ; 780if (!inst .meta .aliases .empty ())info += " (aliases: " + join_set (inst .meta .aliases )+ ")" ; 781if (!inst .meta .tags .empty ())info += " [tags: " + join_set (inst .meta .tags )+ "]" ; 782SRV_INF (" %c %s%s\n" ,has_custom ?'*' :' ' ,name .c_str (),info .c_str ()); 783 } 784 }; 785auto apply_stop_timeout = [& ]() { 786for (auto & [name ,inst ] :mapping ) { 787 std::string val ; 788if (inst .meta .preset .get_option (COMMON_ARG_PRESET_STOP_TIMEOUT ,val )) { 789try { 790inst .meta .stop_timeout = std::stoi (val ); 791 }catch (...) { 792SRV_WRN ("invalid stop-timeout value '%s' for model '%s', using default %d seconds\n" , 793val .c_str (),name .c_str (),DEFAULT_STOP_TIMEOUT ); 794inst .meta .stop_timeout = DEFAULT_STOP_TIMEOUT ; 795 } 796 } 797 } 798 }; 799auto apply_hidden = [& ]() { 800for (auto & [name ,inst ] :mapping ) { 801inst .meta .hidden = hidden_models .count (name )> 0 ; 802 } 803 }; 804// update_args() injects HOST/PORT/ALIAS, so strip them before comparing presets 805auto preset_options_for_compare = [](common_preset p ) { 806p .unset_option ("LLAMA_ARG_HOST" ); 807p .unset_option ("LLAMA_ARG_PORT" ); 808p .unset_option ("LLAMA_ARG_ALIAS" ); 809return p .options ; 810 }; 811 812// Phase 2: acquire the lock once for all mapping mutations. 813// We temporarily release it only when calling functions that acquire it internally (unload) 814 std::unique_lock < std::mutex > lk (mutex ); 815 816need_reload = false; 817bool is_first_load = mapping .empty (); 818 819if (is_first_load ) { 820// FIRST LOAD: add all models, then unlock for autoloading 821for (const auto & [name ,preset ] :final_presets ) { 822server_model_meta meta { 823/* source */ get_source (name ), 824/* preset */ preset , 825/* name */ name , 826/* aliases */ {}, 827/* tags */ {}, 828/* port */ 0 , 829/* status */ SERVER_MODEL_STATUS_UNLOADED , 830/* last_used */ 0 , 831/* args */ std::vector < std::string > (), 832/* loaded_info */ {}, 833/* progress */ {}, 834/* exit_code */ 0 , 835/* stop_timeout */ DEFAULT_STOP_TIMEOUT , 836/* multimodal */ mtmd_caps {false, false}, 837// /* need_download */ false, 838 }; 839add_model (std::move (meta )); 840 } 841apply_stop_timeout (); 842apply_hidden (); 843log_available_models (); 844 845// skipped on reload, see startup_models 846if (startup_models .has_value ()) { 847 std::vector < std::string > models_to_load ; 848for (const auto & [name ,inst ] :mapping ) { 849 std::string val ; 850if (inst .meta .preset .get_option (COMMON_ARG_PRESET_LOAD_ON_STARTUP ,val )&& common_arg_utils::is_truthy (val )) { 851models_to_load .push_back (name ); 852 } 853 } 854if ((int )models_to_load .size ()> base_params .models_max ) { 855throw std::runtime_error (string_format ( 856"number of models to load on startup (%zu) exceeds models_max (%d)" , 857models_to_load .size (),base_params .models_max )); 858 } 859 860// to be lazy-loaded after main() setup phase is completed 861startup_models = std::move (models_to_load ); 862 } 863 864lk .unlock (); 865 }else { 866// RELOAD: diff the new preset list against the current mapping and reconcile 867is_reloading = true; 868 869// find running models whose source was removed or whose preset changed 870 std::vector < std::string > to_unload ; 871for (const auto & [name ,inst ] :mapping ) { 872if (!inst .meta .is_running ())continue ; 873auto it = final_presets .find (name ); 874if (it == final_presets .end ()) { 875to_unload .push_back (name );// removed from source 876 }else if (preset_options_for_compare (inst .meta .preset )!= preset_options_for_compare (it -> second )) { 877to_unload .push_back (name );// preset changed 878 } 879 } 880 881// unload() acquires the lock internally, so release before each call 882for (const auto & name :to_unload ) { 883SRV_INF ("(reload) unloading model name=%s (source updated or removed)\n" ,name .c_str ()); 884lk .unlock (); 885unload (name ); 886lk .lock (); 887 } 888 889// wait for all targeted models to reach UNLOADED; cv.wait handles unlock/relock 890cv .wait (lk , [& ]() { 891for (const auto & name :to_unload ) { 892auto it = mapping .find (name ); 893if (it != mapping .end ()&& it -> second .meta .is_running ())return false; 894 } 895return true; 896 }); 897 898// erase models no longer in any source 899for (auto it = mapping .begin ();it != mapping .end (); ) { 900if (it -> second .meta .status == SERVER_MODEL_STATUS_DOWNLOADING ) { 901++ it ;// download thread is still busy, skip 902 }else if (it -> second .meta .status == SERVER_MODEL_STATUS_DOWNLOADED ) { 903// download finished, safe to erase 904it = mapping .erase (it ); 905 }else if (final_presets .find (it -> first )== final_presets .end ()) { 906SRV_INF ("(reload) removing model name=%s (no longer in source)\n" ,it -> first .c_str ()); 907it = mapping .erase (it ); 908 }else { 909++ it ; 910 } 911 } 912 913// update presets for non-running models still in source 914for (auto & [name ,inst ] :mapping ) { 915if (inst .meta .is_running ())continue ; 916auto it = final_presets .find (name ); 917if (it == final_presets .end ())continue ;// erased above 918 919inst .meta .preset = it -> second ; 920 921// re-parse aliases, then validate against other models 922 std::set < std::string > new_aliases ; 923 std::string alias_str ; 924if (inst .meta .preset .get_option ("LLAMA_ARG_ALIAS" ,alias_str )&& !alias_str .empty ()) { 925for (auto & alias :string_split < std::string > (alias_str ,',' )) { 926alias = string_strip (alias ); 927if (!alias .empty ())new_aliases .insert (alias ); 928 } 929 } 930inst .meta .aliases .clear (); 931for (const auto & alias :new_aliases ) { 932bool conflict = false; 933for (const auto & [other_name ,other_inst ] :mapping ) { 934if (other_name == name )continue ; 935if (other_name == alias || other_inst .meta .aliases .count (alias )) { 936SRV_WRN ("(reload) alias '%s' for model '%s' conflicts with model '%s', skipping\n" , 937alias .c_str (),name .c_str (),other_name .c_str ()); 938conflict = true; 939break ; 940 } 941 } 942if (!conflict )inst .meta .aliases .insert (alias ); 943 } 944 945// re-parse tags 946inst .meta .tags .clear (); 947 std::string tags_str ; 948if (inst .meta .preset .get_option ("LLAMA_ARG_TAGS" ,tags_str )&& !tags_str .empty ()) { 949for (auto & tag :string_split < std::string > (tags_str ,',' )) { 950tag = string_strip (tag ); 951if (!tag .empty ())inst .meta .tags .insert (tag ); 952 } 953 } 954 955inst .meta .exit_code = 0 ;// clear failed state so the model can be reloaded 956inst .meta .update_args (ctx_preset ,bin_path ); 957inst .meta .update_caps (); 958 } 959 960// add models that are new in this reload, load-on-startup is not honored here since a 961// reload never spawns an instance 962for (const auto & [name ,preset ] :final_presets ) { 963if (mapping .find (name )== mapping .end ()) { 964server_model_meta meta { 965/* source */ get_source (name ), 966/* preset */ preset , 967/* name */ name , 968/* aliases */ {}, 969/* tags */ {}, 970/* port */ 0 , 971/* status */ SERVER_MODEL_STATUS_UNLOADED , 972/* last_used */ 0 , 973/* args */ std::vector < std::string > (), 974/* loaded_info */ {}, 975/* progress */ {}, 976/* exit_code */ 0 , 977/* stop_timeout */ DEFAULT_STOP_TIMEOUT , 978/* multimodal */ mtmd_caps {false, false}, 979// /* need_download */ false, 980 }; 981add_model (std::move (meta )); 982 } 983 } 984 985apply_stop_timeout (); 986apply_hidden (); 987 988// clear reload flag under the lock, this releases the load() calls waiting on !is_reloading 989is_reloading = false; 990cv .notify_all (); 991 992log_available_models (); 993 994lk .unlock (); 995 996notify_sse ("models_reload" ,"*" ); 997 } 998} 999 1000void server_models::load_startup_models () { 1001 std::vector < std::string > to_load ; 1002 { 1003 std::lock_guard < std::mutex > lk (mutex ); 1004if (!startup_models .has_value ()) { 1005return ;// already drained 1006 } 1007to_load = std::move (* startup_models ); 1008startup_models .reset (); 1009 } 1010for (const auto & name :to_load ) { 1011SRV_INF ("(startup) loading model %s\n" ,name .c_str ()); 1012load (name ); 1013 } 1014} 1015 1016void server_models::update_meta (const std::string & name ,const server_model_meta & meta ) { 1017 std::lock_guard < std::mutex > lk (mutex ); 1018auto it = mapping .find (name ); 1019if (it != mapping .end ()) { 1020it -> second .meta = meta ; 1021 } 1022cv .notify_all ();// notify wait_until_loading_finished 1023} 1024 1025bool server_models::has_model (const std::string & name ) { 1026 std::lock_guard < std::mutex > lk (mutex ); 1027if (mapping .find (name )!= mapping .end ()) { 1028return true; 1029 } 1030for (const auto & [key ,inst ] :mapping ) { 1031if (inst .meta .aliases .count (name )) { 1032return true; 1033 } 1034 } 1035return false; 1036} 1037 1038std::optional < server_model_meta > server_models::get_meta (const std::string & name ) { 1039 std::unique_lock < std::mutex > lk (mutex ); 1040if (need_reload ) { 1041lk .unlock (); 1042load_models (); 1043lk .lock (); 1044 } 1045 1046auto it = mapping .find (name ); 1047if (it != mapping .end ()) { 1048return it -> second .meta ; 1049 } 1050for (const auto & [key ,inst ] :mapping ) { 1051if (inst .meta .aliases .count (name )) { 1052return inst .meta ; 1053 } 1054 } 1055return std::nullopt ; 1056} 1057 1058std::vector < server_model_meta > server_models::get_all_meta () { 1059 std::unique_lock < std::mutex > lk (mutex ); 1060if (need_reload ) { 1061lk .unlock (); 1062load_models (); 1063lk .lock (); 1064 } 1065 1066 std::vector < server_model_meta > result ; 1067result .reserve (mapping .size ()); 1068for (const auto & [name ,inst ] :mapping ) { 1069result .push_back (inst .meta ); 1070 } 1071return result ; 1072} 1073 1074void server_models::unload_lru () { 1075if (base_params .models_max <=0 ) { 1076return ;// no limit 1077 } 1078// remove one of the servers if we passed the models_max (least recently used - LRU) 1079 std::string lru_model_name ; 1080 { 1081 std::unique_lock < std::mutex > lk (mutex ); 1082if (sched -> has_capacity (lk )) { 1083return ; 1084 } 1085lru_model_name = sched -> pick_victim (lk ); 1086 } 1087if (!lru_model_name .empty ()) { 1088SRV_INF ("models_max limit reached, removing LRU name=%s\n" ,lru_model_name .c_str ()); 1089unload (lru_model_name ); 1090// wait for unload to complete 1091 { 1092 std::unique_lock < std::mutex > lk (mutex ); 1093cv .wait (lk , [this ,& lru_model_name ]() { 1094return mapping [lru_model_name ].meta .status == SERVER_MODEL_STATUS_UNLOADED ; 1095 }); 1096 } 1097 } 1098} 1099 1100void server_models::load (const std::string & name ) { 1101load (name ,load_options {}); 1102} 1103 1104void server_models::load (const std::string & name ,const load_options & opts ) { 1105if (debug_fake_timing ) { 1106// do not hold the mutex here, other requests must keep making progress 1107 std::this_thread::sleep_for (std::chrono::seconds (2 )); 1108 } 1109 1110if (!opts .custom_meta .has_value ()) { 1111if (!has_model (name )) { 1112throw std::runtime_error ("model name=" + name + " is not found" ); 1113 } 1114unload_lru (); 1115 } 1116 1117 std::unique_lock < std::mutex > lk (mutex ); 1118// edge case: block until any in-progress reload has finished so we always load 1119// against the freshest preset and a consistent mapping state 1120cv .wait (lk , [this ]() {return !is_reloading ; }); 1121 1122auto meta = opts .custom_meta .has_value () ?* opts .custom_meta :mapping [name ].meta ; 1123if (meta .status != SERVER_MODEL_STATUS_UNLOADED ) { 1124SRV_INF ("model %s is not ready\n" ,name .c_str ()); 1125return ; 1126 } 1127 1128// Re-check capacity under the lock to prevent concurrent loads from 1129// exceeding models_max. Without this, the window between unload_lru() 1130// releasing its lock and this lock_guard acquiring allows multiple 1131// threads to each observe capacity and all proceed to load. 1132// Download workers do not use models_max slots. 1133if (opts .mode == SERVER_CHILD_MODE_NORMAL && base_params .models_max > 0 ) { 1134size_t count_active = 0 ; 1135for (const auto & m :mapping ) { 1136if (m .second .meta .is_running ()) { 1137count_active ++ ; 1138 } 1139 } 1140if (count_active >= (size_t )base_params .models_max ) { 1141throw std::runtime_error ("model limit reached, try again later" ); 1142 } 1143 } 1144 1145// prepare new instance info 1146instance_t inst ; 1147inst .meta = meta ; 1148inst .meta .port = common_http_get_free_port (); 1149inst .meta .status = SERVER_MODEL_STATUS_LOADING ; 1150inst .meta .loaded_info = json {}; 1151inst .meta .last_used = ggml_time_ms (); 1152 1153if (inst .meta .port <=0 ) { 1154throw std::runtime_error ("failed to get a port number" ); 1155 } 1156 1157inst .subproc = std::make_shared < server_subproc > (); 1158 { 1159SRV_INF ("spawning server instance with name=%s on port %d\n" ,inst .meta .name .c_str (),inst .meta .port ); 1160 1161inst .meta .update_args (ctx_preset ,bin_path );// render args 1162 1163 std::vector < std::string > child_args = inst .meta .args ;// copy 1164 std::vector < std::string > child_env = base_env ;// copy 1165child_env .push_back ("LLAMA_SERVER_ROUTER_PORT=" + std::to_string (base_params .port )); 1166 1167if (opts .mode == SERVER_CHILD_MODE_DOWNLOAD ) { 1168inst .meta .status = SERVER_MODEL_STATUS_DOWNLOADING ; 1169child_env .push_back ("LLAMA_SERVER_CHILD_MODE=download" ); 1170child_env .push_back ("LLAMA_ARG_HF_REPO=" + name ); 1171 } 1172 1173SRV_INF ("%s" ,"spawning server instance with args:\n" ); 1174for (const auto & arg :child_args ) { 1175SRV_INF (" %s\n" ,arg .c_str ()); 1176 } 1177inst .meta .args = child_args ;// save for debugging 1178 1179// TODO @ngxson : maybe separate stdout and stderr in the future 1180// so that we can use stdout for commands and stderr for logging 1181int options = subprocess_option_no_window |subprocess_option_combined_stdout_stderr ; 1182if (!inst .subproc -> sproc .create (child_args ,options ,child_env )) { 1183throw std::runtime_error ("failed to spawn server instance" ); 1184 } 1185 } 1186 1187// old process should have exited already, but just in case, we clean it up here 1188 { 1189auto it = mapping .find (name ); 1190if (it != mapping .end ()&& it -> second .subproc && it -> second .subproc -> is_alive ()) { 1191SRV_WRN ("old process for model name=%s is still alive, this is unexpected\n" ,name .c_str ()); 1192it -> second .subproc -> terminate ();// force kill 1193 } 1194 } 1195 1196notify_sse ("model_status" ,name , { 1197 {"status" ,server_model_status_to_string (inst .meta .status )}, 1198 }); 1199 1200auto proc = inst .subproc ; 1201int port = inst .meta .port ; 1202mapping [name ]= std::move (inst ); 1203monitor -> watch (name ,proc ,opts .mode ,port ); 1204cv .notify_all (); 1205} 1206 1207void server_models::request_stop (const std::string & name ,bool send_exit ) { 1208auto it = mapping .find (name ); 1209if (it == mapping .end ()|| stopping_models .count (name )) { 1210return ; 1211 } 1212stopping_models .insert (name ); 1213monitor -> stop (name ,it -> second .meta .stop_timeout ,send_exit ); 1214} 1215 1216void server_models::on_child_exit (const std::string & name ,const std::shared_ptr < server_subproc > & proc ,server_child_mode mode ,int exit_code ) { 1217 { 1218 std::lock_guard < std::mutex > lk (mutex ); 1219stopping_models .erase (name ); 1220auto it = mapping .find (name ); 1221if (it == mapping .end ()|| it -> second .subproc != proc ) { 1222return ;// entry erased, or a newer instance took the name 1223 } 1224 } 1225if (mode == SERVER_CHILD_MODE_DOWNLOAD ) { 1226// instance will be cleaned up on next load_models() call 1227 std::lock_guard < std::mutex > lk (mutex ); 1228cv .notify_all (); 1229 }else { 1230update_status (name , { 1231SERVER_MODEL_STATUS_UNLOADED , 1232exit_code 1233 }); 1234 } 1235} 1236 1237void server_models::unload (const std::string & name ) { 1238 std::unique_lock < std::mutex > lk (mutex ); 1239auto it = mapping .find (name ); 1240if (it != mapping .end ()) { 1241if (it -> second .meta .status == SERVER_MODEL_STATUS_DOWNLOADING ) { 1242SRV_INF ("cancelling download for model name=%s\n" ,name .c_str ()); 1243it -> second .request_exit (); 1244// for convenience, we wait the status change here 1245wait (lk ,name , [](const server_model_meta & new_meta ) { 1246return new_meta .status != SERVER_MODEL_STATUS_DOWNLOADING ; 1247 }); 1248 }else if (it -> second .meta .is_running ()) { 1249SRV_INF ("stopping model instance name=%s\n" ,name .c_str ()); 1250bool loading = it -> second .meta .status == SERVER_MODEL_STATUS_LOADING ; 1251if (loading ) { 1252// special case: if model is in loading state, unloading means force-killing it 1253SRV_WRN ("model name=%s is still loading, force-killing\n" ,name .c_str ()); 1254it -> second .subproc -> terminate (); 1255 } 1256request_stop (name , !loading ); 1257// status change will be handled by the monitor 1258 }else { 1259SRV_WRN ("model instance name=%s is not running\n" ,name .c_str ()); 1260 } 1261 } 1262} 1263 1264void server_models::unload_all () { 1265 std::unique_lock < std::mutex > lk (mutex ); 1266for (auto & [name ,inst ] :mapping ) { 1267if (inst .meta .status == SERVER_MODEL_STATUS_DOWNLOADING ) { 1268SRV_INF ("cancelling download for model name=%s\n" ,name .c_str ()); 1269inst .request_exit (); 1270 }else if (inst .meta .is_running ()) { 1271SRV_INF ("stopping model instance name=%s\n" ,name .c_str ()); 1272bool loading = inst .meta .status == SERVER_MODEL_STATUS_LOADING ; 1273if (loading ) { 1274inst .subproc -> terminate (); 1275 } 1276request_stop (name , !loading ); 1277 } 1278 } 1279// wait for every child to exit, the monitor force-kills the ones that ignore the exit command 1280cv .wait (lk , [this ]() { 1281for (const auto & [name ,inst ] :mapping ) { 1282if (inst .meta .is_running ()|| inst .meta .status == SERVER_MODEL_STATUS_DOWNLOADING ) { 1283return false; 1284 } 1285 } 1286return true; 1287 }); 1288} 1289 1290void server_models::update_status (const std::string & name ,const update_status_args & args ) { 1291 std::unique_lock < std::mutex > lk (mutex ); 1292auto it = mapping .find (name ); 1293if (it != mapping .end ()) { 1294auto & meta = it -> second .meta ; 1295meta .status = args .status ; 1296meta .exit_code = args .exit_code ; 1297if (!args .loaded_info .is_null ()) { 1298meta .loaded_info = args .loaded_info ; 1299 } 1300if (!args .progress .is_null ()) { 1301meta .progress = args .progress ; 1302 } 1303// a model that comes up idle or goes down changes the slot count for queued requests 1304sched -> tick (lk ); 1305 } 1306// broadcast status change to SSE 1307 { 1308json data = { 1309 {"status" ,server_model_status_to_string (args .status )}, 1310 }; 1311if (args .status == SERVER_MODEL_STATUS_UNLOADED ) { 1312data ["exit_code" ]= args .exit_code ; 1313 } 1314if (!args .loaded_info .is_null ()) { 1315data ["info" ]= args .loaded_info ; 1316 } 1317if (!args .progress .is_null ()) { 1318data ["progress" ]= args .progress ; 1319 } 1320// note: notify_sse doesn't acquire the lock, so no deadlock here 1321notify_sse ("status_change" ,name ,data ); 1322 } 1323cv .notify_all (); 1324} 1325 1326void server_models::update_download_progress (const std::string & name ,const common_download_progress & progress ,bool done ,bool ok ) { 1327json curr ; 1328 { 1329 std::lock_guard < std::mutex > lk (mutex ); 1330auto it = mapping .find (name ); 1331if (it != mapping .end ()) { 1332if (done ) { 1333// mark the instance to be erased on next load_models() call 1334it -> second .meta .status = SERVER_MODEL_STATUS_DOWNLOADED ; 1335need_reload = true; 1336 }else { 1337json & info = it -> second .meta .loaded_info ; 1338if (!info .contains ("progress" )) { 1339info ["progress" ]= json {}; 1340 } 1341info ["progress" ][progress .url ]= { 1342 {"done" ,progress .downloaded }, 1343 {"total" ,progress .total }, 1344 }; 1345curr = it -> second .meta .loaded_info ;// copy 1346 } 1347 } 1348 } 1349if (done ) { 1350cv .notify_all ();// notify in case unload() is waiting for download to be cancelled 1351notify_sse (ok ?"download_finished" :"download_failed" ,name , {}); 1352 }else { 1353notify_sse ("download_progress" ,name ,curr ); 1354 } 1355} 1356 1357bool server_models::remove (const std::string & name ) { 1358// do everything under one lock acquisition; avoid get_meta() / 1359// unload() because they can trigger load_models() which erases 1360// transient DOWNLOADING / DOWNLOADED entries as a side-effect 1361 std::unique_lock < std::mutex > lk (mutex ); 1362 1363auto it = mapping .find (name ); 1364if (it == mapping .end ()) { 1365throw std::runtime_error ("model name=" + name + " is not found" ); 1366 } 1367if (it -> second .meta .source != SERVER_MODEL_SOURCE_CACHE ) { 1368throw std::runtime_error ("model name=" + name + " is not removable (not from cache)" ); 1369 } 1370 1371if (it -> second .meta .status == SERVER_MODEL_STATUS_DOWNLOADING ) { 1372// cancel in-flight download 1373SRV_INF ("cancelling download for model name=%s\n" ,name .c_str ()); 1374it -> second .request_exit (); 1375 }else if (it -> second .meta .is_running ()) { 1376// stop running instance 1377SRV_INF ("stopping model instance name=%s\n" ,name .c_str ()); 1378bool loading = it -> second .meta .status == SERVER_MODEL_STATUS_LOADING ; 1379if (loading ) { 1380it -> second .subproc -> terminate (); 1381 } 1382request_stop (name , !loading ); 1383 } 1384 1385// wait until the child is gone 1386wait (lk ,name , [](const server_model_meta & meta ) { 1387return meta .status == SERVER_MODEL_STATUS_UNLOADED 1388|| meta .status == SERVER_MODEL_STATUS_DOWNLOADED ; 1389 }); 1390 1391// re-find after wait - load_models() may have erased the entry during the wait 1392it = mapping .find (name ); 1393if (it == mapping .end ()) { 1394// load_models() already erased the entry; we just need to clean up the cached files on disk 1395lk .unlock (); 1396bool ok = common_download_remove (name ); 1397SRV_INF ("removing model name=%s from cache (%s)\n" ,name .c_str (),ok ?"succeeded" :"partial" ); 1398notify_sse ("model_remove" ,name , {}); 1399return true; 1400 } 1401 1402// remove from disk (best-effort: cancelled downloads may have no cached files) 1403bool ok = common_download_remove (name ); 1404mapping .erase (name ); 1405if (!ok ) { 1406SRV_WRN ("removing model name=%s from disk returned false (no cached files?)\n" ,name .c_str ()); 1407 } 1408SRV_INF ("removing model name=%s from cache (%s)\n" ,name .c_str (),ok ?"succeeded" :"partial" ); 1409notify_sse ("model_remove" ,name , {}); 1410return true; 1411} 1412 1413void server_models::wait (const std::string & name , std::function < bool (const server_model_meta & )> predicate ) { 1414 std::unique_lock < std::mutex > lk (mutex ); 1415wait (lk ,name ,predicate ); 1416} 1417 1418void server_models::wait (std::unique_lock < std::mutex > & lk ,const std::string & name , std::function < bool (const server_model_meta & )> predicate ) { 1419cv .wait (lk , [this ,& name ,& predicate ]() { 1420auto it = mapping .find (name ); 1421if (it != mapping .end ()) { 1422return predicate (it -> second .meta ); 1423 1424 } 1425// model was removed from mapping by another code path (e.g. load_models()). 1426// nothing left to wait for - tell the caller to proceed. 1427return true; 1428 }); 1429} 1430 1431bool server_models::ensure_model_ready (const std::string & name ,const std::function < bool ()> & should_stop ) { 1432auto meta = get_meta (name ); 1433if (!meta .has_value ()) { 1434throw std::runtime_error ("model name=" + name + " is not found" ); 1435 } 1436if (meta -> is_ready ()) { 1437return false;// ready for taking requests 1438 } 1439if (meta -> status == SERVER_MODEL_STATUS_SLEEPING ) { 1440return false;// child is sleeping but still running; new request will wake it up 1441 } 1442 1443bool queued = false; 1444bool did_load = false; 1445 { 1446 std::unique_lock < std::mutex > lk (mutex ); 1447auto it = mapping .find (name ); 1448if (it != mapping .end ()&& it -> second .meta .status == SERVER_MODEL_STATUS_UNLOADED ) { 1449if (sched -> has_capacity (lk )&& sched -> queue_empty (lk )) { 1450lk .unlock (); 1451SRV_INF ("model name=%s is not loaded, loading...\n" ,name .c_str ()); 1452load (name ); 1453did_load = true; 1454 }else { 1455// also queue when a slot looks free but others wait already, else they starve 1456sched -> join (lk ,name ); 1457sched -> tick (lk ); 1458queued = true; 1459 } 1460 } 1461 } 1462 1463// while queued, this is also where the load happens: the head of the queue does it 1464SRV_INF ("waiting until model name=%s is fully loaded...\n" ,name .c_str ()); 1465 std::unique_lock < std::mutex > lk (mutex ); 1466auto leave_queue = [this ,& queued ,& lk ,& name ]() { 1467if (queued ) { 1468sched -> leave (lk ,name ); 1469queued = false; 1470 } 1471 }; 1472 1473try { 1474bool saw_loading = false; 1475while (true) { 1476auto it = mapping .find (name ); 1477if (it == mapping .end ()) { 1478break ;// removed by another code path, nothing to wait for 1479 } 1480const server_model_status status = it -> second .meta .status ; 1481 1482if (status == SERVER_MODEL_STATUS_LOADED || status == SERVER_MODEL_STATUS_SLEEPING ) { 1483break ; 1484 } 1485if (status == SERVER_MODEL_STATUS_DOWNLOADING || status == SERVER_MODEL_STATUS_DOWNLOADED ) { 1486break ;// do not wait on a download child 1487 } 1488if (status == SERVER_MODEL_STATUS_LOADING ) { 1489saw_loading = true; 1490 }else if (status == SERVER_MODEL_STATUS_UNLOADED ) { 1491if (did_load || saw_loading ) { 1492// a spawn happened and the instance came back down 1493if (it -> second .meta .is_failed ()) { 1494throw std::runtime_error ("model name=" + name + " failed to load" ); 1495 } 1496break ;// unloaded by another code path, caller reports "not running" 1497 } 1498if (!queued ) { 1499break ;// not queued, and the load someone else started fell over 1500 } 1501 } 1502 1503if (should_stop && should_stop ()) { 1504// if a model was evicted for us, the free slot goes to the next waiter 1505throw std::runtime_error ("request cancelled while waiting for model name=" + name ); 1506 } 1507 1508// our turn: our model is at the head, and a slot really did free up 1509if (status == SERVER_MODEL_STATUS_UNLOADED && sched -> try_claim (lk ,name )) { 1510lk .unlock (); 1511bool ok = true; 1512try { 1513SRV_INF ("slot available, loading queued model name=%s\n" ,name .c_str ()); 1514load (name ); 1515did_load = true; 1516 }catch (const std::exception & e ) { 1517// lost a race for the slot, stay in line and retry 1518SRV_WRN ("queued load of name=%s did not go through: %s\n" ,name .c_str (),e .what ()); 1519ok = false; 1520 } 1521lk .lock (); 1522sched -> claim_done (lk ,name ,ok ); 1523sched -> tick (lk ); 1524continue ; 1525 } 1526 1527cv .wait_for (lk , std::chrono::milliseconds (200 )); 1528 } 1529 }catch (...) { 1530leave_queue (); 1531sched -> tick (lk );// a slot freed for this waiter goes to the next one 1532throw ; 1533 } 1534leave_queue (); 1535 1536return true; 1537} 1538 1539server_http_res_ptr server_models::proxy_request (const server_http_req & req ,const std::string & method ,const std::string & name ,bool update_last_used ,bool detached ) { 1540auto meta = get_meta (name ); 1541if (!meta .has_value ()) { 1542throw std::runtime_error ("model name=" + name + " is not found" ); 1543 } 1544if (!meta -> is_running ()) { 1545throw std::invalid_argument ("model name=" + name + " is not running" ); 1546 } 1547 { 1548 std::unique_lock < std::mutex > lk (mutex ); 1549if (update_last_used ) { 1550mapping [name ].meta .last_used = ggml_time_ms (); 1551 } 1552mapping [name ].req_count ++ ; 1553 } 1554if (debug_fake_timing ) { 1555// sleep after req_count++, so the model counts as busy while we wait here 1556 std::this_thread::sleep_for (std::chrono::seconds (2 )); 1557 } 1558SRV_INF ("proxying request to model %s on port %d\n" ,name .c_str (),meta -> port ); 1559 std::string proxy_path = req .path ; 1560if (!req .query_string .empty ()) { 1561proxy_path += '?' + req .query_string ; 1562 } 1563auto proxy = std::make_unique < server_http_proxy > ( 1564method , 1565"http" , 1566CHILD_ADDR , 1567meta -> port , 1568proxy_path , 1569req .headers , 1570req .body , 1571req .files , 1572// a detached request belongs to a replay session 1573detached 1574 ? std::function < bool ()> ([]() {return false; }) 1575 :req .should_stop , 1576base_params .timeout_read , 1577base_params .timeout_write 1578 ); 1579 1580proxy -> cleanup = [this ,name ]() { 1581 std::unique_lock < std::mutex > lk (mutex ); 1582auto it = mapping .find (name ); 1583if (it != mapping .end ()&& it -> second .req_count > 0 ) { 1584it -> second .req_count -- ; 1585if (it -> second .req_count == 0 ) { 1586sched -> tick (lk ); 1587 } 1588 } 1589 }; 1590 1591return proxy ; 1592} 1593 1594void server_models::handle_child_state (const std::string & name ,const std::string & raw_input ) { 1595server_state state ; 1596json payload ; 1597 1598try { 1599json data = json::parse (raw_input .substr (strlen (CMD_CHILD_TO_ROUTER_STATE ))); 1600state = server_state_from_str (json_value (data ,"state" , std::string ())); 1601payload = json_value (data ,"payload" ,json {}); 1602 }catch (const std::exception & e ) { 1603SRV_ERR ("failed to parse child state update for name=%s: %s\n" ,name .c_str (),e .what ()); 1604return ; 1605 } 1606 1607switch (state ) { 1608case SERVER_STATE_DOWNLOADING : 1609 { 1610 std::string result = json_value (payload ,"result" , std::string ()); 1611 std::string url = json_value (payload ,"url" , std::string ()); 1612auto request_exit = [& ]() { 1613 std::lock_guard < std::mutex > lk (mutex ); 1614auto it = mapping .find (name ); 1615if (it != mapping .end ()) { 1616return it -> second .request_exit (); 1617 } 1618 }; 1619if (result == "download_finished" ) { 1620update_download_progress (name , {}, true, true); 1621request_exit (); 1622 }else if (result == "download_failed" ) { 1623update_download_progress (name , {}, true, false); 1624request_exit (); 1625 }else if (!url .empty ()) { 1626common_download_progress p ; 1627p .url = url ; 1628p .downloaded = json_value (payload ,"downloaded" , (size_t )0 ); 1629p .total = json_value (payload ,"total" , (size_t )0 ); 1630update_download_progress (name ,p , false); 1631 } 1632 }break ; 1633case SERVER_STATE_LOADING : 1634 { 1635update_status (name , { 1636SERVER_MODEL_STATUS_LOADING , 16370 , 1638nullptr ,// no loaded_info yet 1639payload , 1640 }); 1641 }break ; 1642case SERVER_STATE_READY : 1643 { 1644update_status (name , { 1645SERVER_MODEL_STATUS_LOADED , 16460 , 1647// note: payload can be empty if this is a wakeup from sleep 1648payload .size ()> 0 ?payload :nullptr , 1649 {},// reset progress info 1650 }); 1651 }break ; 1652case SERVER_STATE_SLEEPING : 1653 { 1654update_status (name , {SERVER_MODEL_STATUS_SLEEPING }); 1655 }break ; 1656default : 1657// should never happen, but just in case 1658GGML_ASSERT (false&& "unexpected state from child server" ); 1659 } 1660} 1661 1662// 1663// server_child 1664// 1665 1666bool server_child::is_child () { 1667const char * router_port = std::getenv ("LLAMA_SERVER_ROUTER_PORT" ); 1668return router_port != nullptr ; 1669} 1670 1671server_child_mode server_child::get_mode () { 1672const char * mode = std::getenv ("LLAMA_SERVER_CHILD_MODE" ); 1673 std::string mode_str (mode ?mode :"" ); 1674if (mode_str == "download" ) { 1675return SERVER_CHILD_MODE_DOWNLOAD ; 1676 }else { 1677return SERVER_CHILD_MODE_NORMAL ; 1678 } 1679} 1680 1681struct server_download_state :public common_download_callback { 1682server_child * self ; 1683 std::function < bool ()> should_stop ; 1684 std::atomic < int64_t > last_progress_time {0 };// multiple files downloading in different threads 1685bool is_ok = false; 1686 1687server_download_state (server_child * s ) :self (s ) {} 1688 1689bool run (common_params & params ) { 1690try { 1691common_models_handler handler = common_models_handler_init (params ,LLAMA_EXAMPLE_SERVER ); 1692common_models_handler_apply (handler ,params ,this ); 1693is_ok = true; 1694 }catch (const std::exception & e ) { 1695auto model_name = params .model .get_name (); 1696SRV_ERR ("download failed for model name=%s: %s\n" ,model_name .c_str (),e .what ()); 1697is_ok = false; 1698 } 1699return is_ok ; 1700 } 1701void on_progress (const common_download_progress & p ) { 1702json data = { 1703 {"url" ,p .url }, 1704 {"downloaded" ,p .downloaded }, 1705 {"total" ,p .total }, 1706 }; 1707self -> notify_to_router (server_state_to_str (SERVER_STATE_DOWNLOADING ),data ); 1708 } 1709void on_start (const common_download_progress & p )override { 1710on_progress (p ); 1711 } 1712void on_update (const common_download_progress & p )override { 1713int64_t now = ggml_time_ms (); 1714// throttle progress updates to avoid flooding logs 1715if (now - last_progress_time .load (std::memory_order_relaxed ) >=100 ) { 1716on_progress (p ); 1717last_progress_time .store (now , std::memory_order_relaxed ); 1718 } 1719 } 1720void on_done (const common_download_progress & p ,bool )override { 1721on_progress (p ); 1722 } 1723bool is_cancelled ()const override { 1724return should_stop ?should_stop () : false; 1725 } 1726}; 1727 1728int server_child::run_download (common_params & params ) { 1729auto cancelled = std::make_shared < std::atomic < bool >> (false); 1730 1731// monitor stdin for cancellation command from the router 1732 std::thread signal_thread = setup ([cancelled ](int ) { 1733cancelled -> store (true, std::memory_order_relaxed ); 1734 }); 1735 1736server_download_state dl (this ); 1737dl .should_stop = [cancelled ]() { 1738return cancelled -> load (std::memory_order_relaxed ); 1739 }; 1740 1741bool ok = dl .run (params ); 1742 1743notify_to_router (server_state_to_str (SERVER_STATE_DOWNLOADING ), { 1744 {"result" ,ok ?"download_finished" :"download_failed" }, 1745 }); 1746 1747// router should send CMD_ROUTER_TO_CHILD_EXIT after receiving the result 1748if (signal_thread .joinable ()) { 1749signal_thread .join (); 1750 } 1751 1752SRV_INF ("download completed %s\n" ,ok ?"successfully" :"with errors" ); 1753return 0 ; 1754} 1755 1756std::thread server_child::setup (const std::function < void (int )> & shutdown_handler ) { 1757// setup thread for monitoring stdin 1758return std::thread ([shutdown_handler ]() { 1759// wait for EOF on stdin 1760SRV_INF ("%s" ,"child server monitoring thread started, waiting for EOF on stdin...\n" ); 1761bool eof = false; 1762while (true) { 1763 std::string line ; 1764if (!std::getline (std::cin ,line )) { 1765// EOF detected, that means the router server is unexpectedly exit or killed 1766eof = true; 1767break ; 1768 } 1769if (line .find (CMD_ROUTER_TO_CHILD_EXIT )!= std::string::npos ) { 1770SRV_INF ("%s" ,"exit command received, exiting...\n" ); 1771shutdown_handler (0 ); 1772break ; 1773 } 1774 } 1775if (eof ) { 1776SRV_INF ("%s" ,"EOF on stdin detected, forcing shutdown...\n" ); 1777exit (1 ); 1778 } 1779 }); 1780} 1781 1782void server_child::notify_to_router (const std::string & state ,const json & payload ) { 1783json data = { 1784 {"state" ,state }, 1785 {"payload" ,payload }, 1786 }; 1787 std::lock_guard < std::mutex > lk (mtx_stdout ); 1788common_log_pause (common_log_main ()); 1789fflush (stdout ); 1790// the router matches the command on a line prefix, so the leading newline 1791// closes whatever the logger left open on the shared pipe, down to the 1792// trailing color reset that carries no newline of its own 1793fprintf (stdout ,"\n%s%s\n" ,CMD_CHILD_TO_ROUTER_STATE ,safe_json_to_str (data ).c_str ()); 1794fflush (stdout ); 1795common_log_resume (common_log_main ()); 1796} 1797 1798 1799// 1800// server_models_routes 1801// 1802 1803// RAII wrapper similar to server_response_reader, but doesn't use server_queue 1804static std::atomic < int > sse_client_id_counter = 0 ; 1805struct server_models_sse_client { 1806server_response & queue_results ; 1807int client_id ; 1808server_models_sse_client (server_response & q ) 1809 :queue_results (q ),client_id (sse_client_id_counter .fetch_add (1 , std::memory_order_relaxed )) { 1810SRV_DBG ("new SSE client connected, assigned client_id=%d\n" ,client_id ); 1811queue_results .add_waiting_task_id (client_id ); 1812 } 1813 ~server_models_sse_client () { 1814SRV_DBG ("SSE client disconnected, removing client_id=%d\n" ,client_id ); 1815queue_results .remove_waiting_task_id (client_id ); 1816 } 1817 1818// return nullptr if should_stop() is true before receiving a result 1819// note: if one error is received, it will stop further processing and return error result 1820server_task_result_ptr next (const std::function < bool ()> & should_stop ) { 1821while (true) { 1822static const int http_polling_seconds = 1 ;// check should_stop every 1 second 1823server_task_result_ptr result = queue_results .recv_with_timeout ({client_id },http_polling_seconds ); 1824if (result == nullptr ) { 1825// timeout, check stop condition 1826if (should_stop ()) { 1827return nullptr ; 1828 } 1829// continue waiting otherwise 1830 }else { 1831SRV_DBG ("recv result for client_id=%d: %s\n" ,client_id ,safe_json_to_str (result -> to_json ()).c_str ()); 1832return result ; 1833 } 1834 } 1835// should not reach here 1836 } 1837}; 1838 1839static void res_ok (std::unique_ptr < server_http_res > & res ,const json & response_data ) { 1840res -> status = 200 ; 1841res -> data = safe_json_to_str (response_data ); 1842} 1843 1844static void res_err (std::unique_ptr < server_http_res > & res ,const json & error_data ) { 1845res -> status = json_value (error_data ,"code" ,500 ); 1846res -> data = safe_json_to_str ({{"error" ,error_data }}); 1847} 1848 1849static bool router_validate_model (std::string & name ,server_models & models ,bool models_autoload , std::unique_ptr < server_http_res > & res ) { 1850if (name .empty ()) { 1851res_err (res ,format_error_response ("model name is missing from the request" ,ERROR_TYPE_INVALID_REQUEST )); 1852return false; 1853 } 1854auto meta = models .get_meta (name ); 1855if (!meta .has_value ()) { 1856res_err (res ,format_error_response (string_format ("model '%s' not found" ,name .c_str ()),ERROR_TYPE_INVALID_REQUEST )); 1857return false; 1858 } 1859// resolve alias to canonical model name 1860name = meta -> name ; 1861if (!models_autoload && !meta -> is_running ()) { 1862res_err (res ,format_error_response ("model is not loaded" ,ERROR_TYPE_INVALID_REQUEST )); 1863return false; 1864 } 1865return true; 1866} 1867 1868static bool is_autoload (const common_params & params ,const server_http_req & req ) { 1869 std::string autoload = req .get_param ("autoload" ); 1870if (autoload .empty ()) { 1871return params .models_autoload ; 1872 }else { 1873return autoload == "true" || autoload == "1" ; 1874 } 1875} 1876 1877// percent encode one query or path component, covers reserved chars without pulling in 1878// httplib::detail. used by the stream routes to forward conversation_id to children safely 1879static std::string encode_qs (const std::string & in ) { 1880 std::string out ; 1881out .reserve (in .size ()* 3 ); 1882for (unsigned char c :in ) { 1883bool safe = (c >='A' && c <='Z' )|| (c >='a' && c <='z' )|| (c >='0' && c <='9' ) 1884|| c == '-' || c == '_' || c == '.' || c == '~' ; 1885if (safe ) { 1886out .push_back (char (c )); 1887 }else { 1888char buf [4 ]; 1889 std::snprintf (buf ,sizeof (buf ),"%%%02X" ,c ); 1890out .append (buf ,3 ); 1891 } 1892 } 1893return out ; 1894} 1895 1896// resolve the child that owns a conversation's stream session via the conv_id -> model map 1897// populated when the POST was routed. single map lookup then a meta lookup, no polling, no 1898// parsing of the conv id. returns nullopt when nothing maps, the caller answers not found and 1899// the client recovers 1900static std::optional < server_model_meta > resolve_child_for_conv ( 1901server_models & models ,const std::string & conversation_id ) { 1902if (conversation_id .empty ()) { 1903return std::nullopt ; 1904 } 1905auto tracked = models .conv_models .lookup (conversation_id ); 1906if (!tracked .has_value ()) { 1907return std::nullopt ; 1908 } 1909auto meta = models .get_meta (* tracked ); 1910if (meta .has_value ()&& meta -> is_ready ()) { 1911return meta ; 1912 } 1913return std::nullopt ; 1914} 1915 1916void server_models_routes::init_routes () { 1917if (!common_subproc::is_supported ()) { 1918throw std::runtime_error ("subprocess is not enabled on this build" ); 1919 } 1920 1921this -> get_router_props = [this ](const server_http_req & req ) { 1922 std::string name = req .get_param ("model" ); 1923if (name .empty ()) { 1924// main instance 1925auto res = std::make_unique < server_http_res > (); 1926res_ok (res , { 1927// TODO: add support for this on web UI 1928 {"role" ,"router" }, 1929 {"max_instances" ,params .models_max }, 1930 {"models_autoload" ,params .models_autoload }, 1931// this is a dummy response to make sure the UI doesn't break 1932 {"model_alias" ,"llama-server" }, 1933 {"model_path" ,"none" }, 1934 {"default_generation_settings" , { 1935 {"params" ,json {}}, 1936 {"n_ctx" ,0 }, 1937 }}, 1938// New key 1939 {"ui_settings" ,ui_settings }, 1940 {"build_info" , std::string (llama_build_info ())}, 1941 {"cors_proxy_enabled" ,params .ui_mcp_proxy }, 1942 }); 1943return res ; 1944 } 1945return proxy_get (req ); 1946 }; 1947 1948this -> proxy_get = [this ](const server_http_req & req ) { 1949 std::string method = "GET" ; 1950 std::string name = req .get_param ("model" ); 1951bool autoload = is_autoload (params ,req ); 1952auto error_res = std::make_unique < server_http_res > (); 1953if (!router_validate_model (name ,models ,autoload ,error_res )) { 1954return error_res ; 1955 } 1956if (autoload ) { 1957models .ensure_model_ready (name ,req .should_stop ); 1958 } 1959return models .proxy_request (req ,method ,name , false); 1960 }; 1961 1962this -> proxy_post = [this ](const server_http_req & req ) { 1963 std::string method = "POST" ; 1964json body = json::parse (req .body ); 1965 std::string name = json_value (body ,"model" , std::string ()); 1966bool autoload = is_autoload (params ,req ); 1967auto error_res = std::make_unique < server_http_res > (); 1968if (!router_validate_model (name ,models ,autoload ,error_res )) { 1969return error_res ; 1970 } 1971// remember which child serves this conversation so the stream routes can route straight 1972// to it without polling, keyed on the exact conv id from the header. registered before 1973// the load wait so a stop issued while the model loads can erase the entry and cancel 1974// this request instead of leaving an orphan generation 1975 std::string conv_id = server_stream_conv_id_from_headers (req .headers ); 1976uint64_t ticket = models .conv_models .remember (conv_id ,name ); 1977// a dead socket must not cancel a session request, only a stop does (checked right below) 1978auto should_stop = ticket == 0 ?req .should_stop :nullptr ; 1979bool waited = autoload && models .ensure_model_ready (name ,should_stop ); 1980if (ticket != 0 && !models .conv_models .alive (conv_id ,ticket )) { 1981SRV_INF ("request for conv_id=%s cancelled while model name=%s was loading\n" , 1982conv_id .c_str (),name .c_str ()); 1983res_err (error_res ,format_error_response ( 1984"request cancelled by a stop while the model was loading" ,ERROR_TYPE_INVALID_REQUEST )); 1985return error_res ; 1986 } 1987// a session request that waited for a load detaches from the client socket: the 1988// client may have dropped during the wait (page reload) and the session buffer must 1989// still receive the generation for a later resume 1990return models .proxy_request (req ,method ,name , true,waited && ticket != 0 );// update last usage for POST request only 1991 }; 1992 1993this -> post_router_models_load = [this ](const server_http_req & req ) { 1994auto res = std::make_unique < server_http_res > (); 1995json body = json::parse (req .body ); 1996 std::string name = json_value (body ,"model" , std::string ()); 1997auto meta = models .get_meta (name ); 1998if (!meta .has_value ()) { 1999res_err (res ,format_error_response ("model is not found" ,ERROR_TYPE_NOT_FOUND )); 2000return res ; 2001 } 2002if (meta -> is_running ()) { 2003res_err (res ,format_error_response ("model is already running" ,ERROR_TYPE_INVALID_REQUEST )); 2004return res ; 2005 } 2006models .load (meta -> name ); 2007res_ok (res , {{"success" , true}}); 2008return res ; 2009 }; 2010 2011this -> get_router_models = [this ](const server_http_req & req ) { 2012bool reload = !req .get_param ("reload" ,"" ).empty (); 2013if (reload ) { 2014models .load_models (); 2015 } 2016auto res = std::make_unique < server_http_res > (); 2017json models_json = json::array (); 2018auto all_models = models .get_all_meta (); 2019 std::time_t t = std::time (0 ); 2020for (const auto & meta :all_models ) { 2021if (meta .hidden ) { 2022continue ;// cache model deduplicated by a preset 2023 } 2024json status { 2025 {"value" ,server_model_status_to_string (meta .status )}, 2026 {"args" ,meta .args }, 2027 }; 2028if (!meta .preset .name .empty ()) { 2029common_preset preset_copy = meta .preset ; 2030unset_reserved_args (preset_copy , false); 2031preset_copy .unset_option ("LLAMA_ARG_HOST" ); 2032preset_copy .unset_option ("LLAMA_ARG_PORT" ); 2033preset_copy .unset_option ("LLAMA_ARG_ALIAS" ); 2034preset_copy .unset_option ("LLAMA_ARG_TAGS" ); 2035status ["preset" ]= preset_copy .to_ini (); 2036 } 2037if (meta .is_failed ()) { 2038status ["exit_code" ]= meta .exit_code ; 2039status ["failed" ]= true; 2040 } 2041 2042// pi coding agent multimodal compatibility 2043json input_modalities = json::array ({"text" }); 2044if (meta .multimodal .inp_vision ) { 2045input_modalities .push_back ("image" ); 2046 } 2047if (meta .multimodal .inp_audio ) { 2048input_modalities .push_back ("audio" ); 2049 } 2050json architecture { 2051 {"input_modalities" ,input_modalities }, 2052 {"output_modalities" , json::array ({"text" })}, 2053 }; 2054 2055json model_info = json { 2056 {"id" ,meta .name }, 2057 {"aliases" ,meta .aliases }, 2058 {"tags" ,meta .tags }, 2059 {"object" ,"model" },// for OAI-compat 2060 {"owned_by" ,"llamacpp" },// for OAI-compat 2061 {"created" ,t },// for OAI-compat 2062 {"status" ,status }, 2063 {"architecture" ,architecture }, 2064 {"source" ,server_model_source_to_string (meta .source )}, 2065 {"can_remove" ,meta .source == SERVER_MODEL_SOURCE_CACHE }, 2066// {"need_download", meta.need_download}, 2067// TODO: add other fields, may require reading GGUF metadata 2068 }; 2069 2070// merge with loaded_info from the child process if available 2071if (meta .is_running ()) { 2072for (auto it = meta .loaded_info .begin ();it != meta .loaded_info .end ();++ it ) { 2073if (!model_info .contains (it .key ())) { 2074model_info [it .key ()]= it .value (); 2075 } 2076 } 2077 } 2078models_json .push_back (model_info ); 2079 } 2080res_ok (res , { 2081 {"data" ,models_json }, 2082 {"object" ,"list" }, 2083 }); 2084return res ; 2085 }; 2086 2087this -> post_router_models_unload = [this ](const server_http_req & req ) { 2088auto res = std::make_unique < server_http_res > (); 2089json body = json::parse (req .body ); 2090 std::string name = json_value (body ,"model" , std::string ()); 2091auto model = models .get_meta (name ); 2092if (!model .has_value ()) { 2093res_err (res ,format_error_response ("model is not found" ,ERROR_TYPE_INVALID_REQUEST )); 2094return res ; 2095 } 2096if (!model -> is_running ()&& model -> status != SERVER_MODEL_STATUS_DOWNLOADING ) { 2097res_err (res ,format_error_response ("model is not running" ,ERROR_TYPE_INVALID_REQUEST )); 2098return res ; 2099 } 2100models .unload (model -> name ); 2101res_ok (res , {{"success" , true}}); 2102return res ; 2103 }; 2104 2105this -> get_router_models_sse = [this ](const server_http_req & req ) { 2106auto res = std::make_unique < server_http_res > (); 2107res -> status = 200 ; 2108res -> content_type = "text/event-stream" ; 2109auto sse_client = std::make_shared < server_models_sse_client > (models .sse ); 2110res -> next = [this ,sse_client ,& req ](std::string & output )-> bool { 2111auto result = sse_client -> next ([& ]() { 2112return stopping .load (std::memory_order_relaxed )|| req .should_stop (); 2113 }); 2114if (result == nullptr ) { 2115return false;// client disconnected or should_stop 2116 } 2117output = "data: " + safe_json_to_str (result -> to_json ())+ "\n\n" ; 2118return true;// listen for the next event 2119 }; 2120return res ; 2121 }; 2122 2123this -> post_router_models = [this ](const server_http_req & req ) { 2124auto res = std::make_unique < server_http_res > (); 2125 2126json body = json::parse (req .body ); 2127 std::string name = json_value (body ,"model" , std::string ()); 2128if (name .empty ()) { 2129throw std::invalid_argument ("model must be a non-empty string" ); 2130 } 2131 2132common_params p ; 2133p .model .hf_repo = name ; 2134p .hf_token = params .hf_token ; 2135 2136// validate by fetching metadata 2137bool ok = false; 2138try { 2139common_models_handler_init (p ,LLAMA_EXAMPLE_SERVER ); 2140ok = true; 2141 }catch (...) { 2142SRV_ERR ("unknown error while validating model '%s'\n" ,name .c_str ()); 2143// other exceptions will be handled by the outer ex_wrapper() 2144throw ; 2145 } 2146 2147if (!ok ) { 2148throw std::invalid_argument ("model validation failed, unable to download" ); 2149 } 2150 2151// reject if model already exists 2152if (models .has_model (name )) { 2153throw std::invalid_argument ("model '" + name + "' already exists" ); 2154 } 2155 2156// then, proceed with the actual download 2157SRV_INF ("starting download for model '%s'\n" ,name .c_str ()); 2158 { 2159 server_models::load_options load_opts ; 2160load_opts .mode = SERVER_CHILD_MODE_DOWNLOAD ; 2161load_opts .custom_meta = server_model_meta {}; 2162load_opts .custom_meta -> source = SERVER_MODEL_SOURCE_CACHE ; 2163load_opts .custom_meta -> name = name ; 2164models .load (name ,load_opts ); 2165 } 2166 2167res_ok (res , {{"success" , true}}); 2168return res ; 2169 }; 2170 2171this -> del_router_models = [this ](const server_http_req & req ) { 2172auto res = std::make_unique < server_http_res > (); 2173 2174 std::string name = req .get_param ("model" ); 2175if (name .empty ()) { 2176throw std::invalid_argument ("model must be a non-empty string" ); 2177 } 2178 2179models .remove (name );// throws on error 2180 2181res_ok (res , {{"success" , true}}); 2182return res ; 2183 }; 2184 2185this -> router_stream_get = [this ](const server_http_req & req ) { 2186// GET /v1/stream?conv_id=<id>&from=N. resolve the owning child from the conv_id -> model 2187// map, 404 when nothing maps 2188auto res = std::make_unique < server_http_res > (); 2189 std::string conv_id = req .get_param ("conv_id" ); 2190if (conv_id .empty ()) { 2191res_err (res ,format_error_response ("Missing conversation id in path" ,ERROR_TYPE_INVALID_REQUEST )); 2192return res ; 2193 } 2194 std::optional < server_model_meta > owner = resolve_child_for_conv (models ,conv_id ); 2195if (!owner .has_value ()) { 2196// a registered conv whose model is still loading earns a retry: the session appears 2197// once the load ends and the pending request reaches the child 2198auto tracked = models .conv_models .lookup (conv_id ); 2199auto meta = tracked .has_value () ?models .get_meta (* tracked ) : std::nullopt ; 2200bool transient = meta .has_value ()&& (meta -> status == SERVER_MODEL_STATUS_LOADING || 2201meta -> status == SERVER_MODEL_STATUS_DOWNLOADING || 2202meta -> status == SERVER_MODEL_STATUS_DOWNLOADED ); 2203if (transient ) { 2204res_err (res ,format_error_response ("Stream owner model is loading, retry later" ,ERROR_TYPE_UNAVAILABLE )); 2205 }else { 2206res_err (res ,format_error_response ("Stream not found or expired" ,ERROR_TYPE_NOT_FOUND )); 2207 } 2208return res ; 2209 } 2210 std::string from = req .get_param ("from" ); 2211 std::string child_path = "/v1/stream?conv_id=" + encode_qs (conv_id ); 2212if (!from .empty ()) { 2213child_path += "&from=" + from ; 2214 } 2215SRV_TRC ("proxying stream resume to model %s on port %d, path=%s\n" , 2216owner -> name .c_str (),owner -> port ,child_path .c_str ()); 2217auto proxy = std::make_unique < server_http_proxy > ( 2218"GET" , 2219"http" , 2220CHILD_ADDR , 2221owner -> port , 2222child_path , 2223req .headers , 2224req .body , 2225req .files , 2226req .should_stop , 2227params .timeout_read , 2228params .timeout_write ); 2229return std::unique_ptr < server_http_res > (std::move (proxy )); 2230 }; 2231 2232this -> router_streams_lookup = [this ](const server_http_req & req ) { 2233// POST /v1/streams/lookup. resolve each requested conv id to its owning child via the 2234// map, group the ids per child, and query only the children that actually own some of 2235// them instead of fanning out to every ready child. a child only answers for the ids 2236// it owns, never lists anything else 2237auto res = std::make_unique < server_http_res > (); 2238 std::vector < std::string > requested ; 2239try { 2240json body = json::parse (req .body ); 2241if (body .contains ("conversation_ids" )&& body ["conversation_ids" ].is_array ()) { 2242for (const auto & v :body ["conversation_ids" ]) { 2243if (v .is_string ()&& !v .get < std::string > ().empty ()) { 2244requested .push_back (v .get < std::string > ()); 2245 } 2246 } 2247 } 2248 }catch (const std::exception & ) { 2249res_ok (res , json::array ()); 2250return res ; 2251 } 2252 2253// group requested ids by the child port that owns them, drop ids that map to nothing 2254 std::unordered_map < int ,json > per_child ; 2255for (const auto & cid :requested ) { 2256auto owner = resolve_child_for_conv (models ,cid ); 2257if (!owner .has_value ()) { 2258continue ; 2259 } 2260per_child [owner -> port ].push_back (cid ); 2261 } 2262 2263json aggregated = json::array (); 2264for (auto & [port ,ids ] :per_child ) { 2265json child_body = {{"conversation_ids" ,ids }}; 2266 httplib::Client cli (CHILD_ADDR ,port ); 2267cli .set_connection_timeout (0 ,STREAM_LOOKUP_TIMEOUT_MS * 1000 ); 2268cli .set_read_timeout (0 ,STREAM_LOOKUP_TIMEOUT_MS * 1000 ); 2269cli .set_write_timeout (0 ,STREAM_LOOKUP_TIMEOUT_MS * 1000 ); 2270auto resp = cli .Post ("/v1/streams/lookup" ,child_body .dump (),"application/json" ); 2271if (!resp || resp -> status != 200 ) { 2272continue ; 2273 } 2274try { 2275json child_arr = json::parse (resp -> body ); 2276if (!child_arr .is_array ()) { 2277continue ; 2278 } 2279for (auto & entry :child_arr ) { 2280if (entry .is_object ()) { 2281aggregated .push_back (entry ); 2282 } 2283 } 2284 }catch (const std::exception & ) { 2285continue ; 2286 } 2287 } 2288res_ok (res ,aggregated ); 2289return res ; 2290 }; 2291 2292this -> router_stream_delete = [this ](const server_http_req & req ) { 2293// DELETE /v1/stream?conv_id=<id>. resolve the owning child via the map and forward only to 2294// it, evict_and_cancel is idempotent on the child 2295auto res = std::make_unique < server_http_res > (); 2296 std::string conv_id = req .get_param ("conv_id" ); 2297if (conv_id .empty ()) { 2298res_err (res ,format_error_response ("Missing conversation id in path" ,ERROR_TYPE_INVALID_REQUEST )); 2299return res ; 2300 } 2301 std::string child_path = "/v1/stream?conv_id=" + encode_qs (conv_id ); 2302auto owner = resolve_child_for_conv (models ,conv_id ); 2303if (owner .has_value ()) { 2304 httplib::Client cli (CHILD_ADDR ,owner -> port ); 2305cli .set_connection_timeout (0 ,STREAM_LOOKUP_TIMEOUT_MS * 1000 ); 2306cli .set_read_timeout (0 ,STREAM_LOOKUP_TIMEOUT_MS * 1000 ); 2307cli .set_write_timeout (0 ,STREAM_LOOKUP_TIMEOUT_MS * 1000 ); 2308auto resp = cli .Delete (child_path .c_str ()); 2309 (void )resp ;// the child logs its own miss when the session is unknown there 2310 }else if (auto tracked = models .conv_models .lookup (conv_id );tracked .has_value ()) { 2311// the entry exists but its model is still loading: the forget below erases it, 2312// which cancels the request parked in proxy_post before the generation starts 2313SRV_INF ("router stop for conv_id=%s while model name=%s is loading, cancelling the pending request\n" , 2314conv_id .c_str (),tracked -> c_str ()); 2315 }else { 2316SRV_WRN ("router stop for unknown conv_id=%s, no owning child in the conv map\n" , 2317conv_id .c_str ()); 2318 } 2319// drop the tracking entry, the session is being torn down 2320models .conv_models .forget (conv_id ); 2321res -> status = 204 ; 2322res -> content_type = "application/json" ; 2323return res ; 2324 }; 2325} 2326 2327 2328 2329// 2330// server_http_proxy 2331// 2332 2333static std::string to_lower_copy (const std::string & value ) { 2334 std::string lowered (value .size (),'\0' ); 2335 std::transform (value .begin (),value .end (),lowered .begin (), [](unsigned char c ) {return std::tolower (c ); }); 2336return lowered ; 2337} 2338 2339static bool should_strip_proxy_header (const std::string & header_name ) { 2340// Headers that get duplicated when router forwards child responses 2341if (header_name == "server" || 2342header_name == "transfer-encoding" || 2343header_name == "content-length" || // quick fix for https://github.com/ggml-org/llama.cpp/issues/17710 2344header_name == "keep-alive" ) { 2345return true; 2346 } 2347 2348// Router injects CORS, child also sends them: duplicate 2349if (header_name .rfind ("access-control-" ,0 )== 0 ) { 2350return true; 2351 } 2352 2353return false; 2354} 2355 2356static std::string generate_multipart_boundary () { 2357 thread_local std::mt19937 gen (std::random_device {}()); 2358static const char chars []= "0123456789abcdefghijklmnopqrstuvwxyz" ; 2359 std::uniform_int_distribution <> dis (0 ,sizeof (chars )- 2 ); 2360 std::string boundary = "----llama-cpp-proxy-" ; 2361for (int i = 0 ;i < 16 ;i ++ ) { 2362boundary += chars [dis (gen )]; 2363 } 2364return boundary ; 2365} 2366 2367static std::string build_multipart_body ( 2368const json & form_fields , 2369const std::map < std::string ,uploaded_file > & files , 2370const std::string & boundary ) { 2371static auto sanitize_field = [](const std::string & text ) { 2372 std::string result ; 2373result .reserve (text .size ()); 2374for (char c :text ) { 2375if (c != '\n' && c != '\r' && c != '"' ) { 2376result += c ; 2377 } 2378 } 2379return result ; 2380 }; 2381 2382 std::ostringstream body ; 2383 2384for (const auto & [key ,value ] :form_fields .items ()) { 2385if (value .is_array ()) { 2386for (const auto & item :value ) { 2387body <<"--" <<boundary <<"\r\n" ; 2388body <<"Content-Disposition: form-data; name=\"" <<sanitize_field (key ) <<"\"\r\n" ; 2389body <<"\r\n" ; 2390if (!item .is_string ()) { 2391throw std::invalid_argument ("expected string" ); 2392 } 2393body <<item .get < std::string > () <<"\r\n" ; 2394 } 2395 }else { 2396body <<"--" <<boundary <<"\r\n" ; 2397body <<"Content-Disposition: form-data; name=\"" <<sanitize_field (key ) <<"\"\r\n" ; 2398body <<"\r\n" ; 2399if (!value .is_string ()) { 2400throw std::invalid_argument ("expected string" ); 2401 } 2402body <<value .get < std::string > () <<"\r\n" ; 2403 } 2404 } 2405 2406for (const auto & [key ,file ] :files ) { 2407body <<"--" <<boundary <<"\r\n" ; 2408body <<"Content-Disposition: form-data; name=\"" <<sanitize_field (key ) <<"\"" ; 2409if (!file .filename .empty ()) { 2410body <<"; filename=\"" <<sanitize_field (file .filename ) <<"\"" ; 2411 } 2412body <<"\r\n" ; 2413if (!file .content_type .empty ()) { 2414body <<"Content-Type: " <<sanitize_field (file .content_type ) <<"\r\n" ; 2415 }else { 2416body <<"Content-Type: application/octet-stream\r\n" ; 2417 } 2418body <<"\r\n" ; 2419body .write (reinterpret_cast < const char *> (file .data .data ()),file .data .size ()); 2420body <<"\r\n" ; 2421 } 2422 2423body <<"--" <<boundary <<"--\r\n" ; 2424return body .str (); 2425} 2426 2427server_http_proxy::server_http_proxy ( 2428const std::string & method , 2429const std::string & scheme , 2430const std::string & host , 2431int port , 2432const std::string & path , 2433const std::map < std::string , std::string > & headers , 2434const std::string & body , 2435const std::map < std::string ,uploaded_file > & files , 2436const std::function < bool ()> should_stop , 2437int32_t timeout_read , 2438int32_t timeout_write 2439 ) { 2440// shared between reader and writer threads 2441auto cli = std::make_shared < httplib::ClientImpl > (host ,port ); 2442auto pipe = std::make_shared < server_pipe < msg_t >> (); 2443 2444if (scheme == "https" ) { 2445#ifdef CPPHTTPLIB_OPENSSL_SUPPORT 2446cli .reset (new httplib::SSLClient (host ,port )); 2447#else 2448throw std::runtime_error ("HTTPS requested but CPPHTTPLIB_OPENSSL_SUPPORT is not defined" ); 2449#endif 2450 } 2451 2452// setup Client 2453cli -> set_follow_location (true); 2454cli -> set_connection_timeout (timeout_read ,0 );// use --timeout value instead of hardcoded 5 s 2455cli -> set_write_timeout (timeout_read ,0 );// reversed for cli (client) vs srv (server) 2456cli -> set_read_timeout (timeout_write ,0 ); 2457this -> status = 500 ;// to be overwritten upon response 2458this -> cleanup_pipes = [pipe ]() { 2459pipe -> close_read (); 2460pipe -> close_write (); 2461 }; 2462 2463// wire up the receive end of the pipe 2464this -> next = [pipe ,should_stop ](std::string & out )-> bool { 2465msg_t msg ; 2466bool has_next = pipe -> read (msg ,should_stop ); 2467if (!msg .data .empty ()) { 2468out = std::move (msg .data ); 2469 } 2470return has_next ;// false if EOF or pipe broken 2471 }; 2472 2473// build the header message forwarded to the reader thread, stripping internal proxy headers 2474auto make_header_msg = [](const httplib::Response & response ) { 2475msg_t msg ; 2476msg .status = response .status ; 2477for (const auto & [key ,value ] :response .headers ) { 2478const auto lowered = to_lower_copy (key ); 2479if (should_strip_proxy_header (lowered )) { 2480continue ; 2481 } 2482if (lowered == "content-type" ) { 2483msg .content_type = value ; 2484continue ; 2485 } 2486msg .headers [key ]= value ; 2487 } 2488return msg ; 2489 }; 2490 2491// true once response_handler has already forwarded the headers 2492auto headers_sent = std::make_shared < std::atomic < bool >> (false); 2493 2494// wire up the HTTP client 2495// note: do NOT capture `this` pointer, as it may be destroyed before the thread ends 2496 httplib::ResponseHandler response_handler = [pipe ,headers_sent ,make_header_msg ](const httplib::Response & response ) { 2497headers_sent -> store (true); 2498return pipe -> write (make_header_msg (response ));// send headers first 2499 }; 2500 httplib::ContentReceiverWithProgress content_receiver = [pipe ](const char * data ,size_t data_length ,size_t ,size_t ) { 2501// send data chunks 2502// returns false if pipe is closed / broken (signal to stop receiving) 2503return pipe -> write ({{},0 , std::string (data ,data_length ),"" }); 2504 }; 2505 2506// when files are present, the body was converted from multipart form data to JSON 2507// we need to reconstruct the multipart body for the downstream server 2508 std::string effective_body = body ; 2509 std::string override_content_type ; 2510bool has_files = !files .empty (); 2511 2512if (has_files ) { 2513json form_fields = json::parse_no_throw (body ); 2514if (!form_fields .is_discarded ()) { 2515auto boundary = generate_multipart_boundary (); 2516effective_body = build_multipart_body (form_fields ,files ,boundary ); 2517override_content_type = "multipart/form-data; boundary=" + boundary ; 2518 }else { 2519throw std::runtime_error ("failed to parse multipart form fields JSON" ); 2520 } 2521 } 2522 2523// prepare the request to destination server 2524 httplib::Request req ; 2525 { 2526req .method = method ; 2527req .path = path ; 2528for (const auto & [key ,value ] :headers ) { 2529const auto lowered = to_lower_copy (key ); 2530if (lowered == "accept-encoding" ) { 2531// disable Accept-Encoding to avoid compressed responses 2532continue ; 2533 } 2534if (lowered == "transfer-encoding" ) { 2535// the body is already decoded 2536continue ; 2537 } 2538if (lowered == "content-length" ) { 2539// let httplib calculate Content-Length from the actual body 2540continue ; 2541 } 2542if (lowered == "content-type" ) { 2543if (has_files ) { 2544// we set our own Content-Type with the new boundary 2545continue ; 2546 } 2547// when no files but the original request was multipart, 2548// the body is now JSON, so correct the Content-Type 2549if (value .find ("multipart/form-data" )!= std::string::npos ) { 2550override_content_type = "application/json; charset=utf-8" ; 2551continue ; 2552 } 2553 } 2554if (lowered == "host" ) { 2555bool is_default_port = (scheme == "https" && port == 443 )|| (scheme == "http" && port == 80 ); 2556const std::string url_host = common_http_format_host (host ); 2557req .set_header (key ,is_default_port ?url_host :url_host + ":" + std::to_string (port )); 2558 }else { 2559req .set_header (key ,value ); 2560 } 2561 } 2562req .body = effective_body ; 2563if (!override_content_type .empty ()) { 2564req .set_header ("Content-Type" ,override_content_type ); 2565 } 2566req .response_handler = response_handler ; 2567req .content_receiver = content_receiver ; 2568 } 2569 2570// start the proxy thread 2571SRV_DBG ("start proxy thread %s %s\n" ,req .method .c_str (),req .path .c_str ()); 2572this -> thread = std::thread ([cli ,pipe ,req ,headers_sent ,make_header_msg ]() { 2573auto result = cli -> send (std::move (req )); 2574if (result .error ()!= httplib::Error ::Success ) { 2575auto err_str = httplib::to_string (result .error ()); 2576SRV_ERR ("http client error: %s\n" ,err_str .c_str ()); 2577pipe -> write ({{},500 ,"" ,"" });// header 2578pipe -> write ({{},0 ,"proxy error: " + err_str ,"" });// body 2579 }else if (!headers_sent -> load ()) { 2580// httplib skips response_handler for bodyless statuses like 204, send headers here instead 2581pipe -> write (make_header_msg (* result )); 2582 } 2583pipe -> close_write ();// signal EOF to reader 2584SRV_DBG ("%s" ,"client request thread ended\n" ); 2585 }); 2586this -> thread .detach (); 2587 2588// wait for the first chunk (headers) 2589 { 2590msg_t header ; 2591if (pipe -> read (header ,should_stop )) { 2592SRV_DBG ("%s" ,"received response headers\n" ); 2593this -> status = header .status ; 2594this -> headers = std::move (header .headers ); 2595if (!header .content_type .empty ()) { 2596this -> content_type = std::move (header .content_type ); 2597 } 2598 }else { 2599SRV_DBG ("%s" ,"no response headers received (request cancelled?)\n" ); 2600 } 2601 } 2602}