From df1c0cddabdb5000500b9e48116c523290e4e143 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Sun, 12 Jul 2026 02:47:19 +0200 Subject: [PATCH 01/15] initial version --- src/capi_frontend/server_settings.hpp | 1 + src/cli_parser.cpp | 65 +++++++++----- src/cli_parser.hpp | 1 + src/config.cpp | 23 +++-- src/config.hpp | 2 +- src/config_export_module/config_export.cpp | 85 +++++++++++++------ .../embeddings_graph_cli_parser.cpp | 4 +- src/graph_export/graph_cli_parser.cpp | 4 +- .../image_generation_graph_cli_parser.cpp | 4 +- src/graph_export/rerank_graph_cli_parser.cpp | 4 +- src/graph_export/s2t_graph_cli_parser.cpp | 4 +- src/graph_export/t2s_graph_cli_parser.cpp | 12 ++- src/server.cpp | 16 ++++ 13 files changed, 155 insertions(+), 70 deletions(-) diff --git a/src/capi_frontend/server_settings.hpp b/src/capi_frontend/server_settings.hpp index 2dda86a6f4..39c269c9f8 100644 --- a/src/capi_frontend/server_settings.hpp +++ b/src/capi_frontend/server_settings.hpp @@ -91,6 +91,7 @@ enum OvmsServerMode : int { LIST_MODELS_MODE, MODIFY_CONFIG_MODE, IN_MEMORY_GRAPH_MODE, + CONFIGURE_MODE, UNKNOWN_MODE }; diff --git a/src/cli_parser.cpp b/src/cli_parser.cpp index e05109d905..887f6b0a37 100644 --- a/src/cli_parser.cpp +++ b/src/cli_parser.cpp @@ -57,7 +57,7 @@ std::variant> CLIParser::parse(int argc, char* std::stringstream ss; try { options = std::make_unique(argv[0], "OpenVINO Model Server"); - auto configOptions = std::make_unique("ovms --model_name --add_to_config --config_path --model_repository_path \n ovms --model_path --model_name --add_to_config --config_path \n ovms --remove_from_config --config_path --model_name ", "config management commands:"); + auto configOptions = std::make_unique("ovms --add_to_config --config_path --model_name --model_repository_path \n ovms --add_to_config --config_path --model_path --model_name \n ovms --remove_from_config --config_path --model_name ", "config management commands:"); // Adding this option to parse unrecognised options in another parser options->allow_unrecognised_options(); @@ -237,11 +237,7 @@ std::variant> CLIParser::parse(int argc, char* ("extra_quantization_params", "Model quantization parameters used in optimum-cli export with conversion for text generation models", cxxopts::value(), - "EXTRA_QUANTIZATION_PARAMS") - ("vocoder", - "The vocoder model to use for text2speech. For example microsoft/speecht5_hifigan", - cxxopts::value(), - "VOCODER"); + "EXTRA_QUANTIZATION_PARAMS"); options->add_options("single model") ("model_name", @@ -297,11 +293,15 @@ std::variant> CLIParser::parse(int argc, char* cxxopts::value(), "PLUGIN_CONFIG"); - options->add_options("generative task (applies to: pull hf model, single model)") + options->add_options("generative task (applies to: pull hf model, configure, single model)") ("task", - "Specifies the generative task for the local model. It should be followed by task specific parameters. Supported tasks: text_generation, embeddings, rerank, image_generation, text2speech, speech2text. It creates the pipeline graph in memory based on the provided task-specific options.", + "Specifies the generative task. Supported tasks: text_generation, embeddings, rerank, image_generation, text2speech, speech2text. Used with --pull to export graph after download, with --configure to create/update graph.pbtxt, or with --model_path to create graph in memory.", cxxopts::value(), - "TASK"); + "TASK") + ("configure", + "Create or update graph.pbtxt for the model specified by --model_path and --task. Does not start the server.", + cxxopts::value()->default_value("false"), + "CONFIGURE"); configOptions->custom_help(""); configOptions->add_options(CONFIG_MANAGEMENT_HELP_GROUP) ("list_models", @@ -416,6 +416,18 @@ std::variant> CLIParser::parse(int argc, char* ss << "error parsing options - --add_to_config cannot be used with --pull or --task" << std::endl; return std::make_pair(OVMS_EX_USAGE, ss.str()); } + if (result->count("configure") && !result->count("model_path")) { + ss << "error parsing options - --configure requires --model_path" << std::endl; + return std::make_pair(OVMS_EX_USAGE, ss.str()); + } + if (result->count("configure") && !result->count("task")) { + ss << "error parsing options - --configure requires --task" << std::endl; + return std::make_pair(OVMS_EX_USAGE, ss.str()); + } + if (result->count("configure") && result->count("pull")) { + ss << "error parsing options - --configure cannot be used with --pull" << std::endl; + return std::make_pair(OVMS_EX_USAGE, ss.str()); + } if (result->count("add_to_config") && result->count("list_models")) { ss << "error parsing options - --list_models cannot be used with --add_to_config" << std::endl; return std::make_pair(OVMS_EX_USAGE, ss.str()); @@ -445,8 +457,13 @@ std::variant> CLIParser::parse(int argc, char* } if (result->count("help") || result->arguments().size() == 0) { - ss << options->help({"", "multi model", "single model", "pull hf model"}) << std::endl; + ss << options->help({"", "multi model", "single model", "pull hf model", "generative task (applies to: pull hf model, configure, single model)"}) << std::endl; + ss << "configure mode (create or update graph.pbtxt for a local model):" << std::endl; + ss << " ovms --configure --model_path --task [TASK OPTIONS ...]" << std::endl; + ss << std::endl; ss << configOptions->help({CONFIG_MANAGEMENT_HELP_GROUP}) << std::endl; + // Print main options first, then task-specific graph options + std::cout << ss.str(); GraphCLIParser parser1; RerankGraphCLIParser parser2; EmbeddingsGraphCLIParser parser3; @@ -457,7 +474,9 @@ std::variant> CLIParser::parse(int argc, char* parser2.printHelp(); parser3.printHelp(); imageGenParser.printHelp(); - return std::make_pair(OVMS_EX_OK, ss.str()); + ttsParser.printHelp(); + sttParser.printHelp(); + return std::make_pair(OVMS_EX_OK, ""); } return true; @@ -605,7 +624,7 @@ void CLIParser::prepareModel(ModelsSettingsImpl& modelsSettings, HFSettingsImpl& } if (result->count("mean")) { - if (modelsSettings.layout.empty()) { + if (modelsSettings.layout.empty() && !result->count("add_to_config")) { throw std::logic_error("error parsing options - --mean parameter requires --layout to be set"); } modelsSettings.mean = result->operator[]("mean").as(); @@ -613,7 +632,7 @@ void CLIParser::prepareModel(ModelsSettingsImpl& modelsSettings, HFSettingsImpl& } if (result->count("scale")) { - if (modelsSettings.layout.empty()) { + if (modelsSettings.layout.empty() && !result->count("add_to_config")) { throw std::logic_error("error parsing options - --scale parameter requires --layout to be set"); } modelsSettings.scale = result->operator[]("scale").as(); @@ -621,7 +640,7 @@ void CLIParser::prepareModel(ModelsSettingsImpl& modelsSettings, HFSettingsImpl& } if (result->count("color_format")) { - if (modelsSettings.layout.empty()) { + if (modelsSettings.layout.empty() && !result->count("add_to_config")) { throw std::logic_error("error parsing options - --color_format parameter requires --layout to be set"); } modelsSettings.colorFormat = result->operator[]("color_format").as(); @@ -629,7 +648,7 @@ void CLIParser::prepareModel(ModelsSettingsImpl& modelsSettings, HFSettingsImpl& } if (result->count("precision")) { - if (modelsSettings.layout.empty()) { + if (modelsSettings.layout.empty() && !result->count("add_to_config")) { throw std::logic_error("error parsing options - --precision parameter requires --layout to be set"); } modelsSettings.precision = result->operator[]("precision").as(); @@ -670,12 +689,16 @@ bool CLIParser::isHFPullOrPullAndStart(const std::unique_ptrcount("pull") || result->count("task")); } bool CLIParser::isInMemoryGraphMode(const std::unique_ptr& result) { - return (result->count("task") && !result->count("source_model") && !result->count("pull")); + return (result->count("task") && !result->count("source_model") && !result->count("pull") && !result->count("configure")); +} + +bool CLIParser::isConfigureMode(const std::unique_ptr& result) { + return (result->count("configure") && result->count("task")); } void CLIParser::prepareGraph(ServerSettingsImpl& serverSettings, HFSettingsImpl& hfSettings, const std::string& modelName) { @@ -683,9 +706,11 @@ void CLIParser::prepareGraph(ServerSettingsImpl& serverSettings, HFSettingsImpl& if (result->count("source_model")) { hfSettings.sourceModel = result->operator[]("source_model").as(); } - // Ovms Pull models mode || pull and start models mode + // Ovms Pull models mode || pull and start models mode || configure mode if (isHFPullOrPullAndStart(this->result) || isInMemoryGraphMode(this->result)) { - if (isInMemoryGraphMode(this->result)) { + if (isConfigureMode(this->result)) { + serverSettings.serverMode = CONFIGURE_MODE; + } else if (isInMemoryGraphMode(this->result)) { serverSettings.serverMode = IN_MEMORY_GRAPH_MODE; } else if (result->count("pull")) { serverSettings.serverMode = HF_PULL_MODE; @@ -724,8 +749,6 @@ void CLIParser::prepareGraph(ServerSettingsImpl& serverSettings, HFSettingsImpl& hfSettings.exportSettings.precision = result->operator[]("weight-format").as(); if (result->count("extra_quantization_params")) hfSettings.exportSettings.extraQuantizationParams = result->operator[]("extra_quantization_params").as(); - if (result->count("vocoder")) - hfSettings.exportSettings.vocoder = result->operator[]("vocoder").as(); hfSettings.exportSettings.restWorkers = serverSettings.restWorkers; hfSettings.downloadPath = result->operator[]("model_repository_path").as(); // When --task is used with --model_path but without --pull/--source_model, diff --git a/src/cli_parser.hpp b/src/cli_parser.hpp index 11731d856c..fcef0aa4bf 100644 --- a/src/cli_parser.hpp +++ b/src/cli_parser.hpp @@ -52,6 +52,7 @@ class CLIParser { void prepareConfigExport(ModelsSettingsImpl& modelsSettings); bool isHFPullOrPullAndStart(const std::unique_ptr& result); bool isInMemoryGraphMode(const std::unique_ptr& result); + bool isConfigureMode(const std::unique_ptr& result); }; } // namespace ovms diff --git a/src/config.cpp b/src/config.cpp index aa888aa4de..9e1e8d11f2 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -137,8 +137,13 @@ bool Config::check_hostname_or_ip(const std::string& input) { } } -bool Config::validateUserSettingsInConfigAddRemoveModel(const ModelsSettingsImpl& modelsSettings) { - static const std::vector allowedUserSettings = {"model_name", "model_path", "config_path"}; +bool Config::validateUserSettingsInConfigAddRemoveModel(const ModelsSettingsImpl& modelsSettings, ConfigExportType exportType) { + static const std::vector allowedForRemove = {"model_name", "config_path"}; + static const std::vector allowedForAdd = {"model_name", "model_path", "config_path", + "batch_size", "shape", "layout", "mean", "scale", "color_format", "precision", + "model_version_policy", "nireq", "target_device", "plugin_config"}; + + const auto& allowedUserSettings = (exportType == ENABLE_MODEL) ? allowedForAdd : allowedForRemove; std::vector usedButDisallowedUserSettings; for (const std::string& userSetting : modelsSettings.userSetSingleModelArguments) { bool isAllowed = false; @@ -156,7 +161,11 @@ bool Config::validateUserSettingsInConfigAddRemoveModel(const ModelsSettingsImpl for (const std::string& userSetting : usedButDisallowedUserSettings) { arguments += userSetting + ", "; } - std::cerr << "Adding or removing models from the configuration file, allows passing only model_name and model_path parameters. Invalid parameters passed: " << arguments << std::endl; + if (exportType == ENABLE_MODEL) { + std::cerr << "Adding models to the configuration file does not support parameters: " << arguments << std::endl; + } else { + std::cerr << "Removing models from the configuration file allows passing only model_name parameter. Invalid parameters passed: " << arguments << std::endl; + } return false; } @@ -169,9 +178,9 @@ bool Config::validate() { std::cerr << "--source_model should be used combined with --task" << std::endl; return false; } - if (this->serverSettings.serverMode == HF_PULL_MODE || this->serverSettings.serverMode == HF_PULL_AND_START_MODE || this->serverSettings.serverMode == IN_MEMORY_GRAPH_MODE) { + if (this->serverSettings.serverMode == HF_PULL_MODE || this->serverSettings.serverMode == HF_PULL_AND_START_MODE || this->serverSettings.serverMode == IN_MEMORY_GRAPH_MODE || this->serverSettings.serverMode == CONFIGURE_MODE) { // When --task is used with --model_path (no HF pulling), sourceModel and downloadPath are not required - bool taskWithModelPath = this->serverSettings.serverMode == IN_MEMORY_GRAPH_MODE && !this->modelsSettings.modelPath.empty(); + bool taskWithModelPath = (this->serverSettings.serverMode == IN_MEMORY_GRAPH_MODE || this->serverSettings.serverMode == CONFIGURE_MODE) && !this->modelsSettings.modelPath.empty(); if (!taskWithModelPath) { if (!serverSettings.hfSettings.sourceModel.size()) { std::cerr << "source_model parameter is required for pull mode"; @@ -245,7 +254,7 @@ bool Config::validate() { } } // No more validation needed - if (this->serverSettings.serverMode == HF_PULL_MODE) { + if (this->serverSettings.serverMode == HF_PULL_MODE || this->serverSettings.serverMode == CONFIGURE_MODE) { return true; } } @@ -313,7 +322,7 @@ bool Config::validate() { return false; } - if (!Config::validateUserSettingsInConfigAddRemoveModel(this->modelsSettings)) + if (!Config::validateUserSettingsInConfigAddRemoveModel(this->modelsSettings, this->serverSettings.exportConfigType)) return false; } diff --git a/src/config.hpp b/src/config.hpp index 881cccf7f3..d710bc4e9a 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -78,7 +78,7 @@ class Config { * * @return bool */ - static bool validateUserSettingsInConfigAddRemoveModel(const ModelsSettingsImpl& modelsSettings); + static bool validateUserSettingsInConfigAddRemoveModel(const ModelsSettingsImpl& modelsSettings, ConfigExportType exportType); /** * @brief Validate passed arguments * diff --git a/src/config_export_module/config_export.cpp b/src/config_export_module/config_export.cpp index eed6eba16a..522ede2332 100644 --- a/src/config_export_module/config_export.cpp +++ b/src/config_export_module/config_export.cpp @@ -32,6 +32,41 @@ namespace ovms { +static void addJsonOrStringMember(rapidjson::Value& obj, const char* key, const std::string& value, rapidjson::Document::AllocatorType& alloc) { + rapidjson::Document parsed(&alloc); + if (!parsed.Parse(value.c_str()).HasParseError() && parsed.IsObject()) { + rapidjson::Value copy(parsed, alloc); + obj.AddMember(rapidjson::Value(key, alloc), copy, alloc); + } else { + obj.AddMember(rapidjson::Value(key, alloc), rapidjson::Value(value.c_str(), alloc), alloc); + } +} + +static void addOptionalModelFields(rapidjson::Value& configObj, const ModelsSettingsImpl& modelSettings, rapidjson::Document::AllocatorType& alloc) { + if (!modelSettings.batchSize.empty()) + configObj.AddMember("batch_size", rapidjson::Value(modelSettings.batchSize.c_str(), alloc), alloc); + if (!modelSettings.shape.empty()) + addJsonOrStringMember(configObj, "shape", modelSettings.shape, alloc); + if (!modelSettings.layout.empty()) + addJsonOrStringMember(configObj, "layout", modelSettings.layout, alloc); + if (modelSettings.mean.has_value()) + configObj.AddMember("mean", rapidjson::Value(modelSettings.mean.value().c_str(), alloc), alloc); + if (modelSettings.scale.has_value()) + configObj.AddMember("scale", rapidjson::Value(modelSettings.scale.value().c_str(), alloc), alloc); + if (modelSettings.colorFormat.has_value()) + configObj.AddMember("color_format", rapidjson::Value(modelSettings.colorFormat.value().c_str(), alloc), alloc); + if (modelSettings.precision.has_value()) + configObj.AddMember("precision", rapidjson::Value(modelSettings.precision.value().c_str(), alloc), alloc); + if (!modelSettings.modelVersionPolicy.empty()) + addJsonOrStringMember(configObj, "model_version_policy", modelSettings.modelVersionPolicy, alloc); + if (modelSettings.nireq != 0) + configObj.AddMember("nireq", modelSettings.nireq, alloc); + if (!modelSettings.targetDevice.empty()) + configObj.AddMember("target_device", rapidjson::Value(modelSettings.targetDevice.c_str(), alloc), alloc); + if (!modelSettings.pluginConfig.empty()) + addJsonOrStringMember(configObj, "plugin_config", modelSettings.pluginConfig, alloc); +} + Status loadJsonConfig(const std::string& jsonFilename, rapidjson::Document& configJson) { std::string md5; Status status = parseConfig(jsonFilename, configJson, md5); @@ -49,34 +84,27 @@ Status loadJsonConfig(const std::string& jsonFilename, rapidjson::Document& conf } Status createModelConfig(const std::string& fullPath, const ModelsSettingsImpl& modelSettings) { - std::ostringstream oss; - - auto escapeBackslashes = [](const std::string& path) -> std::string { - std::string result; - result.reserve(path.size()); - for (char c : path) { - if (c == '\\') { - result += "\\\\"; - } else { - result += c; - } - } - return result; - }; - - // clang-format off - oss << R"({ - "model_config_list": [ - { - "config": { - "name": ")" << modelSettings.modelName << R"(", - "base_path": ")" << escapeBackslashes(modelSettings.modelPath) << R"(" - } - } - ] -})"; - // clang-format on - return FileSystem::createFileOverwrite(fullPath, oss.str()); + rapidjson::Document configJson; + configJson.SetObject(); + auto& alloc = configJson.GetAllocator(); + + rapidjson::Value modelConfigList(rapidjson::kArrayType); + rapidjson::Value configItem(rapidjson::kObjectType); + rapidjson::Value configObj(rapidjson::kObjectType); + + configObj.AddMember("name", rapidjson::Value(modelSettings.modelName.c_str(), alloc), alloc); + configObj.AddMember("base_path", rapidjson::Value(modelSettings.modelPath.c_str(), alloc), alloc); + addOptionalModelFields(configObj, modelSettings, alloc); + + configItem.AddMember("config", configObj, alloc); + modelConfigList.PushBack(configItem, alloc); + configJson.AddMember("model_config_list", modelConfigList, alloc); + + rapidjson::StringBuffer buffer; + rapidjson::PrettyWriter writer(buffer); + configJson.Accept(writer); + + return FileSystem::createFileOverwrite(fullPath, buffer.GetString()); } Status removeModelFromConfig(const std::string& fullPath, const ModelsSettingsImpl& modelSettings) { @@ -157,6 +185,7 @@ Status updateConfigAddModel(const std::string& fullPath, const ModelsSettingsImp rapidjson::Value path; path.SetString(modelSettings.modelPath.c_str(), alloc); newConfig.AddMember("base_path", path, alloc); + addOptionalModelFields(newConfig, modelSettings, alloc); rapidjson::Value newConfigItem; newConfigItem.SetObject(); diff --git a/src/graph_export/embeddings_graph_cli_parser.cpp b/src/graph_export/embeddings_graph_cli_parser.cpp index 192dd6c748..12fcc1e71c 100644 --- a/src/graph_export/embeddings_graph_cli_parser.cpp +++ b/src/graph_export/embeddings_graph_cli_parser.cpp @@ -35,7 +35,7 @@ EmbeddingsGraphSettingsImpl& EmbeddingsGraphCLIParser::defaultGraphSettings() { } void EmbeddingsGraphCLIParser::createOptions() { - this->options = std::make_unique("ovms --pull [PULL OPTIONS ... ]", "-pull --task embeddings graph options"); + this->options = std::make_unique("ovms --pull --task embeddings [OPTIONS...]\n ovms --configure --model_path --task embeddings [OPTIONS...]", "--task embeddings options"); options->allow_unrecognised_options(); // clang-format off @@ -89,7 +89,7 @@ void EmbeddingsGraphCLIParser::prepare(OvmsServerMode serverMode, HFSettingsImpl } if (nullptr == result) { // Pull with default arguments - no arguments from user - if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE) { + if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE && serverMode != CONFIGURE_MODE) { throw std::logic_error("Tried to prepare server and model settings without graph parse result"); } } else { diff --git a/src/graph_export/graph_cli_parser.cpp b/src/graph_export/graph_cli_parser.cpp index 2a8e281c94..dd838fe265 100644 --- a/src/graph_export/graph_cli_parser.cpp +++ b/src/graph_export/graph_cli_parser.cpp @@ -36,7 +36,7 @@ TextGenGraphSettingsImpl& GraphCLIParser::defaultGraphSettings() { } void GraphCLIParser::createOptions() { - this->options = std::make_unique("ovms --pull [PULL OPTIONS ... ]", "--pull --task text_generation graph options"); + this->options = std::make_unique("ovms --pull --task text_generation [OPTIONS...]\n ovms --configure --model_path --task text_generation [OPTIONS...]", "--task text_generation options"); options->allow_unrecognised_options(); // clang-format off @@ -134,7 +134,7 @@ void GraphCLIParser::prepare(OvmsServerMode serverMode, HFSettingsImpl& hfSettin if (nullptr == result) { // Pull with default arguments - no arguments from user - if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE) { + if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE && serverMode != CONFIGURE_MODE) { throw std::logic_error("Tried to prepare server and model settings without graph parse result"); } } else { diff --git a/src/graph_export/image_generation_graph_cli_parser.cpp b/src/graph_export/image_generation_graph_cli_parser.cpp index 4f32140632..5b5177f432 100644 --- a/src/graph_export/image_generation_graph_cli_parser.cpp +++ b/src/graph_export/image_generation_graph_cli_parser.cpp @@ -76,7 +76,7 @@ ImageGenerationGraphSettingsImpl& ImageGenerationGraphCLIParser::defaultGraphSet } void ImageGenerationGraphCLIParser::createOptions() { - this->options = std::make_unique("ovms --pull [PULL OPTIONS ... ]", "--pull --task image generation/edit/inpainting graph options"); + this->options = std::make_unique("ovms --pull --task image_generation [OPTIONS...]\n ovms --configure --model_path --task image_generation [OPTIONS...]", "--task image_generation options"); options->allow_unrecognised_options(); // clang-format off @@ -150,7 +150,7 @@ void ImageGenerationGraphCLIParser::prepare(ServerSettingsImpl& serverSettings, } if (nullptr == result) { // Pull with default arguments - no arguments from user - if (serverSettings.serverMode != HF_PULL_MODE && serverSettings.serverMode != HF_PULL_AND_START_MODE) { + if (serverSettings.serverMode != HF_PULL_MODE && serverSettings.serverMode != HF_PULL_AND_START_MODE && serverSettings.serverMode != CONFIGURE_MODE) { throw std::logic_error("Tried to prepare server and model settings without graph parse result"); } } else { diff --git a/src/graph_export/rerank_graph_cli_parser.cpp b/src/graph_export/rerank_graph_cli_parser.cpp index 80f1561a4a..ea79a31e72 100644 --- a/src/graph_export/rerank_graph_cli_parser.cpp +++ b/src/graph_export/rerank_graph_cli_parser.cpp @@ -35,7 +35,7 @@ RerankGraphSettingsImpl& RerankGraphCLIParser::defaultGraphSettings() { } void RerankGraphCLIParser::createOptions() { - this->options = std::make_unique("ovms --pull [PULL OPTIONS ... ]", "-pull --task rerank graph options"); + this->options = std::make_unique("ovms --pull --task rerank [OPTIONS...]\n ovms --configure --model_path --task rerank [OPTIONS...]", "--task rerank options"); options->allow_unrecognised_options(); // clang-format off @@ -82,7 +82,7 @@ void RerankGraphCLIParser::prepare(OvmsServerMode serverMode, HFSettingsImpl& hf } if (nullptr == result) { // Pull with default arguments - no arguments from user - if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE) { + if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE && serverMode != CONFIGURE_MODE) { throw std::logic_error("Tried to prepare server and model settings without graph parse result"); } } else { diff --git a/src/graph_export/s2t_graph_cli_parser.cpp b/src/graph_export/s2t_graph_cli_parser.cpp index c516581468..6ae7835f91 100644 --- a/src/graph_export/s2t_graph_cli_parser.cpp +++ b/src/graph_export/s2t_graph_cli_parser.cpp @@ -35,7 +35,7 @@ SpeechToTextGraphSettingsImpl& SpeechToTextGraphCLIParser::defaultGraphSettings( } void SpeechToTextGraphCLIParser::createOptions() { - this->options = std::make_unique("ovms --pull [PULL OPTIONS ... ]", "-pull --task speech2text graph options"); + this->options = std::make_unique("ovms --pull --task speech2text [OPTIONS...]\n ovms --configure --model_path --task speech2text [OPTIONS...]", "--task speech2text options"); options->allow_unrecognised_options(); // clang-format off @@ -77,7 +77,7 @@ void SpeechToTextGraphCLIParser::prepare(OvmsServerMode serverMode, HFSettingsIm } if (nullptr == result) { // Pull with default arguments - no arguments from user - if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE) { + if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE && serverMode != CONFIGURE_MODE) { throw std::logic_error("Tried to prepare server and model settings without graph parse result"); } } else { diff --git a/src/graph_export/t2s_graph_cli_parser.cpp b/src/graph_export/t2s_graph_cli_parser.cpp index 61ac98336d..3cce1a2d0d 100644 --- a/src/graph_export/t2s_graph_cli_parser.cpp +++ b/src/graph_export/t2s_graph_cli_parser.cpp @@ -35,7 +35,7 @@ TextToSpeechGraphSettingsImpl& TextToSpeechGraphCLIParser::defaultGraphSettings( } void TextToSpeechGraphCLIParser::createOptions() { - this->options = std::make_unique("ovms --pull [PULL OPTIONS ... ]", "-pull --task text2speech graph options"); + this->options = std::make_unique("ovms --pull --task text2speech [OPTIONS...]\n ovms --configure --model_path --task text2speech [OPTIONS...]", "--task text2speech options"); options->allow_unrecognised_options(); // clang-format off @@ -47,7 +47,11 @@ void TextToSpeechGraphCLIParser::createOptions() { ("model_type", "Type of the source TTS model: speecht5 (default) or kokoro.", cxxopts::value()->default_value("speecht5"), - "MODEL_TYPE"); + "MODEL_TYPE") + ("vocoder", + "The vocoder model to use for text2speech. For example microsoft/speecht5_hifigan", + cxxopts::value(), + "VOCODER"); // clang-format on } @@ -82,7 +86,7 @@ void TextToSpeechGraphCLIParser::prepare(OvmsServerMode serverMode, HFSettingsIm } if (nullptr == result) { // Pull with default arguments - no arguments from user - if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE) { + if (serverMode != HF_PULL_MODE && serverMode != HF_PULL_AND_START_MODE && serverMode != CONFIGURE_MODE) { throw std::logic_error("Tried to prepare server and model settings without graph parse result"); } } else { @@ -92,6 +96,8 @@ void TextToSpeechGraphCLIParser::prepare(OvmsServerMode serverMode, HFSettingsIm throw std::invalid_argument("--model_type must be one of: speecht5, kokoro"); } hfSettings.exportSettings.modelType = modelType; + if (result->count("vocoder")) + hfSettings.exportSettings.vocoder = result->operator[]("vocoder").as(); } hfSettings.graphSettings = std::move(textToSpeechGraphSettings); } diff --git a/src/server.cpp b/src/server.cpp index d182d6feb5..a09f81f66d 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -127,6 +127,10 @@ static void logConfig(const Config& config) { SPDLOG_DEBUG("model_repository_path: {}", config.getServerSettings().hfSettings.downloadPath); return; } + if (config.getServerSettings().serverMode == CONFIGURE_MODE) { + SPDLOG_DEBUG("model_path: {}", config.modelPath()); + return; + } if (config.configPath().empty()) { SPDLOG_DEBUG("model_path: {}", config.modelPath()); SPDLOG_DEBUG("model_name: {}", config.modelName()); @@ -408,6 +412,18 @@ Status Server::startModules(ovms::Config& config) { status = hfModule->clone(); return status; } + if (config.getServerSettings().serverMode == CONFIGURE_MODE) { + GraphExport graphExporter; + const auto& hfSettings = config.getServerSettings().hfSettings; + std::string modelPath = config.modelPath(); + status = graphExporter.createServableConfig(modelPath, hfSettings, true); + if (!status.ok()) { + SPDLOG_ERROR("Failed to create graph config: {}", status.string()); + return status; + } + std::cout << "Graph: graph.pbtxt created in: " << modelPath << std::endl; + return status; + } #if (PYTHON_DISABLE == 0) if (config.getServerSettings().withPython) { From 17e38254a335f69a2c81de80961fe87b4107937d Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Sun, 12 Jul 2026 02:54:57 +0200 Subject: [PATCH 02/15] docs --- docs/parameters.md | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/docs/parameters.md b/docs/parameters.md index 102816dba4..60e9ef1edf 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -68,10 +68,29 @@ Configuration options for the config management mode, which is used to manage co | `list_models` | `NA` | List all models paths in the model repository. | | `model_name` | `string` | Name of the model as visible in serving. If `--model_path` is not provided, path is deduced from name. | | `model_path` | `string` | Optional. Path to the model repository. If path is relative then it is prefixed with `--model_repository_path`. | -| `add_to_config` | `NA` | Directive to add new model to the config file. | +| `add_to_config` | `NA` | Directive to add new model to the config file. Accepts optional model parameters: `--batch_size`, `--shape`, `--layout`, `--mean`, `--scale`, `--color_format`, `--precision`, `--model_version_policy`, `--nireq`, `--target_device`, `--plugin_config`. | | `remove_from_config` | `NA` | Directive to remove model from the config file. | | `config_path` | `string` | Path to the configuration file. | +## Configure mode options + +Configure mode creates or updates `graph.pbtxt` for a local model without starting the server. It requires `--model_path` and `--task` parameters along with task-specific options. + +| Option | Value format | Description | +|-------------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------------| +| `--configure` | `NA` | Runs in configure mode to create or update `graph.pbtxt` for a local model. Does not start the server. | +| `--model_path` | `string` | Path to the local model directory where `graph.pbtxt` will be created. | +| `--model_name` | `string` | Optional. Name of the model as exposed by the server. | +| `--task` | `string` | Task type for the model (`text_generation`, `embeddings`, `rerank`, `image_generation`, `text2speech`, `speech2text`). | +| `--target_device` | `string` | Device name to be used to execute inference operations. Accepted values are: `"CPU"/"GPU"/"NPU"/"MULTI"/"HETERO"`. | + +Task-specific options (e.g., `--max_num_seqs`, `--cache_size`, `--num_streams`) are the same as documented in the [pull mode task options](#text-generation) below. + +**Example:** +```bash +ovms --configure --model_path /models/my_llm --task text_generation --target_device GPU --max_num_seqs 128 --cache_size 8 +``` + ## Pull mode configuration options Shared configuration options for the pull, and pull & start mode. In the presence of ```--pull``` parameter OVMS will only pull model without serving. @@ -85,7 +104,7 @@ Shared configuration options for the pull, and pull & start mode. In the presenc | `--model_repository_path` | `string` | Directory where all required model files will be saved. | | `--model_name` | `string` | Name of the model as exposed externally by the server. | | `--target_device` | `string` | Device name to be used to execute inference operations. Accepted values are: `"CPU"/"GPU"/"MULTI"/"HETERO"` | -| `--task` | `string` | Task type the model will support (`text_generation`, `embeddings`, `rerank`, `image_generation`). | +| `--task` | `string` | Task type the model will support (`text_generation`, `embeddings`, `rerank`, `image_generation`, `text2speech`, `speech2text`). | | `--overwrite_models` | `NA` | If set, an existing model with the same name will be overwritten. If not set, the server will use existing model files if available. | | `--gguf_filename` | `string` | Filename of the wanted quantization type from Hugging Face GGUF repository. | From 2910c3361bd051da6532c9e5b707134bd3f7455c Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Sun, 12 Jul 2026 23:15:39 +0200 Subject: [PATCH 03/15] style --- src/config_export_module/config_export.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config_export_module/config_export.cpp b/src/config_export_module/config_export.cpp index 522ede2332..384a37fd4f 100644 --- a/src/config_export_module/config_export.cpp +++ b/src/config_export_module/config_export.cpp @@ -35,8 +35,8 @@ namespace ovms { static void addJsonOrStringMember(rapidjson::Value& obj, const char* key, const std::string& value, rapidjson::Document::AllocatorType& alloc) { rapidjson::Document parsed(&alloc); if (!parsed.Parse(value.c_str()).HasParseError() && parsed.IsObject()) { - rapidjson::Value copy(parsed, alloc); - obj.AddMember(rapidjson::Value(key, alloc), copy, alloc); + rapidjson::Value jsonValue(parsed, alloc); + obj.AddMember(rapidjson::Value(key, alloc), jsonValue, alloc); } else { obj.AddMember(rapidjson::Value(key, alloc), rapidjson::Value(value.c_str(), alloc), alloc); } From a21302abad0ed0fa7708f811f3b358ce12bfd8b1 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Thu, 16 Jul 2026 15:40:36 +0200 Subject: [PATCH 04/15] fix tests --- src/test/config_export_full_test.cpp | 2 +- src/test/config_export_test.cpp | 4 ++-- src/test/ovmsconfig_test.cpp | 16 ---------------- 3 files changed, 3 insertions(+), 19 deletions(-) diff --git a/src/test/config_export_full_test.cpp b/src/test/config_export_full_test.cpp index 3fb363db51..248b8bc2b8 100644 --- a/src/test/config_export_full_test.cpp +++ b/src/test/config_export_full_test.cpp @@ -34,7 +34,7 @@ namespace { const std::string expectedConfigContents = R"({ "model_config_list": [ - { + { "config": { "name": "model1", "base_path": "/model1/Path" diff --git a/src/test/config_export_test.cpp b/src/test/config_export_test.cpp index 20d6af46da..fcc72371b5 100644 --- a/src/test/config_export_test.cpp +++ b/src/test/config_export_test.cpp @@ -31,7 +31,7 @@ const std::string expectedConfigContents = R"({ "model_config_list": [ - { + { "config": { "name": "model1", "base_path": "/model1/Path" @@ -43,7 +43,7 @@ const std::string expectedConfigContents = R"({ const std::string expectedConfigContentsWindows = R"({ "model_config_list": [ - { + { "config": { "name": "model1", "base_path": "model1\\Path" diff --git a/src/test/ovmsconfig_test.cpp b/src/test/ovmsconfig_test.cpp index 10674798af..a660c9f0eb 100644 --- a/src/test/ovmsconfig_test.cpp +++ b/src/test/ovmsconfig_test.cpp @@ -792,22 +792,6 @@ TEST_F(OvmsConfigDeathTest, modifyModelConfigEnableButMissingModelPath) { EXPECT_EXIT(ovms::Config::instance().parse(arg_count, n_argv), ::testing::ExitedWithCode(OVMS_EX_USAGE), "Set model_name either with model_path or model_repository_path with add_to_config"); } -TEST_F(OvmsConfigDeathTest, modifyModelConfigEnableWithBadAdditionalParameters) { - char* n_argv[] = { - "ovms", - "--model_name", - "name", - "--add_to_config", - "--config_path", - "/config/path", - "--target_device", - "GPU", - "--model_path", - "/model/path"}; - int arg_count = 10; - EXPECT_EXIT(ovms::Config::instance().parse(arg_count, n_argv), ::testing::ExitedWithCode(OVMS_EX_USAGE), "Adding or removing models from the configuration file, allows passing only model_name and model_path parameters. Invalid parameters passed: target_device,"); -} - TEST_F(OvmsConfigDeathTest, modifyModelConfigDisableMissingModelName) { char* n_argv[] = { "ovms", From b589fed58139a2ea3a88c8104ed58942437402a8 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Thu, 16 Jul 2026 16:54:13 +0200 Subject: [PATCH 05/15] more tests --- src/server.cpp | 3 +- src/test/config_export_full_test.cpp | 70 ++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/server.cpp b/src/server.cpp index 5159338368..a896fe052a 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -412,8 +412,9 @@ Status Server::startModules(ovms::Config& config) { } if (config.getServerSettings().serverMode == CONFIGURE_MODE) { GraphExport graphExporter; - const auto& hfSettings = config.getServerSettings().hfSettings; + HFSettingsImpl hfSettings = config.getServerSettings().hfSettings; std::string modelPath = config.modelPath(); + hfSettings.exportSettings.modelPath = "."; status = graphExporter.createServableConfig(modelPath, hfSettings, true); if (!status.ok()) { SPDLOG_ERROR("Failed to create graph config: {}", status.string()); diff --git a/src/test/config_export_full_test.cpp b/src/test/config_export_full_test.cpp index 248b8bc2b8..8c857bbdca 100644 --- a/src/test/config_export_full_test.cpp +++ b/src/test/config_export_full_test.cpp @@ -133,3 +133,73 @@ TEST_F(ConfigCreationFullTest, positiveEndToEndEnableDisable) { configContents = GetFileContents(this->modelsSettings.configPath); ASSERT_EQ(expectedEmptyConfigContents, configContents) << configContents; } + +TEST_F(ConfigCreationFullTest, positiveConfigureGraphForLLMModel) { + ovms::Server& server = ovms::Server::instance(); + server.setShutdownRequest(0); + + std::string modelPath = this->directoryPath; + char* argv[] = { + (char*)"ovms", + (char*)"--configure", + (char*)"--model_path", + (char*)modelPath.c_str(), + (char*)"--task", + (char*)"text_generation", + (char*)"--target_device", + (char*)"CPU", + (char*)"--cache_size", + (char*)"1", + }; + int argc = 10; + + ASSERT_EQ(EXIT_SUCCESS, server.start(argc, argv)); + + std::string graphPath = ovms::FileSystem::appendSlash(modelPath) + "graph.pbtxt"; + std::string graphContents = GetFileContents(graphPath); + ASSERT_FALSE(graphContents.empty()) << "graph.pbtxt should be created"; + EXPECT_NE(std::string::npos, graphContents.find("models_path: \".\"")) << graphContents; + EXPECT_NE(std::string::npos, graphContents.find("device: \"CPU\"")) << graphContents; + EXPECT_NE(std::string::npos, graphContents.find("cache_size: 1,")) << graphContents; +} + +TEST_F(ConfigCreationFullTest, positiveAddToConfigWithBatchSize) { + ovms::Server& server = ovms::Server::instance(); + std::unique_ptr t; + server.setShutdownRequest(0); + + std::string dummyModelPath = getGenericFullPathForSrcTest("/ovms/src/test/dummy"); + std::string modelName = "dummy"; + std::string batchSize = "10"; + char* argv[] = { + (char*)"ovms", + (char*)"--add_to_config", + (char*)"--config_path", + (char*)this->modelsSettings.configPath.c_str(), + (char*)"--model_name", + (char*)modelName.c_str(), + (char*)"--model_path", + (char*)dummyModelPath.c_str(), + (char*)"--batch_size", + (char*)batchSize.c_str(), + }; + int argc = 10; + + t.reset(new std::thread([&argc, &argv, &server]() { + ASSERT_EQ(EXIT_SUCCESS, server.start(argc, argv)); + })); + + auto start = std::chrono::high_resolution_clock::now(); + while ((server.getModuleState(ovms::SERVABLES_CONFIG_MANAGER_MODULE_NAME) != ovms::ModuleState::NOT_INITIALIZED) && + (std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() < 3)) { + } + + ASSERT_EQ(server.getModuleState(ovms::SERVABLES_CONFIG_MANAGER_MODULE_NAME), ovms::ModuleState::NOT_INITIALIZED); + server.setShutdownRequest(1); + t->join(); + + std::string configContents = GetFileContents(this->modelsSettings.configPath); + EXPECT_NE(std::string::npos, configContents.find("\"name\": \"dummy\"")) << configContents; + EXPECT_NE(std::string::npos, configContents.find("\"base_path\": \"" + dummyModelPath + "\"")) << configContents; + EXPECT_NE(std::string::npos, configContents.find("\"batch_size\": \"10\"")) << configContents; +} From e6162b4408fe112919c394d6ad6c0ee579988719 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 00:27:17 +0200 Subject: [PATCH 06/15] fix documentation --- docs/parameters.md | 24 ++++++++++++++++++++---- src/pull_module/hf_pull_model_module.cpp | 2 +- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/docs/parameters.md b/docs/parameters.md index 0a83218ae3..b20c93fc9c 100644 --- a/docs/parameters.md +++ b/docs/parameters.md @@ -57,6 +57,7 @@ Configuration options for the server are defined only via command-line options a | `api_key_file` | `string` | Path to the text file with the API key for generative endpoints `/v3/`. The value of first line is used. If not specified, server is using environment variable API_KEY. If not set, requests will not require authorization.| | `allowed_local_media_path` | `string` | Path to the directory containing images to include in requests. If unset, local filesystem images in requests are not supported.| | `allowed_media_domains` | `string` | Comma separated list of media domains from which URLs can be used as input for LLMs. Set to \"all\" to disable this restrictions. If unset, URLs in requests are not supported." +| `verbose_response` | `NA` | When enabled, responses include an extra `__verbose` object with additional debug information. Applies for text generation models | ## Config management mode options @@ -87,7 +88,7 @@ Configure mode creates or updates `graph.pbtxt` for a local model without starti Task-specific options (e.g., `--max_num_seqs`, `--cache_size`, `--num_streams`) are the same as documented in the [pull mode task options](#text-generation) below. **Example:** -```bash +```text ovms --configure --model_path /models/my_llm --task text_generation --target_device GPU --max_num_seqs 128 --cache_size 8 ``` @@ -135,10 +136,9 @@ There are also additional environment variables that may change the behavior of |-------------------------------------|---------|------------------------------------------------------------------------------------------------------------| | `GIT_OPT_SET_SERVER_CONNECT_TIMEOUT`| `int` | Timeout to attempt connections to a remote server. Default value 4000 ms. | | `GIT_OPT_SET_SERVER_TIMEOUT` | `int` | Timeout for reading from and writing to a remote server. Default value 4000 ms. | -| `GIT_OPT_SET_SSL_CERT_LOCATIONS` | `string`| Path to check for ssl certificates. | -| `GIT_OPT_SET_ENABLE_SEARCH_PATHS`| `int` | When set to 1, the pull functionality reads host-level git configuration locations like ~/.gitconfig. Default value 0. | +| `GIT_OPT_SET_SSL_CERT_LOCATIONS` | `string`| Path to check for ssl certificates. | -Task specific parameters for different tasks (text generation/image generation/embeddings/rerank) are listed below: +Task specific parameters for different tasks (text generation/image generation/embeddings/rerank/text2speech/speech2text) are listed below: ### Text generation | option | Value format | Description | @@ -156,6 +156,7 @@ Task specific parameters for different tasks (text generation/image generation/e | `--reasoning_parser` | `string` | Type of parser to use for reasoning content extraction from model output. Auto-detected from chat template if not specified. Use `none` to explicitly disable. Supported: [qwen3, gptoss, lfm2, gemma4] | | `--tool_parser` | `string` | Type of parser to use for tool calls extraction from model output. Auto-detected from chat template if not specified. Use `none` to explicitly disable. Supported: [llama3, phi4, hermes3, mistral, qwen3coder, gptoss, devstral, lfm2, gemma4] | | `--enable_tool_guided_generation` | `bool` | Enables enforcing tool schema during generation. Requires setting response parser. Default: false. | +| `--cache_interval_multiplier` | `integer` | Multiplier for the KV cache block interval. Controls the granularity of cache allocation. Default: adaptive for the model. | ### Image generation | option | Value format | Description | @@ -184,3 +185,18 @@ Task specific parameters for different tasks (text generation/image generation/e |---------------------------|--------------|--------------------------------------------------------------------------------| | `--num_streams` | `integer` | The number of parallel execution streams to use for the model. Use at least 2 on 2 socket CPU systems. Default: 1. | | `--max_allowed_chunks` | `integer` | Maximum allowed chunks. Default: 10000. | + +### Text to speech +| option | Value format | Description | +|---------------------------|--------------|--------------------------------------------------------------------------------| +| `--num_streams` | `integer` | The number of parallel execution streams to use for the model. Use at least 2 on 2 socket CPU systems. Default: 1. | +| `--model_type` | `string` | Type of the source TTS model: `speecht5` (default) or `kokoro`. | +| `--vocoder` | `string` | The vocoder model to use for text2speech. For example `microsoft/speecht5_hifigan`. | + +### Speech to text +| option | Value format | Description | +|---------------------------|--------------|--------------------------------------------------------------------------------| +| `--num_streams` | `integer` | The number of parallel execution streams to use for the model. Use at least 2 on 2 socket CPU systems. Default: 1. | + + + diff --git a/src/pull_module/hf_pull_model_module.cpp b/src/pull_module/hf_pull_model_module.cpp index 08c3b97b98..9cbb18cd2c 100644 --- a/src/pull_module/hf_pull_model_module.cpp +++ b/src/pull_module/hf_pull_model_module.cpp @@ -58,7 +58,7 @@ static std::string getEnvReturnOrDefaultIfNotSet(const std::string& envName, con HfPullModelModule::HfPullModelModule() {} -#define RETURN_IF_ERROR(StatusOr) \ +#define RETURN_IF_ERROR(StatusOr) \ do { \ if (std::holds_alternative(StatusOr)) { \ return std::get(StatusOr); \ From d86d18dfef1f81a45b5e5798a137f23a86c0285e Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 02:19:30 +0200 Subject: [PATCH 07/15] fix relative path in staring ovms with model_path --- src/pull_module/hf_pull_model_module.cpp | 2 +- src/server.cpp | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pull_module/hf_pull_model_module.cpp b/src/pull_module/hf_pull_model_module.cpp index 9cbb18cd2c..db01f3aa12 100644 --- a/src/pull_module/hf_pull_model_module.cpp +++ b/src/pull_module/hf_pull_model_module.cpp @@ -58,7 +58,7 @@ static std::string getEnvReturnOrDefaultIfNotSet(const std::string& envName, con HfPullModelModule::HfPullModelModule() {} -#define RETURN_IF_ERROR(StatusOr) \ +#define RETURN_IF_ERROR(StatusOr) \ do { \ if (std::holds_alternative(StatusOr)) { \ return std::get(StatusOr); \ diff --git a/src/server.cpp b/src/server.cpp index a896fe052a..f0a27a770b 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -462,7 +462,8 @@ Status Server::startModules(ovms::Config& config) { if (config.getServerSettings().serverMode == IN_MEMORY_GRAPH_MODE) { // --task with --model_path: create graph in memory without HF download GraphExport graphExporter; - const auto& hfSettings = config.getServerSettings().hfSettings; + HFSettingsImpl hfSettings = config.getServerSettings().hfSettings; + hfSettings.exportSettings.modelPath = "."; status = graphExporter.createServableConfig(config.modelPath(), hfSettings, false); if (!status.ok()) { SPDLOG_ERROR("Failed to create in-memory graph config: {}", status.string()); From 3f427391681a5b3cd814d6da0b43c83f71416b79 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 02:28:30 +0200 Subject: [PATCH 08/15] style --- src/pull_module/hf_pull_model_module.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pull_module/hf_pull_model_module.cpp b/src/pull_module/hf_pull_model_module.cpp index db01f3aa12..08c3b97b98 100644 --- a/src/pull_module/hf_pull_model_module.cpp +++ b/src/pull_module/hf_pull_model_module.cpp @@ -58,7 +58,7 @@ static std::string getEnvReturnOrDefaultIfNotSet(const std::string& envName, con HfPullModelModule::HfPullModelModule() {} -#define RETURN_IF_ERROR(StatusOr) \ +#define RETURN_IF_ERROR(StatusOr) \ do { \ if (std::holds_alternative(StatusOr)) { \ return std::get(StatusOr); \ From a58ae064dc0048265c141da8ead582b6f3b1dd9c Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 16:33:57 +0200 Subject: [PATCH 09/15] more tests --- src/graph_export/t2s_graph_cli_parser.cpp | 2 +- src/test/ovmsconfig_test.cpp | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/graph_export/t2s_graph_cli_parser.cpp b/src/graph_export/t2s_graph_cli_parser.cpp index 3cce1a2d0d..3a42e2c9b8 100644 --- a/src/graph_export/t2s_graph_cli_parser.cpp +++ b/src/graph_export/t2s_graph_cli_parser.cpp @@ -49,7 +49,7 @@ void TextToSpeechGraphCLIParser::createOptions() { cxxopts::value()->default_value("speecht5"), "MODEL_TYPE") ("vocoder", - "The vocoder model to use for text2speech. For example microsoft/speecht5_hifigan", + "The vocoder model to use for text2speech. For example microsoft/speecht5_hifigan, used only with export via optimum-cli.", cxxopts::value(), "VOCODER"); // clang-format on diff --git a/src/test/ovmsconfig_test.cpp b/src/test/ovmsconfig_test.cpp index a660c9f0eb..5be80face9 100644 --- a/src/test/ovmsconfig_test.cpp +++ b/src/test/ovmsconfig_test.cpp @@ -792,6 +792,24 @@ TEST_F(OvmsConfigDeathTest, modifyModelConfigEnableButMissingModelPath) { EXPECT_EXIT(ovms::Config::instance().parse(arg_count, n_argv), ::testing::ExitedWithCode(OVMS_EX_USAGE), "Set model_name either with model_path or model_repository_path with add_to_config"); } +TEST_F(OvmsConfigDeathTest, modifyModelConfigEnableWithBadAdditionalParameters) { + char* n_argv[] = { + "ovms", + "--model_name", + "name", + "--add_to_config", + "--config_path", + "/config/path", + "--target_device", + "GPU", + "--invalid_param", + "value", + "--model_path", + "/model/path"}; + int arg_count = 12; + EXPECT_EXIT(ovms::Config::instance().parse(arg_count, n_argv), ::testing::ExitedWithCode(OVMS_EX_USAGE), "error parsing options - unmatched arguments: --invalid_param, value,"); +} + TEST_F(OvmsConfigDeathTest, modifyModelConfigDisableMissingModelName) { char* n_argv[] = { "ovms", From e879d174309778e91ec82f9dcb10d89d78062de1 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 17:22:38 +0200 Subject: [PATCH 10/15] review changes --- src/cli_parser.cpp | 4 ++-- src/config_export_module/config_export.cpp | 11 +---------- src/utils/rapidjson_utils.cpp | 10 ++++++++++ src/utils/rapidjson_utils.hpp | 1 + 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/cli_parser.cpp b/src/cli_parser.cpp index 631c0bb5b1..acd885c107 100644 --- a/src/cli_parser.cpp +++ b/src/cli_parser.cpp @@ -336,7 +336,7 @@ std::variant> CLIParser::parse(int argc, char* result = std::make_unique(options->parse(argc, argv)); // HF pull mode or pull and start mode or starting from local folder with graph created in memory - if (isHFPullOrPullAndStart(this->result) || isInMemoryGraphMode(this->result)) { + if (isHFPullOrPullAndStart(this->result)) { std::vector unmatchedOptions; GraphExportType task; if (result->count("task")) { @@ -709,7 +709,7 @@ void CLIParser::prepareGraph(ServerSettingsImpl& serverSettings, HFSettingsImpl& hfSettings.sourceModel = result->operator[]("source_model").as(); } // Ovms Pull models mode || pull and start models mode || configure mode - if (isHFPullOrPullAndStart(this->result) || isInMemoryGraphMode(this->result)) { + if (isHFPullOrPullAndStart(this->result)) { if (isConfigureMode(this->result)) { serverSettings.serverMode = CONFIGURE_MODE; } else if (isInMemoryGraphMode(this->result)) { diff --git a/src/config_export_module/config_export.cpp b/src/config_export_module/config_export.cpp index 384a37fd4f..f2f5ecc39c 100644 --- a/src/config_export_module/config_export.cpp +++ b/src/config_export_module/config_export.cpp @@ -29,19 +29,10 @@ #include "src/logging.hpp" #include "src/schema.hpp" #include "src/status.hpp" +#include "src/utils/rapidjson_utils.hpp" namespace ovms { -static void addJsonOrStringMember(rapidjson::Value& obj, const char* key, const std::string& value, rapidjson::Document::AllocatorType& alloc) { - rapidjson::Document parsed(&alloc); - if (!parsed.Parse(value.c_str()).HasParseError() && parsed.IsObject()) { - rapidjson::Value jsonValue(parsed, alloc); - obj.AddMember(rapidjson::Value(key, alloc), jsonValue, alloc); - } else { - obj.AddMember(rapidjson::Value(key, alloc), rapidjson::Value(value.c_str(), alloc), alloc); - } -} - static void addOptionalModelFields(rapidjson::Value& configObj, const ModelsSettingsImpl& modelSettings, rapidjson::Document::AllocatorType& alloc) { if (!modelSettings.batchSize.empty()) configObj.AddMember("batch_size", rapidjson::Value(modelSettings.batchSize.c_str(), alloc), alloc); diff --git a/src/utils/rapidjson_utils.cpp b/src/utils/rapidjson_utils.cpp index e428cc6b47..bec15b650d 100644 --- a/src/utils/rapidjson_utils.cpp +++ b/src/utils/rapidjson_utils.cpp @@ -37,6 +37,16 @@ std::string documentToString(const rapidjson::Document& doc) { return buffer.GetString(); } +void addJsonOrStringMember(rapidjson::Value& obj, const char* key, const std::string& value, rapidjson::Document::AllocatorType& alloc) { + rapidjson::Document parsed(&alloc); + if (!parsed.Parse(value.c_str()).HasParseError() && parsed.IsObject()) { + rapidjson::Value jsonValue(parsed, alloc); + obj.AddMember(rapidjson::Value(key, alloc), jsonValue, alloc); + } else { + obj.AddMember(rapidjson::Value(key, alloc), rapidjson::Value(value.c_str(), alloc), alloc); + } +} + // Lightweight SAX handler that only tracks nesting depth. // No DOM allocation — all SAX events are accepted and discarded. struct DepthOnlyHandler : public rapidjson::BaseReaderHandler, DepthOnlyHandler> { diff --git a/src/utils/rapidjson_utils.hpp b/src/utils/rapidjson_utils.hpp index f7c70db217..18db649bef 100644 --- a/src/utils/rapidjson_utils.hpp +++ b/src/utils/rapidjson_utils.hpp @@ -22,6 +22,7 @@ namespace ovms { std::string documentToString(const rapidjson::Document& doc); +void addJsonOrStringMember(rapidjson::Value& obj, const char* key, const std::string& value, rapidjson::Document::AllocatorType& alloc); // Default maximum nesting depth allowed for incoming JSON request bodies. inline constexpr std::size_t DEFAULT_MAX_JSON_NESTING_DEPTH = 100; From a7cd8d2c8c53f742431c0e8c0d057a599004463e Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 17:27:26 +0200 Subject: [PATCH 11/15] build fix --- src/config_export_module/BUILD | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config_export_module/BUILD b/src/config_export_module/BUILD index cbad7badab..77f78cd7da 100644 --- a/src/config_export_module/BUILD +++ b/src/config_export_module/BUILD @@ -27,6 +27,7 @@ ovms_cc_library( "//src/filesystem:libovmslocalfilesystem", "//src:libovmslogging", "//src:libovmsschema", + "//src/utils:rapidjson_utils", ], visibility = ["//visibility:public",], ) From ebc919d6cb537a2d09f3aed73b91674681f1d6ae Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 22:29:21 +0200 Subject: [PATCH 12/15] style --- src/default_task.cpp | 146 +++++++++++++++++------------------ src/test/ovmsconfig_test.cpp | 9 ++- 2 files changed, 79 insertions(+), 76 deletions(-) diff --git a/src/default_task.cpp b/src/default_task.cpp index b00b0d974f..261a7637ed 100644 --- a/src/default_task.cpp +++ b/src/default_task.cpp @@ -17,7 +17,7 @@ #include #include -#include +src / test / ovmsconfig_test.cpp #include #include #include #include @@ -27,100 +27,100 @@ #include "default_task_detector.hpp" #include "status.hpp" -namespace ovms { + namespace ovms { -static std::string getEnvOrDefault(const char* envName, const std::string& defaultValue = "") { - const char* envValue = std::getenv(envName); - if (envValue == nullptr) { - return defaultValue; + static std::string getEnvOrDefault(const char* envName, const std::string& defaultValue = "") { + const char* envValue = std::getenv(envName); + if (envValue == nullptr) { + return defaultValue; + } + return envValue; } - return envValue; -} -static std::string ensureTrailingSlash(std::string path) { - if (path.empty() || path.back() == '/') { + static std::string ensureTrailingSlash(std::string path) { + if (path.empty() || path.back() == '/') { + return path; + } + path.push_back('/'); return path; } - path.push_back('/'); - return path; -} - -bool graphPbtxtExists(const std::string& modelPath) { - const auto graphPath = std::filesystem::path(modelPath) / "graph.pbtxt"; - return std::filesystem::exists(graphPath); -} - -bool hasTaskSpecificParameters(const std::vector& unmatchedOptions) { - return !unmatchedOptions.empty(); -} -std::optional determineDefaultTaskParameter(const std::optional& modelPath, const std::optional& sourceModel, const std::optional& modelRepositoryPath) { - DefaultTaskDetector detector; - - if (modelPath.has_value() && !modelPath->empty()) { - // Normalize first to remove any trailing separator so that filename() - // always returns the leaf directory name, e.g. "/models/llama/" → "llama". - const std::filesystem::path pathFs = std::filesystem::path(*modelPath).lexically_normal(); - // Use only the leaf directory name for keyword-based disambiguation - // (e.g. "Qwen3-Embedding-0.6B"), not the full path which may contain - // unrelated keywords from parent directories. - const std::string identifier = pathFs.filename().empty() ? pathFs.string() : pathFs.filename().string(); - ModelCatalogContext ctx(pathFs, identifier); - const std::string task = detector.detect(ctx); - if (task.empty()) { - return std::nullopt; - } - return task; + bool graphPbtxtExists(const std::string& modelPath) { + const auto graphPath = std::filesystem::path(modelPath) / "graph.pbtxt"; + return std::filesystem::exists(graphPath); } - if (!sourceModel.has_value() || sourceModel->empty()) { - return std::nullopt; + bool hasTaskSpecificParameters(const std::vector& unmatchedOptions) { + return !unmatchedOptions.empty(); } - // Try local model repository path before downloading from HuggingFace - if (modelRepositoryPath.has_value() && !modelRepositoryPath->empty()) { - const auto localModelDir = std::filesystem::path(*modelRepositoryPath) / *sourceModel; - if (std::filesystem::exists(localModelDir)) { - ModelCatalogContext ctx(localModelDir, *sourceModel); + std::optional determineDefaultTaskParameter(const std::optional& modelPath, const std::optional& sourceModel, const std::optional& modelRepositoryPath) { + DefaultTaskDetector detector; + + if (modelPath.has_value() && !modelPath->empty()) { + // Normalize first to remove any trailing separator so that filename() + // always returns the leaf directory name, e.g. "/models/llama/" → "llama". + const std::filesystem::path pathFs = std::filesystem::path(*modelPath).lexically_normal(); + // Use only the leaf directory name for keyword-based disambiguation + // (e.g. "Qwen3-Embedding-0.6B"), not the full path which may contain + // unrelated keywords from parent directories. + const std::string identifier = pathFs.filename().empty() ? pathFs.string() : pathFs.filename().string(); + ModelCatalogContext ctx(pathFs, identifier); const std::string task = detector.detect(ctx); if (task.empty()) { return std::nullopt; } return task; } - } - // Download config files from HuggingFace. - // config.json is tried first (covers transformer-style models). - // model_index.json is only attempted when config.json is absent — it is - // specific to Diffusers pipeline repos (e.g. StableDiffusion, Flux) that - // do not have a standard config.json architectures array. - const std::string hfEndpoint = ensureTrailingSlash(getEnvOrDefault(HF_ENDPOINT_ENV_VAR, DEFAULT_HF_ENDPOINT)); - const std::string token = getEnvOrDefault(HF_TOKEN_ENV_VAR); + if (!sourceModel.has_value() || sourceModel->empty()) { + return std::nullopt; + } + + // Try local model repository path before downloading from HuggingFace + if (modelRepositoryPath.has_value() && !modelRepositoryPath->empty()) { + const auto localModelDir = std::filesystem::path(*modelRepositoryPath) / *sourceModel; + if (std::filesystem::exists(localModelDir)) { + ModelCatalogContext ctx(localModelDir, *sourceModel); + const std::string task = detector.detect(ctx); + if (task.empty()) { + return std::nullopt; + } + return task; + } + } + + // Download config files from HuggingFace. + // config.json is tried first (covers transformer-style models). + // model_index.json is only attempted when config.json is absent — it is + // specific to Diffusers pipeline repos (e.g. StableDiffusion, Flux) that + // do not have a standard config.json architectures array. + const std::string hfEndpoint = ensureTrailingSlash(getEnvOrDefault(HF_ENDPOINT_ENV_VAR, DEFAULT_HF_ENDPOINT)); + const std::string token = getEnvOrDefault(HF_TOKEN_ENV_VAR); - ModelCatalogContext ctx(std::filesystem::path{}, *sourceModel); + ModelCatalogContext ctx(std::filesystem::path{}, *sourceModel); - std::string configBody; - const std::string configUrl = hfEndpoint + *sourceModel + "/resolve/main/config.json"; - const auto configStatus = fetchUrlToString(configUrl, token, configBody); - if (configStatus.ok()) { - ctx.addContent("config.json", std::move(configBody)); - } else { - // config.json not available — try model_index.json (Diffusers repos) - std::string indexBody; - const std::string indexUrl = hfEndpoint + *sourceModel + "/resolve/main/model_index.json"; - const auto indexStatus = fetchUrlToString(indexUrl, token, indexBody); - if (indexStatus.ok()) { - ctx.addContent("model_index.json", std::move(indexBody)); + std::string configBody; + const std::string configUrl = hfEndpoint + *sourceModel + "/resolve/main/config.json"; + const auto configStatus = fetchUrlToString(configUrl, token, configBody); + if (configStatus.ok()) { + ctx.addContent("config.json", std::move(configBody)); } else { + // config.json not available — try model_index.json (Diffusers repos) + std::string indexBody; + const std::string indexUrl = hfEndpoint + *sourceModel + "/resolve/main/model_index.json"; + const auto indexStatus = fetchUrlToString(indexUrl, token, indexBody); + if (indexStatus.ok()) { + ctx.addContent("model_index.json", std::move(indexBody)); + } else { + return std::nullopt; + } + } + const std::string task = detector.detect(ctx); + if (task.empty()) { return std::nullopt; } + return task; } - const std::string task = detector.detect(ctx); - if (task.empty()) { - return std::nullopt; - } - return task; -} } // namespace ovms diff --git a/src/test/ovmsconfig_test.cpp b/src/test/ovmsconfig_test.cpp index e4473d8393..12c91db149 100644 --- a/src/test/ovmsconfig_test.cpp +++ b/src/test/ovmsconfig_test.cpp @@ -3161,8 +3161,10 @@ TEST_F(OvmsInferredTaskTest, positiveConfigureModeInfersTaskFromModel) { char* n_argv[] = { (char*)"ovms", (char*)"--configure", - (char*)"--model_path", (char*)modelPath.c_str(), - (char*)"--cache_size", (char*)"3", + (char*)"--model_path", + (char*)modelPath.c_str(), + (char*)"--cache_size", + (char*)"3", }; int arg_count = 6; ConstructorEnabledConfig config; @@ -3175,7 +3177,8 @@ TEST_F(OvmsConfigDeathTest, negativeConfigureModeRequiresTaskWhenCannotInfer) { char* n_argv[] = { (char*)"ovms", (char*)"--configure", - (char*)"--model_path", (char*)"/non/existing/model/path", + (char*)"--model_path", + (char*)"/non/existing/model/path", }; int arg_count = 4; EXPECT_EXIT(ovms::Config::instance().parse(arg_count, n_argv), From e358ba2b1ef20406f1aee75e8fc306fbf0f5d2bb Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 23:21:49 +0200 Subject: [PATCH 13/15] build fix --- src/default_task.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/default_task.cpp b/src/default_task.cpp index 261a7637ed..dc49af1cca 100644 --- a/src/default_task.cpp +++ b/src/default_task.cpp @@ -17,7 +17,7 @@ #include #include -src / test / ovmsconfig_test.cpp #include +#include #include #include #include From d18e22d8b2fe7c3bae6631a8426b5b5dc51f2359 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Fri, 17 Jul 2026 23:47:59 +0200 Subject: [PATCH 14/15] style --- src/default_task.cpp | 144 +++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 72 deletions(-) diff --git a/src/default_task.cpp b/src/default_task.cpp index dc49af1cca..b00b0d974f 100644 --- a/src/default_task.cpp +++ b/src/default_task.cpp @@ -27,100 +27,100 @@ #include "default_task_detector.hpp" #include "status.hpp" - namespace ovms { +namespace ovms { - static std::string getEnvOrDefault(const char* envName, const std::string& defaultValue = "") { - const char* envValue = std::getenv(envName); - if (envValue == nullptr) { - return defaultValue; - } - return envValue; +static std::string getEnvOrDefault(const char* envName, const std::string& defaultValue = "") { + const char* envValue = std::getenv(envName); + if (envValue == nullptr) { + return defaultValue; } + return envValue; +} - static std::string ensureTrailingSlash(std::string path) { - if (path.empty() || path.back() == '/') { - return path; - } - path.push_back('/'); +static std::string ensureTrailingSlash(std::string path) { + if (path.empty() || path.back() == '/') { return path; } + path.push_back('/'); + return path; +} - bool graphPbtxtExists(const std::string& modelPath) { - const auto graphPath = std::filesystem::path(modelPath) / "graph.pbtxt"; - return std::filesystem::exists(graphPath); - } +bool graphPbtxtExists(const std::string& modelPath) { + const auto graphPath = std::filesystem::path(modelPath) / "graph.pbtxt"; + return std::filesystem::exists(graphPath); +} + +bool hasTaskSpecificParameters(const std::vector& unmatchedOptions) { + return !unmatchedOptions.empty(); +} - bool hasTaskSpecificParameters(const std::vector& unmatchedOptions) { - return !unmatchedOptions.empty(); +std::optional determineDefaultTaskParameter(const std::optional& modelPath, const std::optional& sourceModel, const std::optional& modelRepositoryPath) { + DefaultTaskDetector detector; + + if (modelPath.has_value() && !modelPath->empty()) { + // Normalize first to remove any trailing separator so that filename() + // always returns the leaf directory name, e.g. "/models/llama/" → "llama". + const std::filesystem::path pathFs = std::filesystem::path(*modelPath).lexically_normal(); + // Use only the leaf directory name for keyword-based disambiguation + // (e.g. "Qwen3-Embedding-0.6B"), not the full path which may contain + // unrelated keywords from parent directories. + const std::string identifier = pathFs.filename().empty() ? pathFs.string() : pathFs.filename().string(); + ModelCatalogContext ctx(pathFs, identifier); + const std::string task = detector.detect(ctx); + if (task.empty()) { + return std::nullopt; + } + return task; } - std::optional determineDefaultTaskParameter(const std::optional& modelPath, const std::optional& sourceModel, const std::optional& modelRepositoryPath) { - DefaultTaskDetector detector; + if (!sourceModel.has_value() || sourceModel->empty()) { + return std::nullopt; + } - if (modelPath.has_value() && !modelPath->empty()) { - // Normalize first to remove any trailing separator so that filename() - // always returns the leaf directory name, e.g. "/models/llama/" → "llama". - const std::filesystem::path pathFs = std::filesystem::path(*modelPath).lexically_normal(); - // Use only the leaf directory name for keyword-based disambiguation - // (e.g. "Qwen3-Embedding-0.6B"), not the full path which may contain - // unrelated keywords from parent directories. - const std::string identifier = pathFs.filename().empty() ? pathFs.string() : pathFs.filename().string(); - ModelCatalogContext ctx(pathFs, identifier); + // Try local model repository path before downloading from HuggingFace + if (modelRepositoryPath.has_value() && !modelRepositoryPath->empty()) { + const auto localModelDir = std::filesystem::path(*modelRepositoryPath) / *sourceModel; + if (std::filesystem::exists(localModelDir)) { + ModelCatalogContext ctx(localModelDir, *sourceModel); const std::string task = detector.detect(ctx); if (task.empty()) { return std::nullopt; } return task; } + } - if (!sourceModel.has_value() || sourceModel->empty()) { - return std::nullopt; - } - - // Try local model repository path before downloading from HuggingFace - if (modelRepositoryPath.has_value() && !modelRepositoryPath->empty()) { - const auto localModelDir = std::filesystem::path(*modelRepositoryPath) / *sourceModel; - if (std::filesystem::exists(localModelDir)) { - ModelCatalogContext ctx(localModelDir, *sourceModel); - const std::string task = detector.detect(ctx); - if (task.empty()) { - return std::nullopt; - } - return task; - } - } - - // Download config files from HuggingFace. - // config.json is tried first (covers transformer-style models). - // model_index.json is only attempted when config.json is absent — it is - // specific to Diffusers pipeline repos (e.g. StableDiffusion, Flux) that - // do not have a standard config.json architectures array. - const std::string hfEndpoint = ensureTrailingSlash(getEnvOrDefault(HF_ENDPOINT_ENV_VAR, DEFAULT_HF_ENDPOINT)); - const std::string token = getEnvOrDefault(HF_TOKEN_ENV_VAR); + // Download config files from HuggingFace. + // config.json is tried first (covers transformer-style models). + // model_index.json is only attempted when config.json is absent — it is + // specific to Diffusers pipeline repos (e.g. StableDiffusion, Flux) that + // do not have a standard config.json architectures array. + const std::string hfEndpoint = ensureTrailingSlash(getEnvOrDefault(HF_ENDPOINT_ENV_VAR, DEFAULT_HF_ENDPOINT)); + const std::string token = getEnvOrDefault(HF_TOKEN_ENV_VAR); - ModelCatalogContext ctx(std::filesystem::path{}, *sourceModel); + ModelCatalogContext ctx(std::filesystem::path{}, *sourceModel); - std::string configBody; - const std::string configUrl = hfEndpoint + *sourceModel + "/resolve/main/config.json"; - const auto configStatus = fetchUrlToString(configUrl, token, configBody); - if (configStatus.ok()) { - ctx.addContent("config.json", std::move(configBody)); + std::string configBody; + const std::string configUrl = hfEndpoint + *sourceModel + "/resolve/main/config.json"; + const auto configStatus = fetchUrlToString(configUrl, token, configBody); + if (configStatus.ok()) { + ctx.addContent("config.json", std::move(configBody)); + } else { + // config.json not available — try model_index.json (Diffusers repos) + std::string indexBody; + const std::string indexUrl = hfEndpoint + *sourceModel + "/resolve/main/model_index.json"; + const auto indexStatus = fetchUrlToString(indexUrl, token, indexBody); + if (indexStatus.ok()) { + ctx.addContent("model_index.json", std::move(indexBody)); } else { - // config.json not available — try model_index.json (Diffusers repos) - std::string indexBody; - const std::string indexUrl = hfEndpoint + *sourceModel + "/resolve/main/model_index.json"; - const auto indexStatus = fetchUrlToString(indexUrl, token, indexBody); - if (indexStatus.ok()) { - ctx.addContent("model_index.json", std::move(indexBody)); - } else { - return std::nullopt; - } - } - const std::string task = detector.detect(ctx); - if (task.empty()) { return std::nullopt; } - return task; } + const std::string task = detector.detect(ctx); + if (task.empty()) { + return std::nullopt; + } + return task; +} } // namespace ovms From a5366d592d7b9c744ec3107990778a118a2e4277 Mon Sep 17 00:00:00 2001 From: Dariusz Trawinski Date: Sat, 18 Jul 2026 00:58:24 +0200 Subject: [PATCH 15/15] tests --- src/cli_parser.cpp | 4 ++++ src/test/ovmsconfig_test.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cli_parser.cpp b/src/cli_parser.cpp index 60e6373e71..629281a697 100644 --- a/src/cli_parser.cpp +++ b/src/cli_parser.cpp @@ -380,6 +380,10 @@ std::variant> CLIParser::parse(int argc, char* if (shouldInferTask) { inferredTaskParameter = determineDefaultTaskParameter(modelPath, sourceModel, modelRepositoryPath); + if (!result->count("task") && !inferredTaskParameter.has_value()) { + ss << "error parsing options - Could not infer model task - specify --task value explicitly" << std::endl; + return std::make_pair(OVMS_EX_USAGE, ss.str()); + } } } if (result->count("task") || inferredTaskParameter.has_value()) { diff --git a/src/test/ovmsconfig_test.cpp b/src/test/ovmsconfig_test.cpp index 12c91db149..656743ef28 100644 --- a/src/test/ovmsconfig_test.cpp +++ b/src/test/ovmsconfig_test.cpp @@ -3183,7 +3183,7 @@ TEST_F(OvmsConfigDeathTest, negativeConfigureModeRequiresTaskWhenCannotInfer) { int arg_count = 4; EXPECT_EXIT(ovms::Config::instance().parse(arg_count, n_argv), ::testing::ExitedWithCode(OVMS_EX_USAGE), - "--configure requires --task"); + "Could not infer model task"); } #pragma GCC diagnostic pop