char-fork/llama.cpp

git clone https://git.t4t.associates/char-fork/llama.cpp

MiaoMing Chenserver : allow model downloads at model limit fix issue #26809 (#28530)56381e407

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