char-fork/llama.cpp

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

Aldehir Rojascommon : implement common_schema internal representation for JSON schemas (#28736)acecd5603

char/aliceai-t5-moe
68.1 KiB1986 linesraw
1#include "common.h"
2#include "download.h"
3#include "log.h"
4#include "llama.h"
5#include "mtmd.h"
6#include "mtmd-helper.h"
7#include "chat.h"
8#include "base64.hpp"
9
10#include "server-common.h"
11
12#include <random>
13#include <sstream>
14#include <fstream>
15#include <limits>
16#include <cstring>
17#include <type_traits>
18#include <chrono>
19#include <thread>
20
21#ifdef _WIN32
22// windows.h defines min and max as macros, which breaks std::min and std::max
23#define WIN32_LEAN_AND_MEAN
24#ifndef NOMINMAX
25#   define NOMINMAX
26#endif
27#include <windows.h>
28#include <io.h>
29#else
30#include <errno.h>
31#include <fcntl.h>
32#include <poll.h>
33#include <unistd.h>
34#endif
35
36json format_error_response(const std::string & message, const enum error_type type) {
37    std::string type_str;
38    int code = 500;
39    switch (type) {
40        case ERROR_TYPE_INVALID_REQUEST:
41            type_str = "invalid_request_error";
42            code = 400;
43            break;
44        case ERROR_TYPE_AUTHENTICATION:
45            type_str = "authentication_error";
46            code = 401;
47            break;
48        case ERROR_TYPE_NOT_FOUND:
49            type_str = "not_found_error";
50            code = 404;
51            break;
52        case ERROR_TYPE_SERVER:
53            type_str = "server_error";
54            code = 500;
55            break;
56        case ERROR_TYPE_PERMISSION:
57            type_str = "permission_error";
58            code = 403;
59            break;
60        case ERROR_TYPE_NOT_SUPPORTED:
61            type_str = "not_supported_error";
62            code = 501;
63            break;
64        case ERROR_TYPE_UNAVAILABLE:
65            type_str = "unavailable_error";
66            code = 503;
67            break;
68        case ERROR_TYPE_EXCEED_CONTEXT_SIZE:
69            type_str = "exceed_context_size_error";
70            code = 400;
71            break;
72    }
73    return json {
74        {"code", code},
75        {"message", message},
76        {"type", type_str},
77    };
78}
79
80//
81// server_slot_stats
82//
83
84json server_slot_stats::to_json() const {
85    json base = {
86        {"cache_n",                n_prompt_cached},
87
88        {"prompt_n",               n_prompt_processed},
89        {"prompt_ms",              t_prompt_ms()},
90        {"prompt_per_token_ms",    t_prompt_per_token_ms()},
91        {"prompt_per_second",      n_prompt_tps()},
92
93        {"predicted_n",            n_gen},
94        {"predicted_ms",           t_gen_ms()},
95        {"predicted_per_token_ms", t_gen_per_token_ms()},
96        {"predicted_per_second",   n_gen_tps()},
97    };
98
99    if (n_draft_tokens > 0) {
100        base["draft_n"]          = n_draft_tokens;
101        base["draft_n_accepted"] = n_draft_accepted;
102    }
103
104    return base;
105}
106
107//
108// random string / id
109//
110
111std::string random_string() {
112    static const std::string str("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz");
113
114    std::random_device rd;
115    std::mt19937 generator(rd());
116
117    std::string result(32, ' ');
118
119    for (int i = 0; i < 32; ++i) {
120        result[i] = str[generator() % str.size()];
121    }
122
123    return result;
124}
125
126std::string gen_chatcmplid() {
127    return "chatcmpl-" + random_string();
128}
129
130std::string gen_tool_call_id() {
131    return random_string();
132}
133
134const char * get_media_marker() {
135    static const std::string marker = []() {
136        // allow user to pin a reproducible marker via env var
137        const char * env = getenv("LLAMA_MEDIA_MARKER");
138        if (env && env[0] != '\0') {
139            return std::string(env);
140        }
141        return std::string("<__media_") + random_string() + "__>";
142    }();
143    return marker.c_str();
144}
145
146//
147// lora utils
148//
149
150bool lora_all_alora(const std::vector<common_adapter_lora_info> & loras) {
151    bool found_alora = false;
152    for (const auto & lora : loras) {
153        if (lora.scale != 0) {
154            if (llama_adapter_get_alora_n_invocation_tokens(lora.ptr) == 0) {
155                return false;
156            }
157            found_alora = true;
158        }
159    }
160    return found_alora;
161}
162
163bool lora_should_clear_cache(
164        const std::vector<common_adapter_lora_info> & current,
165        const std::vector<common_adapter_lora_info> & next) {
166
167    // This should always be called after determining that the two sets are
168    // _not_ equal. This assert is therefore some slightly wasted work and
169    // should be safe to remove as long as this method is called correctly.
170    GGML_ASSERT(!are_lora_equal(current, next));
171
172    return (
173        !(lora_get_enabled_ids(current).empty() || lora_all_alora(current)) ||
174        !lora_all_alora(next));
175}
176
177std::map<int, float> parse_lora_request(const json & data) {
178    std::map<int, float> lora;
179
180    // set value
181    for (const auto & entry : data) {
182        int id      = json_value(entry, "id", -1);
183        float scale = json_value(entry, "scale", 0.0f);
184        lora[id] = scale;
185    }
186
187    return lora;
188}
189
190bool are_lora_equal(
191        const std::vector<common_adapter_lora_info> & l1,
192        const std::vector<common_adapter_lora_info> & l2) {
193    if (l1.size() != l2.size()) {
194        return false;
195    }
196    for (size_t i = 0; i < l1.size(); ++i) {
197        // we don't check lora.path to reduce the time complexity
198        if (l1[i].scale != l2[i].scale || l1[i].ptr != l2[i].ptr) {
199            return false;
200        }
201    }
202    return true;
203}
204
205std::vector<size_t> lora_get_enabled_ids(const std::vector<common_adapter_lora_info> & loras) {
206    std::vector<size_t> enabled_ids;
207    for (size_t i = 0; i < loras.size(); ++i) {
208        if (loras[i].scale > 0) {
209            enabled_ids.push_back(i);
210        }
211    }
212    return enabled_ids;
213}
214
215//
216// base64 utils (TODO: use the base64::decode from base64.hpp)
217//
218
219static const std::string base64_chars =
220             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
221             "abcdefghijklmnopqrstuvwxyz"
222             "0123456789+/";
223
224static inline bool is_base64(uint8_t c) {
225    return (isalnum(c) || (c == '+') || (c == '/'));
226}
227
228static inline raw_buffer base64_decode(const std::string & encoded_string) {
229    int i = 0;
230    int j = 0;
231    int in_ = 0;
232
233    int in_len = encoded_string.size();
234
235    uint8_t char_array_4[4];
236    uint8_t char_array_3[3];
237
238    raw_buffer ret;
239
240    while (in_len-- && (encoded_string[in_] != '=') && is_base64(encoded_string[in_])) {
241        char_array_4[i++] = encoded_string[in_]; in_++;
242        if (i == 4) {
243            for (i = 0; i < 4; i++) {
244                char_array_4[i] = base64_chars.find(char_array_4[i]);
245            }
246
247            char_array_3[0] = ((char_array_4[0]      ) << 2) + ((char_array_4[1] & 0x30) >> 4);
248            char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
249            char_array_3[2] = ((char_array_4[2] & 0x3) << 6) +   char_array_4[3];
250
251            for (i = 0; (i < 3); i++) {
252                ret.push_back(char_array_3[i]);
253            }
254
255            i = 0;
256        }
257    }
258
259    if (i) {
260        for (j = i; j < 4; j++) {
261            char_array_4[j] = 0;
262        }
263
264        for (j = 0; j < 4; j++) {
265            char_array_4[j] = base64_chars.find(char_array_4[j]);
266        }
267
268        char_array_3[0] = ((char_array_4[0]      ) << 2) + ((char_array_4[1] & 0x30) >> 4);
269        char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
270        char_array_3[2] = ((char_array_4[2] & 0x3) << 6) +   char_array_4[3];
271
272        for (j = 0; j < i - 1; j++) {
273            ret.push_back(char_array_3[j]);
274        }
275    }
276
277    return ret;
278}
279
280//
281// server_tokens implementation
282//
283
284namespace {
285
286constexpr uint32_t SERVER_TOKENS_STATE_VERSION = 1;
287
288uint32_t server_tokens_state_u32(size_t value) {
289    if (value > std::numeric_limits<uint32_t>::max()) {
290        throw std::runtime_error("Server tokens state is too large");
291    }
292    return value;
293}
294
295class server_tokens_state_writer {
296public:
297    template <typename T>
298    void write(T value) {
299        static_assert(std::is_trivially_copyable<T>::value, "T must be trivially copyable");
300        const auto * ptr = reinterpret_cast<const char *>(&value);
301        data.insert(data.end(), ptr, ptr + sizeof(value));
302    }
303
304    template <typename T>
305    void write(const std::vector<T> & values) {
306        static_assert(std::is_trivially_copyable<T>::value, "T must be trivially copyable");
307        write(server_tokens_state_u32(values.size()));
308        if (values.empty()) {
309            return;
310        }
311        const auto * ptr = reinterpret_cast<const char *>(values.data());
312        data.insert(data.end(), ptr, ptr + values.size() * sizeof(T));
313    }
314
315    void write_media_chunk(const mtmd_input_chunk * chunk) {
316        size_t chunk_size = 0;
317        if (mtmd_input_chunk_save(chunk, nullptr, 0, &chunk_size) != 0 || chunk_size == 0) {
318            throw std::runtime_error("Cannot serialize media chunk in server tokens");
319        }
320        std::vector<char> chunk_data(server_tokens_state_u32(chunk_size));
321        if (mtmd_input_chunk_save(chunk, chunk_data.data(), chunk_data.size(), nullptr) != 0) {
322            throw std::runtime_error("Cannot serialize media chunk in server tokens");
323        }
324        write(chunk_data);
325    }
326
327    std::vector<char> take() {
328        data.resize((data.size() + sizeof(llama_token) - 1) / sizeof(llama_token) * sizeof(llama_token), 0);
329        return std::move(data);
330    }
331
332private:
333    std::vector<char> data;
334};
335
336class server_tokens_state_reader {
337public:
338    server_tokens_state_reader(const char * data, size_t size) : data(data), size(size) {}
339
340    template <typename T>
341    T read() {
342        static_assert(std::is_trivially_copyable<T>::value, "T must be trivially copyable");
343        if (size - pos < sizeof(T)) {
344            throw std::runtime_error("Unexpected end of server tokens state");
345        }
346        T value;
347        std::memcpy(&value, data + pos, sizeof(value));
348        pos += sizeof(value);
349        return value;
350    }
351
352    template <typename T>
353    std::vector<T> read_vector() {
354        static_assert(std::is_trivially_copyable<T>::value, "T must be trivially copyable");
355        const uint32_t n_values = read<uint32_t>();
356        // reject before resizing, so that a small corrupted payload cannot request a huge allocation
357        if (n_values > remaining() / sizeof(T)) {
358            throw std::runtime_error("Unexpected end of server tokens state");
359        }
360        std::vector<T> values(n_values);
361        if (n_values > 0) {
362            std::memcpy(values.data(), data + pos, values.size() * sizeof(T));
363            pos += values.size() * sizeof(T);
364        }
365        return values;
366    }
367
368    size_t remaining() const {
369        return size - pos;
370    }
371
372private:
373    const char * data;
374    size_t size;
375    size_t pos = 0;
376};
377
378} // namespace
379
380server_tokens::server_tokens(mtmd::input_chunks & mtmd_chunks, bool has_mtmd) : has_mtmd(has_mtmd) {
381    for (size_t i = 0; i < mtmd_chunks.size(); ++i) {
382        push_back(mtmd_chunks[i]);
383    }
384}
385
386server_tokens::server_tokens(const llama_tokens & tokens, bool has_mtmd) : has_mtmd(has_mtmd), tokens(tokens) {
387}
388
389llama_pos server_tokens::pos_next(int64_t n_tokens) const {
390    if (!has_mtmd) {
391        if (n_tokens < 0) {
392            return tokens.size();
393        }
394
395        return n_tokens;
396    }
397
398    if (n_tokens < 0) {
399        llama_pos res = tokens.size();
400
401        for (auto it = map_idx_to_media.begin(); it != map_idx_to_media.end(); ++it) {
402            const auto & chunk = it->second;
403            res += mtmd_input_chunk_get_n_pos(chunk.get()) - mtmd_input_chunk_get_n_tokens(chunk.get());
404        }
405
406        return res;
407    }
408
409    int64_t idx = 0;
410    llama_pos pos = 0;
411
412    GGML_ASSERT(n_tokens <= (int64_t)tokens.size());
413
414    while (idx < n_tokens) {
415        const auto media_it = map_idx_to_media.find(idx);
416        if (media_it != map_idx_to_media.end()) {
417            const auto & chunk = media_it->second;
418            const llama_pos n_pos = mtmd_input_chunk_get_n_pos(chunk.get());
419            const size_t n_tok = mtmd_input_chunk_get_n_tokens(chunk.get());
420
421            pos += n_pos;
422            idx += n_tok;
423        } else {
424            pos++;
425            idx++;
426        }
427    }
428
429    return pos;
430}
431
432size_t server_tokens::size_up_to_pos(llama_pos max_pos) const {
433    if (!has_mtmd) {
434        return std::min((size_t)max_pos, tokens.size());
435    }
436
437    size_t idx = 0;
438    llama_pos pos = 0;
439
440    while (idx < tokens.size()) {
441        const auto media_it = map_idx_to_media.find(idx);
442        if (media_it != map_idx_to_media.end()) {
443            const auto & chunk = media_it->second;
444            const llama_pos n_pos = mtmd_input_chunk_get_n_pos(chunk.get());
445            const size_t n_tok = mtmd_input_chunk_get_n_tokens(chunk.get());
446
447            pos += n_pos;
448            idx += n_tok;
449        } else {
450            pos++;
451            idx++;
452        }
453
454        if (pos >= max_pos) {
455            break;
456        }
457    }
458
459    return idx;
460}
461
462std::string server_tokens::str() const {
463    std::ostringstream oss;
464    oss << "tokens: ";
465    for (size_t idx = 0; idx < tokens.size(); ++idx) {
466        llama_token t = tokens[idx];
467        oss << "idx:" << idx << " ";
468        if (t == LLAMA_TOKEN_NULL) {
469            oss << "<embd> ";
470        } else {
471            oss << t << " ";
472        }
473    }
474    oss << "\n";
475    oss << "image idx: ";
476    for (const auto & it : map_idx_to_media) {
477        oss << it.first << ", ";
478    }
479    return oss.str();
480}
481
482const mtmd::input_chunk_ptr & server_tokens::find_chunk(size_t idx) const {
483    auto it = map_idx_to_media.find(idx);
484    if (it != map_idx_to_media.end()) {
485        return it->second;
486    }
487    throw std::runtime_error("Chunk not found");
488}
489
490std::pair<const mtmd::input_chunk_ptr *, size_t> server_tokens::find_next_media_chunk(size_t idx) const {
491    auto it = map_idx_to_media.upper_bound(idx);
492    if (it != map_idx_to_media.end()) {
493        return { &it->second, it->first };
494    }
495    return { nullptr, 0 };
496}
497
498void server_tokens::push_back(llama_token tok) {
499    if (tok == LLAMA_TOKEN_NULL) {
500        throw std::runtime_error("Invalid token");
501    }
502    tokens.emplace_back(tok);
503}
504
505void server_tokens::push_back(const mtmd_input_chunk * chunk) {
506    auto type = mtmd_input_chunk_get_type(chunk);
507    if (type == MTMD_INPUT_CHUNK_TYPE_IMAGE || type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
508        GGML_ASSERT(has_mtmd);
509        const size_t n_tokens = mtmd_input_chunk_get_n_tokens(chunk);
510        size_t start_idx = tokens.size();
511        for (size_t i = 0; i < n_tokens; ++i) {
512            tokens.emplace_back(LLAMA_TOKEN_NULL);
513        }
514        mtmd::input_chunk_ptr new_chunk(mtmd_input_chunk_copy(chunk));
515        map_idx_to_media[start_idx] = std::move(new_chunk);
516    } else if (type == MTMD_INPUT_CHUNK_TYPE_TEXT) {
517        size_t n_tokens;
518        const auto * text_tokens = mtmd_input_chunk_get_tokens_text(chunk, &n_tokens);
519        for (size_t i = 0; i < n_tokens; ++i) {
520            push_back(text_tokens[i]);
521        }
522    } else {
523        GGML_ABORT("Invalid chunk type");
524    }
525}
526
527void server_tokens::push_back_placeholder(const mtmd_input_chunk * chunk) {
528    auto type = mtmd_input_chunk_get_type(chunk);
529    if (type == MTMD_INPUT_CHUNK_TYPE_IMAGE || type == MTMD_INPUT_CHUNK_TYPE_AUDIO) {
530        GGML_ASSERT(has_mtmd);
531        mtmd::input_chunk_ptr new_chunk(mtmd_input_chunk_get_placeholder(chunk));
532        GGML_ASSERT(new_chunk != nullptr && "failed to create placeholder chunk");
533        const size_t n_tokens = mtmd_input_chunk_get_n_tokens(chunk);
534        size_t start_idx = tokens.size();
535        for (size_t i = 0; i < n_tokens; ++i) {
536            tokens.emplace_back(LLAMA_TOKEN_NULL);
537        }
538        map_idx_to_media[start_idx] = std::move(new_chunk);
539    } else {
540        push_back(chunk);
541    }
542}
543
544void server_tokens::push_back(server_tokens & tokens) {
545    size_t start_idx = size();
546    for (size_t i = 0; i < tokens.size(); i++) {
547        push_back(tokens[i]);
548    }
549    if (tokens.has_mtmd) {
550        // Assert if we are copying MTMD chunks to a server_tokens that does not have mtmd.
551        // We could also just check, but this will prevent silently dropping MTMD data.
552        GGML_ASSERT(has_mtmd);
553        for (auto it = tokens.map_idx_to_media.begin(); it != tokens.map_idx_to_media.end(); ) {
554            auto * chunk = tokens.map_idx_to_media[it->first].get();
555            mtmd::input_chunk_ptr new_chunk(mtmd_input_chunk_copy(chunk));
556            map_idx_to_media[start_idx + it->first] = std::move(new_chunk);
557        }
558    }
559}
560
561void server_tokens::insert(const llama_tokens & inp_tokens) {
562    tokens.insert(tokens.end(), inp_tokens.begin(), inp_tokens.end());
563}
564
565const llama_tokens & server_tokens::get_tokens() const {
566    GGML_ASSERT(!has_mtmd);
567    return tokens;
568}
569
570std::vector<char> server_tokens::serialize() const {
571    static_assert(sizeof(llama_token) == sizeof(uint32_t), "unexpected llama_token size");
572
573    server_tokens_state_writer writer;
574    writer.write((llama_token) LLAMA_TOKEN_NULL);
575    writer.write(SERVER_TOKENS_STATE_VERSION);
576    writer.write(tokens);
577
578    std::vector<uint32_t> media_keys;
579    media_keys.reserve(map_idx_to_media.size());
580    for (const auto & item : map_idx_to_media) {
581        media_keys.push_back(server_tokens_state_u32(item.first));
582    }
583    writer.write(media_keys);
584
585    for (const auto & item : map_idx_to_media) {
586        writer.write_media_chunk(item.second.get());
587    }
588
589    return writer.take();
590}
591
592server_tokens server_tokens::deserialize(const llama_tokens & packed, bool has_mtmd) {
593    static_assert(sizeof(llama_token) == sizeof(uint32_t), "unexpected llama_token size");
594
595    if (packed.empty() || packed[0] != LLAMA_TOKEN_NULL) {
596        // plain token list, as written by older versions
597        return server_tokens(packed, has_mtmd);
598    }
599
600    server_tokens_state_reader reader(reinterpret_cast<const char *>(packed.data()), packed.size() * sizeof(llama_token));
601    reader.read<llama_token>(); // format marker
602    if (reader.read<uint32_t>() != SERVER_TOKENS_STATE_VERSION) {
603        throw std::runtime_error("Unsupported server tokens state version");
604    }
605
606    const llama_tokens tokens = reader.read_vector<llama_token>();
607
608    // the media start indices, followed by the media chunks in the same order
609    const std::vector<uint32_t> media_keys = reader.read_vector<uint32_t>();
610    if (!media_keys.empty() && !has_mtmd) {
611        throw std::runtime_error("Cannot restore media tokens without an mmproj");
612    }
613
614    server_tokens result(tokens, has_mtmd);
615
616    for (const uint32_t key : media_keys) {
617        const size_t start_idx = key;
618        const std::vector<char> chunk_data = reader.read_vector<char>();
619        if (chunk_data.empty()) {
620            throw std::runtime_error("Cannot load media chunk from server tokens state");
621        }
622
623        mtmd::input_chunk_ptr chunk(mtmd_input_chunk_load(chunk_data.data(), chunk_data.size()));
624        if (!chunk) {
625            throw std::runtime_error("Cannot load media chunk from server tokens state");
626        }
627        result.map_idx_to_media[start_idx] = std::move(chunk);
628    }
629
630    if (reader.remaining() >= sizeof(llama_token)) {
631        throw std::runtime_error("Trailing data in server tokens state");
632    }
633
634    return result;
635}
636
637llama_tokens server_tokens::get_text_tokens() const {
638    llama_tokens res;
639    res.reserve(tokens.size());
640    for (llama_token t : tokens) {
641        if (t != LLAMA_TOKEN_NULL) {
642            res.push_back(t);
643        }
644    }
645    return res;
646}
647
648void server_tokens::set_token(llama_pos pos, llama_token id) {
649    GGML_ASSERT(!has_mtmd); // only allow this if mtmd is disabled
650    tokens[pos] = id;
651}
652
653void server_tokens::keep_first(size_t n) {
654    GGML_ASSERT(n <= tokens.size());
655    if (has_mtmd) {
656        if (n == tokens.size()) {
657            return; // nothing to do
658        }
659        // we throw an error if we try to remove a token in the middle of an image
660        // for ex. with input of 5 text tokens and 2 images:
661        //    [0] [1] [2] [3] [4] [img0] [img0] [img0] [img1] [img1]
662        // n  1   2   3   4   5   6      7      8      9      10
663        // allowed to resize      ^                    ^
664        // disallowed to resize          ^      ^             ^
665        if (n > 0) {
666            // make sure we never remove tokens in the middle of an image
667            // note that the case where we keep a full image at the end is allowed:
668            //   tokens[n - 1] == LLAMA_TOKEN_NULL && tokens[n] != LLAMA_TOKEN_NULL
669            if (tokens[n - 1] == LLAMA_TOKEN_NULL && tokens[n] == LLAMA_TOKEN_NULL) {
670                find_chunk(n - 1); // will throw an error if the token is not begin-of-chunk
671            }
672        }
673        // remove all image chunks that are not used anymore
674        for (auto it = map_idx_to_media.begin(); it != map_idx_to_media.end(); ) {
675            size_t idx = it->first;
676            if (idx >= n) {
677                it = map_idx_to_media.erase(it);
678            } else {
679                ++it;
680            }
681        }
682    }
683    tokens.resize(n);
684}
685
686std::string server_tokens::detokenize(const llama_context * ctx, bool special) const {
687    llama_tokens text_tokens;
688    text_tokens.reserve(tokens.size());
689    for (const auto & t : tokens) {
690        if (t != LLAMA_TOKEN_NULL) {
691            text_tokens.push_back(t);
692        }
693    }
694    return common_detokenize(ctx, text_tokens, special);
695}
696
697size_t server_tokens::get_common_prefix(const server_tokens & b) const {
698    const size_t max_idx = std::min(tokens.size(), b.tokens.size());
699
700    if (!has_mtmd) {
701        for (size_t i = 0; i < max_idx; ++i) {
702            if (tokens[i] == b.tokens[i]) {
703                continue;
704            }
705
706            return i;
707        }
708
709        return max_idx;
710    }
711
712    for (size_t i = 0; i < max_idx; ++i) {
713        const llama_token ai =   tokens[i];
714        const llama_token bi = b.tokens[i];
715
716        if (ai == LLAMA_TOKEN_NULL && bi == LLAMA_TOKEN_NULL) {
717            const auto & a_chunk =   find_chunk(i);
718            const auto & b_chunk = b.find_chunk(i);
719
720            GGML_ASSERT(a_chunk && b_chunk);
721
722            const std::string id_ai = mtmd_input_chunk_get_id(a_chunk.get());
723            const std::string id_bi = mtmd_input_chunk_get_id(b_chunk.get());
724
725            const size_t n_tok_a = mtmd_input_chunk_get_n_tokens(a_chunk.get());
726            const size_t n_tok_b = mtmd_input_chunk_get_n_tokens(b_chunk.get());
727
728            if (id_ai == id_bi && n_tok_a == n_tok_b) {
729                GGML_ASSERT(n_tok_a > 0 && "Invalid media chunk"); // should never happen
730                i += n_tok_a - 1; // will be +1 by the for loop
731                continue;
732            }
733
734            return i;
735        }
736
737        if (ai == bi) {
738            continue;
739        }
740
741        return i;
742    }
743
744    return max_idx; // all tokens are equal
745}
746
747common_chat_msg_spans server_tokens::find_message_spans(const common_chat_msg_delimiters & delims) const {
748    std::map<size_t, size_t> skips;
749    for (const auto & it : map_idx_to_media) {
750        skips[it.first] = mtmd_input_chunk_get_n_tokens(it.second.get());
751    }
752    return delims.split(tokens, skips);
753}
754
755bool server_tokens::validate(const struct llama_context * ctx) const {
756    const llama_model * model = llama_get_model(ctx);
757    const llama_vocab * vocab = llama_model_get_vocab(model);
758    const int32_t n_vocab = llama_vocab_n_tokens(vocab);
759    size_t n_media = 0;
760
761    for (size_t i = 0; i < tokens.size(); ++i) {
762        const auto & t = tokens[i];
763        if (t == LLAMA_TOKEN_NULL) {
764            try {
765                const auto & chunk = find_chunk(i);
766                if (mtmd_input_chunk_get_type(chunk.get()) == MTMD_INPUT_CHUNK_TYPE_TEXT) {
767                    return false;
768                }
769                const size_t n_tokens = mtmd_input_chunk_get_n_tokens(chunk.get());
770                const llama_pos n_pos = mtmd_input_chunk_get_n_pos(chunk.get());
771                if (n_tokens == 0 || n_pos <= 0 || n_tokens > tokens.size() - i) {
772                    return false;
773                }
774                for (size_t j = i; j < i + n_tokens; ++j) {
775                    if (tokens[j] != LLAMA_TOKEN_NULL) {
776                        return false;
777                    }
778                }
779                ++n_media;
780                i += n_tokens - 1;
781            } catch (const std::exception & e) {
782                return false;
783            }
784        } else if (t < 0 || t >= n_vocab) {
785            return false;
786        }
787    }
788    return n_media == map_idx_to_media.size();
789}
790
791server_tokens server_tokens::clone() const {
792    server_tokens res;
793    res.has_mtmd = has_mtmd;
794    res.tokens   = tokens;
795    for (auto it = map_idx_to_media.begin(); it != map_idx_to_media.end(); ++it) {
796        size_t idx = it->first;
797        const mtmd::input_chunk_ptr & chunk = it->second;
798        res.map_idx_to_media[idx] = mtmd::input_chunk_ptr(mtmd_input_chunk_copy(chunk.get()));
799    }
800    return res;
801}
802
803//
804// tokenizer and input processing utils
805//
806
807bool json_is_array_of_numbers(const json & data) {
808    if (data.is_array()) {
809        for (const auto & e : data) {
810            if (!e.is_number_integer()) {
811                return false;
812            }
813        }
814        return true;
815    }
816    return false;
817}
818
819bool json_is_array_of_mixed_numbers_strings(const json & data) {
820    bool seen_string = false;
821    bool seen_number = false;
822    if (data.is_array()) {
823        for (const auto & e : data) {
824            seen_string |= e.is_string();
825            seen_number |= e.is_number_integer();
826            if (seen_number && seen_string) {
827                return true;
828            }
829        }
830    }
831    return false;
832}
833
834bool json_is_array_and_contains_numbers(const json & data) {
835    if (data.is_array()) {
836        for (const auto & e : data) {
837            if (e.is_number_integer()) {
838                return true;
839            }
840        }
841        return false;
842    }
843    return false;
844}
845
846json json_get_nested_values(const std::vector<std::string> & paths, const json & js) {
847    json result = json::object();
848
849    for (const std::string & path : paths) {
850        json current = js;
851        const auto keys = string_split<std::string>(path, /*separator*/ '/');
852        bool valid_path = true;
853        for (const std::string & k : keys) {
854            if (valid_path && current.is_object() && current.contains(k)) {
855                current = current[k];
856            } else {
857                valid_path = false;
858            }
859        }
860        if (valid_path) {
861            result[path] = current;
862        }
863    }
864    return result;
865}
866
867llama_tokens tokenize_mixed(const llama_vocab * vocab, const json & json_prompt, bool add_special, bool parse_special) {
868    // If `add_bos` is true, we only add BOS, when json_prompt is a string,
869    // or the first element of the json_prompt array is a string.
870    llama_tokens prompt_tokens;
871
872    if (json_prompt.is_array()) {
873        bool first = true;
874        for (const auto & p : json_prompt) {
875            if (p.is_string()) {
876                auto s = p.template get<std::string>();
877
878                llama_tokens p;
879                if (first) {
880                    p = common_tokenize(vocab, s, add_special, parse_special);
881                    first = false;
882                } else {
883                    p = common_tokenize(vocab, s, false, parse_special);
884                }
885
886                prompt_tokens.insert(prompt_tokens.end(), p.begin(), p.end());
887            } else {
888                if (first) {
889                    first = false;
890                }
891
892                prompt_tokens.push_back(p.template get<llama_token>());
893            }
894        }
895    } else {
896        auto s = json_prompt.template get<std::string>();
897        prompt_tokens = common_tokenize(vocab, s, add_special, parse_special);
898    }
899
900    return prompt_tokens;
901}
902
903size_t validate_utf8(const std::string& text) {
904    size_t len = text.size();
905    if (len == 0) return 0;
906
907    // Check the last few bytes to see if a multi-byte character is cut off
908    for (size_t i = 1; i <= 4 && i <= len; ++i) {
909        unsigned char c = text[len - i];
910        // Check for start of a multi-byte sequence from the end
911        if ((c & 0xE0) == 0xC0) {
912            // 2-byte character start: 110xxxxx
913            // Needs at least 2 bytes
914            if (i < 2) return len - i;
915        } else if ((c & 0xF0) == 0xE0) {
916            // 3-byte character start: 1110xxxx
917            // Needs at least 3 bytes
918            if (i < 3) return len - i;
919        } else if ((c & 0xF8) == 0xF0) {
920            // 4-byte character start: 11110xxx
921            // Needs at least 4 bytes
922            if (i < 4) return len - i;
923        }
924    }
925
926    // If no cut-off multi-byte character is found, return full length
927    return len;
928}
929
930server_tokens process_mtmd_prompt(
931        mtmd_context * mctx,
932        const std::string & prompt,
933        const std::vector<raw_buffer> & files,
934        const mtmd_helper_init_opt & init_opt,
935        bool is_placeholder) {
936    // these will be freed upon going out of scope
937    mtmd::bitmaps bitmaps;
938    std::vector<mtmd_helper::video_ptr> videos;
939    for (auto & file : files) {
940        auto out = mtmd_helper_bitmap_init_from_buf(mctx, file.data(), file.size(), is_placeholder, init_opt);
941        if (!out.bitmap) {
942            throw std::runtime_error("Failed to load image or audio file");
943        }
944        bitmaps.entries.emplace_back(out.bitmap);
945        if (out.video_ctx) {
946            videos.emplace_back(out.video_ctx);
947        }
948    }
949    // process prompt
950    std::vector<server_tokens> inputs;
951    // multimodal
952    mtmd_input_text inp_txt = {
953        prompt.data(),
954        prompt.size(),
955        /* add_special */   true,
956        /* parse_special */ true,
957    };
958    mtmd::input_chunks chunks(mtmd_input_chunks_init());
959    auto bitmaps_c_ptr = bitmaps.c_ptr();
960    int32_t tokenized = mtmd_tokenize(mctx,
961                                      chunks.ptr.get(),
962                                      &inp_txt,
963                                      bitmaps_c_ptr.data(),
964                                      bitmaps_c_ptr.size());
965    if (tokenized != 0) {
966        throw std::runtime_error("Failed to tokenize prompt");
967    }
968    auto result = server_tokens(chunks, true);
969    return result;
970}
971
972/**
973 * break the input "prompt" object into multiple prompt if needed, then tokenize them
974 * use tokenize_input_prompts() if the input could be an array.
975 * this supports these cases:
976 * - "prompt": "string"
977 * - "prompt": [12, 34, 56]
978 * - "prompt": [12, 34, "string", 56, 78]
979 * - "prompt": { "prompt_string": "string", "multimodal_data": [ "base64" ] }
980 */
981static server_tokens tokenize_input_subprompt(const llama_vocab * vocab, mtmd_context * mctx, const json & json_prompt, bool add_special, bool parse_special, const mtmd_helper_init_opt & init_opt) {
982    constexpr char JSON_STRING_PROMPT_KEY[] = "prompt_string";
983    constexpr char JSON_MTMD_DATA_KEY[] = "multimodal_data";
984    const bool has_mtmd = mctx != nullptr;
985    if (json_prompt.is_string() || json_is_array_of_mixed_numbers_strings(json_prompt)) {
986        // string or mixed
987        llama_tokens tmp = tokenize_mixed(vocab, json_prompt, add_special, parse_special);
988        return server_tokens(tmp, false);
989    } else if (json_is_array_of_numbers(json_prompt)) {
990        // array of tokens
991        llama_tokens tmp = json_prompt.get<llama_tokens>();
992        return server_tokens(tmp, false);
993    } else if (json_prompt.contains(JSON_STRING_PROMPT_KEY)) {
994        // JSON object with prompt key.
995        if (json_prompt.contains(JSON_MTMD_DATA_KEY)) {
996            if (!has_mtmd)
997                throw std::runtime_error("Multimodal data provided, but model does not support multimodal requests.");
998
999            // JSON object with prompt and multimodal key.
1000            std::vector<raw_buffer> files;
1001            for (const auto & entry : json_prompt.at(JSON_MTMD_DATA_KEY)) {
1002                files.push_back(base64_decode(entry));
1003            }
1004            return process_mtmd_prompt(mctx, json_prompt.at(JSON_STRING_PROMPT_KEY), files, init_opt);
1005        } else {
1006            // Not multimodal, but contains a subobject.
1007            llama_tokens tmp = tokenize_mixed(vocab, json_prompt.at(JSON_STRING_PROMPT_KEY), add_special, parse_special);
1008            return server_tokens(tmp, false);
1009        }
1010   } else {
1011       throw std::runtime_error("\"prompt\" elements must be a string, a list of tokens, a JSON object containing a prompt string, or a list of mixed strings & tokens.");
1012   }
1013}
1014
1015std::vector<server_tokens> tokenize_input_prompts(const llama_vocab * vocab, mtmd_context * mctx, const json & json_prompt, bool add_special, bool parse_special, const mtmd_helper_init_opt & init_opt) {
1016    std::vector<server_tokens> result;
1017    if (json_prompt.is_array() && !json_is_array_and_contains_numbers(json_prompt)) {
1018        result.reserve(json_prompt.size());
1019        for (const auto & p : json_prompt) {
1020            result.push_back(tokenize_input_subprompt(vocab, mctx, p, add_special, parse_special, init_opt));
1021        }
1022    } else {
1023        result.push_back(tokenize_input_subprompt(vocab, mctx, json_prompt, add_special, parse_special, init_opt));
1024    }
1025    if (result.empty()) {
1026        throw std::runtime_error("\"prompt\" must not be empty");
1027    }
1028    return result;
1029}
1030
1031//
1032// OAI utils
1033//
1034
1035// used by /completions endpoint
1036json oaicompat_completion_params_parse(const json & body) {
1037    json llama_params;
1038
1039    if (!body.contains("prompt")) {
1040        throw std::runtime_error("\"prompt\" is required");
1041    }
1042
1043    // Handle "stop" field
1044    if (body.contains("stop") && body.at("stop").is_string()) {
1045        llama_params["stop"] = json::array({body.at("stop").get<std::string>()});
1046    } else {
1047        llama_params["stop"] = json_value(body, "stop", json::array());
1048    }
1049
1050    // Handle "echo" field
1051    if (json_value(body, "echo", false)) {
1052        throw std::runtime_error("Only no echo is supported");
1053    }
1054
1055    // Params supported by OAI but unsupported by llama.cpp
1056    static const std::vector<std::string> unsupported_params { "best_of", "suffix" };
1057    for (const auto & param : unsupported_params) {
1058        if (body.contains(param)) {
1059            throw std::runtime_error("Unsupported param: " + param);
1060        }
1061    }
1062
1063    // Copy remaining properties to llama_params
1064    for (const auto & item : body.items()) {
1065        // Exception: if "n_predict" is present, we overwrite the value specified earlier by "max_tokens"
1066        if (!llama_params.contains(item.key()) || item.key() == "n_predict") {
1067            llama_params[item.key()] = item.value();
1068        }
1069    }
1070
1071    return llama_params;
1072}
1073
1074// url can be
1075// - http(s):// for remote files
1076// - file:// for local files (only allowed if media_path is set)
1077// - data: for base64 encoded data with uri scheme (e.g. data:image/png;base64,...)
1078// - raw base64 encoded data
1079static void handle_media(
1080        std::vector<raw_buffer> & out_files,
1081        const std::string & url,
1082        const std::string & media_path) {
1083    if (!media_path.empty()) {
1084        // should already be enforced by arg.cpp, but checking just in case
1085        GGML_ASSERT(media_path.back() == DIRECTORY_SEPARATOR);
1086    }
1087
1088    if (string_starts_with(url, "http")) {
1089        // download remote image
1090        // TODO @ngxson : maybe make these params configurable
1091        common_remote_params params;
1092        params.max_size = 1024 * 1024 * 10; // 10MB
1093        params.timeout  = 10; // seconds
1094        SRV_INF("downloading image from '%s'\n", url.c_str());
1095        auto res = common_remote_get_content(url, params);
1096        if (200 <= res.first && res.first < 300) {
1097            SRV_INF("downloaded %zu bytes\n", res.second.size());
1098            raw_buffer data;
1099            data.insert(data.end(), res.second.begin(), res.second.end());
1100            out_files.push_back(data);
1101        } else {
1102            throw std::runtime_error("Failed to download image");
1103        }
1104
1105    } else if (string_starts_with(url, "file://")) {
1106        if (media_path.empty()) {
1107            throw std::invalid_argument("file:// URLs are not allowed unless --media-path is specified");
1108        }
1109        // load local image file
1110        std::string file_path = url.substr(7); // remove "file://"
1111        raw_buffer data;
1112        if (!fs_validate_filename(file_path, true)) {
1113            throw std::invalid_argument("file path is not allowed: " + file_path);
1114        }
1115        SRV_INF("loading image from local file '%s'\n", (media_path + file_path).c_str());
1116        std::ifstream file(media_path + file_path, std::ios::binary);
1117        if (!file) {
1118            throw std::invalid_argument("file does not exist or cannot be opened: " + file_path);
1119        }
1120        data.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
1121        out_files.push_back(data);
1122
1123    } else if (string_starts_with(url, "data:")) {
1124        // try to decode base64 image, video, or audio
1125        std::vector<std::string> parts = string_split<std::string>(url, /*separator*/ ',');
1126        if (parts.size() != 2) {
1127            throw std::invalid_argument("Invalid uri-encoded base64 value");
1128        } else if (!string_starts_with(parts[0], "data:image/")
1129                && !string_starts_with(parts[0], "data:video/")
1130                && !string_starts_with(parts[0], "data:audio/")) {
1131            throw std::invalid_argument("Invalid uri format: " + parts[0]);
1132        } else if (!string_ends_with(parts[0], "base64")) {
1133            throw std::invalid_argument("uri must be base64 encoded");
1134        } else {
1135            auto base64_data = parts[1];
1136            auto decoded_data = base64_decode(base64_data);
1137            out_files.push_back(decoded_data);
1138        }
1139
1140    } else {
1141        // try as raw base64 string
1142        auto decoded_data = base64_decode(url);
1143        if (decoded_data.empty()) {
1144            throw std::runtime_error("Invalid base64 value");
1145        }
1146        out_files.push_back(decoded_data);
1147    }
1148}
1149
1150// used by /chat/completions endpoint
1151json oaicompat_chat_params_parse(
1152    json & body, /* openai api json semantics */
1153    const server_chat_params & opt,
1154    std::vector<raw_buffer> & out_files)
1155{
1156    json llama_params;
1157
1158    auto tools = json_value(body, "tools", json());
1159    auto has_tools = tools.is_array() && !tools.empty();
1160    auto stream = json_value(body, "stream", false);
1161    auto tool_choice = json_value(body, "tool_choice", std::string("auto"));
1162
1163    if (!opt.use_jinja) {
1164        if (has_tools) {
1165            throw std::runtime_error("tools param requires --jinja flag");
1166        }
1167        if (tool_choice != "auto") {
1168            throw std::runtime_error("tool_choice param requires --jinja flag");
1169        }
1170    }
1171
1172    // Handle "stop" field
1173    if (body.contains("stop") && body.at("stop").is_string()) {
1174        llama_params["stop"] = json::array({body.at("stop").get<std::string>()});
1175    } else {
1176        llama_params["stop"] = json_value(body, "stop", json::array());
1177    }
1178
1179    auto json_schema = json_value(body, "json_schema", json());
1180    auto grammar = json_value(body, "grammar", std::string());
1181    if (!json_schema.is_null() && !grammar.empty()) {
1182        throw std::runtime_error("Cannot use both json_schema and grammar");
1183    }
1184
1185    // Handle "response_format" field
1186    if (body.contains("response_format")) {
1187        json response_format      = json_value(body, "response_format", json::object());
1188        std::string response_type = json_value(response_format, "type", std::string());
1189        if (response_type == "json_object") {
1190            if (response_format.contains("schema") || json_schema.empty()) {
1191                json_schema = json_value(response_format, "schema", json::object());
1192            }
1193        } else if (response_type == "json_schema") {
1194            auto schema_wrapper = json_value(response_format, "json_schema", json::object());
1195            json_schema = json_value(schema_wrapper, "schema", json::object());
1196        } else if (!response_type.empty() && response_type != "text") {
1197            throw std::invalid_argument("response_format type must be one of \"text\" or \"json_object\", but got: " + response_type);
1198        }
1199    }
1200
1201    // an absent or empty schema means any object
1202    if (json_schema.is_object() && json_schema.empty()) {
1203        json_schema["type"] = "object";
1204    }
1205
1206    // get input files
1207    if (!body.contains("messages")) {
1208        throw std::invalid_argument("'messages' is required");
1209    }
1210    json & messages = body.at("messages");
1211    if (!messages.is_array()) {
1212        throw std::invalid_argument("Expected 'messages' to be an array");
1213    }
1214    for (auto & msg : messages) {
1215        std::string role = json_value(msg, "role", std::string());
1216        if (role != "assistant" && !msg.contains("content")) {
1217            throw std::invalid_argument("All non-assistant messages must contain 'content'");
1218        }
1219        if (role == "assistant") {
1220            if (!msg.contains("content") && !msg.contains("tool_calls")) {
1221                throw std::invalid_argument("Assistant message must contain either 'content' or 'tool_calls'!");
1222            }
1223            if (!msg.contains("content")) {
1224                continue; // avoid errors with no content
1225            }
1226        }
1227        json & content = msg.at("content");
1228        if (content.is_string() || content.is_null()) {
1229            continue;
1230        }
1231
1232        if (!content.is_array()) {
1233            throw std::invalid_argument("Expected 'content' to be a string or an array");
1234        }
1235
1236        for (auto & p : content) {
1237            std::string type = json_value(p, "type", std::string());
1238            if (type == "image_url") {
1239                if (!opt.allow_image) {
1240                    throw std::runtime_error("image input is not supported - hint: if this is unexpected, you may need to provide the mmproj");
1241                }
1242
1243                json image_url = json_value(p, "image_url", json::object());
1244                std::string url = json_value(image_url, "url", std::string());
1245                handle_media(out_files, url, opt.media_path);
1246
1247                p["type"] = "media_marker";
1248                p["text"] = get_media_marker();
1249                p.erase("image_url");
1250
1251            } else if (type == "input_audio") {
1252                if (!opt.allow_audio) {
1253                    throw std::runtime_error("audio input is not supported - hint: if this is unexpected, you may need to provide the mmproj");
1254                }
1255
1256                // note: don't need to validate "format", it's redundant
1257                json input_audio = json_value(p, "input_audio", json::object());
1258                std::string url  = json_value(input_audio, "data",
1259                                        json_value(input_audio, "url", std::string()));
1260                handle_media(out_files, url, opt.media_path);
1261
1262                p["type"] = "media_marker";
1263                p["text"] = get_media_marker();
1264                p.erase("input_audio");
1265
1266            } else if (type == "input_video") {
1267                if (!opt.allow_video) {
1268                    throw std::runtime_error("video input is not supported - hint: if this is unexpected, you may need to provide the mmproj");
1269                }
1270
1271                json input_video = json_value(p, "input_video", json::object());
1272                std::string url  = json_value(input_video, "data",
1273                                        json_value(input_video, "url", std::string()));
1274                handle_media(out_files, url, opt.media_path);
1275
1276                p["type"] = "media_marker";
1277                p["text"] = get_media_marker();
1278                p.erase("input_video");
1279
1280            } else if (type != "text") {
1281                throw std::invalid_argument("unsupported content[].type");
1282            }
1283        }
1284    }
1285
1286    auto caps = common_chat_templates_get_caps(opt.tmpls.get());
1287
1288    common_chat_templates_inputs inputs;
1289    inputs.messages               = common_chat_msgs_parse_oaicompat(messages);
1290    inputs.tools                  = common_chat_tools_parse_oaicompat(tools);
1291    inputs.tool_choice            = common_chat_tool_choice_parse_oaicompat(tool_choice);
1292    inputs.json_schema            = json_schema.is_null() ? "" : json_schema.dump();
1293    inputs.grammar                = grammar;
1294    inputs.use_jinja              = opt.use_jinja;
1295    inputs.parallel_tool_calls    = json_value(body, "parallel_tool_calls", caps["supports_parallel_tool_calls"]);
1296    inputs.add_generation_prompt  = json_value(body, "add_generation_prompt", true);
1297    inputs.continue_final_message = body.contains("continue_final_message") ?
1298        common_chat_continuation_parse(body.at("continue_final_message")) :
1299        COMMON_CHAT_CONTINUATION_NONE;
1300    if (inputs.continue_final_message == COMMON_CHAT_CONTINUATION_NONE && opt.prefill_assistant
1301        && !inputs.messages.empty() && inputs.messages.back().role == "assistant") {
1302        if (inputs.messages.size() >= 2 && inputs.messages[inputs.messages.size() - 2].role == "assistant") {
1303            throw std::invalid_argument("Cannot have 2 or more assistant messages at the end of the list.");
1304        }
1305        inputs.continue_final_message = COMMON_CHAT_CONTINUATION_AUTO;
1306        inputs.add_generation_prompt  = false;
1307    }
1308    if (inputs.continue_final_message != COMMON_CHAT_CONTINUATION_NONE && inputs.add_generation_prompt) {
1309        throw std::invalid_argument("Cannot set both add_generation_prompt and continue_final_message to true.");
1310    }
1311    if (inputs.continue_final_message != COMMON_CHAT_CONTINUATION_NONE
1312        && !inputs.messages.empty()
1313        && inputs.messages.back().role == "assistant"
1314        && !inputs.messages.back().tool_calls.empty()) {
1315        throw std::invalid_argument("Cannot continue an assistant message that contains tool calls.");
1316    }
1317    inputs.reasoning_format = opt.reasoning_format;
1318    if (body.contains("reasoning_format")) {
1319        inputs.reasoning_format = common_reasoning_format_from_name(body.at("reasoning_format").get<std::string>());
1320    }
1321    inputs.enable_thinking = opt.enable_thinking;
1322    if (!inputs.tools.empty() && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE) {
1323        if (body.contains("grammar")) {
1324            throw std::invalid_argument("Cannot use custom grammar constraints with tools.");
1325        }
1326        llama_params["parse_tool_calls"] = true;
1327    }
1328
1329    // merge the template args provided from command line with the args provided in the user request
1330    auto chat_template_kwargs_object = json_value(body, "chat_template_kwargs", json::object());
1331    inputs.chat_template_kwargs = opt.chat_template_kwargs;
1332    for (const auto & item : chat_template_kwargs_object.items()) {
1333        inputs.chat_template_kwargs[item.key()] = item.value().dump();
1334    }
1335
1336    // parse the "enable_thinking" kwarg to override the default value
1337    auto enable_thinking_kwarg = json_value(inputs.chat_template_kwargs, "enable_thinking", std::string(""));
1338    if (enable_thinking_kwarg == "true") {
1339        inputs.enable_thinking = true;
1340    } else if (enable_thinking_kwarg == "false") {
1341        inputs.enable_thinking = false;
1342    } else if (!enable_thinking_kwarg.empty() && enable_thinking_kwarg[0] == '"') {
1343        throw std::invalid_argument("invalid type for \"enable_thinking\" (expected boolean, got string)");
1344    }
1345
1346    // Parse the OAI "reasoning_effort" field; "none" disables reasoning.
1347    if (body.contains("reasoning_effort")) {
1348        auto reasoning_effort = json_value(body, "reasoning_effort", std::string(""));
1349        if (reasoning_effort == "none") {
1350            inputs.enable_thinking = false;
1351            inputs.chat_template_kwargs.erase("reasoning_effort");
1352        } else if (!reasoning_effort.empty()) {
1353            inputs.chat_template_kwargs["reasoning_effort"] = json(reasoning_effort).dump();
1354        }
1355    }
1356
1357    inputs.force_pure_content = opt.force_pure_content;
1358
1359    // Apply chat template to the list of messages
1360    auto chat_params = common_chat_templates_apply(opt.tmpls.get(), inputs);
1361
1362    llama_params["chat_format"] = static_cast<int>(chat_params.format);
1363    llama_params["prompt"]      = chat_params.prompt;
1364    if (!chat_params.grammar.empty()) {
1365        llama_params["grammar"]      = chat_params.grammar;
1366        llama_params["grammar_type"] = std::string("tool_calls");
1367    }
1368    llama_params["grammar_lazy"] = chat_params.grammar_lazy;
1369    auto grammar_triggers        = json::array();
1370    for (const auto & trigger : chat_params.grammar_triggers) {
1371        server_grammar_trigger ct(trigger);
1372        grammar_triggers.push_back(ct.to_json());
1373    }
1374    llama_params["grammar_triggers"]  = grammar_triggers;
1375    llama_params["preserved_tokens"]  = chat_params.preserved_tokens;
1376    llama_params["generation_prompt"] = chat_params.generation_prompt;
1377    for (const auto & stop : chat_params.additional_stops) {
1378        llama_params["stop"].push_back(stop);
1379    }
1380    if (!chat_params.parser.empty()) {
1381        llama_params["chat_parser"] = chat_params.parser;
1382    }
1383
1384    llama_params["message_delimiters"] = chat_params.message_delimiters.to_json();
1385
1386    // Reasoning budget: pass parameters through to sampling layer
1387    {
1388        int reasoning_budget = json_value(body, "reasoning_budget_tokens",
1389                               json_value(body, "thinking_budget_tokens", -1));
1390        if (reasoning_budget == -1) {
1391            reasoning_budget = opt.reasoning_budget;
1392        }
1393
1394        if (!chat_params.thinking_end_tags.empty()) {
1395            llama_params["reasoning_budget_tokens"] = reasoning_budget;
1396            llama_params["reasoning_budget_start_tag"] = chat_params.thinking_start_tag;
1397            llama_params["reasoning_budget_end_tags"] = chat_params.thinking_end_tags;
1398            llama_params["reasoning_budget_message"] = json_value(body, "reasoning_budget_message", opt.reasoning_budget_message);
1399            llama_params["reasoning_control"] = json_value(body, "reasoning_control", false);
1400        }
1401    }
1402
1403    // Handle "logprobs" field
1404    // TODO: The response format of this option is not yet OAI-compatible, but seems like no one really using it; We may need to fix it in the future
1405    if (json_value(body, "logprobs", false)) {
1406        if (has_tools && stream) {
1407            throw std::invalid_argument("logprobs is not supported with tools + stream");
1408        }
1409        llama_params["n_probs"] = json_value(body, "top_logprobs", 20);
1410    } else if (body.contains("top_logprobs") && !body.at("top_logprobs").is_null()) {
1411        throw std::invalid_argument("top_logprobs requires logprobs to be set to true");
1412    }
1413
1414    // Copy remaining properties to llama_params
1415    // This allows user to use llama.cpp-specific params like "mirostat", ... via OAI endpoint.
1416    // See "launch_slot_with_task()" for a complete list of params supported by llama.cpp
1417    for (const auto & item : body.items()) {
1418        // Exception: if "n_predict" is present, we overwrite the value specified earlier by "max_tokens"
1419        if (!llama_params.contains(item.key()) || item.key() == "n_predict") {
1420            llama_params[item.key()] = item.value();
1421        }
1422    }
1423
1424    return llama_params;
1425}
1426
1427json format_embeddings_response_oaicompat(
1428        const json & request,
1429        const std::string & model_name,
1430        const json & embeddings,
1431        bool use_base64) {
1432    json data = json::array();
1433    int32_t n_tokens = 0;
1434    int i = 0;
1435    for (const auto & elem : embeddings) {
1436        json embedding_obj;
1437
1438        if (use_base64) {
1439            const auto& vec = json_value(elem, "embedding", json::array()).get<std::vector<float>>();
1440            const char* data_ptr = reinterpret_cast<const char*>(vec.data());
1441            size_t data_size = vec.size() * sizeof(float);
1442            embedding_obj = {
1443                {"embedding", base64::encode(data_ptr, data_size)},
1444                {"index", i++},
1445                {"object", "embedding"},
1446                {"encoding_format", "base64"}
1447            };
1448        } else {
1449            embedding_obj = {
1450                {"embedding", json_value(elem, "embedding", json::array())},
1451                {"index", i++},
1452                {"object", "embedding"}
1453            };
1454        }
1455        data.push_back(embedding_obj);
1456
1457        n_tokens += json_value(elem, "tokens_evaluated", 0);
1458    }
1459
1460    json res = json {
1461        {"model", json_value(request, "model", model_name)},
1462        {"object", "list"},
1463        {"usage", json {
1464            {"prompt_tokens", n_tokens},
1465            {"total_tokens", n_tokens}
1466        }},
1467        {"data", data}
1468    };
1469
1470    return res;
1471}
1472
1473json format_response_rerank(
1474        const json & request,
1475        const std::string & model_name,
1476        const json & ranks,
1477        bool is_tei_format,
1478        std::vector<std::string> & texts,
1479        int top_n) {
1480    int32_t n_tokens = 0;
1481    bool return_text = is_tei_format && json_value(request, "return_text", false);
1482    std::vector<json> elements; // Temporary vector to hold unsorted elements
1483    std::string score_label = is_tei_format ? "score" : "relevance_score";
1484    for (const auto & rank : ranks) {
1485        int index = json_value(rank, "index", 0);
1486        json elem = json{
1487            {"index", index},
1488            {score_label, json_value(rank, "score", 0.0)},
1489        };
1490        n_tokens += json_value(rank, "tokens_evaluated", 0);
1491        if (return_text) {
1492            elem["text"] = std::move(texts[index]);
1493        }
1494        elements.push_back(elem);
1495    }
1496
1497    std::sort(elements.begin(), elements.end(), [score_label](const json& a, const json& b) {
1498        return json_value(a, score_label, 0.0) > json_value(b, score_label, 0.0);
1499    });
1500
1501    elements.resize(std::min(top_n, (int)elements.size()));
1502    json results = elements;
1503
1504    if (is_tei_format) return results;
1505
1506    json res = json{
1507        {"model", json_value(request, "model", model_name)},
1508        {"object", "list"},
1509        {"usage", json{
1510            {"prompt_tokens", n_tokens},
1511            {"total_tokens", n_tokens}
1512        }},
1513        {"results", results}
1514    };
1515
1516    return res;
1517}
1518
1519
1520//
1521// other utils
1522//
1523
1524std::vector<llama_token_data> get_token_probabilities(llama_context * ctx, int idx, size_t n_top) {
1525    std::vector<llama_token_data> cur;
1526
1527    const auto * logits = llama_get_logits_ith(ctx, idx);
1528    const llama_token * sampled_ids = llama_get_sampled_candidates_ith(ctx, idx);
1529
1530    const int n_logits = llama_get_sampled_logits_count_ith(ctx, idx);
1531
1532    cur.resize(n_logits);
1533    if (sampled_ids) {
1534        for (int i = 0; i < n_logits; i++) {
1535            cur[i] = llama_token_data{sampled_ids[i], logits[i], 0.0f};
1536        }
1537    } else {
1538        for (llama_token token_id = 0; token_id < n_logits; token_id++) {
1539            cur[token_id] = llama_token_data{token_id, logits[token_id], 0.0f};
1540        }
1541    }
1542
1543    // sort tokens by logits (partial: only the leading `n_top` need ordering)
1544    if (n_top > cur.size()) {
1545        n_top = cur.size();
1546    }
1547    if (n_top > 0) {
1548        std::partial_sort(cur.begin(), cur.begin() + n_top, cur.end(),
1549            [](const llama_token_data & a, const llama_token_data & b) {
1550                return a.logit > b.logit;
1551            });
1552    }
1553
1554    // apply softmax
1555    float max_l = -std::numeric_limits<float>::infinity();
1556    if (n_top > 0) {
1557        max_l = cur[0].logit; // partial_sort guarantees the absolute maximum is at index 0
1558    } else {
1559        for (const auto & t : cur) {
1560            max_l = std::max(max_l, t.logit);
1561        }
1562    }
1563    float cum_sum = 0.0f;
1564    for (auto & t : cur) {
1565        float p = expf(t.logit - max_l);
1566        t.p = p;
1567        cum_sum += p;
1568    }
1569    for (auto & t : cur) {
1570        t.p /= cum_sum;
1571    }
1572
1573    return cur;
1574}
1575
1576std::string safe_json_to_str(const json & data) {
1577    return data.dump_safe();
1578}
1579
1580// TODO: reuse llama_detokenize
1581template <class Iter>
1582static std::string tokens_to_str(const llama_vocab * ctx, Iter begin, Iter end) {
1583    std::string ret;
1584    for (; begin != end; ++begin) {
1585        ret += common_token_to_piece(ctx, *begin);
1586    }
1587
1588    return ret;
1589}
1590
1591std::string tokens_to_str(llama_context * ctx, const llama_tokens & tokens) {
1592    auto model = llama_get_model(ctx);
1593    return tokens_to_str(llama_model_get_vocab(model), tokens.begin(), tokens.end());
1594}
1595
1596std::string tokens_to_str(const llama_vocab * vocab, const llama_tokens & tokens) {
1597    return tokens_to_str(vocab, tokens.begin(), tokens.end());
1598}
1599
1600// format incomplete utf-8 multibyte character for output
1601std::string tokens_to_output_formatted_string(const llama_context * ctx, const llama_token token) {
1602    std::string out = token == LLAMA_TOKEN_NULL ? "" : common_token_to_piece(ctx, token);
1603
1604    // if the size is 1 and first bit is 1, meaning it's a partial character
1605    //   (size > 1 meaning it's already a known token)
1606    if (out.size() == 1 && (out[0] & 0x80) == 0x80) {
1607        std::stringstream ss;
1608        ss << std::hex << (out[0] & 0xff);
1609        std::string res(ss.str());
1610        out = "byte: \\x" + res;
1611    }
1612
1613    return out;
1614}
1615
1616// format server-sent event (SSE), return the formatted string to send
1617// note: if data is a json array, it will be sent as multiple events, one per item
1618std::string format_oai_sse(const json & data) {
1619    std::ostringstream ss;
1620    auto send_single = [&ss](const json & data) {
1621        ss << "data: " <<
1622            safe_json_to_str(data) <<
1623            "\n\n"; // required by RFC 8895 - A message is terminated by a blank line (two line terminators in a row).
1624    };
1625
1626    if (data.is_array()) {
1627        for (const auto & item : data) {
1628            send_single(item);
1629        }
1630    } else {
1631        send_single(data);
1632    }
1633
1634    return ss.str();
1635}
1636
1637std::string format_oai_resp_sse(const json & data) {
1638    std::ostringstream ss;
1639    auto send_single = [&ss](const json & event_obj) {
1640        ss << "event: " << event_obj.at("event").get<std::string>() << "\n";
1641        ss << "data: " << safe_json_to_str(event_obj.at("data")) << "\n\n";
1642    };
1643
1644    if (data.is_array()) {
1645        for (const auto & item : data) {
1646            send_single(item);
1647        }
1648    } else {
1649        send_single(data);
1650    }
1651
1652    return ss.str();
1653}
1654
1655std::string format_anthropic_sse(const json & data) {
1656    std::ostringstream ss;
1657
1658    auto send_event = [&ss](const json & event_obj) {
1659        if (event_obj.contains("event") && event_obj.contains("data")) {
1660            ss << "event: " << event_obj.at("event").get<std::string>() << "\n";
1661            ss << "data: " << safe_json_to_str(event_obj.at("data")) << "\n\n";
1662        } else {
1663            ss << "data: " << safe_json_to_str(event_obj) << "\n\n";
1664        }
1665    };
1666
1667    if (data.is_array()) {
1668        for (const auto & event : data) {
1669            send_event(event);
1670        }
1671    } else {
1672        send_event(data);
1673    }
1674
1675    return ss.str();
1676}
1677
1678bool is_valid_utf8(const std::string & str) {
1679    const unsigned char* bytes = reinterpret_cast<const unsigned char*>(str.data());
1680    const unsigned char* end = bytes + str.length();
1681
1682    while (bytes < end) {
1683        if (*bytes <= 0x7F) {
1684            // 1-byte sequence (0xxxxxxx)
1685            bytes++;
1686        } else if ((*bytes & 0xE0) == 0xC0) {
1687            // 2-byte sequence (110xxxxx 10xxxxxx)
1688            if (end - bytes < 2 || (bytes[1] & 0xC0) != 0x80)
1689                return false;
1690            bytes += 2;
1691        } else if ((*bytes & 0xF0) == 0xE0) {
1692            // 3-byte sequence (1110xxxx 10xxxxxx 10xxxxxx)
1693            if (end - bytes < 3 || (bytes[1] & 0xC0) != 0x80 || (bytes[2] & 0xC0) != 0x80)
1694                return false;
1695            bytes += 3;
1696        } else if ((*bytes & 0xF8) == 0xF0) {
1697            // 4-byte sequence (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
1698            if (end - bytes < 4 || (bytes[1] & 0xC0) != 0x80 ||
1699                (bytes[2] & 0xC0) != 0x80 || (bytes[3] & 0xC0) != 0x80)
1700                return false;
1701            bytes += 4;
1702        } else {
1703            // Invalid UTF-8 lead byte
1704            return false;
1705        }
1706    }
1707
1708    return true;
1709}
1710
1711llama_tokens format_prompt_infill(
1712        const llama_vocab * vocab,
1713        const json & input_prefix,
1714        const json & input_suffix,
1715        const json & input_extra,
1716        const int n_batch,
1717        const int n_predict,
1718        const int n_ctx,
1719        const bool spm_infill,
1720        const llama_tokens & tokens_prompt
1721    ) {
1722    // TODO: optimize this block by reducing memory allocations and movement
1723
1724    // use FIM repo-level pattern:
1725    // ref: https://arxiv.org/pdf/2409.12186
1726    //
1727    // [FIM_REP]myproject
1728    // [FIM_SEP]filename0
1729    // extra chunk 0
1730    // [FIM_SEP]filename1
1731    // extra chunk 1
1732    // ...
1733    // [FIM_SEP]filename
1734    // [FIM_PRE]prefix[FIM_SUF]suffix[FIM_MID]prompt
1735    //
1736    llama_tokens extra_tokens;
1737    extra_tokens.reserve(n_ctx);
1738
1739    auto tokens_prefix = tokenize_mixed(vocab, input_prefix, false, false);
1740    auto tokens_suffix = tokenize_mixed(vocab, input_suffix, false, false);
1741
1742    if (llama_vocab_fim_rep(vocab) != LLAMA_TOKEN_NULL) {
1743        // TODO: make project name an input
1744        static const auto k_fim_repo = common_tokenize(vocab, "myproject\n", false, false);
1745
1746        extra_tokens.push_back(llama_vocab_fim_rep(vocab));
1747        extra_tokens.insert(extra_tokens.end(), k_fim_repo.begin(), k_fim_repo.end());
1748    }
1749    for (const auto & chunk : input_extra) {
1750        // { "text": string, "filename": string }
1751        const std::string text     = json_value(chunk, "text",     std::string());
1752        const std::string filename = json_value(chunk, "filename", std::string("tmp"));
1753
1754        if (llama_vocab_fim_sep(vocab) != LLAMA_TOKEN_NULL) {
1755            const auto k_fim_file = common_tokenize(vocab, filename + "\n", false, false);
1756
1757            extra_tokens.insert(extra_tokens.end(), llama_vocab_fim_sep(vocab));
1758            extra_tokens.insert(extra_tokens.end(), k_fim_file.begin(), k_fim_file.end());
1759        } else {
1760            // chunk separator in binary form to avoid confusing the AI
1761            static const char k_chunk_prefix_str[] = {0x0a, 0x0a, 0x2d, 0x2d, 0x2d, 0x20, 0x73, 0x6e, 0x69, 0x70, 0x70, 0x65, 0x74, 0x20, 0x2d, 0x2d, 0x2d, 0x0a, 0x0a, 0x00};
1762            static const auto k_chunk_prefix_tokens = common_tokenize(vocab, k_chunk_prefix_str, false, false);
1763
1764            extra_tokens.insert(extra_tokens.end(), k_chunk_prefix_tokens.begin(), k_chunk_prefix_tokens.end());
1765        }
1766
1767        const auto chunk_tokens = common_tokenize(vocab, text, false, false);
1768        extra_tokens.insert(extra_tokens.end(), chunk_tokens.begin(), chunk_tokens.end());
1769    }
1770
1771    if (llama_vocab_fim_sep(vocab) != LLAMA_TOKEN_NULL) {
1772        // TODO: current filename
1773        static const auto k_fim_file = common_tokenize(vocab, "filename\n", false, false);
1774
1775        extra_tokens.insert(extra_tokens.end(), llama_vocab_fim_sep(vocab));
1776        extra_tokens.insert(extra_tokens.end(), k_fim_file.begin(), k_fim_file.end());
1777    }
1778
1779    // for now pick FIM context to fit in a batch (ratio prefix:suffix = 3:1, TODO: configurable?)
1780    const int n_prefix_take = std::min<int>(tokens_prefix.size(),                3*(n_batch/4));
1781    const int n_suffix_take = std::min<int>(tokens_suffix.size(), std::max<int>(0, (n_batch/4) - (2 + tokens_prompt.size())));
1782
1783    SRV_DBG("n_prefix_take = %d, n_suffix_take = %d, total = %d\n", n_prefix_take, n_suffix_take, (n_prefix_take + n_suffix_take));
1784
1785    // fill the rest of the context with extra chunks
1786    const int n_extra_take = std::min<int>(std::max<int>(0, n_ctx - (n_batch) - 2*n_predict), extra_tokens.size());
1787
1788    tokens_prefix.erase(tokens_prefix.begin(), tokens_prefix.begin() + tokens_prefix.size() - n_prefix_take);
1789    tokens_suffix.resize(n_suffix_take);
1790
1791    tokens_prefix.insert(tokens_prefix.begin(), llama_vocab_fim_pre(vocab));
1792    tokens_prefix.insert(tokens_prefix.end(),   tokens_prompt.begin(), tokens_prompt.end());
1793    tokens_suffix.insert(tokens_suffix.begin(), llama_vocab_fim_suf(vocab));
1794
1795    auto embd_inp = spm_infill ? tokens_suffix : tokens_prefix;
1796    auto embd_end = spm_infill ? tokens_prefix : tokens_suffix;
1797
1798    if (llama_vocab_get_add_bos(vocab)) {
1799        embd_inp.insert(embd_inp.begin(), llama_vocab_bos(vocab));
1800    }
1801
1802    SRV_DBG("extra: n_ctx = %d, n_extra_take = %d, n_extra = %d\n", n_ctx, n_extra_take, (int) extra_tokens.size());
1803
1804    // put the extra context before the FIM prefix
1805    embd_inp.insert(embd_inp.begin(), extra_tokens.end() - n_extra_take, extra_tokens.end());
1806
1807    embd_inp.insert(embd_inp.end(), embd_end.begin(), embd_end.end());
1808    embd_inp.push_back(llama_vocab_fim_mid(vocab));
1809
1810    return embd_inp;
1811}
1812
1813server_tokens format_prompt_rerank(
1814        const struct llama_model * model,
1815        const struct llama_vocab * vocab,
1816        mtmd_context * mctx,
1817        const std::string & query,
1818        const std::string & doc,
1819        const mtmd_helper_init_opt & init_opt) {
1820    server_tokens result = {};
1821
1822    const char * rerank_prompt = llama_model_chat_template(model, "rerank");
1823
1824    if (rerank_prompt != nullptr) {
1825        std::string prompt = rerank_prompt;
1826        string_replace_all(prompt, "{query}"   , query);
1827        string_replace_all(prompt, "{document}", doc  );
1828        server_tokens tokens = tokenize_input_subprompt(vocab, mctx, prompt, false, true, init_opt);
1829        result.push_back(tokens);
1830    } else {
1831        // Get EOS token - use SEP token as fallback if EOS is not available
1832        server_tokens query_tokens = tokenize_input_subprompt(vocab, mctx, query, false, false, init_opt);
1833        server_tokens doc_tokens   = tokenize_input_subprompt(vocab, mctx, doc,   false, false, init_opt);
1834        llama_token eos_token = llama_vocab_eos(vocab);
1835        if (eos_token == LLAMA_TOKEN_NULL) {
1836            eos_token = llama_vocab_sep(vocab);
1837        }
1838
1839        if (llama_vocab_get_add_bos(vocab)) {
1840            result.push_back(llama_vocab_bos(vocab));
1841        }
1842        result.push_back(query_tokens);
1843        if (llama_vocab_get_add_eos(vocab)) {
1844            result.push_back(eos_token);
1845        }
1846        if (llama_vocab_get_add_sep(vocab)) {
1847            result.push_back(llama_vocab_sep(vocab));
1848        }
1849        result.push_back(doc_tokens);
1850        if (llama_vocab_get_add_eos(vocab)) {
1851            result.push_back(eos_token);
1852        }
1853    }
1854
1855    return result;
1856}
1857
1858//
1859// server_subproc
1860//
1861
1862bool server_subproc::has_output() {
1863    if (out_handle >= 0) {
1864        return true;
1865    }
1866    FILE * f = sproc.stdout_file(); // combined stdout/stderr
1867    if (!f) {
1868        return false;
1869    }
1870#ifdef _WIN32
1871    HANDLE h = (HANDLE) _get_osfhandle(_fileno(f));
1872    if (h != INVALID_HANDLE_VALUE) {
1873        out_handle = (intptr_t) h;
1874    }
1875#else
1876    int fd = fileno(f);
1877    if (fd >= 0) {
1878        fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
1879        out_handle = fd;
1880    }
1881#endif
1882    return out_handle >= 0;
1883}
1884
1885int server_subproc::read_output(char * buf, size_t len) {
1886    if (!has_output()) {
1887        return -1;
1888    }
1889#ifdef _WIN32
1890    HANDLE h     = (HANDLE) out_handle;
1891    DWORD  avail = 0;
1892    if (!PeekNamedPipe(h, NULL, 0, NULL, &avail, NULL)) {
1893        return -1; // pipe broken, child gone
1894    }
1895    if (avail == 0) {
1896        return 0;
1897    }
1898    DWORD to_read = avail < (DWORD) len ? avail : (DWORD) len;
1899    DWORD got     = 0;
1900    if (!ReadFile(h, buf, to_read, &got, NULL) || got == 0) {
1901        return -1;
1902    }
1903    return (int) got;
1904#else
1905    while (true) {
1906        ssize_t r = read((int) out_handle, buf, len);
1907        if (r > 0) {
1908            return (int) r;
1909        }
1910        if (r == 0) {
1911            return -1; // EOF
1912        }
1913        if (errno == EINTR) {
1914            continue;
1915        }
1916        if (errno == EAGAIN || errno == EWOULDBLOCK) {
1917            return 0;
1918        }
1919        return -1;
1920    }
1921#endif
1922}
1923
1924server_subproc::waiter::waiter() {
1925#ifndef _WIN32
1926    int fds[2];
1927    GGML_ASSERT(pipe(fds) == 0);
1928    for (int fd : fds) {
1929        fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
1930    }
1931    wake_fd[0] = fds[0];
1932    wake_fd[1] = fds[1];
1933#endif
1934}
1935
1936server_subproc::waiter::~waiter() {
1937#ifndef _WIN32
1938    close((int) wake_fd[0]);
1939    close((int) wake_fd[1]);
1940#endif
1941}
1942
1943void server_subproc::waiter::wake() {
1944#ifndef _WIN32
1945    char c = 1;
1946    (void) !write((int) wake_fd[1], &c, 1);
1947#endif
1948}
1949
1950void server_subproc::waiter::wait(const std::vector<server_subproc *> & procs, std::vector<bool> & ready, int64_t timeout_ms) {
1951    ready.assign(procs.size(), false);
1952#ifdef _WIN32
1953    // no waitable wait exists for anonymous pipes, so poll them in 50 ms steps
1954    bool any = false;
1955    for (size_t i = 0; i < procs.size(); i++) {
1956        DWORD avail = 0;
1957        if (!procs[i]->has_output() || !PeekNamedPipe((HANDLE) procs[i]->out_handle, NULL, 0, NULL, &avail, NULL) || avail > 0) {
1958            ready[i] = true; // data or broken pipe, read_output() tells which
1959            any = true;
1960        }
1961    }
1962    if (!any) {
1963        int64_t step = timeout_ms < 0 ? 50 : std::min<int64_t>(timeout_ms, 50);
1964        std::this_thread::sleep_for(std::chrono::milliseconds(step));
1965    }
1966#else
1967    std::vector<pollfd> pfds;
1968    pfds.reserve(procs.size() + 1);
1969    pfds.push_back({ (int) wake_fd[0], POLLIN, 0 });
1970    for (auto * p : procs) {
1971        pfds.push_back({ p->has_output() ? (int) p->out_handle : -1, POLLIN, 0 }); // poll() skips negative fds
1972    }
1973    int timeout = timeout_ms < 0 ? -1 : (int) std::min<int64_t>(timeout_ms, std::numeric_limits<int>::max());
1974    int r = poll(pfds.data(), pfds.size(), timeout);
1975    if (r < 0 && errno != EINTR) {
1976        LOG_ERR("%s: poll() failed: %s\n", __func__, strerror(errno));
1977    }
1978    if (pfds[0].revents) {
1979        char buf[64];
1980        while (read((int) wake_fd[0], buf, sizeof(buf)) > 0) {}
1981    }
1982    for (size_t i = 0; i < procs.size(); i++) {
1983        ready[i] = pfds[i + 1].fd < 0 || pfds[i + 1].revents != 0;
1984    }
1985#endif
1986}