From b17019767de693eacb11c984905b7e429a289272 Mon Sep 17 00:00:00 2001 From: jeremyfowers Date: Mon, 20 Apr 2026 15:40:44 -0400 Subject: [PATCH 1/2] Add a filter arg to the list command --- src/cpp/cli/lemonade_client.cpp | 60 +++++++++++++++++++-- src/cpp/cli/main.cpp | 6 ++- src/cpp/include/lemon_cli/lemonade_client.h | 2 +- 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/src/cpp/cli/lemonade_client.cpp b/src/cpp/cli/lemonade_client.cpp index 21e4c5317..1152e31f5 100644 --- a/src/cpp/cli/lemonade_client.cpp +++ b/src/cpp/cli/lemonade_client.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -13,6 +14,48 @@ static const int DEFAULT_CONNECTION_TIMEOUT_MS = 30000; static const int DEFAULT_READ_TIMEOUT_MS = 30000; static const int LONG_TIMEOUT_MS = 86400000; +static std::regex build_name_filter_regex(const std::string& name_filter) { + std::string regex_pattern; + regex_pattern.reserve(name_filter.size() * 2); + + for (char ch : name_filter) { + switch (ch) { + case '*': + regex_pattern += ".*"; + break; + case '\\': + case '^': + case '$': + case '.': + case '|': + case '?': + case '+': + case '(': + case ')': + case '[': + case ']': + case '{': + case '}': + regex_pattern += '\\'; + regex_pattern += ch; + break; + default: + regex_pattern += ch; + break; + } + } + + return std::regex(regex_pattern, std::regex_constants::ECMAScript | std::regex_constants::icase); +} + +static bool model_name_matches_filter(const std::string& model_name, const std::regex* name_filter_regex) { + if (name_filter_regex == nullptr) { + return true; + } + + return std::regex_search(model_name, *name_filter_regex); +} + HttpError::HttpError(int status, std::string body, const std::string& message) : std::runtime_error(message), status_code_(status), response_body_(std::move(body)) {} @@ -297,11 +340,22 @@ std::vector LemonadeClient::get_models(bool show_all) const { return models; } -int LemonadeClient::list_models(bool show_all) const { +int LemonadeClient::list_models(bool show_all, const std::string& name_filter) const { try { std::vector models = get_models(show_all); + std::vector filtered_models; + filtered_models.reserve(models.size()); + const bool has_name_filter = !name_filter.empty(); + const std::regex name_filter_regex = has_name_filter ? build_name_filter_regex(name_filter) : std::regex(); + const std::regex* active_name_filter_regex = has_name_filter ? &name_filter_regex : nullptr; - if (models.empty()) { + for (const auto& model : models) { + if (model_name_matches_filter(model.id, active_name_filter_regex)) { + filtered_models.push_back(model); + } + } + + if (filtered_models.empty()) { std::cout << "No models available" << std::endl; return 0; } @@ -311,7 +365,7 @@ int LemonadeClient::list_models(bool show_all) const { << "Details" << std::endl; std::cout << std::string(100, '-') << std::endl; - for (const auto& model : models) { + for (const auto& model : filtered_models) { std::string downloaded = model.downloaded ? "Yes" : "No"; std::string details = model.recipe.empty() ? "-" : model.recipe; diff --git a/src/cpp/cli/main.cpp b/src/cpp/cli/main.cpp index d17c905b0..5cabf9917 100644 --- a/src/cpp/cli/main.cpp +++ b/src/cpp/cli/main.cpp @@ -96,6 +96,7 @@ struct CliConfig { int port = 13305; std::string api_key; std::string model; + std::string list_filter; std::map checkpoints; std::string recipe; std::vector labels; @@ -1013,6 +1014,9 @@ int main(int argc, char* argv[]) { // List options list_cmd->add_flag("--downloaded", config.downloaded, "Save model options for future loads"); + list_cmd->add_option("name_filter", config.list_filter, + "Optional case-insensitive model-name filter; supports * wildcards") + ->type_name("NAME_FILTER"); // Backend management options backends_install_cmd->add_option("spec", config.backend_spec, "Backend spec (recipe:backend)")->required()->type_name("SPEC"); @@ -1151,7 +1155,7 @@ int main(int argc, char* argv[]) { } return client.status(config.port); } else if (list_cmd->count() > 0) { - return client.list_models(!config.downloaded); + return client.list_models(!config.downloaded, config.list_filter); } else if (pull_cmd->count() > 0) { if (config.model.empty()) { std::cerr << "Error: 'lemonade pull' requires a model name or Hugging Face checkpoint." << std::endl; diff --git a/src/cpp/include/lemon_cli/lemonade_client.h b/src/cpp/include/lemon_cli/lemonade_client.h index 2cf6e5496..6f2977304 100644 --- a/src/cpp/include/lemon_cli/lemonade_client.h +++ b/src/cpp/include/lemon_cli/lemonade_client.h @@ -75,7 +75,7 @@ class LemonadeClient { ~LemonadeClient(); // Model management commands - int list_models(bool show_all) const; + int list_models(bool show_all, const std::string& name_filter = "") const; int pull_model(const nlohmann::json& model_data); int delete_model(const std::string& model_name) const; int load_model(const std::string& model_name, const nlohmann::json& recipe_options, bool save_options = false) const; From 3b6897813c01dd07ff2ff5ca4b6789f1ec50bae6 Mon Sep 17 00:00:00 2001 From: Jeremy Fowers Date: Mon, 20 Apr 2026 15:51:47 -0400 Subject: [PATCH 2/2] polish --- src/cpp/cli/lemonade_client.cpp | 28 +++++++++------------------- src/cpp/cli/main.cpp | 2 +- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/src/cpp/cli/lemonade_client.cpp b/src/cpp/cli/lemonade_client.cpp index 1152e31f5..2d68b91c4 100644 --- a/src/cpp/cli/lemonade_client.cpp +++ b/src/cpp/cli/lemonade_client.cpp @@ -1,6 +1,7 @@ #include "lemon_cli/lemonade_client.h" #include #include +#include #include #include #include @@ -48,14 +49,6 @@ static std::regex build_name_filter_regex(const std::string& name_filter) { return std::regex(regex_pattern, std::regex_constants::ECMAScript | std::regex_constants::icase); } -static bool model_name_matches_filter(const std::string& model_name, const std::regex* name_filter_regex) { - if (name_filter_regex == nullptr) { - return true; - } - - return std::regex_search(model_name, *name_filter_regex); -} - HttpError::HttpError(int status, std::string body, const std::string& message) : std::runtime_error(message), status_code_(status), response_body_(std::move(body)) {} @@ -343,19 +336,16 @@ std::vector LemonadeClient::get_models(bool show_all) const { int LemonadeClient::list_models(bool show_all, const std::string& name_filter) const { try { std::vector models = get_models(show_all); - std::vector filtered_models; - filtered_models.reserve(models.size()); - const bool has_name_filter = !name_filter.empty(); - const std::regex name_filter_regex = has_name_filter ? build_name_filter_regex(name_filter) : std::regex(); - const std::regex* active_name_filter_regex = has_name_filter ? &name_filter_regex : nullptr; - for (const auto& model : models) { - if (model_name_matches_filter(model.id, active_name_filter_regex)) { - filtered_models.push_back(model); - } + if (!name_filter.empty()) { + const std::regex filter_regex = build_name_filter_regex(name_filter); + models.erase( + std::remove_if(models.begin(), models.end(), + [&](const ModelInfo& m) { return !std::regex_search(m.id, filter_regex); }), + models.end()); } - if (filtered_models.empty()) { + if (models.empty()) { std::cout << "No models available" << std::endl; return 0; } @@ -365,7 +355,7 @@ int LemonadeClient::list_models(bool show_all, const std::string& name_filter) c << "Details" << std::endl; std::cout << std::string(100, '-') << std::endl; - for (const auto& model : filtered_models) { + for (const auto& model : models) { std::string downloaded = model.downloaded ? "Yes" : "No"; std::string details = model.recipe.empty() ? "-" : model.recipe; diff --git a/src/cpp/cli/main.cpp b/src/cpp/cli/main.cpp index 5cabf9917..0d4c48990 100644 --- a/src/cpp/cli/main.cpp +++ b/src/cpp/cli/main.cpp @@ -1013,7 +1013,7 @@ int main(int argc, char* argv[]) { CLI::App* cleanup_cmd = app.add_subcommand("cleanup-cache", "Clean up orphaned files in HuggingFace cache")->group("Model management"); // List options - list_cmd->add_flag("--downloaded", config.downloaded, "Save model options for future loads"); + list_cmd->add_flag("--downloaded", config.downloaded, "Show only downloaded models"); list_cmd->add_option("name_filter", config.list_filter, "Optional case-insensitive model-name filter; supports * wildcards") ->type_name("NAME_FILTER");