@@ -3371,19 +3371,32 @@ void ModelManager::populate_collection_components_from_cache_locked(ModelInfo& i
33713371 }
33723372}
33733373
3374+ void ModelManager::register_model (const std::string& model_name,
3375+ const json& model_data,
3376+ bool allow_missing_checkpoint,
3377+ bool replace_existing) {
3378+ std::set<std::string> visited;
3379+ download_model (model_name, model_data, true , nullptr , visited,
3380+ true , allow_missing_checkpoint, replace_existing);
3381+ }
3382+
33743383void ModelManager::download_model (const std::string& model_name,
33753384 const json& model_data,
33763385 bool do_not_upgrade,
33773386 DownloadProgressCallback progress_callback) {
33783387 std::set<std::string> visited;
3379- download_model (model_name, model_data, do_not_upgrade, progress_callback, visited);
3388+ download_model (model_name, model_data, do_not_upgrade, progress_callback, visited,
3389+ false , false , false );
33803390}
33813391
33823392void ModelManager::download_model (const std::string& model_name,
33833393 const json& model_data,
33843394 bool do_not_upgrade,
33853395 DownloadProgressCallback progress_callback,
3386- std::set<std::string>& visited) {
3396+ std::set<std::string>& visited,
3397+ bool register_only,
3398+ bool allow_missing_checkpoint,
3399+ bool replace_existing) {
33873400 // Keep a mutable registration payload so legacy re-pulls that omit the
33883401 // registry retain the source recorded on the existing model. The original
33893402 // request remains untouched for validation and download semantics.
@@ -3446,8 +3459,13 @@ void ModelManager::download_model(const std::string& model_name,
34463459 }
34473460 LOG (INFO , " ModelManager" ) << " Registering new collection: " << model_name << std::endl;
34483461 } else {
3449- // Check that required arguments are provided
3450- if (actual_checkpoint.empty () || actual_recipe.empty ()) {
3462+ if (actual_recipe.empty ()) {
3463+ throw std::runtime_error (
3464+ " Model " + model_name + " is not registered with Lemonade Server. "
3465+ " To register it, provide the `recipe` argument."
3466+ );
3467+ }
3468+ if (actual_checkpoint.empty () && !allow_missing_checkpoint) {
34513469 throw std::runtime_error (
34523470 " Model " + model_name + " is not registered with Lemonade Server. "
34533471 " To register and install it, provide the `checkpoint` and `recipe` "
@@ -3456,10 +3474,12 @@ void ModelManager::download_model(const std::string& model_name,
34563474 }
34573475
34583476 // Backend-specific checkpoint validation (llamacpp: GGUF needs :variant).
3459- if (auto err = backends::ops_for (actual_recipe)->validate_registration_checkpoint (
3460- actual_checkpoint);
3461- !err.empty ()) {
3462- throw std::runtime_error (err);
3477+ if (!actual_checkpoint.empty ()) {
3478+ if (auto err = backends::ops_for (actual_recipe)->validate_registration_checkpoint (
3479+ actual_checkpoint);
3480+ !err.empty ()) {
3481+ throw std::runtime_error (err);
3482+ }
34633483 }
34643484
34653485 LOG (INFO , " ModelManager" ) << " Registering new user model: " << model_name << std::endl;
@@ -3477,34 +3497,49 @@ void ModelManager::download_model(const std::string& model_name,
34773497 registration_data[" source" ] = effective_registry_source (info);
34783498 }
34793499
3480- bool is_collection_overwrite = is_model_collection_recipe (actual_recipe) &&
3481- model_data.contains (" components" );
3482- if (is_collection_overwrite) {
3483- // Validate the original user-authored request, not registration_data:
3484- // the latter is enriched with the persisted registry source, which is
3485- // not part of the public routing-policy document the parser accepts.
3486- if (auto err = validate_collection_request (model_name, model_data)) {
3487- throw std::runtime_error (*err);
3500+ const bool explicit_definition =
3501+ !actual_recipe.empty () || !actual_checkpoint.empty () ||
3502+ model_data.contains (" checkpoints" ) || model_data.contains (" components" );
3503+ if (register_only && replace_existing &&
3504+ is_user_model_name (model_name) && explicit_definition) {
3505+ if (is_model_collection_recipe (actual_recipe)) {
3506+ if (auto err = validate_collection_request (model_name, model_data)) {
3507+ throw std::runtime_error (*err);
3508+ }
34883509 }
34893510 model_registered = false ;
3490- LOG (INFO , " ModelManager" ) << " Overwriting collection : "
3511+ LOG (INFO , " ModelManager" ) << " Replacing user model definition : "
34913512 << model_name << std::endl;
3492- } else if (actual_checkpoint.empty ()) {
3493- actual_checkpoint = info.checkpoint ();
3494- actual_recipe = info.recipe ;
34953513 } else {
3496- std::string conflict = describe_registration_conflict (info, registration_data);
3497- if (!conflict.empty ()) {
3498- throw std::runtime_error (
3499- " Model '" + model_name + " ' is already registered with different "
3500- " model metadata: " + conflict + " . Choose a different model name "
3501- " for this registry checkpoint."
3502- );
3503- }
3504- if (actual_recipe.empty ()) {
3514+ bool is_collection_overwrite = is_model_collection_recipe (actual_recipe) &&
3515+ model_data.contains (" components" );
3516+ if (is_collection_overwrite) {
3517+ // Validate the original user-authored request, not registration_data:
3518+ // the latter is enriched with the persisted registry source, which is
3519+ // not part of the public routing-policy document the parser accepts.
3520+ if (auto err = validate_collection_request (model_name, model_data)) {
3521+ throw std::runtime_error (*err);
3522+ }
3523+ model_registered = false ;
3524+ LOG (INFO , " ModelManager" ) << " Overwriting collection: "
3525+ << model_name << std::endl;
3526+ } else if (actual_checkpoint.empty ()) {
3527+ actual_checkpoint = info.checkpoint ();
35053528 actual_recipe = info.recipe ;
35063529 } else {
3507- model_registered = false ;
3530+ std::string conflict = describe_registration_conflict (info, registration_data);
3531+ if (!conflict.empty ()) {
3532+ throw std::runtime_error (
3533+ " Model '" + model_name + " ' is already registered with different "
3534+ " model metadata: " + conflict + " . Choose a different model name "
3535+ " for this registry checkpoint."
3536+ );
3537+ }
3538+ if (actual_recipe.empty ()) {
3539+ actual_recipe = info.recipe ;
3540+ } else {
3541+ model_registered = false ;
3542+ }
35083543 }
35093544 }
35103545 }
@@ -3529,6 +3564,10 @@ void ModelManager::download_model(const std::string& model_name,
35293564 collection_registered_this_call = true ;
35303565 }
35313566
3567+ if (register_only && is_model_collection_recipe (actual_recipe)) {
3568+ return ;
3569+ }
3570+
35323571 // Collections don't have their own backend - download each component instead.
35333572 //
35343573 // Persistence follows one rule, uniform across models and collections: a
@@ -3635,7 +3674,7 @@ void ModelManager::download_model(const std::string& model_name,
36353674 }
36363675 LOG (INFO , " ModelManager" ) << " Downloading component: " << component << std::endl;
36373676 json comp_data = json::object ();
3638- download_model (component, comp_data, do_not_upgrade, forward, visited);
3677+ download_model (component, comp_data, do_not_upgrade, forward, visited, false , false , false );
36393678 }
36403679
36413680 // A registry-backed collection's in-memory components were empty until the
@@ -3676,20 +3715,6 @@ void ModelManager::download_model(const std::string& model_name,
36763715 );
36773716 }
36783717
3679- LOG (INFO , " ModelManager" ) << " Downloading model: " << repo_id;
3680- if (!variant.empty ()) {
3681- LOG (INFO , " ModelManager" ) << " (variant: " << variant << " )" ;
3682- }
3683- LOG (INFO , " ModelManager" ) << std::endl;
3684-
3685- // Check if offline mode
3686- if (auto * cfg = RuntimeConfig::global ()) {
3687- if (cfg->offline ()) {
3688- LOG (INFO , " ModelManager" ) << " Offline mode enabled, skipping download" << std::endl;
3689- return ;
3690- }
3691- }
3692-
36933718 // Persist registration and recipe options BEFORE the cache-first shortcut
36943719 // below. A registration/import/overwrite that targets an already-downloaded
36953720 // model must still update user_models.json and recipe_options.json. The
@@ -3716,6 +3741,24 @@ void ModelManager::download_model(const std::string& model_name,
37163741 save_model_options (model_info);
37173742 }
37183743
3744+ if (register_only) {
3745+ return ;
3746+ }
3747+
3748+ LOG (INFO , " ModelManager" ) << " Downloading model: " << repo_id;
3749+ if (!variant.empty ()) {
3750+ LOG (INFO , " ModelManager" ) << " (variant: " << variant << " )" ;
3751+ }
3752+ LOG (INFO , " ModelManager" ) << std::endl;
3753+
3754+ // Check if offline mode
3755+ if (auto * cfg = RuntimeConfig::global ()) {
3756+ if (cfg->offline ()) {
3757+ LOG (INFO , " ModelManager" ) << " Offline mode enabled, skipping download" << std::endl;
3758+ return ;
3759+ }
3760+ }
3761+
37193762 // CRITICAL: If do_not_upgrade=true AND model is already downloaded, skip the
37203763 // remote-registry update check. Registration and recipe options were already
37213764 // persisted above, so an import/overwrite still takes effect on disk.
0 commit comments