Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
b637bc6
make memset range dynamic
Apr 18, 2025
218d19e
rpc : add RPC_CMD_HELLO (#12955)
rgerganov Apr 18, 2025
5ffc9a2
mtmd : add methods to access `mtmd_image_tokens` (#12906)
ngxson Apr 18, 2025
c1079ff
SYCL: Refactor and enable FP16 in binary broadcast OPs (#12975)
qnixsynapse Apr 18, 2025
16f1384
server : use std::move whenever possible (#12936)
ngxson Apr 18, 2025
853b98e
gguf-py : GGUF Editor GUI - Python + Qt6 (#12930)
christopherthompson81 Apr 18, 2025
ed7eed0
main : Fix Ctrl+D/newline handling (#12951)
danielzgtg Apr 18, 2025
ad6c775
clip : refactor, add `image_manipulation` and `llava_uhd` classes (#1…
ngxson Apr 19, 2025
46303f5
gguf-py : fix upload python package workflow (#13020)
CISC Apr 19, 2025
6eb4bb0
Disable CI cross-compile builds (#13022)
bandoti Apr 19, 2025
1f3ca67
metal: add neg operator (#13029)
jmorganca Apr 20, 2025
bd281db
vulkan: support noncontiguous rms_norm (#13031)
jeffbolznv Apr 20, 2025
104cf86
llava: fix errors in clip.h on certain compilers (#13030)
jmorganca Apr 20, 2025
361eb6f
convert : experimental support for `--mmproj` flag (#13023)
ngxson Apr 20, 2025
3ee8936
mtmd : merge llava, gemma3 and minicpmv CLI into single `llama-mtmd-c…
ngxson Apr 21, 2025
0da1f2c
SYCL: Add non-contiguous support in ROPE (#12993)
qnixsynapse Apr 21, 2025
0190b62
ggml : add SSE 4.2 and x64 base variant for CPUs without AVX (#12871)
slaren Apr 21, 2025
a034e66
llava : update documentations (#13055)
ngxson Apr 22, 2025
949e67e
metal : add memory pool for temp allocs (#12850)
ggerganov Apr 22, 2025
2d59f44
security : add note about RPC and server functionality (#13061)
ggerganov Apr 22, 2025
cb04870
mtmd : support SmolVLM (version 1 and 2) (#13050)
ngxson Apr 22, 2025
e8558fd
CUDA: noncont MMVQ + batched bs1 MUL_MAT_ID (#13014)
JohannesGaessler Apr 22, 2025
7513bbe
rpc : add command line option for number of threads for the CPU backe…
rgerganov Apr 23, 2025
5978039
convert : Append mult-eos,half-rope,bos to GLM4-0414 and Z (#13021)
piDack Apr 23, 2025
5027efb
mtmd : Support Pixtral 12B (#13065)
ngxson Apr 23, 2025
777310b
llama-mtmd-cli: Sigint rework in mtmd vision example (#13080)
pl752 Apr 23, 2025
58e6fbc
vulkan: matmul gcn tuning (#13016)
netrunnereve Apr 24, 2025
230d0c6
metal : fix floating-point range of attention scores in FA kernels (#…
ggerganov Apr 24, 2025
95aaaf1
arg : clean up handling --mmproj with -hf (#13082)
ngxson Apr 24, 2025
c58030f
arg : add --no-mmproj-offload (#13093)
ngxson Apr 24, 2025
af62651
clang-tidy : disable warning about missing math parenthesis (#13091)
ggerganov Apr 24, 2025
3f08e60
cmake : do not include ./src as public for libllama (#13062)
ggerganov Apr 24, 2025
6de7809
CUDA: use switch statements in constexpr functions (#13095)
JohannesGaessler Apr 24, 2025
dfedb1b
ggml : Depthwise 2D convolution (ggml/1152)
Acly Apr 17, 2025
2e595bd
sync : ggml
ggerganov Apr 24, 2025
24c0b67
ggml : fix trailing whitespaces (#0)
ggerganov Apr 24, 2025
0414304
embeddings : fix batch sizes (#13076)
ggerganov Apr 24, 2025
01f08e9
clip : remove boi/eoi embeddings for GLM-edge model (#13081)
ngxson Apr 24, 2025
1763285
rpc : do not wait for response when sending RPC_CMD_SET_TENSOR (#12943)
rgerganov Apr 25, 2025
9db1139
change the reorder tensor from init to execute OP (#13003)
NeoZhangJianyu Apr 25, 2025
e306c33
clip : fix pixtral on some GPU backends (#13097)
ngxson Apr 25, 2025
a9f6135
Force FP32 compute in GLM4 FFN Down (#13101)
city96 Apr 25, 2025
1f7a36f
llama : fix K-shift with quantized K and BLAS backend (#13113)
slaren Apr 25, 2025
dd23581
grammar : handle maxItems == 0 in JSON schema (#13117)
rick-github Apr 26, 2025
4b6575c
ggml: move fp16/bf16 conversion optimizations to CPU backend + export…
SongXiaoXi Apr 26, 2025
cc00eb2
clip : improve projector naming (#13118)
ngxson Apr 26, 2025
4f3ba1c
common : add common_remote_get_content (#13123)
ngxson Apr 26, 2025
35b7fec
clip : Add Qwen2.5VL support (#12402)
HimariO Apr 27, 2025
d350aed
Fixes Qwen2.5VL segfault during inference with https://github.com/ggm…
LostRuins Apr 27, 2025
aff089e
musa: fix build warning (#13129)
yeahdongcn Apr 27, 2025
201056c
llama-chat : fix wrong template in GLM4-0414 (#13140)
matteoserva Apr 27, 2025
b2604c3
llama-bench : Add `--override-tensors` arg (#12922)
4onen Apr 27, 2025
558ab0a
arg : fix unused variable (#13142)
ngxson Apr 28, 2025
1ba4571
delete set size and delete buffer clear as suggested
Apr 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Checks: >
-readability-magic-numbers,
-readability-uppercase-literal-suffix,
-readability-simplify-boolean-expr,
-readability-math-missing-parentheses,
clang-analyzer-*,
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
performance-*,
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -601,8 +601,9 @@ jobs:
-DGGML_SYCL_F16=ON
cmake --build build --config Release -j $(nproc)

build-linux-cross:
uses: ./.github/workflows/build-linux-cross.yml
# Disabled for now due to sporadic issue syncing.
# build-linux-cross:
# uses: ./.github/workflows/build-linux-cross.yml

macOS-latest-cmake-ios:
runs-on: macos-latest
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Inference of Meta's [LLaMA](https://arxiv.org/abs/2302.13971) model (and others)

## Hot topics

- A new binary `llama-mtmd-cli` is introduced to replace `llava-cli`, `minicpmv-cli` and `gemma3-cli` https://github.com/ggml-org/llama.cpp/pull/13012, `libllava` will be deprecated
- **How to use [MTLResidencySet](https://developer.apple.com/documentation/metal/mtlresidencyset?language=objc) to keep the GPU memory active?** https://github.com/ggml-org/llama.cpp/pull/11427
- **VS Code extension for FIM completions:** https://github.com/ggml-org/llama.vscode
- Universal [tool call support](./docs/function-calling.md) in `llama-server` https://github.com/ggml-org/llama.cpp/pull/9639
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ To protect sensitive data from potential leaks or unauthorized access, it is cru
### Untrusted environments or networks

If you can't run your models in a secure and isolated environment or if it must be exposed to an untrusted network, make sure to take the following security precautions:
* Confirm the hash of any downloaded artifact (e.g. pre-trained model weights) matches a known-good value
* Do not use the RPC backend, [rpc-server](https://github.com/ggml-org/llama.cpp/tree/master/examples/rpc) and [llama-server](https://github.com/ggml-org/llama.cpp/tree/master/examples/server) functionality (see https://github.com/ggml-org/llama.cpp/pull/13061).
* Confirm the hash of any downloaded artifact (e.g. pre-trained model weights) matches a known-good value.
* Encrypt your data if sending it over the network.

### Multi-Tenant environments
Expand Down
187 changes: 131 additions & 56 deletions common/arg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@

using json = nlohmann::ordered_json;

std::initializer_list<enum llama_example> mmproj_examples = {
LLAMA_EXAMPLE_LLAVA,
// TODO: add LLAMA_EXAMPLE_SERVER when it's ready
};

common_arg & common_arg::set_examples(std::initializer_list<enum llama_example> examples) {
this->examples = std::move(examples);
return *this;
Expand Down Expand Up @@ -157,6 +162,10 @@ struct common_hf_file_res {

#ifdef LLAMA_USE_CURL

bool common_has_curl() {
return true;
}

#ifdef __linux__
#include <linux/limits.h>
#elif defined(_WIN32)
Expand Down Expand Up @@ -522,64 +531,89 @@ static bool common_download_model(
return true;
}

/**
* Allow getting the HF file from the HF repo with tag (like ollama), for example:
* - bartowski/Llama-3.2-3B-Instruct-GGUF:q4
* - bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M
* - bartowski/Llama-3.2-3B-Instruct-GGUF:q5_k_s
* Tag is optional, default to "latest" (meaning it checks for Q4_K_M first, then Q4, then if not found, return the first GGUF file in repo)
*
* Return pair of <repo, file> (with "repo" already having tag removed)
*
* Note: we use the Ollama-compatible HF API, but not using the blobId. Instead, we use the special "ggufFile" field which returns the value for "hf_file". This is done to be backward-compatible with existing cache files.
*/
static struct common_hf_file_res common_get_hf_file(const std::string & hf_repo_with_tag, const std::string & bearer_token) {
auto parts = string_split<std::string>(hf_repo_with_tag, ':');
std::string tag = parts.size() > 1 ? parts.back() : "latest";
std::string hf_repo = parts[0];
if (string_split<std::string>(hf_repo, '/').size() != 2) {
throw std::invalid_argument("error: invalid HF repo format, expected <user>/<model>[:quant]\n");
}

// fetch model info from Hugging Face Hub API
std::pair<long, std::vector<char>> common_remote_get_content(const std::string & url, const common_remote_params & params) {
curl_ptr curl(curl_easy_init(), &curl_easy_cleanup);
curl_slist_ptr http_headers;
std::string res_str;
std::vector<char> res_buffer;

std::string model_endpoint = get_model_endpoint();

std::string url = model_endpoint + "v2/" + hf_repo + "/manifests/" + tag;
curl_easy_setopt(curl.get(), CURLOPT_URL, url.c_str());
curl_easy_setopt(curl.get(), CURLOPT_NOPROGRESS, 1L);
curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L);
typedef size_t(*CURLOPT_WRITEFUNCTION_PTR)(void * ptr, size_t size, size_t nmemb, void * data);
auto write_callback = [](void * ptr, size_t size, size_t nmemb, void * data) -> size_t {
static_cast<std::string *>(data)->append((char * ) ptr, size * nmemb);
auto data_vec = static_cast<std::vector<char> *>(data);
data_vec->insert(data_vec->end(), (char *)ptr, (char *)ptr + size * nmemb);
return size * nmemb;
};
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, static_cast<CURLOPT_WRITEFUNCTION_PTR>(write_callback));
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &res_str);
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &res_buffer);
#if defined(_WIN32)
curl_easy_setopt(curl.get(), CURLOPT_SSL_OPTIONS, CURLSSLOPT_NATIVE_CA);
#endif
if (!bearer_token.empty()) {
std::string auth_header = "Authorization: Bearer " + bearer_token;
http_headers.ptr = curl_slist_append(http_headers.ptr, auth_header.c_str());
if (params.timeout > 0) {
curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT, params.timeout);
}
if (params.max_size > 0) {
curl_easy_setopt(curl.get(), CURLOPT_MAXFILESIZE, params.max_size);
}
// Important: the User-Agent must be "llama-cpp" to get the "ggufFile" field in the response
http_headers.ptr = curl_slist_append(http_headers.ptr, "User-Agent: llama-cpp");
http_headers.ptr = curl_slist_append(http_headers.ptr, "Accept: application/json");
for (const auto & header : params.headers) {
http_headers.ptr = curl_slist_append(http_headers.ptr, header.c_str());
}
curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, http_headers.ptr);

CURLcode res = curl_easy_perform(curl.get());

if (res != CURLE_OK) {
throw std::runtime_error("error: cannot make GET request to HF API");
std::string error_msg = curl_easy_strerror(res);
throw std::runtime_error("error: cannot make GET request: " + error_msg);
}

long res_code;
std::string ggufFile = "";
std::string mmprojFile = "";
curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &res_code);

return { res_code, std::move(res_buffer) };
}

/**
* Allow getting the HF file from the HF repo with tag (like ollama), for example:
* - bartowski/Llama-3.2-3B-Instruct-GGUF:q4
* - bartowski/Llama-3.2-3B-Instruct-GGUF:Q4_K_M
* - bartowski/Llama-3.2-3B-Instruct-GGUF:q5_k_s
* Tag is optional, default to "latest" (meaning it checks for Q4_K_M first, then Q4, then if not found, return the first GGUF file in repo)
*
* Return pair of <repo, file> (with "repo" already having tag removed)
*
* Note: we use the Ollama-compatible HF API, but not using the blobId. Instead, we use the special "ggufFile" field which returns the value for "hf_file". This is done to be backward-compatible with existing cache files.
*/
static struct common_hf_file_res common_get_hf_file(const std::string & hf_repo_with_tag, const std::string & bearer_token) {
auto parts = string_split<std::string>(hf_repo_with_tag, ':');
std::string tag = parts.size() > 1 ? parts.back() : "latest";
std::string hf_repo = parts[0];
if (string_split<std::string>(hf_repo, '/').size() != 2) {
throw std::invalid_argument("error: invalid HF repo format, expected <user>/<model>[:quant]\n");
}

std::string url = get_model_endpoint() + "v2/" + hf_repo + "/manifests/" + tag;

// headers
std::vector<std::string> headers;
headers.push_back("Accept: application/json");
if (!bearer_token.empty()) {
headers.push_back("Authorization: Bearer " + bearer_token);
}
// Important: the User-Agent must be "llama-cpp" to get the "ggufFile" field in the response
// User-Agent header is already set in common_remote_get_content, no need to set it here

// make the request
common_remote_params params;
params.headers = headers;
auto res = common_remote_get_content(url, params);
long res_code = res.first;
std::string res_str(res.second.data(), res.second.size());
std::string ggufFile;
std::string mmprojFile;

if (res_code == 200) {
// extract ggufFile.rfilename in json, using regex
{
Expand Down Expand Up @@ -613,6 +647,10 @@ static struct common_hf_file_res common_get_hf_file(const std::string & hf_repo_

#else

bool common_has_curl() {
return false;
}

static bool common_download_file_single(const std::string &, const std::string &, const std::string &) {
LOG_ERR("error: built without CURL, cannot download model from internet\n");
return false;
Expand All @@ -635,17 +673,26 @@ static struct common_hf_file_res common_get_hf_file(const std::string &, const s
return {};
}

std::pair<long, std::vector<char>> common_remote_get_content(const std::string &, const common_remote_params &) {
throw std::runtime_error("error: built without CURL, cannot download model from the internet");
}

#endif // LLAMA_USE_CURL

//
// utils
//

static void common_params_handle_model(
struct handle_model_result {
bool found_mmproj = false;
common_params_model mmproj;
};

static handle_model_result common_params_handle_model(
struct common_params_model & model,
const std::string & bearer_token,
const std::string & model_path_default,
bool is_mmproj = false) { // TODO: move is_mmproj to an enum when we have more files?
const std::string & model_path_default) {
handle_model_result result;
// handle pre-fill default model path and url based on hf_repo and hf_file
{
if (!model.hf_repo.empty()) {
Expand All @@ -657,7 +704,12 @@ static void common_params_handle_model(
exit(1); // built without CURL, error message already printed
}
model.hf_repo = auto_detected.repo;
model.hf_file = is_mmproj ? auto_detected.mmprojFile : auto_detected.ggufFile;
model.hf_file = auto_detected.ggufFile;
if (!auto_detected.mmprojFile.empty()) {
result.found_mmproj = true;
result.mmproj.hf_repo = model.hf_repo;
result.mmproj.hf_file = auto_detected.mmprojFile;
}
} else {
model.hf_file = model.path;
}
Expand Down Expand Up @@ -694,6 +746,8 @@ static void common_params_handle_model(
exit(1);
}
}

return result;
}

const std::vector<ggml_type> kv_cache_types = {
Expand Down Expand Up @@ -827,16 +881,25 @@ static bool common_params_parse_ex(int argc, char ** argv, common_params_context
throw std::invalid_argument("error: --prompt-cache-all not supported in interactive mode yet\n");
}

common_params_handle_model(params.model, params.hf_token, DEFAULT_MODEL_PATH);
common_params_handle_model(params.speculative.model, params.hf_token, "");
common_params_handle_model(params.vocoder.model, params.hf_token, "");

// allow --mmproj to be set from -hf
// assuming that mmproj is always in the same repo as text model
if (!params.model.hf_repo.empty() && ctx_arg.ex == LLAMA_EXAMPLE_LLAVA) {
params.mmproj.hf_repo = params.model.hf_repo;
// handle model and download
{
auto res = common_params_handle_model(params.model, params.hf_token, DEFAULT_MODEL_PATH);
if (params.no_mmproj) {
params.mmproj = {};
} else if (res.found_mmproj && params.mmproj.path.empty() && params.mmproj.url.empty()) {
// optionally, handle mmproj model when -hf is specified
params.mmproj = res.mmproj;
}
// only download mmproj if the current example is using it
for (auto & ex : mmproj_examples) {
if (ctx_arg.ex == ex) {
common_params_handle_model(params.mmproj, params.hf_token, "");
break;
}
}
common_params_handle_model(params.speculative.model, params.hf_token, "");
common_params_handle_model(params.vocoder.model, params.hf_token, "");
}
common_params_handle_model(params.mmproj, params.hf_token, "", true);

if (params.escape) {
string_process_escapes(params.prompt);
Expand Down Expand Up @@ -968,28 +1031,25 @@ static void common_params_print_completion(common_params_context & ctx_arg) {
"llama-embedding",
"llama-eval-callback",
"llama-export-lora",
"llama-gbnf-validator",
"llama-gen-docs",
"llama-gguf",
"llama-gguf-hash",
"llama-gguf-split",
"llama-gritlm",
"llama-imatrix",
"llama-infill",
"llama-llava-cli",
"llama-mtmd-cli",
"llama-llava-clip-quantize-cli",
"llama-lookahead",
"llama-lookup",
"llama-lookup-create",
"llama-lookup-merge",
"llama-lookup-stats",
"llama-minicpmv-cli",
"llama-parallel",
"llama-passkey",
"llama-perplexity",
"llama-q8dot",
"llama-quantize",
"llama-quantize-stats",
"llama-qwen2vl-cli",
"llama-retrieval",
"llama-run",
Expand Down Expand Up @@ -2096,18 +2156,32 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
).set_examples({LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_NO_CONT_BATCHING"));
add_opt(common_arg(
{"--mmproj"}, "FILE",
"path to a multimodal projector file for LLaVA. see examples/llava/README.md",
"path to a multimodal projector file. see examples/llava/README.md",
[](common_params & params, const std::string & value) {
params.mmproj.path = value;
}
).set_examples({LLAMA_EXAMPLE_LLAVA}));
).set_examples(mmproj_examples));
add_opt(common_arg(
{"--mmproj-url"}, "URL",
"URL to a multimodal projector file for LLaVA. see examples/llava/README.md",
"URL to a multimodal projector file. see examples/llava/README.md",
[](common_params & params, const std::string & value) {
params.mmproj.url = value;
}
).set_examples({LLAMA_EXAMPLE_LLAVA}));
).set_examples(mmproj_examples));
add_opt(common_arg(
{"--no-mmproj"},
"explicitly disable multimodal projector, useful when using -hf",
[](common_params & params) {
params.no_mmproj = true;
}
).set_examples(mmproj_examples));
add_opt(common_arg(
{"--no-mmproj-offload"},
"do not offload multimodal projector to GPU",
[](common_params & params) {
params.mmproj_use_gpu = false;
}
).set_examples(mmproj_examples));
add_opt(common_arg(
{"--image"}, "FILE",
"path to an image file. use with multimodal models. Specify multiple times for batching",
Expand Down Expand Up @@ -2382,6 +2456,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
add_opt(common_arg(
{"-hf", "-hfr", "--hf-repo"}, "<user>/<model>[:quant]",
"Hugging Face model repository; quant is optional, case-insensitive, default to Q4_K_M, or falls back to the first file in the repo if Q4_K_M doesn't exist.\n"
"mmproj is also downloaded automatically if available. to disable, add --no-mmproj\n"
"example: unsloth/phi-4-GGUF:q4_k_m\n"
"(default: unused)",
[](common_params & params, const std::string & value) {
Expand Down Expand Up @@ -2726,7 +2801,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
[](common_params & params, const std::string & value) {
params.chat_template = value;
}
).set_examples({LLAMA_EXAMPLE_MAIN, LLAMA_EXAMPLE_SERVER}).set_env("LLAMA_ARG_CHAT_TEMPLATE"));
).set_examples({LLAMA_EXAMPLE_MAIN, LLAMA_EXAMPLE_SERVER, LLAMA_EXAMPLE_LLAVA}).set_env("LLAMA_ARG_CHAT_TEMPLATE"));
add_opt(common_arg(
{"--chat-template-file"}, "JINJA_TEMPLATE_FILE",
string_format(
Expand Down
9 changes: 9 additions & 0 deletions common/arg.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,12 @@ bool common_params_parse(int argc, char ** argv, common_params & params, llama_e

// function to be used by test-arg-parser
common_params_context common_params_parser_init(common_params & params, llama_example ex, void(*print_usage)(int, char **) = nullptr);
bool common_has_curl();

struct common_remote_params {
std::vector<std::string> headers;
long timeout = 0; // CURLOPT_TIMEOUT, in seconds ; 0 means no timeout
long max_size = 0; // max size of the response ; unlimited if 0 ; max is 2GB
};
// get remote file content, returns <http_code, raw_response_body>
std::pair<long, std::vector<char>> common_remote_get_content(const std::string & url, const common_remote_params & params);
2 changes: 2 additions & 0 deletions common/common.h
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ struct common_params {

// multimodal models (see examples/llava)
struct common_params_model mmproj;
bool mmproj_use_gpu = true; // use GPU for multimodal model
bool no_mmproj = false; // explicitly disable multimodal model
std::vector<std::string> image; // path to image file(s)

// embedding
Expand Down
3 changes: 3 additions & 0 deletions common/json-schema-to-grammar.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ using json = nlohmann::ordered_json;
static std::string build_repetition(const std::string & item_rule, int min_items, int max_items, const std::string & separator_rule = "") {
auto has_max = max_items != std::numeric_limits<int>::max();

if (max_items == 0) {
return "";
}
if (min_items == 0 && max_items == 1) {
return item_rule + "?";
}
Expand Down
Loading